Add networked crab gathering prototype
This commit is contained in:
parent
f55663c105
commit
a62cce6404
56 changed files with 2692 additions and 56 deletions
|
|
@ -10,6 +10,7 @@ const WORM_MAX_STACK: int = 10
|
||||||
const FISH_FINDER_ID: StringName = &"fish_finder"
|
const FISH_FINDER_ID: StringName = &"fish_finder"
|
||||||
const MAGNET_ID: StringName = &"magnet"
|
const MAGNET_ID: StringName = &"magnet"
|
||||||
const BATTERIES_ID: StringName = &"batteries"
|
const BATTERIES_ID: StringName = &"batteries"
|
||||||
|
const CRAB_NET_ID: StringName = &"crab_net"
|
||||||
|
|
||||||
const ITEM_PRICES: Dictionary[StringName, int] = {
|
const ITEM_PRICES: Dictionary[StringName, int] = {
|
||||||
&"worms": 1,
|
&"worms": 1,
|
||||||
|
|
@ -26,6 +27,7 @@ const ITEM_PRICES: Dictionary[StringName, int] = {
|
||||||
FISH_FINDER_ID: 500,
|
FISH_FINDER_ID: 500,
|
||||||
MAGNET_ID: 250,
|
MAGNET_ID: 250,
|
||||||
BATTERIES_ID: 15,
|
BATTERIES_ID: 15,
|
||||||
|
CRAB_NET_ID: 50,
|
||||||
}
|
}
|
||||||
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
|
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
|
||||||
&"snails": 400,
|
&"snails": 400,
|
||||||
|
|
@ -46,6 +48,7 @@ const ITEM_ORDER: Array[StringName] = [
|
||||||
&"the_standby",
|
&"the_standby",
|
||||||
FISH_FINDER_ID,
|
FISH_FINDER_ID,
|
||||||
MAGNET_ID,
|
MAGNET_ID,
|
||||||
|
CRAB_NET_ID,
|
||||||
&"coffee",
|
&"coffee",
|
||||||
&"energy_drink",
|
&"energy_drink",
|
||||||
&"snack",
|
&"snack",
|
||||||
|
|
@ -108,6 +111,7 @@ static func is_permanent_unlock(
|
||||||
item.is_lure()
|
item.is_lure()
|
||||||
or item_id == FISH_FINDER_ID
|
or item_id == FISH_FINDER_ID
|
||||||
or item_id == MAGNET_ID
|
or item_id == MAGNET_ID
|
||||||
|
or item_id == CRAB_NET_ID
|
||||||
or item is FishingRodDataType
|
or item is FishingRodDataType
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,17 @@ enum Rarity {
|
||||||
LEGENDARY,
|
LEGENDARY,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CollectionMethod {
|
||||||
|
FISHING,
|
||||||
|
NET,
|
||||||
|
DIGGING,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogbookSection {
|
||||||
|
AUTOMATIC,
|
||||||
|
SHELLFISH,
|
||||||
|
}
|
||||||
|
|
||||||
@export var id: StringName
|
@export var id: StringName
|
||||||
@export var display_name: String
|
@export var display_name: String
|
||||||
@export_category("Developer Catalog")
|
@export_category("Developer Catalog")
|
||||||
|
|
@ -32,6 +43,9 @@ enum Rarity {
|
||||||
@export_range(1, 999999, 1) var catalog_number: int = 1
|
@export_range(1, 999999, 1) var catalog_number: int = 1
|
||||||
@export var collection_group: StringName
|
@export var collection_group: StringName
|
||||||
@export_multiline var logbook_fact: String
|
@export_multiline var logbook_fact: String
|
||||||
|
@export var collection_method: CollectionMethod = CollectionMethod.FISHING
|
||||||
|
@export var logbook_section: LogbookSection = LogbookSection.AUTOMATIC
|
||||||
|
@export var habitat_label: String = ""
|
||||||
@export_category("Fishing")
|
@export_category("Fishing")
|
||||||
@export_flags("Fresh Water", "Salt Water")
|
@export_flags("Fresh Water", "Salt Water")
|
||||||
var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
|
var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
|
||||||
|
|
@ -53,9 +67,12 @@ var allowed_water_types: int = WaterType.ALL_FISHABLE_MASK
|
||||||
|
|
||||||
|
|
||||||
func is_selectable() -> bool:
|
func is_selectable() -> bool:
|
||||||
|
return active and is_valid_catalog_entry()
|
||||||
|
|
||||||
|
|
||||||
|
func is_valid_catalog_entry() -> bool:
|
||||||
return (
|
return (
|
||||||
active
|
not id.is_empty()
|
||||||
and not id.is_empty()
|
|
||||||
and base_catch_weight > 0.0
|
and base_catch_weight > 0.0
|
||||||
and catch_profile != null
|
and catch_profile != null
|
||||||
and weight_min_lb > 0.0
|
and weight_min_lb > 0.0
|
||||||
|
|
@ -69,6 +86,16 @@ func is_selectable() -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_fishable() -> bool:
|
||||||
|
return is_selectable() and collection_method == CollectionMethod.FISHING
|
||||||
|
|
||||||
|
|
||||||
|
func get_habitat_label() -> String:
|
||||||
|
if not habitat_label.strip_edges().is_empty():
|
||||||
|
return habitat_label.strip_edges().to_lower()
|
||||||
|
return WaterType.label(get_primary_water_type()).to_lower()
|
||||||
|
|
||||||
|
|
||||||
func is_allowed_in_water(type: WaterType.Type) -> bool:
|
func is_allowed_in_water(type: WaterType.Type) -> bool:
|
||||||
return (allowed_water_types & WaterType.mask_for(type)) != 0
|
return (allowed_water_types & WaterType.mask_for(type)) != 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ func select_fish(
|
||||||
var weights: Array[float] = []
|
var weights: Array[float] = []
|
||||||
var total_weight: float = 0.0
|
var total_weight: float = 0.0
|
||||||
for fish: FishDataType in pool.candidates:
|
for fish: FishDataType in pool.candidates:
|
||||||
if fish == null or not fish.is_selectable():
|
if fish == null or not fish.is_fishable():
|
||||||
continue
|
continue
|
||||||
if not fish.is_allowed_in_water(context.water_type):
|
if not fish.is_allowed_in_water(context.water_type):
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -16,6 +16,9 @@ active = false
|
||||||
catalog_number = 3902
|
catalog_number = 3902
|
||||||
collection_group = &"Marine crabs"
|
collection_group = &"Marine crabs"
|
||||||
logbook_fact = "blue crabs swim with flattened rear legs shaped like paddles. bright blue claws and an olive shell conceal a quick, defensive temperament."
|
logbook_fact = "blue crabs swim with flattened rear legs shaped like paddles. bright blue claws and an olive shell conceal a quick, defensive temperament."
|
||||||
|
collection_method = 1
|
||||||
|
logbook_section = 1
|
||||||
|
habitat_label = "shallow coastal water"
|
||||||
allowed_water_types = 2
|
allowed_water_types = 2
|
||||||
rarity = 0
|
rarity = 0
|
||||||
base_catch_weight = 1.15
|
base_catch_weight = 1.15
|
||||||
|
|
|
||||||
BIN
fish/species/crab_brown/crab_brown.png
Normal file
BIN
fish/species/crab_brown/crab_brown.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
40
fish/species/crab_brown/crab_brown.png.import
Normal file
40
fish/species/crab_brown/crab_brown.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://dmn3rafvxtnb1"
|
||||||
|
path="res://.godot/imported/crab_brown.png-4241f8408bf426d406e45f4cce16bbc8.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://fish/species/crab_brown/crab_brown.png"
|
||||||
|
dest_files=["res://.godot/imported/crab_brown.png-4241f8408bf426d406e45f4cce16bbc8.ctex"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/high_quality=false
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
|
compress/hdr_compression=1
|
||||||
|
compress/normal_map=0
|
||||||
|
compress/channel_pack=0
|
||||||
|
mipmaps/generate=false
|
||||||
|
mipmaps/limit=-1
|
||||||
|
roughness/mode=0
|
||||||
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
process/hdr_as_srgb=false
|
||||||
|
process/hdr_clamp_exposure=false
|
||||||
|
process/size_limit=0
|
||||||
|
detect_3d/compress_to=1
|
||||||
27
fish/species/crab_brown/crab_brown.tres
Normal file
27
fish/species/crab_brown/crab_brown.tres
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"]
|
||||||
|
[ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"]
|
||||||
|
[ext_resource type="Texture2D" path="res://fish/species/crab_brown/crab_brown.png" id="3_texture"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_fish_data")
|
||||||
|
id = &"crab_brown"
|
||||||
|
display_name = "brown crab"
|
||||||
|
active = true
|
||||||
|
catalog_number = 3906
|
||||||
|
collection_group = &"Marine crabs"
|
||||||
|
logbook_fact = "brown crabs hide among rocks and sand along sheltered coasts. broad claws help them feed on shellfish and other small shore animals."
|
||||||
|
collection_method = 1
|
||||||
|
logbook_section = 1
|
||||||
|
habitat_label = "dry beach sand"
|
||||||
|
allowed_water_types = 2
|
||||||
|
rarity = 0
|
||||||
|
base_catch_weight = 1.0
|
||||||
|
catch_profile = ExtResource("2_profile")
|
||||||
|
weight_min_lb = 0.5
|
||||||
|
weight_max_lb = 3.0
|
||||||
|
sell_value_min = 5
|
||||||
|
sell_value_max = 15
|
||||||
|
sell_value_curve = 1.0
|
||||||
|
display_texture = ExtResource("3_texture")
|
||||||
|
|
@ -17,6 +17,9 @@ active = false
|
||||||
catalog_number = 3903
|
catalog_number = 3903
|
||||||
collection_group = &"Marine crabs"
|
collection_group = &"Marine crabs"
|
||||||
logbook_fact = "dungeness crabs bury beneath sand with only their eyes and antennae exposed. broad toothed shells protect them along cool pacific coasts."
|
logbook_fact = "dungeness crabs bury beneath sand with only their eyes and antennae exposed. broad toothed shells protect them along cool pacific coasts."
|
||||||
|
collection_method = 1
|
||||||
|
logbook_section = 1
|
||||||
|
habitat_label = "shallow coastal sand"
|
||||||
allowed_water_types = 2
|
allowed_water_types = 2
|
||||||
rarity = 1
|
rarity = 1
|
||||||
base_catch_weight = 0.65
|
base_catch_weight = 0.65
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,9 @@ active = false
|
||||||
catalog_number = 3901
|
catalog_number = 3901
|
||||||
collection_group = &"Marine crabs"
|
collection_group = &"Marine crabs"
|
||||||
logbook_fact = "atlantic ghost crabs dash across beaches on long pale legs and retreat into deep burrows. raised eyes scan nearly every direction."
|
logbook_fact = "atlantic ghost crabs dash across beaches on long pale legs and retreat into deep burrows. raised eyes scan nearly every direction."
|
||||||
|
collection_method = 1
|
||||||
|
logbook_section = 1
|
||||||
|
habitat_label = "dry beach sand"
|
||||||
allowed_water_types = 2
|
allowed_water_types = 2
|
||||||
rarity = 2
|
rarity = 2
|
||||||
base_catch_weight = 0.28
|
base_catch_weight = 0.28
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,7 @@ const FishingSurfaceResolverType = preload(
|
||||||
"res://fishing/fishing_surface_resolver.gd"
|
"res://fishing/fishing_surface_resolver.gd"
|
||||||
)
|
)
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||||
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
const WorldTimeServiceType = preload("res://world/world_time_service.gd")
|
||||||
const WorldWeatherServiceType = preload(
|
const WorldWeatherServiceType = preload(
|
||||||
"res://world/world_weather_service.gd"
|
"res://world/world_weather_service.gd"
|
||||||
|
|
@ -504,6 +505,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
return
|
return
|
||||||
if not event.is_action("fish_primary"):
|
if not event.is_action("fish_primary"):
|
||||||
return
|
return
|
||||||
|
var selected_item: ItemDataType = _get_active_item()
|
||||||
|
if (
|
||||||
|
state == FishingState.READY
|
||||||
|
and selected_item != null
|
||||||
|
and selected_item.item_id == FishingShopStockType.CRAB_NET_ID
|
||||||
|
):
|
||||||
|
return
|
||||||
if event.is_pressed():
|
if event.is_pressed():
|
||||||
match state:
|
match state:
|
||||||
FishingState.READY:
|
FishingState.READY:
|
||||||
|
|
@ -737,6 +745,36 @@ func refresh_active_item_status() -> void:
|
||||||
status_changed.emit("")
|
status_changed.emit("")
|
||||||
|
|
||||||
|
|
||||||
|
func report_external_status(message: String) -> void:
|
||||||
|
status_changed.emit(message)
|
||||||
|
|
||||||
|
|
||||||
|
func present_external_catch(fish_catch: FishCatchType) -> bool:
|
||||||
|
if (
|
||||||
|
fish_catch == null
|
||||||
|
or not fish_catch.is_valid()
|
||||||
|
or state != FishingState.READY
|
||||||
|
or _local_player == null
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_active_player = _local_player
|
||||||
|
_pending_catch = fish_catch
|
||||||
|
_active_player.set_movement_enabled(false)
|
||||||
|
state = FishingState.SHOWING_CATCH
|
||||||
|
_showcase_ready = true
|
||||||
|
_showcase_outcome_completed = true
|
||||||
|
_put_away_press_armed = false
|
||||||
|
_active_player.begin_catch_showcase(fish_catch)
|
||||||
|
showcase_changed.emit(
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.fish.get_rarity_name(),
|
||||||
|
fish_catch.weight_lb,
|
||||||
|
fish_catch.quality,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _update_cast_charge(delta: float) -> void:
|
func _update_cast_charge(delta: float) -> void:
|
||||||
if state != FishingState.AIMING_CAST:
|
if state != FishingState.AIMING_CAST:
|
||||||
return
|
return
|
||||||
|
|
|
||||||
13
gathering/catalog/crab_blue.tres
Normal file
13
gathering/catalog/crab_blue.tres
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
|
||||||
|
[ext_resource type="Resource" path="res://fish/species/crab_blue/crab_blue.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"crab_blue"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"crab_net"
|
||||||
|
surface_materials = Array[StringName]([&"sand"])
|
||||||
|
minimum_surface_y = -0.8
|
||||||
|
population = 4
|
||||||
26
gathering/catalog/crab_brown.tres
Normal file
26
gathering/catalog/crab_brown.tres
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
|
||||||
|
[ext_resource type="Resource" path="res://fish/species/crab_brown/crab_brown.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"crab_brown"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"crab_net"
|
||||||
|
surface_materials = Array[StringName]([&"sand"])
|
||||||
|
minimum_surface_y = 0.08
|
||||||
|
population = 2
|
||||||
|
movement_speed = 0.35
|
||||||
|
roam_radius = 3.5
|
||||||
|
scare_radius = 2.8
|
||||||
|
capture_radius = 0.7
|
||||||
|
interaction_range = 2.6
|
||||||
|
charge_duration = 2.0
|
||||||
|
sprite_pixel_size = 0.001
|
||||||
|
sprite_tilt_degrees = -45.0
|
||||||
|
capture_respawn_min_seconds = 480.0
|
||||||
|
capture_respawn_max_seconds = 720.0
|
||||||
|
scare_respawn_min_seconds = 45.0
|
||||||
|
scare_respawn_max_seconds = 90.0
|
||||||
|
minimum_respawn_spacing_seconds = 180.0
|
||||||
13
gathering/catalog/crab_dungeness.tres
Normal file
13
gathering/catalog/crab_dungeness.tres
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
|
||||||
|
[ext_resource type="Resource" path="res://fish/species/crab_dungeness/crab_dungeness.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"crab_dungeness"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"crab_net"
|
||||||
|
surface_materials = Array[StringName]([&"sand"])
|
||||||
|
minimum_surface_y = -0.5
|
||||||
|
population = 4
|
||||||
13
gathering/catalog/crab_ghost.tres
Normal file
13
gathering/catalog/crab_ghost.tres
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
[gd_resource type="Resource" script_class="GatherableData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://gathering/gatherable_data.gd" id="1_data"]
|
||||||
|
[ext_resource type="Resource" path="res://fish/species/crab_ghost/crab_ghost.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"crab_ghost"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"crab_net"
|
||||||
|
surface_materials = Array[StringName]([&"sand"])
|
||||||
|
minimum_surface_y = 0.08
|
||||||
|
population = 4
|
||||||
11
gathering/catalog/gatherable_catalog.tres
Normal file
11
gathering/catalog/gatherable_catalog.tres
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
[gd_resource type="Resource" script_class="GatherableCatalog" load_steps=6 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://gathering/gatherable_catalog.gd" id="1_catalog"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/crab_brown.tres" id="2_brown"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/crab_ghost.tres" id="3_ghost"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/crab_blue.tres" id="4_blue"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/crab_dungeness.tres" id="5_dungeness"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_catalog")
|
||||||
|
entries = [ExtResource("2_brown"), ExtResource("3_ghost"), ExtResource("4_blue"), ExtResource("5_dungeness")]
|
||||||
23
gathering/gatherable_catalog.gd
Normal file
23
gathering/gatherable_catalog.gd
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
class_name GatherableCatalog
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const GatherableDataType = preload("res://gathering/gatherable_data.gd")
|
||||||
|
|
||||||
|
@export var entries: Array[GatherableDataType] = []
|
||||||
|
|
||||||
|
|
||||||
|
func get_entry(type_id: StringName) -> GatherableDataType:
|
||||||
|
if type_id.is_empty():
|
||||||
|
return null
|
||||||
|
for entry: GatherableDataType in entries:
|
||||||
|
if entry != null and entry.type_id == type_id:
|
||||||
|
return entry
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func get_available_entries() -> Array[GatherableDataType]:
|
||||||
|
var available: Array[GatherableDataType] = []
|
||||||
|
for entry: GatherableDataType in entries:
|
||||||
|
if entry != null and entry.is_available():
|
||||||
|
available.append(entry)
|
||||||
|
return available
|
||||||
1
gathering/gatherable_catalog.gd.uid
Normal file
1
gathering/gatherable_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bpsncwh7qlfk1
|
||||||
61
gathering/gatherable_data.gd
Normal file
61
gathering/gatherable_data.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
class_name GatherableData
|
||||||
|
extends Resource
|
||||||
|
|
||||||
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
|
||||||
|
@export var type_id: StringName
|
||||||
|
@export var catch_data: FishDataType
|
||||||
|
@export var required_tool_id: StringName
|
||||||
|
@export var surface_materials: Array[StringName] = []
|
||||||
|
@export_range(-100.0, 100.0, 0.01) var minimum_surface_y: float = 0.08
|
||||||
|
@export_range(0, 64, 1) var population: int = 0
|
||||||
|
@export_range(0.0, 10.0, 0.05) var movement_speed: float = 0.35
|
||||||
|
@export_range(0.1, 20.0, 0.1) var roam_radius: float = 3.5
|
||||||
|
@export_range(0.1, 20.0, 0.1) var scare_radius: float = 2.8
|
||||||
|
@export_range(0.1, 5.0, 0.05) var capture_radius: float = 0.7
|
||||||
|
@export_range(0.1, 10.0, 0.05) var interaction_range: float = 2.6
|
||||||
|
@export_range(0.1, 10.0, 0.05) var charge_duration: float = 2.0
|
||||||
|
@export_range(0.1, 5.0, 0.01) var sprite_pixel_size: float = 0.001
|
||||||
|
@export_range(-90.0, 90.0, 1.0) var sprite_tilt_degrees: float = -45.0
|
||||||
|
@export_category("Respawn Budget")
|
||||||
|
@export_range(0.0, 3600.0, 1.0) var capture_respawn_min_seconds: float = 480.0
|
||||||
|
@export_range(0.0, 3600.0, 1.0) var capture_respawn_max_seconds: float = 720.0
|
||||||
|
@export_range(0.0, 3600.0, 1.0) var scare_respawn_min_seconds: float = 45.0
|
||||||
|
@export_range(0.0, 3600.0, 1.0) var scare_respawn_max_seconds: float = 90.0
|
||||||
|
@export_range(0.0, 3600.0, 1.0) var minimum_respawn_spacing_seconds: float = 180.0
|
||||||
|
|
||||||
|
|
||||||
|
func is_valid() -> bool:
|
||||||
|
return (
|
||||||
|
not type_id.is_empty()
|
||||||
|
and catch_data != null
|
||||||
|
and catch_data.is_valid_catalog_entry()
|
||||||
|
and catch_data.collection_method != FishDataType.CollectionMethod.FISHING
|
||||||
|
and not required_tool_id.is_empty()
|
||||||
|
and not surface_materials.is_empty()
|
||||||
|
and population > 0
|
||||||
|
and movement_speed >= 0.0
|
||||||
|
and roam_radius > 0.0
|
||||||
|
and scare_radius > 0.0
|
||||||
|
and capture_radius > 0.0
|
||||||
|
and interaction_range > 0.0
|
||||||
|
and charge_duration > 0.0
|
||||||
|
and sprite_pixel_size > 0.0
|
||||||
|
and capture_respawn_max_seconds >= capture_respawn_min_seconds
|
||||||
|
and scare_respawn_max_seconds >= scare_respawn_min_seconds
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_respawn_delay(reason: StringName, rng: RandomNumberGenerator) -> float:
|
||||||
|
var minimum_seconds: float = capture_respawn_min_seconds
|
||||||
|
var maximum_seconds: float = capture_respawn_max_seconds
|
||||||
|
if reason == &"scared":
|
||||||
|
minimum_seconds = scare_respawn_min_seconds
|
||||||
|
maximum_seconds = scare_respawn_max_seconds
|
||||||
|
if rng == null or maximum_seconds <= minimum_seconds:
|
||||||
|
return minimum_seconds
|
||||||
|
return rng.randf_range(minimum_seconds, maximum_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
func is_available() -> bool:
|
||||||
|
return catch_data != null and catch_data.active and is_valid()
|
||||||
1
gathering/gatherable_data.gd.uid
Normal file
1
gathering/gatherable_data.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://doheo0308illx
|
||||||
247
gathering/gathering_controller.gd
Normal file
247
gathering/gathering_controller.gd
Normal file
|
|
@ -0,0 +1,247 @@
|
||||||
|
class_name GatheringController
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
|
const FishingSpotType = preload("res://fishing/fishing_spot.gd")
|
||||||
|
const NetworkWorldSpawnServiceType = preload(
|
||||||
|
"res://network/network_world_spawn_service.gd"
|
||||||
|
)
|
||||||
|
|
||||||
|
signal status_changed(message: String)
|
||||||
|
|
||||||
|
@export_range(0.5, 5.0, 0.05) var marker_distance: float = 1.7
|
||||||
|
@export_range(0.1, 2.0, 0.05) var marker_radius: float = 0.7
|
||||||
|
@export_flags_3d_physics var terrain_collision_mask: int = 1
|
||||||
|
@export_range(0.5, 20.0, 0.5) var ray_height: float = 5.0
|
||||||
|
@export_range(0.5, 20.0, 0.5) var ray_depth: float = 8.0
|
||||||
|
|
||||||
|
var _player: Player
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _hotbar: PlayerHotbar
|
||||||
|
var _fishing_spot: FishingSpotType
|
||||||
|
var _service: NetworkWorldSpawnServiceType
|
||||||
|
var _marker: MeshInstance3D
|
||||||
|
var _marker_invalid_material: StandardMaterial3D
|
||||||
|
var _marker_valid_material: StandardMaterial3D
|
||||||
|
var _marker_position: Vector3
|
||||||
|
var _marker_has_surface: bool = false
|
||||||
|
var _target_entity_id: String = ""
|
||||||
|
var _charging: bool = false
|
||||||
|
var _charge_elapsed: float = 0.0
|
||||||
|
var _charge_duration: float = 2.0
|
||||||
|
var _request_id: String = ""
|
||||||
|
var _gameplay_input_enabled: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_build_marker()
|
||||||
|
set_process_unhandled_input(true)
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
player: Player,
|
||||||
|
bag: PlayerBag,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
fishing_spot: FishingSpotType,
|
||||||
|
service: NetworkWorldSpawnServiceType,
|
||||||
|
) -> void:
|
||||||
|
_player = player
|
||||||
|
_bag = bag
|
||||||
|
_hotbar = hotbar
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_service = service
|
||||||
|
_service.local_interaction_finished.connect(
|
||||||
|
_on_interaction_finished
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_gameplay_input_enabled(enabled: bool) -> void:
|
||||||
|
_gameplay_input_enabled = enabled
|
||||||
|
if not enabled:
|
||||||
|
_cancel_charge()
|
||||||
|
|
||||||
|
|
||||||
|
func is_net_selected() -> bool:
|
||||||
|
return (
|
||||||
|
_bag != null
|
||||||
|
and _hotbar != null
|
||||||
|
and _hotbar.get_selected_item_id()
|
||||||
|
== FishingShopStockType.CRAB_NET_ID
|
||||||
|
and _bag.owns_item(FishingShopStockType.CRAB_NET_ID)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
var input_available: bool = (
|
||||||
|
_gameplay_input_enabled
|
||||||
|
and is_net_selected()
|
||||||
|
and _player != null
|
||||||
|
and _player.is_local_control_enabled()
|
||||||
|
and _fishing_spot != null
|
||||||
|
and _fishing_spot.can_change_hotbar_selection()
|
||||||
|
)
|
||||||
|
if not input_available:
|
||||||
|
if _charging:
|
||||||
|
_cancel_charge()
|
||||||
|
_set_marker_visible(false)
|
||||||
|
return
|
||||||
|
if not _charging:
|
||||||
|
_set_marker_visible(false)
|
||||||
|
return
|
||||||
|
_charge_elapsed += delta
|
||||||
|
_update_marker_target()
|
||||||
|
_update_target_entity()
|
||||||
|
var target_entry: GatherableData = _service.get_entry_for_entity(
|
||||||
|
_target_entity_id
|
||||||
|
)
|
||||||
|
if target_entry != null:
|
||||||
|
_charge_duration = target_entry.charge_duration
|
||||||
|
var fully_charged: bool = _charge_elapsed >= _charge_duration
|
||||||
|
var valid_target: bool = (
|
||||||
|
fully_charged
|
||||||
|
and not _target_entity_id.is_empty()
|
||||||
|
and _player.is_sneaking()
|
||||||
|
)
|
||||||
|
_marker.material_override = (
|
||||||
|
_marker_valid_material if valid_target else _marker_invalid_material
|
||||||
|
)
|
||||||
|
_set_marker_visible(_marker_has_surface)
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not event.is_action("fish_primary"):
|
||||||
|
return
|
||||||
|
if event.is_pressed():
|
||||||
|
if (
|
||||||
|
_charging
|
||||||
|
or not _gameplay_input_enabled
|
||||||
|
or not is_net_selected()
|
||||||
|
or _fishing_spot == null
|
||||||
|
or not _fishing_spot.can_change_hotbar_selection()
|
||||||
|
):
|
||||||
|
return
|
||||||
|
_request_id = _service.begin_local_interaction()
|
||||||
|
if _request_id.is_empty():
|
||||||
|
return
|
||||||
|
_charging = true
|
||||||
|
_charge_elapsed = 0.0
|
||||||
|
_charge_duration = maxf(
|
||||||
|
_service.get_charge_duration_for_tool(
|
||||||
|
FishingShopStockType.CRAB_NET_ID
|
||||||
|
),
|
||||||
|
0.1,
|
||||||
|
)
|
||||||
|
_update_marker_target()
|
||||||
|
_update_target_entity()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
|
if not _charging:
|
||||||
|
return
|
||||||
|
var fully_charged: bool = _charge_elapsed >= _charge_duration
|
||||||
|
if (
|
||||||
|
fully_charged
|
||||||
|
and _marker_has_surface
|
||||||
|
and not _target_entity_id.is_empty()
|
||||||
|
and _player.is_sneaking()
|
||||||
|
):
|
||||||
|
_service.finish_local_interaction(
|
||||||
|
_request_id,
|
||||||
|
_target_entity_id,
|
||||||
|
_marker_position,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_service.cancel_local_interaction(_request_id)
|
||||||
|
status_changed.emit(
|
||||||
|
"Sneak close, pull the net all the way back, and line up the marker."
|
||||||
|
)
|
||||||
|
_reset_charge_state()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_marker_target() -> void:
|
||||||
|
_marker_has_surface = false
|
||||||
|
if _player == null or get_world_3d() == null:
|
||||||
|
return
|
||||||
|
var facing: Vector3 = _player.get_facing_direction()
|
||||||
|
facing.y = 0.0
|
||||||
|
if facing.is_zero_approx():
|
||||||
|
facing = -_player.global_basis.z
|
||||||
|
facing.y = 0.0
|
||||||
|
if facing.is_zero_approx():
|
||||||
|
return
|
||||||
|
facing = facing.normalized()
|
||||||
|
var query_center: Vector3 = _player.global_position + facing * marker_distance
|
||||||
|
var query := PhysicsRayQueryParameters3D.create(
|
||||||
|
query_center + Vector3.UP * ray_height,
|
||||||
|
query_center - Vector3.UP * ray_depth,
|
||||||
|
terrain_collision_mask,
|
||||||
|
)
|
||||||
|
query.collide_with_areas = false
|
||||||
|
query.collide_with_bodies = true
|
||||||
|
var hit: Dictionary = get_world_3d().direct_space_state.intersect_ray(query)
|
||||||
|
if hit.is_empty():
|
||||||
|
return
|
||||||
|
var position: Variant = hit.get("position")
|
||||||
|
if typeof(position) != TYPE_VECTOR3:
|
||||||
|
return
|
||||||
|
_marker_position = (position as Vector3) + Vector3.UP * 0.045
|
||||||
|
_marker.global_position = _marker_position
|
||||||
|
_marker_has_surface = true
|
||||||
|
|
||||||
|
|
||||||
|
func _update_target_entity() -> void:
|
||||||
|
_target_entity_id = (
|
||||||
|
_service.find_capture_target(
|
||||||
|
_marker_position,
|
||||||
|
FishingShopStockType.CRAB_NET_ID,
|
||||||
|
)
|
||||||
|
if _marker_has_surface and _service != null
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _cancel_charge() -> void:
|
||||||
|
if _charging and not _request_id.is_empty() and _service != null:
|
||||||
|
_service.cancel_local_interaction(_request_id)
|
||||||
|
_reset_charge_state()
|
||||||
|
|
||||||
|
|
||||||
|
func _reset_charge_state() -> void:
|
||||||
|
_charging = false
|
||||||
|
_charge_elapsed = 0.0
|
||||||
|
_request_id = ""
|
||||||
|
_target_entity_id = ""
|
||||||
|
_marker_has_surface = false
|
||||||
|
_set_marker_visible(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_interaction_finished(accepted: bool, message: String) -> void:
|
||||||
|
_reset_charge_state()
|
||||||
|
if not accepted or not message.is_empty():
|
||||||
|
status_changed.emit(message)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_marker() -> void:
|
||||||
|
_marker_invalid_material = StandardMaterial3D.new()
|
||||||
|
_marker_invalid_material.albedo_color = Color(0.86, 0.24, 0.19, 0.9)
|
||||||
|
_marker_invalid_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
_marker_valid_material = StandardMaterial3D.new()
|
||||||
|
_marker_valid_material.albedo_color = Color(0.25, 0.9, 0.36, 0.95)
|
||||||
|
_marker_valid_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
var ring := TorusMesh.new()
|
||||||
|
ring.inner_radius = marker_radius * 0.78
|
||||||
|
ring.outer_radius = marker_radius
|
||||||
|
ring.rings = 16
|
||||||
|
ring.ring_segments = 24
|
||||||
|
_marker = MeshInstance3D.new()
|
||||||
|
_marker.name = "GatheringTargetMarker"
|
||||||
|
_marker.mesh = ring
|
||||||
|
_marker.material_override = _marker_invalid_material
|
||||||
|
_marker.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
_marker.visible = false
|
||||||
|
add_child(_marker)
|
||||||
|
|
||||||
|
|
||||||
|
func _set_marker_visible(value: bool) -> void:
|
||||||
|
if _marker != null:
|
||||||
|
_marker.visible = value
|
||||||
1
gathering/gathering_controller.gd.uid
Normal file
1
gathering/gathering_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cec7nwk815d8a
|
||||||
113
gathering/world_gatherable.gd
Normal file
113
gathering/world_gatherable.gd
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
class_name WorldGatherable
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
const GatherableDataType = preload("res://gathering/gatherable_data.gd")
|
||||||
|
|
||||||
|
var entity_id: String = ""
|
||||||
|
var type_id: StringName
|
||||||
|
var data: GatherableDataType
|
||||||
|
var _sprite: Sprite3D
|
||||||
|
var _target_position: Vector3
|
||||||
|
var _target_yaw: float = 0.0
|
||||||
|
var _has_state: bool = false
|
||||||
|
var _despawning: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
configured_entity_id: String,
|
||||||
|
configured_data: GatherableDataType,
|
||||||
|
position: Vector3,
|
||||||
|
yaw: float,
|
||||||
|
) -> void:
|
||||||
|
entity_id = configured_entity_id
|
||||||
|
data = configured_data
|
||||||
|
type_id = data.type_id if data != null else StringName()
|
||||||
|
_ensure_visual()
|
||||||
|
if data != null and data.catch_data != null:
|
||||||
|
_sprite.texture = data.catch_data.display_texture
|
||||||
|
_sprite.pixel_size = data.sprite_pixel_size
|
||||||
|
_sprite.rotation_degrees.x = data.sprite_tilt_degrees
|
||||||
|
apply_network_state(position, yaw, true)
|
||||||
|
|
||||||
|
|
||||||
|
func apply_network_state(
|
||||||
|
position: Vector3,
|
||||||
|
yaw: float,
|
||||||
|
immediate: bool = false,
|
||||||
|
) -> void:
|
||||||
|
if _despawning or not position.is_finite() or not is_finite(yaw):
|
||||||
|
return
|
||||||
|
_target_position = position
|
||||||
|
_target_yaw = yaw
|
||||||
|
if immediate or not _has_state:
|
||||||
|
global_position = position
|
||||||
|
rotation.y = yaw
|
||||||
|
_has_state = true
|
||||||
|
|
||||||
|
|
||||||
|
func play_despawn(with_dust: bool) -> void:
|
||||||
|
if _despawning:
|
||||||
|
return
|
||||||
|
_despawning = true
|
||||||
|
if with_dust:
|
||||||
|
_emit_dust()
|
||||||
|
var tween: Tween = create_tween()
|
||||||
|
tween.set_parallel(true)
|
||||||
|
tween.set_trans(Tween.TRANS_QUAD)
|
||||||
|
tween.set_ease(Tween.EASE_IN)
|
||||||
|
if _sprite != null:
|
||||||
|
tween.tween_property(_sprite, "position:y", -0.3, 0.3)
|
||||||
|
tween.tween_property(_sprite, "scale", Vector3(0.75, 0.75, 0.75), 0.3)
|
||||||
|
tween.chain().tween_callback(queue_free)
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
if not _has_state or _despawning:
|
||||||
|
return
|
||||||
|
global_position = global_position.lerp(
|
||||||
|
_target_position,
|
||||||
|
1.0 - exp(-10.0 * delta),
|
||||||
|
)
|
||||||
|
rotation.y = lerp_angle(
|
||||||
|
rotation.y,
|
||||||
|
_target_yaw,
|
||||||
|
1.0 - exp(-8.0 * delta),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_visual() -> void:
|
||||||
|
if _sprite != null:
|
||||||
|
return
|
||||||
|
_sprite = Sprite3D.new()
|
||||||
|
_sprite.name = "GatherableSprite"
|
||||||
|
_sprite.position.y = 0.22
|
||||||
|
_sprite.shaded = false
|
||||||
|
_sprite.double_sided = true
|
||||||
|
_sprite.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||||
|
add_child(_sprite)
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_dust() -> void:
|
||||||
|
var particles := CPUParticles3D.new()
|
||||||
|
particles.name = "DustPoof"
|
||||||
|
particles.amount = 9
|
||||||
|
particles.lifetime = 0.55
|
||||||
|
particles.one_shot = true
|
||||||
|
particles.explosiveness = 1.0
|
||||||
|
particles.direction = Vector3.UP
|
||||||
|
particles.spread = 58.0
|
||||||
|
particles.gravity = Vector3(0.0, -2.4, 0.0)
|
||||||
|
particles.initial_velocity_min = 0.65
|
||||||
|
particles.initial_velocity_max = 1.25
|
||||||
|
particles.scale_amount_min = 0.7
|
||||||
|
particles.scale_amount_max = 1.35
|
||||||
|
var dust_material := StandardMaterial3D.new()
|
||||||
|
dust_material.albedo_color = Color(0.58, 0.46, 0.31, 0.85)
|
||||||
|
dust_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||||
|
dust_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
var dust_mesh := BoxMesh.new()
|
||||||
|
dust_mesh.size = Vector3(0.1, 0.1, 0.1)
|
||||||
|
dust_mesh.material = dust_material
|
||||||
|
particles.mesh = dust_mesh
|
||||||
|
add_child(particles)
|
||||||
|
particles.emitting = true
|
||||||
1
gathering/world_gatherable.gd.uid
Normal file
1
gathering/world_gatherable.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://3t8ygawpkgwj
|
||||||
16
items/catalog/crab_net.tres
Normal file
16
items/catalog/crab_net.tres
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
[gd_resource type="Resource" script_class="ItemData" load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_item")
|
||||||
|
item_id = &"crab_net"
|
||||||
|
display_name = "crab net"
|
||||||
|
description = "a hand net for catching crabs and other small shore animals."
|
||||||
|
active = true
|
||||||
|
category = 1
|
||||||
|
stackable = false
|
||||||
|
max_stack = 1
|
||||||
|
usable = false
|
||||||
|
equippable = true
|
||||||
|
hotbar_allowed = true
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=38 format=3]
|
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=39 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
|
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
|
||||||
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
|
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
|
||||||
|
|
@ -37,7 +37,8 @@
|
||||||
[ext_resource type="Resource" path="res://items/catalog/rods/oddity_string_rod.tres" id="36_oddity"]
|
[ext_resource type="Resource" path="res://items/catalog/rods/oddity_string_rod.tres" id="36_oddity"]
|
||||||
[ext_resource type="Resource" path="res://items/catalog/rods/aurora_rod.tres" id="37_aurora"]
|
[ext_resource type="Resource" path="res://items/catalog/rods/aurora_rod.tres" id="37_aurora"]
|
||||||
[ext_resource type="Resource" path="res://items/catalog/magnet.tres" id="38_magnet"]
|
[ext_resource type="Resource" path="res://items/catalog/magnet.tres" id="38_magnet"]
|
||||||
|
[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_script")
|
script = ExtResource("1_script")
|
||||||
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet")]
|
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net")]
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ static func generate_daily_jobs(
|
||||||
))
|
))
|
||||||
var species: Array[FishDataType] = []
|
var species: Array[FishDataType] = []
|
||||||
for fish: FishDataType in candidates:
|
for fish: FishDataType in candidates:
|
||||||
if fish != null and fish.is_selectable():
|
if fish != null and fish.is_fishable():
|
||||||
species.append(fish)
|
species.append(fish)
|
||||||
if not species.is_empty():
|
if not species.is_empty():
|
||||||
species.sort_custom(
|
species.sort_custom(
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,7 @@ func _generate_host_board(cycle: int) -> void:
|
||||||
var candidates: Array[FishDataType] = []
|
var candidates: Array[FishDataType] = []
|
||||||
if _catalog != null:
|
if _catalog != null:
|
||||||
for fish: FishDataType in _catalog.candidates:
|
for fish: FishDataType in _catalog.candidates:
|
||||||
if fish != null and fish.is_selectable():
|
if fish != null and fish.is_fishable():
|
||||||
candidates.append(fish)
|
candidates.append(fish)
|
||||||
var schedule_anchor_index: int = _current_weather_segment()
|
var schedule_anchor_index: int = _current_weather_segment()
|
||||||
var allow_weather_jobs: bool = (
|
var allow_weather_jobs: bool = (
|
||||||
|
|
@ -688,7 +688,7 @@ func _registered_species_count() -> int:
|
||||||
var count: int = 0
|
var count: int = 0
|
||||||
if _catalog != null:
|
if _catalog != null:
|
||||||
for fish: FishDataType in _catalog.candidates:
|
for fish: FishDataType in _catalog.candidates:
|
||||||
if fish != null and fish.is_selectable():
|
if fish != null and fish.is_fishable():
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
||||||
|
|
@ -727,7 +727,7 @@ func _matches_canonical_board(board: Dictionary) -> bool:
|
||||||
var candidates: Array[FishDataType] = []
|
var candidates: Array[FishDataType] = []
|
||||||
if _catalog != null:
|
if _catalog != null:
|
||||||
for fish: FishDataType in _catalog.candidates:
|
for fish: FishDataType in _catalog.candidates:
|
||||||
if fish != null and fish.is_selectable():
|
if fish != null and fish.is_fishable():
|
||||||
candidates.append(fish)
|
candidates.append(fish)
|
||||||
var anchor_index: int = int(board.get("schedule_anchor_index", -1))
|
var anchor_index: int = int(board.get("schedule_anchor_index", -1))
|
||||||
var allow_weather_jobs: bool = (
|
var allow_weather_jobs: bool = (
|
||||||
|
|
|
||||||
53
main/main.gd
53
main/main.gd
|
|
@ -25,6 +25,7 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||||
const ItemDataType = preload("res://items/item_data.gd")
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||||
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
||||||
const FishingShopInteractionType = preload(
|
const FishingShopInteractionType = preload(
|
||||||
"res://world/fishing_shop_interaction.gd"
|
"res://world/fishing_shop_interaction.gd"
|
||||||
|
|
@ -100,6 +101,15 @@ const NetworkWorldWeatherServiceType = preload(
|
||||||
const RainAmbienceType = preload("res://world/rain_ambience.gd")
|
const RainAmbienceType = preload("res://world/rain_ambience.gd")
|
||||||
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
||||||
const NetworkJobServiceType = preload("res://network/network_job_service.gd")
|
const NetworkJobServiceType = preload("res://network/network_job_service.gd")
|
||||||
|
const GatherableCatalogType = preload(
|
||||||
|
"res://gathering/gatherable_catalog.gd"
|
||||||
|
)
|
||||||
|
const GatheringControllerType = preload(
|
||||||
|
"res://gathering/gathering_controller.gd"
|
||||||
|
)
|
||||||
|
const NetworkWorldSpawnServiceType = preload(
|
||||||
|
"res://network/network_world_spawn_service.gd"
|
||||||
|
)
|
||||||
|
|
||||||
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
|
const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
|
||||||
|
|
@ -111,6 +121,7 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
@export var pelican_buyer_profile: FishBuyerProfileType
|
@export var pelican_buyer_profile: FishBuyerProfileType
|
||||||
@export var main_shop_buyer_profile: FishBuyerProfileType
|
@export var main_shop_buyer_profile: FishBuyerProfileType
|
||||||
@export var item_catalog: ItemCatalogType
|
@export var item_catalog: ItemCatalogType
|
||||||
|
@export var gatherable_catalog: GatherableCatalogType
|
||||||
@export_category("Title Music")
|
@export_category("Title Music")
|
||||||
@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -6.0
|
@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -6.0
|
||||||
@export_range(0.0, 10.0, 0.05) var title_music_fade_out_seconds: float = 5.0
|
@export_range(0.0, 10.0, 0.05) var title_music_fade_out_seconds: float = 5.0
|
||||||
|
|
@ -150,6 +161,12 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
)
|
)
|
||||||
@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService
|
@onready var _player_jobs: PlayerJobServiceType = %PlayerJobService
|
||||||
@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService
|
@onready var _network_jobs: NetworkJobServiceType = %NetworkJobService
|
||||||
|
@onready var _network_world_spawns: NetworkWorldSpawnServiceType = (
|
||||||
|
%NetworkWorldSpawnService
|
||||||
|
)
|
||||||
|
@onready var _gathering_controller: GatheringControllerType = (
|
||||||
|
%GatheringController
|
||||||
|
)
|
||||||
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
|
@onready var _data_root: PlayerDataRoot = %PlayerDataRoot
|
||||||
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
|
@onready var _identity_backups: IdentityBackupService = %IdentityBackupService
|
||||||
@onready var _network_profile: NetworkProfilePreferencesType = (
|
@onready var _network_profile: NetworkProfilePreferencesType = (
|
||||||
|
|
@ -191,6 +208,7 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
)
|
)
|
||||||
@onready var _players_root: Node3D = $Players
|
@onready var _players_root: Node3D = $Players
|
||||||
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
|
@onready var _surface_drawings_root: Node3D = $SurfaceDrawings
|
||||||
|
@onready var _world_gatherables_root: Node3D = $WorldGatherables
|
||||||
@onready var _title_background: ColorRect = %TitleBackground
|
@onready var _title_background: ColorRect = %TitleBackground
|
||||||
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
@onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop
|
||||||
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
|
@onready var _shop_backdrop: ColorRect = %ShopBackdrop
|
||||||
|
|
@ -542,6 +560,33 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_save_manager,
|
_save_manager,
|
||||||
_asset_reservations
|
_asset_reservations
|
||||||
)
|
)
|
||||||
|
_network_world_spawns.setup(
|
||||||
|
_network_session,
|
||||||
|
_player_spawn_service,
|
||||||
|
_test_world,
|
||||||
|
_world_gatherables_root,
|
||||||
|
gatherable_catalog,
|
||||||
|
fish_catalog,
|
||||||
|
_player.inventory,
|
||||||
|
_player.collection_log,
|
||||||
|
_player.cooler_capacity,
|
||||||
|
_player.experience,
|
||||||
|
_save_manager,
|
||||||
|
_network_item_use,
|
||||||
|
)
|
||||||
|
_gathering_controller.setup(
|
||||||
|
_player,
|
||||||
|
_player.bag,
|
||||||
|
_player.hotbar,
|
||||||
|
_fishing_spot,
|
||||||
|
_network_world_spawns,
|
||||||
|
)
|
||||||
|
_network_world_spawns.local_capture_received.connect(
|
||||||
|
_fishing_spot.present_external_catch
|
||||||
|
)
|
||||||
|
_gathering_controller.status_changed.connect(
|
||||||
|
_fishing_spot.report_external_status
|
||||||
|
)
|
||||||
_network_fish_showcase.setup(
|
_network_fish_showcase.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -1326,6 +1371,7 @@ func _set_gameplay_active(active: bool) -> void:
|
||||||
_player.set_camera_input_enabled(active)
|
_player.set_camera_input_enabled(active)
|
||||||
_player.set_camera_active(active)
|
_player.set_camera_active(active)
|
||||||
_fishing_spot.set_gameplay_input_enabled(active)
|
_fishing_spot.set_gameplay_input_enabled(active)
|
||||||
|
_gathering_controller.set_gameplay_input_enabled(active)
|
||||||
_water_recovery.set_recovery_enabled(active)
|
_water_recovery.set_recovery_enabled(active)
|
||||||
_game_ui.set_gameplay_ui_enabled(active)
|
_game_ui.set_gameplay_ui_enabled(active)
|
||||||
_save_manager.set_autosave_enabled(active)
|
_save_manager.set_autosave_enabled(active)
|
||||||
|
|
@ -1775,6 +1821,12 @@ func _on_active_hotbar_item_changed(
|
||||||
and item.is_available()
|
and item.is_available()
|
||||||
and _player.bag.owns_item(item_id)
|
and _player.bag.owns_item(item_id)
|
||||||
)
|
)
|
||||||
|
var active_is_catching_net: bool = (
|
||||||
|
item_id == FishingShopStockType.CRAB_NET_ID
|
||||||
|
and item != null
|
||||||
|
and item.is_available()
|
||||||
|
and _player.bag.owns_item(item_id)
|
||||||
|
)
|
||||||
_player.set_active_fishing_rod(
|
_player.set_active_fishing_rod(
|
||||||
item as FishingRodDataType if active_is_rod else null,
|
item as FishingRodDataType if active_is_rod else null,
|
||||||
true,
|
true,
|
||||||
|
|
@ -1783,6 +1835,7 @@ func _on_active_hotbar_item_changed(
|
||||||
item.icon if item != null else null,
|
item.icon if item != null else null,
|
||||||
active_is_art_kit,
|
active_is_art_kit,
|
||||||
)
|
)
|
||||||
|
_player.set_active_catching_net(active_is_catching_net)
|
||||||
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
|
_game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit)
|
||||||
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
||||||
item != null and _player.bag.owns_item(item_id)
|
item != null and _player.bag.owns_item(item_id)
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,9 @@
|
||||||
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
|
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
|
||||||
[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
|
[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
|
||||||
[ext_resource type="AudioStream" uid="uid://c4y8puv0n6xk8" path="res://audio/music/world/craft.mp3" id="dusk_music"]
|
[ext_resource type="AudioStream" uid="uid://c4y8puv0n6xk8" path="res://audio/music/world/craft.mp3" id="dusk_music"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
|
||||||
|
[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
|
||||||
|
|
||||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
|
||||||
shader = ExtResource("40_title_water")
|
shader = ExtResource("40_title_water")
|
||||||
|
|
@ -142,6 +145,7 @@ fish_catalog = ExtResource("6_pool")
|
||||||
pelican_buyer_profile = ExtResource("7_pelicans")
|
pelican_buyer_profile = ExtResource("7_pelicans")
|
||||||
main_shop_buyer_profile = ExtResource("12_main_shop")
|
main_shop_buyer_profile = ExtResource("12_main_shop")
|
||||||
item_catalog = ExtResource("11_items")
|
item_catalog = ExtResource("11_items")
|
||||||
|
gatherable_catalog = ExtResource("58_gatherable_catalog")
|
||||||
|
|
||||||
[node name="TitleBackgroundLayer" type="CanvasLayer" parent="." unique_id=1690754702]
|
[node name="TitleBackgroundLayer" type="CanvasLayer" parent="." unique_id=1690754702]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
@ -283,6 +287,10 @@ script = ExtResource("24_network_item")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("45_fish_showcase")
|
script = ExtResource("45_fish_showcase")
|
||||||
|
|
||||||
|
[node name="NetworkWorldSpawnService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("59_world_spawns")
|
||||||
|
|
||||||
[node name="NetworkSurfaceDrawingService" type="Node" parent="." unique_id=80122665]
|
[node name="NetworkSurfaceDrawingService" type="Node" parent="." unique_id=80122665]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("46_surface_drawing")
|
script = ExtResource("46_surface_drawing")
|
||||||
|
|
@ -378,6 +386,12 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081)
|
||||||
|
|
||||||
[node name="SurfaceDrawings" type="Node3D" parent="." unique_id=234671307]
|
[node name="SurfaceDrawings" type="Node3D" parent="." unique_id=234671307]
|
||||||
|
|
||||||
|
[node name="WorldGatherables" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="GatheringController" type="Node3D" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("60_gathering_controller")
|
||||||
|
|
||||||
[node name="FishingSpot" parent="." unique_id=1461328864 instance=ExtResource("4_spot")]
|
[node name="FishingSpot" parent="." unique_id=1461328864 instance=ExtResource("4_spot")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.02, -18)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.02, -18)
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ extends Node
|
||||||
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||||
|
|
||||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||||
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
|
|
||||||
const MAX_LEDGER_ENTRIES: int = 64
|
const MAX_LEDGER_ENTRIES: int = 64
|
||||||
|
|
||||||
|
|
@ -292,6 +293,13 @@ func get_effects_for_peer(peer_id: int) -> PlayerItemEffects:
|
||||||
return avatar.item_effects if avatar != null else null
|
return avatar.item_effects if avatar != null else null
|
||||||
|
|
||||||
|
|
||||||
|
func get_equipped_item_id(peer_id: int) -> StringName:
|
||||||
|
var state: Dictionary = _equipped_states.get(peer_id, {})
|
||||||
|
if state.is_empty() or not bool(state.get("owns_item", false)):
|
||||||
|
return StringName()
|
||||||
|
return StringName(str(state.get("item_id", "")))
|
||||||
|
|
||||||
|
|
||||||
func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
|
func submit_local_equipped(item_id: StringName, owns_item: bool) -> void:
|
||||||
if _session == null or not _session.is_gameplay_session_active():
|
if _session == null or not _session.is_gameplay_session_active():
|
||||||
return
|
return
|
||||||
|
|
@ -377,6 +385,10 @@ func _apply_equipped(data: Dictionary) -> void:
|
||||||
item.icon if item != null else null,
|
item.icon if item != null else null,
|
||||||
item_id == ArtShopStockType.ART_KIT_ITEM_ID and bool(data["owns_item"]),
|
item_id == ArtShopStockType.ART_KIT_ITEM_ID and bool(data["owns_item"]),
|
||||||
)
|
)
|
||||||
|
avatar.set_active_catching_net(
|
||||||
|
item_id == FishingShopStockType.CRAB_NET_ID
|
||||||
|
and bool(data["owns_item"]),
|
||||||
|
)
|
||||||
equipped_state_changed.emit(
|
equipped_state_changed.emit(
|
||||||
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
class_name NetworkProtocol
|
class_name NetworkProtocol
|
||||||
extends RefCounted
|
extends RefCounted
|
||||||
|
|
||||||
const PROTOCOL_VERSION: int = 4
|
const PROTOCOL_VERSION: int = 5
|
||||||
const GAME_BUILD: String = "prealpha"
|
const GAME_BUILD: String = "prealpha"
|
||||||
const MAX_GAME_VERSION_LENGTH: int = 64
|
const MAX_GAME_VERSION_LENGTH: int = 64
|
||||||
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
const MAX_DISPLAY_NAME_LENGTH: int = 24
|
||||||
|
|
@ -25,6 +25,7 @@ const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
||||||
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
const WORLD_WEATHER_CAPABILITY: String = "world_weather_v1"
|
||||||
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
const FISH_QUALITY_CAPABILITY: String = "fish_quality_v1"
|
||||||
const JOBS_CAPABILITY: String = "jobs_v1"
|
const JOBS_CAPABILITY: String = "jobs_v1"
|
||||||
|
const WORLD_SPAWN_CAPABILITY: String = "world_spawn_envelope_v1"
|
||||||
|
|
||||||
enum RejectionCode {
|
enum RejectionCode {
|
||||||
NONE,
|
NONE,
|
||||||
|
|
@ -172,6 +173,7 @@ static func make_client_hello(
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
JOBS_CAPABILITY,
|
||||||
|
WORLD_SPAWN_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
"cosmetic_snapshot": cosmetic_snapshot,
|
"cosmetic_snapshot": cosmetic_snapshot,
|
||||||
"identity_fingerprint": identity_fingerprint,
|
"identity_fingerprint": identity_fingerprint,
|
||||||
|
|
@ -313,6 +315,7 @@ static func make_server_hello(
|
||||||
WORLD_TIME_CAPABILITY,
|
WORLD_TIME_CAPABILITY,
|
||||||
WORLD_WEATHER_CAPABILITY,
|
WORLD_WEATHER_CAPABILITY,
|
||||||
JOBS_CAPABILITY,
|
JOBS_CAPABILITY,
|
||||||
|
WORLD_SPAWN_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
|
|
@ -267,6 +267,7 @@ func _register_player_host() -> void:
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
NetworkProtocol.JOBS_CAPABILITY,
|
NetworkProtocol.JOBS_CAPABILITY,
|
||||||
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||||
|
|
@ -555,6 +556,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
NetworkProtocol.WORLD_TIME_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||||
"chat_v1",
|
"chat_v1",
|
||||||
"mail_v1",
|
"mail_v1",
|
||||||
"profile_v1",
|
"profile_v1",
|
||||||
|
|
|
||||||
94
network/network_world_spawn_protocol.gd
Normal file
94
network/network_world_spawn_protocol.gd
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
class_name NetworkWorldSpawnProtocol
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const CAPABILITY: StringName = &"world_spawn_envelope_v1"
|
||||||
|
const ENVELOPE_VERSION: int = 1
|
||||||
|
const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
|
||||||
|
const SNAPSHOT_CHANNEL: int = 4
|
||||||
|
const MAX_SESSION_ID_LENGTH: int = 96
|
||||||
|
const MAX_EVENT_ID_LENGTH: int = 48
|
||||||
|
const MAX_ENTITY_ID_LENGTH: int = 128
|
||||||
|
const MAX_TYPE_ID_LENGTH: int = 96
|
||||||
|
const MAX_ENTITIES_PER_SNAPSHOT: int = 128
|
||||||
|
## Keeps unreliable movement envelopes below ENet's practical MTU while
|
||||||
|
## allowing reliable population envelopes to carry a larger initial state.
|
||||||
|
const SNAPSHOT_ENTITIES_PER_ENVELOPE: int = 4
|
||||||
|
|
||||||
|
|
||||||
|
static func make_envelope(
|
||||||
|
session_id: String,
|
||||||
|
sequence: int,
|
||||||
|
event_id: StringName,
|
||||||
|
payload: Dictionary,
|
||||||
|
) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"envelope_version": ENVELOPE_VERSION,
|
||||||
|
"session_id": session_id,
|
||||||
|
"sequence": sequence,
|
||||||
|
"event_id": str(event_id),
|
||||||
|
"payload": payload.duplicate(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_envelope(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var data: Dictionary = value
|
||||||
|
return (
|
||||||
|
typeof(data.get("envelope_version")) == TYPE_INT
|
||||||
|
and int(data["envelope_version"]) == ENVELOPE_VERSION
|
||||||
|
and typeof(data.get("session_id")) == TYPE_STRING
|
||||||
|
and not str(data["session_id"]).is_empty()
|
||||||
|
and str(data["session_id"]).length() <= MAX_SESSION_ID_LENGTH
|
||||||
|
and typeof(data.get("sequence")) == TYPE_INT
|
||||||
|
and int(data["sequence"]) >= 0
|
||||||
|
and typeof(data.get("event_id")) == TYPE_STRING
|
||||||
|
and not str(data["event_id"]).is_empty()
|
||||||
|
and str(data["event_id"]).length() <= MAX_EVENT_ID_LENGTH
|
||||||
|
and typeof(data.get("payload")) == TYPE_DICTIONARY
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func validate_entity_state(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var data: Dictionary = value
|
||||||
|
var position: Variant = data.get("position")
|
||||||
|
if typeof(position) != TYPE_ARRAY or position.size() != 3:
|
||||||
|
return false
|
||||||
|
for component: Variant in position:
|
||||||
|
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
|
||||||
|
return false
|
||||||
|
if not is_finite(float(component)):
|
||||||
|
return false
|
||||||
|
return (
|
||||||
|
typeof(data.get("entity_id")) == TYPE_STRING
|
||||||
|
and not str(data["entity_id"]).is_empty()
|
||||||
|
and str(data["entity_id"]).length() <= MAX_ENTITY_ID_LENGTH
|
||||||
|
and typeof(data.get("type_id")) == TYPE_STRING
|
||||||
|
and not str(data["type_id"]).is_empty()
|
||||||
|
and str(data["type_id"]).length() <= MAX_TYPE_ID_LENGTH
|
||||||
|
and typeof(data.get("yaw")) in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
and is_finite(float(data["yaw"]))
|
||||||
|
and typeof(data.get("revision")) == TYPE_INT
|
||||||
|
and int(data["revision"]) >= 0
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func vector3_to_array(value: Vector3) -> Array[float]:
|
||||||
|
return [value.x, value.y, value.z]
|
||||||
|
|
||||||
|
|
||||||
|
static func array_to_vector3(value: Variant) -> Vector3:
|
||||||
|
if typeof(value) != TYPE_ARRAY or value.size() != 3:
|
||||||
|
return Vector3(INF, INF, INF)
|
||||||
|
for component: Variant in value:
|
||||||
|
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
|
||||||
|
return Vector3(INF, INF, INF)
|
||||||
|
if not is_finite(float(component)):
|
||||||
|
return Vector3(INF, INF, INF)
|
||||||
|
return _array_to_vector3(value)
|
||||||
|
|
||||||
|
|
||||||
|
static func _array_to_vector3(value: Array) -> Vector3:
|
||||||
|
return Vector3(float(value[0]), float(value[1]), float(value[2]))
|
||||||
1
network/network_world_spawn_protocol.gd.uid
Normal file
1
network/network_world_spawn_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://wstcwpwk41mf
|
||||||
1126
network/network_world_spawn_service.gd
Normal file
1126
network/network_world_spawn_service.gd
Normal file
File diff suppressed because it is too large
Load diff
1
network/network_world_spawn_service.gd.uid
Normal file
1
network/network_world_spawn_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://bmuhkkl2cxsck
|
||||||
55
player/net_attachment.tscn
Normal file
55
player/net_attachment.tscn
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
[gd_scene load_steps=7 format=3]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="HandleMaterial"]
|
||||||
|
albedo_color = Color(0.25, 0.13, 0.055, 1)
|
||||||
|
roughness = 0.9
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="HandleMesh"]
|
||||||
|
material = SubResource("HandleMaterial")
|
||||||
|
top_radius = 0.025
|
||||||
|
bottom_radius = 0.035
|
||||||
|
height = 1.25
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="HoopMaterial"]
|
||||||
|
albedo_color = Color(0.34, 0.39, 0.4, 1)
|
||||||
|
metallic = 0.25
|
||||||
|
roughness = 0.65
|
||||||
|
|
||||||
|
[sub_resource type="TorusMesh" id="HoopMesh"]
|
||||||
|
material = SubResource("HoopMaterial")
|
||||||
|
inner_radius = 0.24
|
||||||
|
outer_radius = 0.29
|
||||||
|
rings = 12
|
||||||
|
ring_segments = 18
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="NetMaterial"]
|
||||||
|
transparency = 1
|
||||||
|
albedo_color = Color(0.65, 0.78, 0.75, 0.42)
|
||||||
|
shading_mode = 0
|
||||||
|
cull_mode = 2
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="NetMesh"]
|
||||||
|
material = SubResource("NetMaterial")
|
||||||
|
top_radius = 0.24
|
||||||
|
bottom_radius = 0.08
|
||||||
|
height = 0.34
|
||||||
|
radial_segments = 12
|
||||||
|
rings = 1
|
||||||
|
|
||||||
|
[node name="NetAttachment" type="BoneAttachment3D"]
|
||||||
|
bone_name = "rod_socket"
|
||||||
|
|
||||||
|
[node name="CatchingNet" type="Node3D" parent="."]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Handle" type="MeshInstance3D" parent="CatchingNet"]
|
||||||
|
position = Vector3(0, 0.55, 0)
|
||||||
|
mesh = SubResource("HandleMesh")
|
||||||
|
|
||||||
|
[node name="Hoop" type="MeshInstance3D" parent="CatchingNet"]
|
||||||
|
position = Vector3(0, 1.17, 0)
|
||||||
|
mesh = SubResource("HoopMesh")
|
||||||
|
|
||||||
|
[node name="Bag" type="MeshInstance3D" parent="CatchingNet"]
|
||||||
|
position = Vector3(0, 1.0, 0)
|
||||||
|
mesh = SubResource("NetMesh")
|
||||||
|
|
@ -31,6 +31,7 @@ const ControllerMappingManagerType = preload(
|
||||||
const FishingRodAttachmentScene = preload(
|
const FishingRodAttachmentScene = preload(
|
||||||
"res://player/fishing_rod_attachment.tscn"
|
"res://player/fishing_rod_attachment.tscn"
|
||||||
)
|
)
|
||||||
|
const NetAttachmentScene = preload("res://player/net_attachment.tscn")
|
||||||
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
|
||||||
const HeldItemAttachmentScene = preload(
|
const HeldItemAttachmentScene = preload(
|
||||||
"res://player/held_item_attachment.tscn"
|
"res://player/held_item_attachment.tscn"
|
||||||
|
|
@ -382,6 +383,7 @@ var _remote_recovery_visual_origin: Vector3
|
||||||
var _remote_recovery_elapsed: float = 0.0
|
var _remote_recovery_elapsed: float = 0.0
|
||||||
var _target_zoom: float = 5.0
|
var _target_zoom: float = 5.0
|
||||||
var _showcase_rod_visibility: bool = true
|
var _showcase_rod_visibility: bool = true
|
||||||
|
var _showcase_net_visibility: bool = false
|
||||||
var _remote_presentation_visible := true
|
var _remote_presentation_visible := true
|
||||||
var _showcase_rod_state_stored: bool = false
|
var _showcase_rod_state_stored: bool = false
|
||||||
var _showcase_visual_rotation: Vector3
|
var _showcase_visual_rotation: Vector3
|
||||||
|
|
@ -445,12 +447,14 @@ var _fishing_visual_phase: FishingVisualPhase = FishingVisualPhase.NONE
|
||||||
var _fishing_after_release_pending: bool = false
|
var _fishing_after_release_pending: bool = false
|
||||||
var _retract_animation_completed: bool = true
|
var _retract_animation_completed: bool = true
|
||||||
var _fishing_rod: Node3D
|
var _fishing_rod: Node3D
|
||||||
|
var _catching_net: Node3D
|
||||||
var _fishing_rod_tip: Marker3D
|
var _fishing_rod_tip: Marker3D
|
||||||
var _fishing_rod_model_mount: Node3D
|
var _fishing_rod_model_mount: Node3D
|
||||||
var _fishing_rod_fallback_visual: GeometryInstance3D
|
var _fishing_rod_fallback_visual: GeometryInstance3D
|
||||||
var _custom_fishing_rod_visual: Node3D
|
var _custom_fishing_rod_visual: Node3D
|
||||||
var _active_fishing_rod_id: StringName
|
var _active_fishing_rod_id: StringName
|
||||||
var _active_item_is_rod: bool = false
|
var _active_item_is_rod: bool = false
|
||||||
|
var _active_item_is_net: bool = false
|
||||||
var _controller_mapping_manager: ControllerMappingManagerType
|
var _controller_mapping_manager: ControllerMappingManagerType
|
||||||
var _sprint_dust_landing_ready: bool = false
|
var _sprint_dust_landing_ready: bool = false
|
||||||
var _sprint_dust_airborne: bool = false
|
var _sprint_dust_airborne: bool = false
|
||||||
|
|
@ -475,6 +479,7 @@ func _ready() -> void:
|
||||||
bag.contents_changed.connect(_on_bag_contents_changed)
|
bag.contents_changed.connect(_on_bag_contents_changed)
|
||||||
_apply_presented_appearance()
|
_apply_presented_appearance()
|
||||||
_initialize_fishing_rod()
|
_initialize_fishing_rod()
|
||||||
|
_initialize_catching_net()
|
||||||
_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
|
_target_zoom = clampf(_spring_arm.spring_length, minimum_zoom, maximum_zoom)
|
||||||
_spring_arm.spring_length = _target_zoom
|
_spring_arm.spring_length = _target_zoom
|
||||||
_camera.current = local_control_enabled
|
_camera.current = local_control_enabled
|
||||||
|
|
@ -590,6 +595,23 @@ func _initialize_fishing_rod() -> void:
|
||||||
) as GeometryInstance3D
|
) as GeometryInstance3D
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_catching_net() -> void:
|
||||||
|
var skeleton := get_node_or_null(
|
||||||
|
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||||
|
) as Skeleton3D
|
||||||
|
if skeleton == null:
|
||||||
|
push_error("Player character skeleton is unavailable for the net.")
|
||||||
|
return
|
||||||
|
var attachment := NetAttachmentScene.instantiate() as BoneAttachment3D
|
||||||
|
if attachment == null:
|
||||||
|
push_error("Catching net attachment could not be instantiated.")
|
||||||
|
return
|
||||||
|
skeleton.add_child(attachment)
|
||||||
|
_catching_net = attachment.get_node("CatchingNet") as Node3D
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _initialize_held_item_attachment() -> void:
|
func _initialize_held_item_attachment() -> void:
|
||||||
var skeleton := get_node_or_null(
|
var skeleton := get_node_or_null(
|
||||||
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
"Visuals/CharacterRig/CharacterRig/Skeleton3D"
|
||||||
|
|
@ -1765,6 +1787,19 @@ func is_local_control_enabled() -> bool:
|
||||||
return local_control_enabled
|
return local_control_enabled
|
||||||
|
|
||||||
|
|
||||||
|
func is_sneaking() -> bool:
|
||||||
|
if local_control_enabled:
|
||||||
|
return (
|
||||||
|
_is_movement_input_enabled()
|
||||||
|
and Input.is_action_pressed("sneak")
|
||||||
|
)
|
||||||
|
return _network_sneak
|
||||||
|
|
||||||
|
|
||||||
|
func is_moving_horizontally() -> bool:
|
||||||
|
return Vector2(velocity.x, velocity.z).length_squared() > 0.01
|
||||||
|
|
||||||
|
|
||||||
func set_movement_enabled(enabled: bool) -> void:
|
func set_movement_enabled(enabled: bool) -> void:
|
||||||
_movement_enabled = enabled
|
_movement_enabled = enabled
|
||||||
if not enabled:
|
if not enabled:
|
||||||
|
|
@ -1921,6 +1956,18 @@ func set_active_fishing_rod(
|
||||||
set_active_item_is_rod(rod != null and rod.is_available(), animate_transition)
|
set_active_item_is_rod(rod != null and rod.is_available(), animate_transition)
|
||||||
|
|
||||||
|
|
||||||
|
func set_active_catching_net(should_show: bool) -> void:
|
||||||
|
_active_item_is_net = should_show
|
||||||
|
if _catching_net == null:
|
||||||
|
return
|
||||||
|
if _showcase_rod_state_stored:
|
||||||
|
_showcase_net_visibility = should_show
|
||||||
|
return
|
||||||
|
_catching_net.visible = (
|
||||||
|
should_show and not _has_held_show_item() and not _showcase_animation_active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _apply_fishing_rod_model(rod: FishingRodDataType) -> void:
|
func _apply_fishing_rod_model(rod: FishingRodDataType) -> void:
|
||||||
var next_id: StringName = rod.item_id if rod != null else StringName()
|
var next_id: StringName = rod.item_id if rod != null else StringName()
|
||||||
if next_id == _active_fishing_rod_id:
|
if next_id == _active_fishing_rod_id:
|
||||||
|
|
@ -1983,6 +2030,12 @@ func _apply_active_rod_visibility() -> void:
|
||||||
_showcase_rod_visibility = _active_item_is_rod
|
_showcase_rod_visibility = _active_item_is_rod
|
||||||
return
|
return
|
||||||
_fishing_rod.visible = _active_item_is_rod
|
_fishing_rod.visible = _active_item_is_rod
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = (
|
||||||
|
_active_item_is_net
|
||||||
|
and not _has_held_show_item()
|
||||||
|
and not _showcase_animation_active
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
||||||
|
|
@ -2011,6 +2064,8 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
||||||
if not previous_show_pose:
|
if not previous_show_pose:
|
||||||
_held_art_kit_display.visible = false
|
_held_art_kit_display.visible = false
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
if item_changed:
|
if item_changed:
|
||||||
_begin_pocket_visual(
|
_begin_pocket_visual(
|
||||||
PocketVisualTarget.ART_KIT,
|
PocketVisualTarget.ART_KIT,
|
||||||
|
|
@ -2059,6 +2114,8 @@ func set_held_fish(
|
||||||
if not previous_show_pose:
|
if not previous_show_pose:
|
||||||
_held_fish_display.visible = false
|
_held_fish_display.visible = false
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
if item_changed:
|
if item_changed:
|
||||||
_begin_pocket_visual(
|
_begin_pocket_visual(
|
||||||
PocketVisualTarget.HELD_FISH,
|
PocketVisualTarget.HELD_FISH,
|
||||||
|
|
@ -2283,8 +2340,13 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void:
|
||||||
_turn_showcase_toward_position(showcase_camera_position)
|
_turn_showcase_toward_position(showcase_camera_position)
|
||||||
if not _showcase_rod_state_stored:
|
if not _showcase_rod_state_stored:
|
||||||
_showcase_rod_visibility = _fishing_rod.visible
|
_showcase_rod_visibility = _fishing_rod.visible
|
||||||
|
_showcase_net_visibility = (
|
||||||
|
_catching_net.visible if _catching_net != null else false
|
||||||
|
)
|
||||||
_showcase_rod_state_stored = true
|
_showcase_rod_state_stored = true
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
_catch_sprite.texture = fish_catch.fish.display_texture
|
_catch_sprite.texture = fish_catch.fish.display_texture
|
||||||
_catch_display.scale = (
|
_catch_display.scale = (
|
||||||
Vector3.ONE
|
Vector3.ONE
|
||||||
|
|
@ -2302,8 +2364,13 @@ func begin_remote_catch_showcase(fish_catch: FishCatchType) -> void:
|
||||||
_showcase_animation_active = true
|
_showcase_animation_active = true
|
||||||
set_fishing_visual(false)
|
set_fishing_visual(false)
|
||||||
_showcase_rod_visibility = _fishing_rod.visible
|
_showcase_rod_visibility = _fishing_rod.visible
|
||||||
|
_showcase_net_visibility = (
|
||||||
|
_catching_net.visible if _catching_net != null else false
|
||||||
|
)
|
||||||
_showcase_rod_state_stored = true
|
_showcase_rod_state_stored = true
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = false
|
||||||
_catch_sprite.texture = fish_catch.fish.display_texture
|
_catch_sprite.texture = fish_catch.fish.display_texture
|
||||||
_catch_display.scale = (
|
_catch_display.scale = (
|
||||||
Vector3.ONE
|
Vector3.ONE
|
||||||
|
|
@ -2343,7 +2410,10 @@ func end_catch_showcase(
|
||||||
_catch_sprite.texture = null
|
_catch_sprite.texture = null
|
||||||
if _showcase_rod_state_stored:
|
if _showcase_rod_state_stored:
|
||||||
_fishing_rod.visible = _showcase_rod_visibility
|
_fishing_rod.visible = _showcase_rod_visibility
|
||||||
|
if _catching_net != null:
|
||||||
|
_catching_net.visible = _showcase_net_visibility
|
||||||
_showcase_rod_visibility = true
|
_showcase_rod_visibility = true
|
||||||
|
_showcase_net_visibility = false
|
||||||
_showcase_rod_state_stored = false
|
_showcase_rod_state_stored = false
|
||||||
if (
|
if (
|
||||||
not _showcase_visual_rotation_stored
|
not _showcase_visual_rotation_stored
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ readonly -a QUICK_TESTS=(
|
||||||
"tests/terrain_blender_material_validation.gd"
|
"tests/terrain_blender_material_validation.gd"
|
||||||
"tests/texture_sampling_validation.gd"
|
"tests/texture_sampling_validation.gd"
|
||||||
"tests/world_time_validation.gd"
|
"tests/world_time_validation.gd"
|
||||||
|
"tests/world_spawn_protocol_validation.gd"
|
||||||
"tests/world_weather_validation.gd"
|
"tests/world_weather_validation.gd"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -41,6 +42,7 @@ readonly -a HOST_TESTS=(
|
||||||
"tests/economy_regression_validation.gd"
|
"tests/economy_regression_validation.gd"
|
||||||
"tests/fish_hotbar_showcase_validation.gd"
|
"tests/fish_hotbar_showcase_validation.gd"
|
||||||
"tests/fishing_authority_validation.gd"
|
"tests/fishing_authority_validation.gd"
|
||||||
|
"tests/gathering_showcase_validation.gd"
|
||||||
"tests/job_system_validation.gd"
|
"tests/job_system_validation.gd"
|
||||||
"tests/surface_drawing_runtime_validation.gd"
|
"tests/surface_drawing_runtime_validation.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -54,6 +56,7 @@ readonly -a NETWORK_TESTS=(
|
||||||
"tests/operator_multiplayer_validation.gd"
|
"tests/operator_multiplayer_validation.gd"
|
||||||
"tests/surface_drawing_multiplayer_validation.gd"
|
"tests/surface_drawing_multiplayer_validation.gd"
|
||||||
"tests/world_time_multiplayer_validation.gd"
|
"tests/world_time_multiplayer_validation.gd"
|
||||||
|
"tests/world_spawn_multiplayer_validation.gd"
|
||||||
)
|
)
|
||||||
|
|
||||||
cleanup() {
|
cleanup() {
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ func _run() -> void:
|
||||||
as PlayerAssetReservationService
|
as PlayerAssetReservationService
|
||||||
)
|
)
|
||||||
assert(player != null)
|
assert(player != null)
|
||||||
assert(catalog != null and catalog.candidates.size() == 313)
|
assert(catalog != null and catalog.candidates.size() == 314)
|
||||||
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 53)
|
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 54)
|
||||||
assert(sale_service != null)
|
assert(sale_service != null)
|
||||||
assert(shop_service != null)
|
assert(shop_service != null)
|
||||||
assert(session != null and session.is_host())
|
assert(session != null and session.is_host())
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ func _validate_weight_based_display_scale() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _validate_catalog_and_pools() -> void:
|
func _validate_catalog_and_pools() -> void:
|
||||||
assert(Catalog.candidates.size() == 313)
|
assert(Catalog.candidates.size() == 314)
|
||||||
assert(PondPool.candidates.size() == 19)
|
assert(PondPool.candidates.size() == 19)
|
||||||
assert(OceanPool.candidates.size() == 34)
|
assert(OceanPool.candidates.size() == 34)
|
||||||
var active_count: int = 0
|
var active_count: int = 0
|
||||||
|
|
@ -142,7 +142,7 @@ func _validate_catalog_and_pools() -> void:
|
||||||
inactive_count += 1
|
inactive_count += 1
|
||||||
assert(not fish.is_selectable())
|
assert(not fish.is_selectable())
|
||||||
assert(fish.display_texture == null)
|
assert(fish.display_texture == null)
|
||||||
assert(active_count == 53)
|
assert(active_count == 54)
|
||||||
assert(inactive_count == 260)
|
assert(inactive_count == 260)
|
||||||
var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"bowfin")
|
var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"bowfin")
|
||||||
assert(inactive_fish != null and not inactive_fish.active)
|
assert(inactive_fish != null and not inactive_fish.active)
|
||||||
|
|
@ -173,14 +173,20 @@ func _validate_catalog_and_pools() -> void:
|
||||||
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
||||||
assert(fresh_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
assert(fresh_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
||||||
assert(not fresh_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
assert(not fresh_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
||||||
assert(LogbookCatalog.category_for(fresh_fish) == WaterType.Type.FRESH_WATER)
|
assert(
|
||||||
|
LogbookCatalog.category_for(fresh_fish)
|
||||||
|
== LogbookCatalog.Category.FRESH_WATER
|
||||||
|
)
|
||||||
for fish_id: StringName in SALT_WATER_IDS:
|
for fish_id: StringName in SALT_WATER_IDS:
|
||||||
var salt_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
var salt_fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||||
assert(OceanPool.get_fish_by_id(fish_id) == salt_fish)
|
assert(OceanPool.get_fish_by_id(fish_id) == salt_fish)
|
||||||
assert(PondPool.get_fish_by_id(fish_id) == null)
|
assert(PondPool.get_fish_by_id(fish_id) == null)
|
||||||
assert(salt_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
assert(salt_fish.is_allowed_in_water(WaterType.Type.SALT_WATER))
|
||||||
assert(not salt_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
assert(not salt_fish.is_allowed_in_water(WaterType.Type.FRESH_WATER))
|
||||||
assert(LogbookCatalog.category_for(salt_fish) == WaterType.Type.SALT_WATER)
|
assert(
|
||||||
|
LogbookCatalog.category_for(salt_fish)
|
||||||
|
== LogbookCatalog.Category.SALT_WATER
|
||||||
|
)
|
||||||
for fish_id: StringName in CATFISH_IDS:
|
for fish_id: StringName in CATFISH_IDS:
|
||||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||||
assert(fish != null)
|
assert(fish != null)
|
||||||
|
|
@ -188,13 +194,19 @@ func _validate_catalog_and_pools() -> void:
|
||||||
assert(PondPool.get_fish_by_id(fish_id) == fish)
|
assert(PondPool.get_fish_by_id(fish_id) == fish)
|
||||||
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
||||||
assert(fish.availability.allowed_location_tags == [&"starter_pond"])
|
assert(fish.availability.allowed_location_tags == [&"starter_pond"])
|
||||||
assert(LogbookCatalog.category_for(fish) == WaterType.Type.FRESH_WATER)
|
assert(
|
||||||
|
LogbookCatalog.category_for(fish)
|
||||||
|
== LogbookCatalog.Category.FRESH_WATER
|
||||||
|
)
|
||||||
for fish_id: StringName in NEW_FRESH_WATER_IDS:
|
for fish_id: StringName in NEW_FRESH_WATER_IDS:
|
||||||
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
var fish: FishDataType = Catalog.get_fish_by_id(fish_id)
|
||||||
assert(fish != null and fish.is_selectable())
|
assert(fish != null and fish.is_selectable())
|
||||||
assert(PondPool.get_fish_by_id(fish_id) == fish)
|
assert(PondPool.get_fish_by_id(fish_id) == fish)
|
||||||
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
assert(OceanPool.get_fish_by_id(fish_id) == null)
|
||||||
assert(LogbookCatalog.category_for(fish) == WaterType.Type.FRESH_WATER)
|
assert(
|
||||||
|
LogbookCatalog.category_for(fish)
|
||||||
|
== LogbookCatalog.Category.FRESH_WATER
|
||||||
|
)
|
||||||
|
|
||||||
var expected_values: Dictionary[StringName, Array] = {
|
var expected_values: Dictionary[StringName, Array] = {
|
||||||
&"catfish_blue": [1, 1.25, 3.0, 12.0, 6, 9],
|
&"catfish_blue": [1, 1.25, 3.0, 12.0, 6, 9],
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@ func _run() -> void:
|
||||||
var invalid_state: Dictionary = valid_state.duplicate(true)
|
var invalid_state: Dictionary = valid_state.duplicate(true)
|
||||||
invalid_state["display_scale"] = 1000.0
|
invalid_state["display_scale"] = 1000.0
|
||||||
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state))
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 4)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
assert(
|
assert(
|
||||||
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
NetworkProtocol.FISH_QUALITY_CAPABILITY
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ func _run() -> void:
|
||||||
_validate_mail_round_trip()
|
_validate_mail_round_trip()
|
||||||
_validate_collection_mastery()
|
_validate_collection_mastery()
|
||||||
_validate_version_four_migration()
|
_validate_version_four_migration()
|
||||||
assert(NetworkProtocol.PROTOCOL_VERSION == 4)
|
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
|
||||||
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
assert(NetworkProtocol.ENET_CHANNEL_COUNT == 10)
|
||||||
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
assert(NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1")
|
||||||
print("Fish quality validation: PASS")
|
print("Fish quality validation: PASS")
|
||||||
|
|
|
||||||
89
tests/gathering_showcase_validation.gd
Normal file
89
tests/gathering_showcase_validation.gd
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const TEST_PORT: int = 18142
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main: Node = MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
assert(session.start_private_host(TEST_PORT))
|
||||||
|
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||||
|
assert(save_manager.initialize_new_game())
|
||||||
|
main.call("_enter_gameplay")
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
|
||||||
|
var player := main.get("_player") as Player
|
||||||
|
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||||
|
var catalog := main.get("fish_catalog") as FishPool
|
||||||
|
assert(player != null and fishing_spot != null and catalog != null)
|
||||||
|
assert(player.bag.add_item(&"crab_net"))
|
||||||
|
assert(player.hotbar.assign_item(0, &"crab_net"))
|
||||||
|
assert(player.hotbar.select_slot(0))
|
||||||
|
await process_frame
|
||||||
|
assert(player.hotbar.get_selected_item_id() == &"crab_net")
|
||||||
|
assert(bool(player.get("_active_item_is_net")))
|
||||||
|
|
||||||
|
var crab: FishData = catalog.get_fish_by_id(&"crab_brown")
|
||||||
|
assert(crab != null and crab.is_selectable())
|
||||||
|
var crab_catch := FishCatch.new()
|
||||||
|
crab_catch.fish = crab
|
||||||
|
crab_catch.fish_id = crab.id
|
||||||
|
crab_catch.weight_lb = crab.get_minimum_weight()
|
||||||
|
crab_catch.display_scale = crab.get_display_scale_for_weight(
|
||||||
|
crab_catch.weight_lb
|
||||||
|
)
|
||||||
|
crab_catch.sale_value = crab.get_sale_value_for_weight(
|
||||||
|
crab_catch.weight_lb
|
||||||
|
)
|
||||||
|
crab_catch.ensure_identity()
|
||||||
|
player.inventory.add_catch(crab_catch)
|
||||||
|
player.collection_log.mark_discovered(crab.id)
|
||||||
|
|
||||||
|
assert(fishing_spot.present_external_catch(crab_catch))
|
||||||
|
assert(fishing_spot.state == FishingSpot.FishingState.SHOWING_CATCH)
|
||||||
|
assert(not player.is_movement_enabled())
|
||||||
|
Input.action_release("fish_primary")
|
||||||
|
await process_frame
|
||||||
|
assert(bool(fishing_spot.get("_put_away_press_armed")))
|
||||||
|
|
||||||
|
var pocket_event := InputEventAction.new()
|
||||||
|
pocket_event.action = &"fish_primary"
|
||||||
|
pocket_event.pressed = true
|
||||||
|
Input.parse_input_event(pocket_event)
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.get("_pending_catch") == null)
|
||||||
|
|
||||||
|
var pocket_deadline: int = Time.get_ticks_msec() + 6000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < pocket_deadline
|
||||||
|
and fishing_spot.state != FishingSpot.FishingState.READY
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(fishing_spot.state == FishingSpot.FishingState.READY)
|
||||||
|
assert(player.is_movement_enabled())
|
||||||
|
assert(player.inventory.contains_catch_id(crab_catch.catch_id))
|
||||||
|
assert(bool(player.get("_active_item_is_net")))
|
||||||
|
print("Gathering showcase validation: PASS")
|
||||||
|
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
1
tests/gathering_showcase_validation.gd.uid
Normal file
1
tests/gathering_showcase_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cedarlvsw4bpm
|
||||||
|
|
@ -108,9 +108,9 @@ func _validate_save_round_trip(
|
||||||
var player := main.get("_player") as Player
|
var player := main.get("_player") as Player
|
||||||
var catalog := main.get("fish_catalog") as FishPool
|
var catalog := main.get("fish_catalog") as FishPool
|
||||||
assert(catalog != null)
|
assert(catalog != null)
|
||||||
assert(catalog.candidates.size() == 313)
|
assert(catalog.candidates.size() == 314)
|
||||||
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
|
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
|
||||||
assert(active_species.size() == 53)
|
assert(active_species.size() == 54)
|
||||||
for index: int in 4:
|
for index: int in 4:
|
||||||
_add_test_catch(player, active_species[index])
|
_add_test_catch(player, active_species[index])
|
||||||
assert(save_manager.save_now())
|
assert(save_manager.save_now())
|
||||||
|
|
@ -131,7 +131,7 @@ func _validate_save_round_trip(
|
||||||
assert(player.inventory.replace_all_catches(no_catches, 1))
|
assert(player.inventory.replace_all_catches(no_catches, 1))
|
||||||
assert(player.collection_log.replace_discovered_ids(no_discoveries))
|
assert(player.collection_log.replace_discovered_ids(no_discoveries))
|
||||||
assert(save_manager.load_player_data())
|
assert(save_manager.load_player_data())
|
||||||
assert(player.inventory.get_all_catches().size() == 53)
|
assert(player.inventory.get_all_catches().size() == 54)
|
||||||
for fish: FishData in active_species:
|
for fish: FishData in active_species:
|
||||||
assert(player.inventory.get_count(fish.id) == 1)
|
assert(player.inventory.get_count(fish.id) == 1)
|
||||||
assert(player.collection_log.has_discovered(fish.id))
|
assert(player.collection_log.has_discovered(fish.id))
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,9 @@ func _run() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _validate_catalog() -> void:
|
func _validate_catalog() -> void:
|
||||||
assert(CatalogResource.candidates.size() == 313)
|
assert(CatalogResource.candidates.size() == 314)
|
||||||
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
|
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
|
||||||
assert(ordered.size() == 53)
|
assert(ordered.size() == 54)
|
||||||
var previous_number: int = 0
|
var previous_number: int = 0
|
||||||
var catalog_numbers: Dictionary[int, bool] = {}
|
var catalog_numbers: Dictionary[int, bool] = {}
|
||||||
for fish: FishDataType in CatalogResource.candidates:
|
for fish: FishDataType in CatalogResource.candidates:
|
||||||
|
|
@ -62,7 +62,7 @@ func _validate_catalog() -> void:
|
||||||
assert(LogbookCatalog.catalog_number(fish) == fish.catalog_number)
|
assert(LogbookCatalog.catalog_number(fish) == fish.catalog_number)
|
||||||
previous_number = fish.catalog_number
|
previous_number = fish.catalog_number
|
||||||
assert(
|
assert(
|
||||||
LogbookCatalog.empty_state(WaterType.Type.SALT_WATER)
|
LogbookCatalog.empty_state(LogbookCatalog.Category.SALT_WATER)
|
||||||
== "No saltwater catches cataloged yet."
|
== "No saltwater catches cataloged yet."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -78,7 +78,9 @@ func _validate_page() -> void:
|
||||||
page.setup(collection, inventory, CatalogResource)
|
page.setup(collection, inventory, CatalogResource)
|
||||||
page.activate()
|
page.activate()
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(page.get("_category") == WaterType.Type.FRESH_WATER)
|
assert(
|
||||||
|
page.get("_category") == LogbookCatalog.Category.FRESH_WATER
|
||||||
|
)
|
||||||
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
|
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
|
||||||
var initial_detail_body := page.get("_detail_body") as VBoxContainer
|
var initial_detail_body := page.get("_detail_body") as VBoxContainer
|
||||||
assert(not initial_detail_body.get_parent() is ScrollContainer)
|
assert(not initial_detail_body.get_parent() is ScrollContainer)
|
||||||
|
|
@ -89,14 +91,17 @@ func _validate_page() -> void:
|
||||||
|
|
||||||
var shared_material: Material
|
var shared_material: Material
|
||||||
var silhouette_count: int = 0
|
var silhouette_count: int = 0
|
||||||
for category: WaterType.Type in [
|
for category: LogbookCatalog.Category in [
|
||||||
WaterType.Type.FRESH_WATER,
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
WaterType.Type.SALT_WATER,
|
LogbookCatalog.Category.SALT_WATER,
|
||||||
]:
|
]:
|
||||||
page.call("_select_category", category)
|
page.call("_select_category", category)
|
||||||
await create_timer(0.25).timeout
|
await create_timer(0.25).timeout
|
||||||
var entries: Dictionary = page.get("_entry_buttons")
|
var entries: Dictionary = page.get("_entry_buttons")
|
||||||
assert(entries.size() == (19 if category == WaterType.Type.FRESH_WATER else 34))
|
assert(
|
||||||
|
entries.size()
|
||||||
|
== (19 if category == LogbookCatalog.Category.FRESH_WATER else 34)
|
||||||
|
)
|
||||||
for fish: FishDataType in LogbookCatalog.ordered_species(
|
for fish: FishDataType in LogbookCatalog.ordered_species(
|
||||||
CatalogResource.candidates
|
CatalogResource.candidates
|
||||||
):
|
):
|
||||||
|
|
@ -148,14 +153,20 @@ func _validate_page() -> void:
|
||||||
)
|
)
|
||||||
assert(silhouette_count == 53)
|
assert(silhouette_count == 53)
|
||||||
|
|
||||||
page.call("_select_category", WaterType.Type.OTHER)
|
page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
|
||||||
|
await create_timer(0.25).timeout
|
||||||
|
assert((page.get("_entry_buttons") as Dictionary).size() == 1)
|
||||||
|
assert(
|
||||||
|
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
|
||||||
|
)
|
||||||
|
page.call("_select_category", LogbookCatalog.Category.OTHER)
|
||||||
await create_timer(0.25).timeout
|
await create_timer(0.25).timeout
|
||||||
assert((page.get("_entry_buttons") as Dictionary).is_empty())
|
assert((page.get("_entry_buttons") as Dictionary).is_empty())
|
||||||
assert(
|
assert(
|
||||||
(page.get("_empty_state") as Label).text
|
(page.get("_empty_state") as Label).text
|
||||||
== "No entries available."
|
== "No entries available."
|
||||||
)
|
)
|
||||||
page.call("_select_category", WaterType.Type.FRESH_WATER)
|
page.call("_select_category", LogbookCatalog.Category.FRESH_WATER)
|
||||||
await create_timer(0.25).timeout
|
await create_timer(0.25).timeout
|
||||||
|
|
||||||
var bluegill = CatalogResource.get_fish_by_id(&"bluegill")
|
var bluegill = CatalogResource.get_fish_by_id(&"bluegill")
|
||||||
|
|
|
||||||
168
tests/world_spawn_multiplayer_validation.gd
Normal file
168
tests/world_spawn_multiplayer_validation.gd
Normal file
|
|
@ -0,0 +1,168 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
|
const TEST_PORT: int = 18141
|
||||||
|
const EXPECTED_POPULATION: int = 2
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var arguments: PackedStringArray = OS.get_cmdline_user_args()
|
||||||
|
if arguments.has("host"):
|
||||||
|
await _run_host()
|
||||||
|
return
|
||||||
|
if arguments.has("client"):
|
||||||
|
await _run_client()
|
||||||
|
return
|
||||||
|
push_error("World spawn multiplayer validation needs host or client mode.")
|
||||||
|
quit(1)
|
||||||
|
|
||||||
|
|
||||||
|
func _run_host() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var service: Node = main.get_node("%NetworkWorldSpawnService")
|
||||||
|
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
|
||||||
|
assert(session.set_host_open(true))
|
||||||
|
await _wait_for_population(service)
|
||||||
|
_validate_population(service)
|
||||||
|
|
||||||
|
var remote_peer_id: int = 0
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline and remote_peer_id == 0:
|
||||||
|
await process_frame
|
||||||
|
for peer_id: int in session.get_authenticated_peer_ids():
|
||||||
|
if peer_id != session.get_local_peer_id():
|
||||||
|
remote_peer_id = peer_id
|
||||||
|
break
|
||||||
|
assert(remote_peer_id > 1)
|
||||||
|
assert(session.peer_supports_capability(
|
||||||
|
remote_peer_id,
|
||||||
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||||
|
))
|
||||||
|
|
||||||
|
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
|
while (
|
||||||
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
and session.is_authenticated_peer(remote_peer_id)
|
||||||
|
):
|
||||||
|
await process_frame
|
||||||
|
assert(not session.is_authenticated_peer(remote_peer_id))
|
||||||
|
_validate_population(service)
|
||||||
|
_validate_respawn_budget(service)
|
||||||
|
print("World spawn multiplayer host validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
var main: Node = await _create_initialized_main()
|
||||||
|
main.call(
|
||||||
|
"_on_title_join_game_requested",
|
||||||
|
"127.0.0.1:%d" % TEST_PORT,
|
||||||
|
)
|
||||||
|
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||||
|
var service: Node = main.get_node("%NetworkWorldSpawnService")
|
||||||
|
var join_deadline: int = Time.get_ticks_msec() + 20000
|
||||||
|
while Time.get_ticks_msec() < join_deadline:
|
||||||
|
await process_frame
|
||||||
|
if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY:
|
||||||
|
main.call("_confirm_server_trust")
|
||||||
|
if session.is_joined_client() and bool(main.get("_gameplay_started")):
|
||||||
|
break
|
||||||
|
assert(session.is_joined_client())
|
||||||
|
assert(session.supports_server_capability(
|
||||||
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||||
|
))
|
||||||
|
await _wait_for_population(service)
|
||||||
|
_validate_population(service)
|
||||||
|
print("World spawn multiplayer client validation: PASS")
|
||||||
|
session.disconnect_session("")
|
||||||
|
main.queue_free()
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _wait_for_population(service: Node) -> void:
|
||||||
|
var deadline: int = Time.get_ticks_msec() + 12000
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
await process_frame
|
||||||
|
if (service.get("_entities") as Dictionary).size() == EXPECTED_POPULATION:
|
||||||
|
return
|
||||||
|
assert(false, "Timed out waiting for the world spawn population.")
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_population(service: Node) -> void:
|
||||||
|
var entities: Dictionary = service.get("_entities")
|
||||||
|
var presentations: Dictionary = service.get("_presentations")
|
||||||
|
assert(entities.size() == EXPECTED_POPULATION)
|
||||||
|
assert(presentations.size() == EXPECTED_POPULATION)
|
||||||
|
for state: Dictionary in entities.values():
|
||||||
|
assert(state.get("type_id") == &"crab_brown")
|
||||||
|
var position: Variant = state.get("position")
|
||||||
|
assert(typeof(position) == TYPE_VECTOR3)
|
||||||
|
assert((position as Vector3).is_finite())
|
||||||
|
assert((position as Vector3).y > 0.08)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_respawn_budget(service: Node) -> void:
|
||||||
|
var entities: Dictionary = service.get("_entities")
|
||||||
|
var entity_ids: Array = entities.keys()
|
||||||
|
assert(entity_ids.size() == EXPECTED_POPULATION)
|
||||||
|
for entity_id: Variant in entity_ids:
|
||||||
|
service.call(
|
||||||
|
"_despawn_entity",
|
||||||
|
str(entity_id),
|
||||||
|
&"captured",
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
assert((service.get("_entities") as Dictionary).is_empty())
|
||||||
|
|
||||||
|
var now: float = Time.get_ticks_msec() / 1000.0
|
||||||
|
var respawns: Array = service.get("_respawns")
|
||||||
|
assert(respawns.size() == EXPECTED_POPULATION)
|
||||||
|
for index: int in respawns.size():
|
||||||
|
var respawn: Dictionary = respawns[index]
|
||||||
|
var delay: float = float(respawn.get("due", 0.0)) - now
|
||||||
|
assert(delay >= 479.0 and delay <= 721.0)
|
||||||
|
respawn["due"] = now - 1.0
|
||||||
|
respawns[index] = respawn
|
||||||
|
|
||||||
|
service.call("_update_respawns")
|
||||||
|
assert((service.get("_entities") as Dictionary).size() == 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() == 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() == 2)
|
||||||
|
assert((service.get("_respawns") as Array).is_empty())
|
||||||
|
|
||||||
|
|
||||||
|
func _create_initialized_main() -> Node:
|
||||||
|
root.size = Vector2i(1280, 720)
|
||||||
|
var main: Node = MainScene.instantiate()
|
||||||
|
root.add_child(main)
|
||||||
|
for _frame: int in 4:
|
||||||
|
await process_frame
|
||||||
|
if not bool(main.get("_application_initialized")):
|
||||||
|
main.call("_activate_selected_data_path", "", true)
|
||||||
|
for _frame: int in 8:
|
||||||
|
await process_frame
|
||||||
|
assert(bool(main.get("_application_initialized")))
|
||||||
|
return main
|
||||||
1
tests/world_spawn_multiplayer_validation.gd.uid
Normal file
1
tests/world_spawn_multiplayer_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://wumg5ev7jr6e
|
||||||
102
tests/world_spawn_protocol_validation.gd
Normal file
102
tests/world_spawn_protocol_validation.gd
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const Gatherables: GatherableCatalog = preload(
|
||||||
|
"res://gathering/catalog/gatherable_catalog.tres"
|
||||||
|
)
|
||||||
|
const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
assert(NetworkProtocol.PROTOCOL_VERSION == 5)
|
||||||
|
assert(
|
||||||
|
NetworkWorldSpawnProtocol.CAPABILITY
|
||||||
|
== NetworkProtocol.WORLD_SPAWN_CAPABILITY
|
||||||
|
)
|
||||||
|
assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4)
|
||||||
|
_validate_catalog_statuses()
|
||||||
|
_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))
|
||||||
|
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")
|
||||||
|
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
|
||||||
|
assert(not brown.catch_data.is_fishable())
|
||||||
|
|
||||||
|
for type_id: StringName in [
|
||||||
|
&"crab_ghost",
|
||||||
|
&"crab_blue",
|
||||||
|
&"crab_dungeness",
|
||||||
|
]:
|
||||||
|
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())
|
||||||
|
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()
|
||||||
|
assert(available.size() == 1)
|
||||||
|
assert(available.front() == brown)
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
1
tests/world_spawn_protocol_validation.gd.uid
Normal file
1
tests/world_spawn_protocol_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://clu8j64mh4x54
|
||||||
|
|
@ -3,23 +3,48 @@ extends RefCounted
|
||||||
|
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
|
|
||||||
|
enum Category {
|
||||||
|
FRESH_WATER,
|
||||||
|
SALT_WATER,
|
||||||
|
OTHER,
|
||||||
|
SHELLFISH,
|
||||||
|
}
|
||||||
|
|
||||||
static func category_for(fish: FishDataType) -> WaterType.Type:
|
|
||||||
|
static func category_for(fish: FishDataType) -> Category:
|
||||||
if fish == null:
|
if fish == null:
|
||||||
return WaterType.Type.OTHER
|
return Category.OTHER
|
||||||
return fish.get_primary_water_type()
|
if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH:
|
||||||
|
return Category.SHELLFISH
|
||||||
|
match fish.get_primary_water_type():
|
||||||
static func category_label(category: WaterType.Type) -> String:
|
|
||||||
return WaterType.label(category)
|
|
||||||
|
|
||||||
|
|
||||||
static func empty_state(category: WaterType.Type) -> String:
|
|
||||||
match category:
|
|
||||||
WaterType.Type.FRESH_WATER:
|
WaterType.Type.FRESH_WATER:
|
||||||
return "No freshwater catches cataloged yet."
|
return Category.FRESH_WATER
|
||||||
WaterType.Type.SALT_WATER:
|
WaterType.Type.SALT_WATER:
|
||||||
|
return Category.SALT_WATER
|
||||||
|
_:
|
||||||
|
return Category.OTHER
|
||||||
|
|
||||||
|
|
||||||
|
static func category_label(category: Category) -> String:
|
||||||
|
match category:
|
||||||
|
Category.FRESH_WATER:
|
||||||
|
return "Fresh Water"
|
||||||
|
Category.SALT_WATER:
|
||||||
|
return "Salt Water"
|
||||||
|
Category.SHELLFISH:
|
||||||
|
return "Shellfish"
|
||||||
|
_:
|
||||||
|
return "Misc"
|
||||||
|
|
||||||
|
|
||||||
|
static func empty_state(category: Category) -> String:
|
||||||
|
match category:
|
||||||
|
Category.FRESH_WATER:
|
||||||
|
return "No freshwater catches cataloged yet."
|
||||||
|
Category.SALT_WATER:
|
||||||
return "No saltwater catches cataloged yet."
|
return "No saltwater catches cataloged yet."
|
||||||
|
Category.SHELLFISH:
|
||||||
|
return "No shellfish cataloged yet."
|
||||||
_:
|
_:
|
||||||
return "No entries available."
|
return "No entries available."
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ const DETAIL_BOTTOM_INSET: float = 35.0
|
||||||
var _collection_log: CollectionLogType
|
var _collection_log: CollectionLogType
|
||||||
var _inventory: FishInventoryType
|
var _inventory: FishInventoryType
|
||||||
var _catalog: FishPoolType
|
var _catalog: FishPoolType
|
||||||
var _category: WaterType.Type = WaterType.Type.FRESH_WATER
|
var _category: LogbookCatalog.Category = (
|
||||||
|
LogbookCatalog.Category.FRESH_WATER
|
||||||
|
)
|
||||||
var _selected_id: StringName
|
var _selected_id: StringName
|
||||||
var _selected_entry_key: StringName
|
var _selected_entry_key: StringName
|
||||||
var _active: bool = false
|
var _active: bool = false
|
||||||
|
|
@ -186,13 +188,14 @@ func _build_interface() -> void:
|
||||||
tabs.mouse_filter = Control.MOUSE_FILTER_PASS
|
tabs.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||||
tabs.add_theme_constant_override("separation", 6)
|
tabs.add_theme_constant_override("separation", 6)
|
||||||
stack.add_child(tabs)
|
stack.add_child(tabs)
|
||||||
for category: WaterType.Type in [
|
for category: LogbookCatalog.Category in [
|
||||||
WaterType.Type.FRESH_WATER,
|
LogbookCatalog.Category.FRESH_WATER,
|
||||||
WaterType.Type.SALT_WATER,
|
LogbookCatalog.Category.SALT_WATER,
|
||||||
WaterType.Type.OTHER,
|
LogbookCatalog.Category.OTHER,
|
||||||
|
LogbookCatalog.Category.SHELLFISH,
|
||||||
]:
|
]:
|
||||||
var tab := OrganizerTabType.new()
|
var tab := OrganizerTabType.new()
|
||||||
tab.custom_minimum_size = Vector2(124, 38)
|
tab.custom_minimum_size = Vector2(90, 38)
|
||||||
tab.toggle_mode = true
|
tab.toggle_mode = true
|
||||||
tab.text = LogbookCatalog.category_label(category)
|
tab.text = LogbookCatalog.category_label(category)
|
||||||
tab.palette_index = int(category)
|
tab.palette_index = int(category)
|
||||||
|
|
@ -518,7 +521,7 @@ func _add_entry_content(
|
||||||
content.add_child(name_label)
|
content.add_child(name_label)
|
||||||
|
|
||||||
|
|
||||||
func _select_category(category: WaterType.Type) -> void:
|
func _select_category(category: LogbookCatalog.Category) -> void:
|
||||||
if category == _category:
|
if category == _category:
|
||||||
return
|
return
|
||||||
_category = category
|
_category = category
|
||||||
|
|
@ -717,7 +720,12 @@ func _build_known_details(fish: FishDataType) -> void:
|
||||||
facts_column.size_flags_stretch_ratio = 1.0
|
facts_column.size_flags_stretch_ratio = 1.0
|
||||||
facts_column.add_theme_constant_override("separation", 6)
|
facts_column.add_theme_constant_override("separation", 6)
|
||||||
summary_columns.add_child(facts_column)
|
summary_columns.add_child(facts_column)
|
||||||
var facts_heading := _field_label("fish facts", 16)
|
var facts_heading := _field_label(
|
||||||
|
"shellfish facts"
|
||||||
|
if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH
|
||||||
|
else "fish facts",
|
||||||
|
16,
|
||||||
|
)
|
||||||
facts_column.add_child(facts_heading)
|
facts_column.add_child(facts_heading)
|
||||||
var facts := _label(LogbookCatalog.facts_for(fish), 16)
|
var facts := _label(LogbookCatalog.facts_for(fish), 16)
|
||||||
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
facts.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
|
|
@ -745,8 +753,12 @@ func _build_known_details(fish: FishDataType) -> void:
|
||||||
)
|
)
|
||||||
_add_detail_row(
|
_add_detail_row(
|
||||||
left_stats,
|
left_stats,
|
||||||
"body of water",
|
(
|
||||||
WaterType.label(fish.get_primary_water_type()).to_lower(),
|
"habitat"
|
||||||
|
if fish.logbook_section == FishDataType.LogbookSection.SHELLFISH
|
||||||
|
else "body of water"
|
||||||
|
),
|
||||||
|
fish.get_habitat_label(),
|
||||||
)
|
)
|
||||||
_add_detail_row(
|
_add_detail_row(
|
||||||
left_stats,
|
left_stats,
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,89 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||||
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
||||||
|
|
||||||
|
|
||||||
|
func get_spawn_surface_triangles(
|
||||||
|
material_names: Array[StringName],
|
||||||
|
minimum_global_y: float,
|
||||||
|
minimum_up_dot: float = 0.6,
|
||||||
|
) -> Array[PackedVector3Array]:
|
||||||
|
var triangles: Array[PackedVector3Array] = []
|
||||||
|
if material_names.is_empty():
|
||||||
|
return triangles
|
||||||
|
var terrain_root: Node = get_node_or_null(terrain_visual_root_path)
|
||||||
|
if terrain_root == null:
|
||||||
|
return triangles
|
||||||
|
for mesh_instance: MeshInstance3D in _collect_terrain_mesh_instances(
|
||||||
|
terrain_root
|
||||||
|
):
|
||||||
|
var mesh: Mesh = mesh_instance.mesh
|
||||||
|
if mesh == null:
|
||||||
|
continue
|
||||||
|
for surface_index: int in mesh.get_surface_count():
|
||||||
|
var material: Material = mesh_instance.get_active_material(
|
||||||
|
surface_index
|
||||||
|
)
|
||||||
|
if material == null or not material_names.has(
|
||||||
|
StringName(material.resource_name)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var arrays: Array = mesh.surface_get_arrays(surface_index)
|
||||||
|
if arrays.size() <= Mesh.ARRAY_INDEX:
|
||||||
|
continue
|
||||||
|
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||||
|
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||||
|
if vertices.is_empty():
|
||||||
|
continue
|
||||||
|
if indices.is_empty():
|
||||||
|
for vertex_index: int in range(0, vertices.size() - 2, 3):
|
||||||
|
_append_spawn_triangle(
|
||||||
|
triangles,
|
||||||
|
mesh_instance,
|
||||||
|
vertices[vertex_index],
|
||||||
|
vertices[vertex_index + 1],
|
||||||
|
vertices[vertex_index + 2],
|
||||||
|
minimum_global_y,
|
||||||
|
minimum_up_dot,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
for index_offset: int in range(0, indices.size() - 2, 3):
|
||||||
|
_append_spawn_triangle(
|
||||||
|
triangles,
|
||||||
|
mesh_instance,
|
||||||
|
vertices[indices[index_offset]],
|
||||||
|
vertices[indices[index_offset + 1]],
|
||||||
|
vertices[indices[index_offset + 2]],
|
||||||
|
minimum_global_y,
|
||||||
|
minimum_up_dot,
|
||||||
|
)
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
func _append_spawn_triangle(
|
||||||
|
result: Array[PackedVector3Array],
|
||||||
|
mesh_instance: MeshInstance3D,
|
||||||
|
local_a: Vector3,
|
||||||
|
local_b: Vector3,
|
||||||
|
local_c: Vector3,
|
||||||
|
minimum_global_y: float,
|
||||||
|
minimum_up_dot: float,
|
||||||
|
) -> void:
|
||||||
|
var a: Vector3 = mesh_instance.to_global(local_a)
|
||||||
|
var b: Vector3 = mesh_instance.to_global(local_b)
|
||||||
|
var c: Vector3 = mesh_instance.to_global(local_c)
|
||||||
|
if (
|
||||||
|
a.y <= minimum_global_y
|
||||||
|
or b.y <= minimum_global_y
|
||||||
|
or c.y <= minimum_global_y
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var cross: Vector3 = (b - a).cross(c - a)
|
||||||
|
if cross.length_squared() <= 0.0000001:
|
||||||
|
return
|
||||||
|
if absf(cross.normalized().dot(Vector3.UP)) < minimum_up_dot:
|
||||||
|
return
|
||||||
|
result.append(PackedVector3Array([a, b, c]))
|
||||||
|
|
||||||
|
|
||||||
func has_terrain_collision() -> bool:
|
func has_terrain_collision() -> bool:
|
||||||
var collision_shape: CollisionShape3D = (
|
var collision_shape: CollisionShape3D = (
|
||||||
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D
|
get_node_or_null(terrain_collision_shape_path) as CollisionShape3D
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,16 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||||
return _starter_island.get_saltwater_shoreline_mesh()
|
return _starter_island.get_saltwater_shoreline_mesh()
|
||||||
|
|
||||||
|
|
||||||
|
func get_spawn_surface_triangles(
|
||||||
|
material_names: Array[StringName],
|
||||||
|
minimum_global_y: float,
|
||||||
|
) -> Array[PackedVector3Array]:
|
||||||
|
return _starter_island.get_spawn_surface_triangles(
|
||||||
|
material_names,
|
||||||
|
minimum_global_y,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _get_regions() -> Array[WorldRegion]:
|
func _get_regions() -> Array[WorldRegion]:
|
||||||
var regions: Array[WorldRegion] = []
|
var regions: Array[WorldRegion] = []
|
||||||
for child: Node in _regions_root.get_children():
|
for child: Node in _regions_root.get_children():
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue