Add gathering, unified inventory, and real-time world
This commit is contained in:
parent
173371e6fc
commit
fa57edca83
113 changed files with 6700 additions and 1307 deletions
|
|
@ -11,6 +11,7 @@ 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 CRAB_NET_ID: StringName = &"crab_net"
|
||||||
|
const STANDARD_SHOVEL_ID: StringName = &"standard_shovel"
|
||||||
|
|
||||||
const ITEM_PRICES: Dictionary[StringName, int] = {
|
const ITEM_PRICES: Dictionary[StringName, int] = {
|
||||||
&"worms": 1,
|
&"worms": 1,
|
||||||
|
|
@ -28,6 +29,7 @@ const ITEM_PRICES: Dictionary[StringName, int] = {
|
||||||
MAGNET_ID: 250,
|
MAGNET_ID: 250,
|
||||||
BATTERIES_ID: 15,
|
BATTERIES_ID: 15,
|
||||||
CRAB_NET_ID: 50,
|
CRAB_NET_ID: 50,
|
||||||
|
STANDARD_SHOVEL_ID: 75,
|
||||||
}
|
}
|
||||||
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
|
const BAIT_UNLOCK_PRICES: Dictionary[StringName, int] = {
|
||||||
&"snails": 400,
|
&"snails": 400,
|
||||||
|
|
@ -49,6 +51,7 @@ const ITEM_ORDER: Array[StringName] = [
|
||||||
FISH_FINDER_ID,
|
FISH_FINDER_ID,
|
||||||
MAGNET_ID,
|
MAGNET_ID,
|
||||||
CRAB_NET_ID,
|
CRAB_NET_ID,
|
||||||
|
STANDARD_SHOVEL_ID,
|
||||||
&"coffee",
|
&"coffee",
|
||||||
&"energy_drink",
|
&"energy_drink",
|
||||||
&"snack",
|
&"snack",
|
||||||
|
|
@ -116,6 +119,7 @@ static func is_permanent_unlock(
|
||||||
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_id == CRAB_NET_ID
|
||||||
|
or item_id == STANDARD_SHOVEL_ID
|
||||||
or item is FishingRodDataType
|
or item is FishingRodDataType
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
21
economy/item_resale_policy.gd
Normal file
21
economy/item_resale_policy.gd
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
class_name ItemResalePolicy
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd")
|
||||||
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
|
|
||||||
|
|
||||||
|
static func is_sellable(item: ItemDataType) -> bool:
|
||||||
|
# Bait and lures live in their own tackle collection. Rods, tools, and
|
||||||
|
# utility gear are permanent equipment. The unified sell tray currently
|
||||||
|
# accepts ordinary consumable supplies only.
|
||||||
|
return item != null and item.is_available() and (
|
||||||
|
item.category == ItemDataType.Category.CONSUMABLE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func get_unit_value(item: ItemDataType) -> int:
|
||||||
|
if not is_sellable(item):
|
||||||
|
return -1
|
||||||
|
var purchase_price := FishingShopStockType.get_price(item.item_id)
|
||||||
|
return purchase_price / 2 if purchase_price >= 0 else -1
|
||||||
1
economy/item_resale_policy.gd.uid
Normal file
1
economy/item_resale_policy.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://brsykj7darpyw
|
||||||
File diff suppressed because one or more lines are too long
BIN
fish/species/beetle_stag_common/beetle_stag_common.png
Normal file
BIN
fish/species/beetle_stag_common/beetle_stag_common.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://blw6lydoba8kd"
|
||||||
|
path="res://.godot/imported/beetle_stag_common.png-5eb7426d7dbda5083e39efc4cb980c8d.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://fish/species/beetle_stag_common/beetle_stag_common.png"
|
||||||
|
dest_files=["res://.godot/imported/beetle_stag_common.png-5eb7426d7dbda5083e39efc4cb980c8d.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
|
||||||
26
fish/species/beetle_stag_common/beetle_stag_common.tres
Normal file
26
fish/species/beetle_stag_common/beetle_stag_common.tres
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
[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/beetle_stag_common/beetle_stag_common.png" id="3_texture"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_fish_data")
|
||||||
|
id = &"beetle_stag_common"
|
||||||
|
display_name = "common stag beetle"
|
||||||
|
active = true
|
||||||
|
catalog_number = 8001
|
||||||
|
collection_group = &"Beetles"
|
||||||
|
logbook_fact = "common stag beetles use broad jaws to wrestle rivals and cling to tree bark. adults feed on sap and other sweet plant juices."
|
||||||
|
collection_method = 1
|
||||||
|
habitat_label = "tree trunks"
|
||||||
|
allowed_water_types = 0
|
||||||
|
rarity = 0
|
||||||
|
base_catch_weight = 1.0
|
||||||
|
catch_profile = ExtResource("2_profile")
|
||||||
|
weight_min_lb = 0.02
|
||||||
|
weight_max_lb = 0.12
|
||||||
|
sell_value_min = 4
|
||||||
|
sell_value_max = 12
|
||||||
|
sell_value_curve = 1.0
|
||||||
|
display_texture = ExtResource("3_texture")
|
||||||
BIN
fish/species/clam_manila/clam_manila.png
Normal file
BIN
fish/species/clam_manila/clam_manila.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.7 KiB |
40
fish/species/clam_manila/clam_manila.png.import
Normal file
40
fish/species/clam_manila/clam_manila.png.import
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="CompressedTexture2D"
|
||||||
|
uid="uid://tmbkp1ohcavc"
|
||||||
|
path="res://.godot/imported/clam_manila.png-b04bc787291fc96749979a40380ba399.ctex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://fish/species/clam_manila/clam_manila.png"
|
||||||
|
dest_files=["res://.godot/imported/clam_manila.png-b04bc787291fc96749979a40380ba399.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/clam_manila/clam_manila.tres
Normal file
27
fish/species/clam_manila/clam_manila.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/clam_manila/clam_manila.png" id="3_texture"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_fish_data")
|
||||||
|
id = &"clam_manila"
|
||||||
|
display_name = "Manila clam"
|
||||||
|
active = true
|
||||||
|
catalog_number = 6406
|
||||||
|
collection_group = &"Shellfish"
|
||||||
|
logbook_fact = "manila clams bury themselves in wet coastal sand and reveal their hiding places with small spurts of water."
|
||||||
|
collection_method = 2
|
||||||
|
logbook_section = 1
|
||||||
|
habitat_label = "wet beach sand"
|
||||||
|
allowed_water_types = 2
|
||||||
|
rarity = 0
|
||||||
|
base_catch_weight = 1.0
|
||||||
|
catch_profile = ExtResource("2_profile")
|
||||||
|
weight_min_lb = 0.05
|
||||||
|
weight_max_lb = 0.3
|
||||||
|
sell_value_min = 3
|
||||||
|
sell_value_max = 8
|
||||||
|
sell_value_curve = 1.0
|
||||||
|
display_texture = ExtResource("3_texture")
|
||||||
|
|
@ -563,10 +563,10 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
if (
|
if (
|
||||||
_is_cooler_full()
|
_is_inventory_full()
|
||||||
):
|
):
|
||||||
status_changed.emit(
|
status_changed.emit(
|
||||||
"cooler full. sell fish before casting again."
|
"inventory full. store or sell something before casting."
|
||||||
)
|
)
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -710,17 +710,15 @@ func _get_active_rod() -> FishingRodDataType:
|
||||||
return _get_active_item() as FishingRodDataType
|
return _get_active_item() as FishingRodDataType
|
||||||
|
|
||||||
|
|
||||||
func _is_cooler_full() -> bool:
|
func _is_inventory_full() -> bool:
|
||||||
return (
|
return (
|
||||||
_cooler_capacity != null
|
_local_inventory != null
|
||||||
and _local_inventory != null
|
and not _local_inventory.can_accept_catch()
|
||||||
and _local_inventory.get_all_catches().size()
|
|
||||||
>= _cooler_capacity.get_capacity()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _on_cooler_availability_changed() -> void:
|
func _on_cooler_availability_changed() -> void:
|
||||||
if state == FishingState.READY and not _is_cooler_full():
|
if state == FishingState.READY and not _is_inventory_full():
|
||||||
refresh_active_item_status()
|
refresh_active_item_status()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1644,7 +1642,7 @@ func _build_network_evidence() -> Dictionary:
|
||||||
"bite_multiplier": 1.0,
|
"bite_multiplier": 1.0,
|
||||||
"rarity_multipliers": rarity_multipliers,
|
"rarity_multipliers": rarity_multipliers,
|
||||||
"discovered_fish_ids": discovered_ids,
|
"discovered_fish_ids": discovered_ids,
|
||||||
"capacity_available": not _is_cooler_full(),
|
"capacity_available": not _is_inventory_full(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
26
gathering/catalog/beetle_stag_common.tres
Normal file
26
gathering/catalog/beetle_stag_common.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/beetle_stag_common/beetle_stag_common.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"beetle_stag_common"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"crab_net"
|
||||||
|
spawn_anchor_set_id = &"starter_reachable_tree_trunks"
|
||||||
|
population = 3
|
||||||
|
requires_sneaking = false
|
||||||
|
movement_speed = 0.0
|
||||||
|
roam_radius = 0.1
|
||||||
|
scare_radius = 0.1
|
||||||
|
capture_radius = 0.34
|
||||||
|
interaction_range = 2.8
|
||||||
|
charge_duration = 1.0
|
||||||
|
sprite_pixel_size = 0.005
|
||||||
|
sprite_tilt_degrees = 0.0
|
||||||
|
capture_respawn_min_seconds = 90.0
|
||||||
|
capture_respawn_max_seconds = 150.0
|
||||||
|
scare_respawn_min_seconds = 0.0
|
||||||
|
scare_respawn_max_seconds = 0.0
|
||||||
|
minimum_respawn_spacing_seconds = 30.0
|
||||||
27
gathering/catalog/clam_manila.tres
Normal file
27
gathering/catalog/clam_manila.tres
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
[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/clam_manila/clam_manila.tres" id="2_catch"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_data")
|
||||||
|
type_id = &"clam_manila"
|
||||||
|
catch_data = ExtResource("2_catch")
|
||||||
|
required_tool_id = &"standard_shovel"
|
||||||
|
diggable_area_id = &"starter_beach"
|
||||||
|
minimum_surface_y = -0.44
|
||||||
|
population = 3
|
||||||
|
presentation_mode = 1
|
||||||
|
requires_sneaking = false
|
||||||
|
active_lifetime_seconds = 10.0
|
||||||
|
movement_speed = 0.0
|
||||||
|
roam_radius = 0.1
|
||||||
|
scare_radius = 0.1
|
||||||
|
capture_radius = 0.38
|
||||||
|
interaction_range = 2.4
|
||||||
|
charge_duration = 0.55
|
||||||
|
capture_respawn_min_seconds = 8.0
|
||||||
|
capture_respawn_max_seconds = 14.0
|
||||||
|
scare_respawn_min_seconds = 0.0
|
||||||
|
scare_respawn_max_seconds = 0.0
|
||||||
|
minimum_respawn_spacing_seconds = 4.0
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
[gd_resource type="Resource" script_class="GatherableCatalog" load_steps=6 format=3]
|
[gd_resource type="Resource" script_class="GatherableCatalog" load_steps=8 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://gathering/gatherable_catalog.gd" id="1_catalog"]
|
[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_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_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_blue.tres" id="4_blue"]
|
||||||
[ext_resource type="Resource" path="res://gathering/catalog/crab_dungeness.tres" id="5_dungeness"]
|
[ext_resource type="Resource" path="res://gathering/catalog/crab_dungeness.tres" id="5_dungeness"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/clam_manila.tres" id="6_clam"]
|
||||||
|
[ext_resource type="Resource" path="res://gathering/catalog/beetle_stag_common.tres" id="7_beetle"]
|
||||||
|
|
||||||
[resource]
|
[resource]
|
||||||
script = ExtResource("1_catalog")
|
script = ExtResource("1_catalog")
|
||||||
entries = [ExtResource("2_brown"), ExtResource("3_ghost"), ExtResource("4_blue"), ExtResource("5_dungeness")]
|
entries = [ExtResource("2_brown"), ExtResource("3_ghost"), ExtResource("4_blue"), ExtResource("5_dungeness"), ExtResource("6_clam"), ExtResource("7_beetle")]
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,22 @@ extends Resource
|
||||||
const FishDataType = preload("res://fish/fish_data.gd")
|
const FishDataType = preload("res://fish/fish_data.gd")
|
||||||
const FishQualityType = preload("res://fish/fish_quality.gd")
|
const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
|
enum PresentationMode {
|
||||||
|
VISIBLE_CREATURE,
|
||||||
|
WATER_SPURT,
|
||||||
|
}
|
||||||
|
|
||||||
@export var type_id: StringName
|
@export var type_id: StringName
|
||||||
@export var catch_data: FishDataType
|
@export var catch_data: FishDataType
|
||||||
@export var required_tool_id: StringName
|
@export var required_tool_id: StringName
|
||||||
@export var surface_materials: Array[StringName] = []
|
@export var surface_materials: Array[StringName] = []
|
||||||
|
@export var diggable_area_id: StringName
|
||||||
|
@export var spawn_anchor_set_id: StringName
|
||||||
@export_range(-100.0, 100.0, 0.01) var minimum_surface_y: float = 0.08
|
@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, 64, 1) var population: int = 0
|
||||||
|
@export var presentation_mode: PresentationMode = PresentationMode.VISIBLE_CREATURE
|
||||||
|
@export var requires_sneaking: bool = true
|
||||||
|
@export_range(0.0, 120.0, 0.1) var active_lifetime_seconds: float = 0.0
|
||||||
@export_range(0.0, 10.0, 0.05) var movement_speed: float = 0.35
|
@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 roam_radius: float = 3.5
|
||||||
@export_range(0.1, 20.0, 0.1) var scare_radius: float = 2.8
|
@export_range(0.1, 20.0, 0.1) var scare_radius: float = 2.8
|
||||||
|
|
@ -41,17 +51,33 @@ const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
|
|
||||||
|
|
||||||
func is_valid() -> bool:
|
func is_valid() -> bool:
|
||||||
|
var uses_diggable_area: bool = not diggable_area_id.is_empty()
|
||||||
|
var uses_spawn_anchors: bool = not spawn_anchor_set_id.is_empty()
|
||||||
|
var movement_parameters_valid: bool = (
|
||||||
|
movement_speed >= 0.0
|
||||||
|
and (
|
||||||
|
presentation_mode == PresentationMode.WATER_SPURT
|
||||||
|
or (roam_radius > 0.0 and scare_radius > 0.0)
|
||||||
|
)
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
not type_id.is_empty()
|
not type_id.is_empty()
|
||||||
and catch_data != null
|
and catch_data != null
|
||||||
and catch_data.is_valid_catalog_entry()
|
and catch_data.is_valid_catalog_entry()
|
||||||
and catch_data.collection_method != FishDataType.CollectionMethod.FISHING
|
and catch_data.collection_method != FishDataType.CollectionMethod.FISHING
|
||||||
and not required_tool_id.is_empty()
|
and not required_tool_id.is_empty()
|
||||||
and not surface_materials.is_empty()
|
and (
|
||||||
|
uses_diggable_area
|
||||||
|
or uses_spawn_anchors
|
||||||
|
or not surface_materials.is_empty()
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
not uses_diggable_area
|
||||||
|
or catch_data.collection_method
|
||||||
|
== FishDataType.CollectionMethod.DIGGING
|
||||||
|
)
|
||||||
and population > 0
|
and population > 0
|
||||||
and movement_speed >= 0.0
|
and movement_parameters_valid
|
||||||
and roam_radius > 0.0
|
|
||||||
and scare_radius > 0.0
|
|
||||||
and _quality_multipliers_are_valid(
|
and _quality_multipliers_are_valid(
|
||||||
quality_movement_speed_multipliers
|
quality_movement_speed_multipliers
|
||||||
)
|
)
|
||||||
|
|
@ -67,6 +93,18 @@ func is_valid() -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_stationary_hotspot() -> bool:
|
||||||
|
return presentation_mode == PresentationMode.WATER_SPURT
|
||||||
|
|
||||||
|
|
||||||
|
func is_stationary_spawn() -> bool:
|
||||||
|
return is_stationary_hotspot() or not spawn_anchor_set_id.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
func can_be_scared() -> bool:
|
||||||
|
return not is_stationary_spawn() and scare_radius > 0.0
|
||||||
|
|
||||||
|
|
||||||
func get_movement_speed_for_quality(quality: int) -> float:
|
func get_movement_speed_for_quality(quality: int) -> float:
|
||||||
return movement_speed * _quality_multiplier(
|
return movement_speed * _quality_multiplier(
|
||||||
quality_movement_speed_multipliers,
|
quality_movement_speed_multipliers,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ var _charging: bool = false
|
||||||
var _charge_elapsed: float = 0.0
|
var _charge_elapsed: float = 0.0
|
||||||
var _charge_duration: float = 2.0
|
var _charge_duration: float = 2.0
|
||||||
var _request_id: String = ""
|
var _request_id: String = ""
|
||||||
|
var _active_tool_id: StringName
|
||||||
var _gameplay_input_enabled: bool = false
|
var _gameplay_input_enabled: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,15 +77,40 @@ func is_net_selected() -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func is_shovel_selected() -> bool:
|
||||||
|
return _is_owned_tool_selected(FishingShopStockType.STANDARD_SHOVEL_ID)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_owned_tool_selected(tool_id: StringName) -> bool:
|
||||||
|
return (
|
||||||
|
_bag != null
|
||||||
|
and _hotbar != null
|
||||||
|
and _hotbar.get_selected_item_id() == tool_id
|
||||||
|
and _bag.owns_item(tool_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _get_selected_gathering_tool_id() -> StringName:
|
||||||
|
if is_net_selected():
|
||||||
|
return FishingShopStockType.CRAB_NET_ID
|
||||||
|
if is_shovel_selected():
|
||||||
|
return FishingShopStockType.STANDARD_SHOVEL_ID
|
||||||
|
return StringName()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
|
var selected_tool_id := _get_selected_gathering_tool_id()
|
||||||
var input_available: bool = (
|
var input_available: bool = (
|
||||||
_gameplay_input_enabled
|
_gameplay_input_enabled
|
||||||
and is_net_selected()
|
and not selected_tool_id.is_empty()
|
||||||
and _player != null
|
and _player != null
|
||||||
and _player.is_local_control_enabled()
|
and _player.is_local_control_enabled()
|
||||||
and _fishing_spot != null
|
and _fishing_spot != null
|
||||||
and _fishing_spot.can_change_hotbar_selection()
|
and _fishing_spot.can_change_hotbar_selection()
|
||||||
)
|
)
|
||||||
|
if _charging and selected_tool_id != _active_tool_id:
|
||||||
|
_cancel_charge()
|
||||||
|
input_available = false
|
||||||
if not input_available:
|
if not input_available:
|
||||||
if _charging:
|
if _charging:
|
||||||
_cancel_charge()
|
_cancel_charge()
|
||||||
|
|
@ -105,7 +131,11 @@ func _process(delta: float) -> void:
|
||||||
var valid_target: bool = (
|
var valid_target: bool = (
|
||||||
fully_charged
|
fully_charged
|
||||||
and not _target_entity_id.is_empty()
|
and not _target_entity_id.is_empty()
|
||||||
and _player.is_sneaking()
|
and (
|
||||||
|
target_entry == null
|
||||||
|
or not target_entry.requires_sneaking
|
||||||
|
or _player.is_sneaking()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_marker.material_override = (
|
_marker.material_override = (
|
||||||
_marker_valid_material if valid_target else _marker_invalid_material
|
_marker_valid_material if valid_target else _marker_invalid_material
|
||||||
|
|
@ -127,7 +157,7 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if (
|
if (
|
||||||
_charging
|
_charging
|
||||||
or not _gameplay_input_enabled
|
or not _gameplay_input_enabled
|
||||||
or not is_net_selected()
|
or _get_selected_gathering_tool_id().is_empty()
|
||||||
or _fishing_spot == null
|
or _fishing_spot == null
|
||||||
or not _fishing_spot.can_change_hotbar_selection()
|
or not _fishing_spot.can_change_hotbar_selection()
|
||||||
):
|
):
|
||||||
|
|
@ -136,10 +166,11 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if _request_id.is_empty():
|
if _request_id.is_empty():
|
||||||
return
|
return
|
||||||
_charging = true
|
_charging = true
|
||||||
|
_active_tool_id = _get_selected_gathering_tool_id()
|
||||||
_charge_elapsed = 0.0
|
_charge_elapsed = 0.0
|
||||||
_charge_duration = maxf(
|
_charge_duration = maxf(
|
||||||
_service.get_charge_duration_for_tool(
|
_service.get_charge_duration_for_tool(
|
||||||
FishingShopStockType.CRAB_NET_ID
|
_active_tool_id
|
||||||
),
|
),
|
||||||
0.1,
|
0.1,
|
||||||
)
|
)
|
||||||
|
|
@ -151,11 +182,18 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
if not _charging:
|
if not _charging:
|
||||||
return
|
return
|
||||||
var fully_charged: bool = _charge_elapsed >= _charge_duration
|
var fully_charged: bool = _charge_elapsed >= _charge_duration
|
||||||
|
var target_entry: GatherableData = _service.get_entry_for_entity(
|
||||||
|
_target_entity_id
|
||||||
|
)
|
||||||
var valid_capture: bool = (
|
var valid_capture: bool = (
|
||||||
fully_charged
|
fully_charged
|
||||||
and _marker_has_surface
|
and _marker_has_surface
|
||||||
and not _target_entity_id.is_empty()
|
and not _target_entity_id.is_empty()
|
||||||
and _player.is_sneaking()
|
and (
|
||||||
|
target_entry == null
|
||||||
|
or not target_entry.requires_sneaking
|
||||||
|
or _player.is_sneaking()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_player.play_net_strike_visual()
|
_player.play_net_strike_visual()
|
||||||
if valid_capture:
|
if valid_capture:
|
||||||
|
|
@ -265,7 +303,7 @@ func _update_target_entity() -> void:
|
||||||
_target_entity_id = (
|
_target_entity_id = (
|
||||||
_service.find_capture_target(
|
_service.find_capture_target(
|
||||||
_marker_position,
|
_marker_position,
|
||||||
FishingShopStockType.CRAB_NET_ID,
|
_active_tool_id,
|
||||||
)
|
)
|
||||||
if _marker_has_surface and _service != null
|
if _marker_has_surface and _service != null
|
||||||
else ""
|
else ""
|
||||||
|
|
@ -284,6 +322,7 @@ func _reset_charge_state() -> void:
|
||||||
_charging = false
|
_charging = false
|
||||||
_charge_elapsed = 0.0
|
_charge_elapsed = 0.0
|
||||||
_request_id = ""
|
_request_id = ""
|
||||||
|
_active_tool_id = StringName()
|
||||||
_target_entity_id = ""
|
_target_entity_id = ""
|
||||||
_marker_has_surface = false
|
_marker_has_surface = false
|
||||||
_set_marker_visible(false)
|
_set_marker_visible(false)
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,16 @@ extends Node3D
|
||||||
const GatherableDataType = preload("res://gathering/gatherable_data.gd")
|
const GatherableDataType = preload("res://gathering/gatherable_data.gd")
|
||||||
const REFERENCE_SPRITE_PIXEL_SIZE: float = 0.001
|
const REFERENCE_SPRITE_PIXEL_SIZE: float = 0.001
|
||||||
const REFERENCE_SPRITE_HEIGHT: float = 0.22
|
const REFERENCE_SPRITE_HEIGHT: float = 0.22
|
||||||
|
const WATER_SPURT_INTERVAL_SECONDS: float = 1.15
|
||||||
|
const WATER_SPURT_COLOR := Color(0.48, 0.82, 0.84, 1.0)
|
||||||
|
const WATER_SPURT_HOLE_COLOR := Color(0.23, 0.27, 0.24, 1.0)
|
||||||
|
|
||||||
var entity_id: String = ""
|
var entity_id: String = ""
|
||||||
var type_id: StringName
|
var type_id: StringName
|
||||||
var data: GatherableDataType
|
var data: GatherableDataType
|
||||||
var _sprite: Sprite3D
|
var _sprite: Sprite3D
|
||||||
|
var _water_spurt_root: Node3D
|
||||||
|
var _water_spurt_elapsed: float = 0.0
|
||||||
var _target_position: Vector3
|
var _target_position: Vector3
|
||||||
var _target_yaw: float = 0.0
|
var _target_yaw: float = 0.0
|
||||||
var _has_state: bool = false
|
var _has_state: bool = false
|
||||||
|
|
@ -24,14 +29,25 @@ func configure(
|
||||||
entity_id = configured_entity_id
|
entity_id = configured_entity_id
|
||||||
data = configured_data
|
data = configured_data
|
||||||
type_id = data.type_id if data != null else StringName()
|
type_id = data.type_id if data != null else StringName()
|
||||||
_ensure_visual()
|
if data != null and data.is_stationary_hotspot():
|
||||||
if data != null and data.catch_data != null:
|
_ensure_water_spurt_visual()
|
||||||
|
_water_spurt_elapsed = (
|
||||||
|
float(abs(hash(entity_id)) % 1000) / 1000.0
|
||||||
|
* WATER_SPURT_INTERVAL_SECONDS
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_ensure_visual()
|
||||||
|
if data != null and data.catch_data != null and _sprite != null:
|
||||||
_sprite.texture = data.catch_data.display_texture
|
_sprite.texture = data.catch_data.display_texture
|
||||||
_sprite.pixel_size = data.sprite_pixel_size
|
_sprite.pixel_size = data.sprite_pixel_size
|
||||||
_sprite.position.y = (
|
_sprite.position.y = (
|
||||||
REFERENCE_SPRITE_HEIGHT
|
0.0
|
||||||
* data.sprite_pixel_size
|
if not data.spawn_anchor_set_id.is_empty()
|
||||||
/ REFERENCE_SPRITE_PIXEL_SIZE
|
else (
|
||||||
|
REFERENCE_SPRITE_HEIGHT
|
||||||
|
* data.sprite_pixel_size
|
||||||
|
/ REFERENCE_SPRITE_PIXEL_SIZE
|
||||||
|
)
|
||||||
)
|
)
|
||||||
_sprite.rotation_degrees.x = data.sprite_tilt_degrees
|
_sprite.rotation_degrees.x = data.sprite_tilt_degrees
|
||||||
apply_network_state(position, yaw, true)
|
apply_network_state(position, yaw, true)
|
||||||
|
|
@ -58,6 +74,10 @@ func play_despawn(with_dust: bool) -> void:
|
||||||
_despawning = true
|
_despawning = true
|
||||||
if with_dust:
|
if with_dust:
|
||||||
_emit_dust()
|
_emit_dust()
|
||||||
|
if _water_spurt_root != null:
|
||||||
|
_water_spurt_root.visible = false
|
||||||
|
queue_free()
|
||||||
|
return
|
||||||
var tween: Tween = create_tween()
|
var tween: Tween = create_tween()
|
||||||
tween.set_parallel(true)
|
tween.set_parallel(true)
|
||||||
tween.set_trans(Tween.TRANS_QUAD)
|
tween.set_trans(Tween.TRANS_QUAD)
|
||||||
|
|
@ -80,6 +100,14 @@ func _process(delta: float) -> void:
|
||||||
_target_yaw,
|
_target_yaw,
|
||||||
1.0 - exp(-8.0 * delta),
|
1.0 - exp(-8.0 * delta),
|
||||||
)
|
)
|
||||||
|
if _water_spurt_root != null:
|
||||||
|
_water_spurt_elapsed += delta
|
||||||
|
if _water_spurt_elapsed >= WATER_SPURT_INTERVAL_SECONDS:
|
||||||
|
_water_spurt_elapsed = fmod(
|
||||||
|
_water_spurt_elapsed,
|
||||||
|
WATER_SPURT_INTERVAL_SECONDS,
|
||||||
|
)
|
||||||
|
_emit_water_spurt()
|
||||||
|
|
||||||
|
|
||||||
func _ensure_visual() -> void:
|
func _ensure_visual() -> void:
|
||||||
|
|
@ -95,6 +123,66 @@ func _ensure_visual() -> void:
|
||||||
add_child(_sprite)
|
add_child(_sprite)
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_water_spurt_visual() -> void:
|
||||||
|
if _water_spurt_root != null:
|
||||||
|
return
|
||||||
|
_water_spurt_root = Node3D.new()
|
||||||
|
_water_spurt_root.name = "WaterSpurt"
|
||||||
|
add_child(_water_spurt_root)
|
||||||
|
var hole_material := StandardMaterial3D.new()
|
||||||
|
hole_material.albedo_color = WATER_SPURT_HOLE_COLOR
|
||||||
|
hole_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
hole_material.roughness = 1.0
|
||||||
|
hole_material.metallic = 0.0
|
||||||
|
var hole_mesh := CylinderMesh.new()
|
||||||
|
hole_mesh.top_radius = 0.035
|
||||||
|
hole_mesh.bottom_radius = 0.035
|
||||||
|
hole_mesh.height = 0.004
|
||||||
|
hole_mesh.radial_segments = 12
|
||||||
|
hole_mesh.rings = 1
|
||||||
|
hole_mesh.material = hole_material
|
||||||
|
var hole := MeshInstance3D.new()
|
||||||
|
hole.name = "BurrowMark"
|
||||||
|
hole.position.y = 0.008
|
||||||
|
hole.mesh = hole_mesh
|
||||||
|
hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
_water_spurt_root.add_child(hole)
|
||||||
|
_emit_water_spurt()
|
||||||
|
|
||||||
|
|
||||||
|
func _emit_water_spurt() -> void:
|
||||||
|
if _water_spurt_root == null or not _water_spurt_root.visible:
|
||||||
|
return
|
||||||
|
var particles := CPUParticles3D.new()
|
||||||
|
particles.name = "WaterDroplets"
|
||||||
|
particles.position.y = 0.035
|
||||||
|
particles.amount = 6
|
||||||
|
particles.lifetime = 0.42
|
||||||
|
particles.one_shot = true
|
||||||
|
particles.explosiveness = 1.0
|
||||||
|
particles.direction = Vector3.UP
|
||||||
|
particles.spread = 32.0
|
||||||
|
particles.gravity = Vector3(0.0, -3.4, 0.0)
|
||||||
|
particles.initial_velocity_min = 0.8
|
||||||
|
particles.initial_velocity_max = 1.2
|
||||||
|
particles.scale_amount_min = 0.75
|
||||||
|
particles.scale_amount_max = 1.0
|
||||||
|
var water_material := StandardMaterial3D.new()
|
||||||
|
water_material.albedo_color = WATER_SPURT_COLOR
|
||||||
|
water_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||||
|
water_material.roughness = 1.0
|
||||||
|
water_material.metallic = 0.0
|
||||||
|
var droplet_mesh := BoxMesh.new()
|
||||||
|
droplet_mesh.size = Vector3(0.022, 0.035, 0.022)
|
||||||
|
droplet_mesh.material = water_material
|
||||||
|
particles.mesh = droplet_mesh
|
||||||
|
_water_spurt_root.add_child(particles)
|
||||||
|
particles.emitting = true
|
||||||
|
var cleanup := create_tween()
|
||||||
|
cleanup.tween_interval(0.55)
|
||||||
|
cleanup.tween_callback(particles.queue_free)
|
||||||
|
|
||||||
|
|
||||||
func _emit_dust() -> void:
|
func _emit_dust() -> void:
|
||||||
var particles := CPUParticles3D.new()
|
var particles := CPUParticles3D.new()
|
||||||
particles.name = "DustPoof"
|
particles.name = "DustPoof"
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ signal catches_changed
|
||||||
var _catches: Array[FishCatchType] = []
|
var _catches: Array[FishCatchType] = []
|
||||||
var _next_catch_sequence: int = 1
|
var _next_catch_sequence: int = 1
|
||||||
var _reservation_service: PlayerAssetReservationService
|
var _reservation_service: PlayerAssetReservationService
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
|
|
||||||
|
|
||||||
func set_reservation_service(
|
func set_reservation_service(
|
||||||
|
|
@ -17,11 +18,24 @@ func set_reservation_service(
|
||||||
_reservation_service = reservation_service
|
_reservation_service = reservation_service
|
||||||
|
|
||||||
|
|
||||||
func add_catch(fish_catch: FishCatchType) -> void:
|
func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
|
||||||
|
_inventory_layout = layout
|
||||||
|
|
||||||
|
|
||||||
|
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
||||||
|
return (
|
||||||
|
_inventory_layout == null
|
||||||
|
or _inventory_layout.can_accept_catch(catch_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func add_catch(fish_catch: FishCatchType) -> bool:
|
||||||
if fish_catch == null or not fish_catch.is_valid():
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
return
|
return false
|
||||||
if get_catch_by_id(fish_catch.catch_id) != null:
|
if get_catch_by_id(fish_catch.catch_id) != null:
|
||||||
return
|
return false
|
||||||
|
if not can_accept_catch(fish_catch.catch_id):
|
||||||
|
return false
|
||||||
if fish_catch.catch_sequence <= 0:
|
if fish_catch.catch_sequence <= 0:
|
||||||
fish_catch.catch_sequence = _next_catch_sequence
|
fish_catch.catch_sequence = _next_catch_sequence
|
||||||
_next_catch_sequence = maxi(
|
_next_catch_sequence = maxi(
|
||||||
|
|
@ -34,6 +48,7 @@ func add_catch(fish_catch: FishCatchType) -> void:
|
||||||
get_count(fish_catch.fish_id)
|
get_count(fish_catch.fish_id)
|
||||||
)
|
)
|
||||||
catches_changed.emit()
|
catches_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func get_count(fish_id: StringName) -> int:
|
func get_count(fish_id: StringName) -> int:
|
||||||
|
|
|
||||||
|
|
@ -13,11 +13,14 @@ enum StorageGroup {
|
||||||
NONE,
|
NONE,
|
||||||
EQUIPMENT,
|
EQUIPMENT,
|
||||||
ITEMS,
|
ITEMS,
|
||||||
|
BAIT,
|
||||||
|
LURES,
|
||||||
}
|
}
|
||||||
|
|
||||||
signal contents_changed
|
signal contents_changed
|
||||||
|
|
||||||
var _catalog: ItemCatalogType
|
var _catalog: ItemCatalogType
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
var _items: Array[OwnedItemType] = []
|
var _items: Array[OwnedItemType] = []
|
||||||
var _unlocked_bait_ids: Array[StringName] = (
|
var _unlocked_bait_ids: Array[StringName] = (
|
||||||
DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
|
DEFAULT_UNLOCKED_BAIT_IDS.duplicate()
|
||||||
|
|
@ -28,11 +31,21 @@ func setup(catalog: ItemCatalogType) -> void:
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
|
|
||||||
|
|
||||||
|
func set_inventory_layout(layout: PlayerInventoryLayout) -> void:
|
||||||
|
_inventory_layout = layout
|
||||||
|
|
||||||
|
|
||||||
func add_item(item_id: StringName, quantity: int = 1) -> bool:
|
func add_item(item_id: StringName, quantity: int = 1) -> bool:
|
||||||
if not can_add_item(item_id, quantity):
|
if not can_add_item(item_id, quantity):
|
||||||
return false
|
return false
|
||||||
var item: ItemDataType = _resolve_valid_item(item_id)
|
var item: ItemDataType = _resolve_valid_item(item_id)
|
||||||
var existing: OwnedItemType = get_owned_item(item_id)
|
var existing: OwnedItemType = get_owned_item(item_id)
|
||||||
|
if (
|
||||||
|
existing == null
|
||||||
|
and _inventory_layout != null
|
||||||
|
and not _inventory_layout.can_accept_item(item_id)
|
||||||
|
):
|
||||||
|
return false
|
||||||
if existing != null:
|
if existing != null:
|
||||||
existing.quantity += quantity
|
existing.quantity += quantity
|
||||||
else:
|
else:
|
||||||
|
|
@ -57,6 +70,11 @@ func can_add_item(item_id: StringName, quantity: int = 1) -> bool:
|
||||||
item.stackable
|
item.stackable
|
||||||
and existing.quantity + quantity <= item.max_stack
|
and existing.quantity + quantity <= item.max_stack
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
_inventory_layout != null
|
||||||
|
and not _inventory_layout.can_accept_item(item_id)
|
||||||
|
):
|
||||||
|
return false
|
||||||
return quantity <= (item.max_stack if item.stackable else 1)
|
return quantity <= (item.max_stack if item.stackable else 1)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -252,8 +270,12 @@ func _first_available_storage_slot(
|
||||||
|
|
||||||
|
|
||||||
func _storage_group_for_item(item: ItemDataType) -> StorageGroup:
|
func _storage_group_for_item(item: ItemDataType) -> StorageGroup:
|
||||||
if item == null or item.is_bait() or item.is_lure():
|
if item == null:
|
||||||
return StorageGroup.NONE
|
return StorageGroup.NONE
|
||||||
|
if item.is_bait():
|
||||||
|
return StorageGroup.BAIT
|
||||||
|
if item.is_lure():
|
||||||
|
return StorageGroup.LURES
|
||||||
return (
|
return (
|
||||||
StorageGroup.ITEMS
|
StorageGroup.ITEMS
|
||||||
if item.category == ItemDataType.Category.CONSUMABLE
|
if item.category == ItemDataType.Category.CONSUMABLE
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ signal selected_assignment_changed(
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
var _catalog: ItemCatalogType
|
var _catalog: ItemCatalogType
|
||||||
var _fish_inventory: FishInventoryType
|
var _fish_inventory: FishInventoryType
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
var _slots: Array[StringName] = []
|
var _slots: Array[StringName] = []
|
||||||
var _fish_slots: Array[StringName] = []
|
var _fish_slots: Array[StringName] = []
|
||||||
var _selected_slot: int = 0
|
var _selected_slot: int = 0
|
||||||
|
|
@ -40,10 +41,12 @@ func setup(
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
catalog: ItemCatalogType,
|
catalog: ItemCatalogType,
|
||||||
fish_inventory: FishInventoryType = null,
|
fish_inventory: FishInventoryType = null,
|
||||||
|
inventory_layout: PlayerInventoryLayout = null,
|
||||||
) -> void:
|
) -> void:
|
||||||
_bag = bag
|
_bag = bag
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
_fish_inventory = fish_inventory
|
_fish_inventory = fish_inventory
|
||||||
|
_inventory_layout = inventory_layout
|
||||||
if _bag != null and not _bag.contents_changed.is_connected(
|
if _bag != null and not _bag.contents_changed.is_connected(
|
||||||
_on_bag_contents_changed
|
_on_bag_contents_changed
|
||||||
):
|
):
|
||||||
|
|
@ -55,6 +58,11 @@ func setup(
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
_fish_inventory.catches_changed.connect(_on_fish_inventory_changed)
|
_fish_inventory.catches_changed.connect(_on_fish_inventory_changed)
|
||||||
|
if (
|
||||||
|
_inventory_layout != null
|
||||||
|
and not _inventory_layout.layout_changed.is_connected(_validate_assignments)
|
||||||
|
):
|
||||||
|
_inventory_layout.layout_changed.connect(_validate_assignments)
|
||||||
_validate_assignments()
|
_validate_assignments()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -63,13 +71,26 @@ func assign_item(slot_index: int, item_id: StringName) -> bool:
|
||||||
return false
|
return false
|
||||||
if _slots[slot_index] == item_id:
|
if _slots[slot_index] == item_id:
|
||||||
return true
|
return true
|
||||||
|
var source_index: int = _slots.find(item_id)
|
||||||
|
var displaced_item: StringName = _slots[slot_index]
|
||||||
|
var displaced_fish: StringName = _fish_slots[slot_index]
|
||||||
|
if (
|
||||||
|
_inventory_layout != null
|
||||||
|
and not _inventory_layout.move_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
item_id,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
slot_index,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||||
for index: int in range(SLOT_COUNT):
|
if source_index >= 0 and source_index != slot_index:
|
||||||
if index != slot_index and _slots[index] == item_id:
|
_slots[source_index] = displaced_item
|
||||||
_slots[index] = StringName()
|
_fish_slots[source_index] = displaced_fish
|
||||||
selected_slot_was_affected = (
|
selected_slot_was_affected = (
|
||||||
selected_slot_was_affected or index == _selected_slot
|
selected_slot_was_affected or source_index == _selected_slot
|
||||||
)
|
)
|
||||||
_slots[slot_index] = item_id
|
_slots[slot_index] = item_id
|
||||||
_fish_slots[slot_index] = StringName()
|
_fish_slots[slot_index] = StringName()
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
|
|
@ -83,13 +104,26 @@ func assign_fish(slot_index: int, catch_id: StringName) -> bool:
|
||||||
return false
|
return false
|
||||||
if _fish_slots[slot_index] == catch_id:
|
if _fish_slots[slot_index] == catch_id:
|
||||||
return true
|
return true
|
||||||
|
var source_index: int = _fish_slots.find(catch_id)
|
||||||
|
var displaced_item: StringName = _slots[slot_index]
|
||||||
|
var displaced_fish: StringName = _fish_slots[slot_index]
|
||||||
|
if (
|
||||||
|
_inventory_layout != null
|
||||||
|
and not _inventory_layout.move_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
catch_id,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
slot_index,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
var selected_slot_was_affected: bool = slot_index == _selected_slot
|
||||||
for index: int in range(SLOT_COUNT):
|
if source_index >= 0 and source_index != slot_index:
|
||||||
if index != slot_index and _fish_slots[index] == catch_id:
|
_slots[source_index] = displaced_item
|
||||||
_fish_slots[index] = StringName()
|
_fish_slots[source_index] = displaced_fish
|
||||||
selected_slot_was_affected = (
|
selected_slot_was_affected = (
|
||||||
selected_slot_was_affected or index == _selected_slot
|
selected_slot_was_affected or source_index == _selected_slot
|
||||||
)
|
)
|
||||||
_slots[slot_index] = StringName()
|
_slots[slot_index] = StringName()
|
||||||
_fish_slots[slot_index] = catch_id
|
_fish_slots[slot_index] = catch_id
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
|
|
@ -107,6 +141,11 @@ func clear_slot(slot_index: int) -> bool:
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
|
if (
|
||||||
|
_inventory_layout != null
|
||||||
|
and not _inventory_layout.return_hotbar_entry_to_inventory(slot_index)
|
||||||
|
):
|
||||||
|
return false
|
||||||
_slots[slot_index] = StringName()
|
_slots[slot_index] = StringName()
|
||||||
_fish_slots[slot_index] = StringName()
|
_fish_slots[slot_index] = StringName()
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
|
|
@ -122,6 +161,39 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
||||||
or from_index == to_index
|
or from_index == to_index
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
|
if _inventory_layout != null:
|
||||||
|
var kind: PlayerInventoryLayout.EntryKind
|
||||||
|
var identity: StringName
|
||||||
|
if not _fish_slots[from_index].is_empty():
|
||||||
|
kind = PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
identity = _fish_slots[from_index]
|
||||||
|
elif not _slots[from_index].is_empty():
|
||||||
|
kind = PlayerInventoryLayout.EntryKind.ITEM
|
||||||
|
identity = _slots[from_index]
|
||||||
|
else:
|
||||||
|
kind = (
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
if not _fish_slots[to_index].is_empty()
|
||||||
|
else PlayerInventoryLayout.EntryKind.ITEM
|
||||||
|
)
|
||||||
|
identity = (
|
||||||
|
_fish_slots[to_index]
|
||||||
|
if not _fish_slots[to_index].is_empty()
|
||||||
|
else _slots[to_index]
|
||||||
|
)
|
||||||
|
var temporary_index: int = from_index
|
||||||
|
from_index = to_index
|
||||||
|
to_index = temporary_index
|
||||||
|
if (
|
||||||
|
identity.is_empty()
|
||||||
|
or not _inventory_layout.move_entry(
|
||||||
|
kind,
|
||||||
|
identity,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
to_index,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
var temporary: StringName = _slots[from_index]
|
var temporary: StringName = _slots[from_index]
|
||||||
_slots[from_index] = _slots[to_index]
|
_slots[from_index] = _slots[to_index]
|
||||||
_slots[to_index] = temporary
|
_slots[to_index] = temporary
|
||||||
|
|
@ -134,6 +206,50 @@ func swap_slots(from_index: int, to_index: int) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func move_slot_to_container(
|
||||||
|
slot_index: int,
|
||||||
|
target_container: int,
|
||||||
|
target_slot: int,
|
||||||
|
) -> bool:
|
||||||
|
if not _is_slot_valid(slot_index) or _inventory_layout == null:
|
||||||
|
return false
|
||||||
|
var kind: PlayerInventoryLayout.EntryKind
|
||||||
|
var identity: StringName
|
||||||
|
if not _fish_slots[slot_index].is_empty():
|
||||||
|
kind = PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
identity = _fish_slots[slot_index]
|
||||||
|
elif not _slots[slot_index].is_empty():
|
||||||
|
kind = PlayerInventoryLayout.EntryKind.ITEM
|
||||||
|
identity = _slots[slot_index]
|
||||||
|
else:
|
||||||
|
return false
|
||||||
|
if not _inventory_layout.move_entry(
|
||||||
|
kind,
|
||||||
|
identity,
|
||||||
|
target_container,
|
||||||
|
target_slot,
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_slots[slot_index] = StringName()
|
||||||
|
_fish_slots[slot_index] = StringName()
|
||||||
|
var replacement := _inventory_layout.get_entry_at(
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
slot_index,
|
||||||
|
)
|
||||||
|
if not replacement.is_empty():
|
||||||
|
var replacement_identity := StringName(
|
||||||
|
str(replacement.get("identity", ""))
|
||||||
|
)
|
||||||
|
if int(replacement.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
_fish_slots[slot_index] = replacement_identity
|
||||||
|
else:
|
||||||
|
_slots[slot_index] = replacement_identity
|
||||||
|
slots_changed.emit()
|
||||||
|
if slot_index == _selected_slot:
|
||||||
|
_emit_selected_assignment()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func select_slot(slot_index: int) -> bool:
|
func select_slot(slot_index: int) -> bool:
|
||||||
if not _is_slot_valid(slot_index):
|
if not _is_slot_valid(slot_index):
|
||||||
return false
|
return false
|
||||||
|
|
@ -202,7 +318,13 @@ func replace_state(
|
||||||
slots: Array[StringName],
|
slots: Array[StringName],
|
||||||
selected_slot: int,
|
selected_slot: int,
|
||||||
fish_slots: Array[StringName] = [],
|
fish_slots: Array[StringName] = [],
|
||||||
|
synchronize_layout: bool = true,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
if (
|
||||||
|
synchronize_layout and _inventory_layout != null
|
||||||
|
and not _inventory_layout.return_all_hotbar_entries_to_inventory()
|
||||||
|
):
|
||||||
|
return false
|
||||||
var normalized: Array[StringName] = []
|
var normalized: Array[StringName] = []
|
||||||
normalized.resize(SLOT_COUNT)
|
normalized.resize(SLOT_COUNT)
|
||||||
normalized.fill(StringName())
|
normalized.fill(StringName())
|
||||||
|
|
@ -227,6 +349,22 @@ func replace_state(
|
||||||
seen_fish[catch_id] = true
|
seen_fish[catch_id] = true
|
||||||
_slots = normalized
|
_slots = normalized
|
||||||
_fish_slots = normalized_fish
|
_fish_slots = normalized_fish
|
||||||
|
if _inventory_layout != null and synchronize_layout:
|
||||||
|
for index: int in SLOT_COUNT:
|
||||||
|
if not _slots[index].is_empty():
|
||||||
|
_inventory_layout.move_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
_slots[index],
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
elif not _fish_slots[index].is_empty():
|
||||||
|
_inventory_layout.move_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
_fish_slots[index],
|
||||||
|
PlayerInventoryLayout.InventoryContainer.HOTBAR,
|
||||||
|
index,
|
||||||
|
)
|
||||||
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
_selected_slot = clampi(selected_slot, 0, SLOT_COUNT - 1)
|
||||||
slots_changed.emit()
|
slots_changed.emit()
|
||||||
_emit_selected_assignment()
|
_emit_selected_assignment()
|
||||||
|
|
@ -249,7 +387,17 @@ func _can_assign(item_id: StringName) -> bool:
|
||||||
if _catalog == null:
|
if _catalog == null:
|
||||||
return false
|
return false
|
||||||
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
var item: ItemDataType = _catalog.get_item_by_id(item_id)
|
||||||
return item != null and item.is_available() and item.hotbar_allowed
|
return (
|
||||||
|
item != null
|
||||||
|
and item.is_available()
|
||||||
|
and item.hotbar_allowed
|
||||||
|
and (
|
||||||
|
_inventory_layout == null
|
||||||
|
or _inventory_layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM, item_id
|
||||||
|
) >= 0
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _can_assign_fish(catch_id: StringName) -> bool:
|
func _can_assign_fish(catch_id: StringName) -> bool:
|
||||||
|
|
@ -257,6 +405,12 @@ func _can_assign_fish(catch_id: StringName) -> bool:
|
||||||
not catch_id.is_empty()
|
not catch_id.is_empty()
|
||||||
and _fish_inventory != null
|
and _fish_inventory != null
|
||||||
and _fish_inventory.contains_catch_id(catch_id)
|
and _fish_inventory.contains_catch_id(catch_id)
|
||||||
|
and (
|
||||||
|
_inventory_layout == null
|
||||||
|
or _inventory_layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH, catch_id
|
||||||
|
) >= 0
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
519
inventory/player_inventory_layout.gd
Normal file
519
inventory/player_inventory_layout.gd
Normal file
|
|
@ -0,0 +1,519 @@
|
||||||
|
class_name PlayerInventoryLayout
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
|
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
|
||||||
|
const PlayerCoolerCapacityType = preload(
|
||||||
|
"res://progression/player_cooler_capacity.gd"
|
||||||
|
)
|
||||||
|
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||||
|
|
||||||
|
const INVENTORY_COLUMNS: int = 9
|
||||||
|
const INVENTORY_CAPACITIES: Array[int] = [9, 18, 27, 36]
|
||||||
|
const BACKPACK_EXPANSION_COSTS: Array[int] = [1500, 4500, 9000]
|
||||||
|
const MAX_BACKPACK_LEVEL: int = 3
|
||||||
|
const MAX_INVENTORY_SLOT_COUNT: int = 36
|
||||||
|
const MAX_STORAGE_SLOT_COUNT: int = PlayerCoolerCapacityType.MAX_CAPACITY
|
||||||
|
const HOTBAR_SLOT_COUNT: int = 9
|
||||||
|
|
||||||
|
enum EntryKind {
|
||||||
|
ITEM,
|
||||||
|
CATCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum InventoryContainer {
|
||||||
|
INVENTORY,
|
||||||
|
STORAGE,
|
||||||
|
HOTBAR,
|
||||||
|
}
|
||||||
|
|
||||||
|
signal layout_changed
|
||||||
|
signal backpack_capacity_changed(level: int, capacity: int)
|
||||||
|
|
||||||
|
var _bag: PlayerBagType
|
||||||
|
var _fish_inventory: FishInventoryType
|
||||||
|
var _item_catalog: ItemCatalogType
|
||||||
|
var _storage_capacity: PlayerCoolerCapacityType
|
||||||
|
var _placements: Dictionary[String, Dictionary] = {}
|
||||||
|
var _reconciling: bool = false
|
||||||
|
var _backpack_level: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
bag: PlayerBagType,
|
||||||
|
fish_inventory: FishInventoryType,
|
||||||
|
item_catalog: ItemCatalogType,
|
||||||
|
storage_capacity: PlayerCoolerCapacityType,
|
||||||
|
) -> void:
|
||||||
|
_bag = bag
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
|
_item_catalog = item_catalog
|
||||||
|
_storage_capacity = storage_capacity
|
||||||
|
if _bag != null and not _bag.contents_changed.is_connected(_on_contents_changed):
|
||||||
|
_bag.contents_changed.connect(_on_contents_changed)
|
||||||
|
if (
|
||||||
|
_fish_inventory != null
|
||||||
|
and not _fish_inventory.catches_changed.is_connected(_on_contents_changed)
|
||||||
|
):
|
||||||
|
_fish_inventory.catches_changed.connect(_on_contents_changed)
|
||||||
|
if (
|
||||||
|
_storage_capacity != null
|
||||||
|
and not _storage_capacity.capacity_changed.is_connected(
|
||||||
|
_on_storage_capacity_changed
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_storage_capacity.capacity_changed.connect(_on_storage_capacity_changed)
|
||||||
|
_reconcile(true)
|
||||||
|
|
||||||
|
|
||||||
|
func can_accept_item(item_id: StringName) -> bool:
|
||||||
|
if not is_managed_item(item_id):
|
||||||
|
return true
|
||||||
|
var key := item_key(item_id)
|
||||||
|
return _placements.has(key) or _first_free_slot(InventoryContainer.INVENTORY) >= 0
|
||||||
|
|
||||||
|
|
||||||
|
func can_accept_catch(catch_id: StringName = StringName()) -> bool:
|
||||||
|
if not catch_id.is_empty() and _placements.has(catch_key(catch_id)):
|
||||||
|
return true
|
||||||
|
return _first_free_slot(InventoryContainer.INVENTORY) >= 0
|
||||||
|
|
||||||
|
|
||||||
|
func is_managed_item(item_id: StringName) -> bool:
|
||||||
|
var item: ItemDataType = (
|
||||||
|
_item_catalog.get_item_by_id(item_id)
|
||||||
|
if _item_catalog != null else null
|
||||||
|
)
|
||||||
|
return item != null and not item.is_bait() and not item.is_lure()
|
||||||
|
|
||||||
|
|
||||||
|
func is_item_in_inventory(item_id: StringName) -> bool:
|
||||||
|
return _is_entry_in_container(item_key(item_id), InventoryContainer.INVENTORY)
|
||||||
|
|
||||||
|
|
||||||
|
func is_catch_in_inventory(catch_id: StringName) -> bool:
|
||||||
|
return _is_entry_in_container(catch_key(catch_id), InventoryContainer.INVENTORY)
|
||||||
|
|
||||||
|
|
||||||
|
func is_item_carried(item_id: StringName) -> bool:
|
||||||
|
return _is_entry_carried(item_key(item_id))
|
||||||
|
|
||||||
|
|
||||||
|
func is_catch_carried(catch_id: StringName) -> bool:
|
||||||
|
return _is_entry_carried(catch_key(catch_id))
|
||||||
|
|
||||||
|
|
||||||
|
func get_inventory_count() -> int:
|
||||||
|
return get_entries(InventoryContainer.INVENTORY).size()
|
||||||
|
|
||||||
|
|
||||||
|
func get_inventory_capacity() -> int:
|
||||||
|
return INVENTORY_CAPACITIES[_backpack_level]
|
||||||
|
|
||||||
|
|
||||||
|
func get_backpack_level() -> int:
|
||||||
|
return _backpack_level
|
||||||
|
|
||||||
|
|
||||||
|
func get_next_inventory_capacity() -> int:
|
||||||
|
if _backpack_level >= MAX_BACKPACK_LEVEL:
|
||||||
|
return -1
|
||||||
|
return INVENTORY_CAPACITIES[_backpack_level + 1]
|
||||||
|
|
||||||
|
|
||||||
|
func get_next_backpack_cost() -> int:
|
||||||
|
if _backpack_level >= MAX_BACKPACK_LEVEL:
|
||||||
|
return -1
|
||||||
|
return BACKPACK_EXPANSION_COSTS[_backpack_level]
|
||||||
|
|
||||||
|
|
||||||
|
func can_purchase_backpack(wallet: PlayerWalletType) -> bool:
|
||||||
|
var cost := get_next_backpack_cost()
|
||||||
|
return wallet != null and cost >= 0 and wallet.can_afford(cost)
|
||||||
|
|
||||||
|
|
||||||
|
func purchase_backpack(wallet: PlayerWalletType) -> bool:
|
||||||
|
if not can_purchase_backpack(wallet):
|
||||||
|
return false
|
||||||
|
var cost := get_next_backpack_cost()
|
||||||
|
if not wallet.debit(cost):
|
||||||
|
return false
|
||||||
|
_backpack_level += 1
|
||||||
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
||||||
|
layout_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func restore_backpack_level(level: int) -> bool:
|
||||||
|
var validated := clampi(level, 0, MAX_BACKPACK_LEVEL)
|
||||||
|
var changed := _backpack_level != validated
|
||||||
|
_backpack_level = validated
|
||||||
|
if changed:
|
||||||
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
||||||
|
_reconcile(changed)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_storage_count() -> int:
|
||||||
|
return get_entries(InventoryContainer.STORAGE).size()
|
||||||
|
|
||||||
|
|
||||||
|
func get_hotbar_count() -> int:
|
||||||
|
return get_entries(InventoryContainer.HOTBAR).size()
|
||||||
|
|
||||||
|
|
||||||
|
func get_storage_capacity() -> int:
|
||||||
|
return _storage_capacity.get_capacity() if _storage_capacity != null else 0
|
||||||
|
|
||||||
|
|
||||||
|
func get_entries(container: InventoryContainer) -> Array[Dictionary]:
|
||||||
|
var entries: Array[Dictionary] = []
|
||||||
|
for key: String in _placements:
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
if int(placement.get("container", -1)) != int(container):
|
||||||
|
continue
|
||||||
|
var entry: Dictionary = placement.duplicate(true)
|
||||||
|
entry["key"] = key
|
||||||
|
entries.append(entry)
|
||||||
|
entries.sort_custom(
|
||||||
|
func(left: Dictionary, right: Dictionary) -> bool:
|
||||||
|
return int(left.get("slot", -1)) < int(right.get("slot", -1))
|
||||||
|
)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
func get_entry(kind: EntryKind, identity: StringName) -> Dictionary:
|
||||||
|
var key := _entry_key(kind, identity)
|
||||||
|
return _placements.get(key, {}).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func get_container(kind: EntryKind, identity: StringName) -> int:
|
||||||
|
return int(
|
||||||
|
get_entry(kind, identity).get("container", -1)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func get_key_at(container: InventoryContainer, slot: int) -> String:
|
||||||
|
return _key_at(container, slot)
|
||||||
|
|
||||||
|
|
||||||
|
func get_entry_at(container: InventoryContainer, slot: int) -> Dictionary:
|
||||||
|
var key := _key_at(container, slot)
|
||||||
|
if key.is_empty():
|
||||||
|
return {}
|
||||||
|
var entry: Dictionary = _placements[key].duplicate(true)
|
||||||
|
entry["key"] = key
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
func move_entry(
|
||||||
|
kind: EntryKind,
|
||||||
|
identity: StringName,
|
||||||
|
target_container: InventoryContainer,
|
||||||
|
target_slot: int,
|
||||||
|
) -> bool:
|
||||||
|
var key := _entry_key(kind, identity)
|
||||||
|
if not _placements.has(key) or not _is_slot_valid(target_container, target_slot):
|
||||||
|
return false
|
||||||
|
var source: Dictionary = _placements[key]
|
||||||
|
if (
|
||||||
|
int(source.get("container", -1)) == int(target_container)
|
||||||
|
and int(source.get("slot", -1)) == target_slot
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
var displaced_key := _key_at(target_container, target_slot)
|
||||||
|
var source_container: int = int(source.get("container", InventoryContainer.INVENTORY))
|
||||||
|
var source_slot: int = int(source.get("slot", -1))
|
||||||
|
source["container"] = int(target_container)
|
||||||
|
source["slot"] = target_slot
|
||||||
|
_placements[key] = source
|
||||||
|
if not displaced_key.is_empty():
|
||||||
|
var displaced: Dictionary = _placements[displaced_key]
|
||||||
|
displaced["container"] = source_container
|
||||||
|
displaced["slot"] = source_slot
|
||||||
|
_placements[displaced_key] = displaced
|
||||||
|
layout_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func move_entry_to_first_free(
|
||||||
|
kind: EntryKind,
|
||||||
|
identity: StringName,
|
||||||
|
target_container: InventoryContainer,
|
||||||
|
) -> bool:
|
||||||
|
var slot := _first_free_slot(target_container)
|
||||||
|
return slot >= 0 and move_entry(kind, identity, target_container, slot)
|
||||||
|
|
||||||
|
|
||||||
|
func return_hotbar_entry_to_inventory(slot: int) -> bool:
|
||||||
|
var key := _key_at(InventoryContainer.HOTBAR, slot)
|
||||||
|
if key.is_empty():
|
||||||
|
return true
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
return move_entry_to_first_free(
|
||||||
|
int(placement.get("kind", -1)) as EntryKind,
|
||||||
|
StringName(str(placement.get("identity", ""))),
|
||||||
|
InventoryContainer.INVENTORY,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func return_all_hotbar_entries_to_inventory() -> bool:
|
||||||
|
var entries := get_entries(InventoryContainer.HOTBAR)
|
||||||
|
if get_inventory_count() + entries.size() > get_inventory_capacity():
|
||||||
|
return false
|
||||||
|
for entry: Dictionary in entries:
|
||||||
|
if not move_entry_to_first_free(
|
||||||
|
int(entry.get("kind", -1)) as EntryKind,
|
||||||
|
StringName(str(entry.get("identity", ""))),
|
||||||
|
InventoryContainer.INVENTORY,
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func to_save_data() -> Dictionary:
|
||||||
|
var serialized: Array[Dictionary] = []
|
||||||
|
var keys: Array[String] = []
|
||||||
|
keys.assign(_placements.keys())
|
||||||
|
keys.sort()
|
||||||
|
for key: String in keys:
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
serialized.append({
|
||||||
|
"kind": int(placement.get("kind", EntryKind.ITEM)),
|
||||||
|
"identity": str(placement.get("identity", "")),
|
||||||
|
"container": int(placement.get("container", InventoryContainer.INVENTORY)),
|
||||||
|
"slot": int(placement.get("slot", -1)),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
"backpack_level": _backpack_level,
|
||||||
|
"placements": serialized,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func restore_from_save_data(data: Dictionary) -> bool:
|
||||||
|
var level_value: Variant = data.get("backpack_level", 0)
|
||||||
|
if (
|
||||||
|
typeof(level_value) not in [TYPE_INT, TYPE_FLOAT]
|
||||||
|
or not is_equal_approx(float(level_value), floorf(float(level_value)))
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_backpack_level = clampi(int(level_value), 0, MAX_BACKPACK_LEVEL)
|
||||||
|
var restored: Dictionary[String, Dictionary] = {}
|
||||||
|
var values: Variant = data.get("placements", [])
|
||||||
|
if typeof(values) != TYPE_ARRAY:
|
||||||
|
return false
|
||||||
|
for value: Variant in values as Array:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var record := value as Dictionary
|
||||||
|
var kind: int = int(record.get("kind", -1))
|
||||||
|
var identity := StringName(str(record.get("identity", "")))
|
||||||
|
var container: int = int(record.get("container", -1))
|
||||||
|
var slot: int = int(record.get("slot", -1))
|
||||||
|
if (
|
||||||
|
kind not in [EntryKind.ITEM, EntryKind.CATCH]
|
||||||
|
or identity.is_empty()
|
||||||
|
or container not in [
|
||||||
|
InventoryContainer.INVENTORY,
|
||||||
|
InventoryContainer.STORAGE,
|
||||||
|
InventoryContainer.HOTBAR,
|
||||||
|
]
|
||||||
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
var key := _entry_key(kind as EntryKind, identity)
|
||||||
|
if restored.has(key) or _placement_slot_occupied(restored, container, slot):
|
||||||
|
continue
|
||||||
|
restored[key] = {
|
||||||
|
"kind": kind,
|
||||||
|
"identity": identity,
|
||||||
|
"container": container,
|
||||||
|
"slot": slot,
|
||||||
|
}
|
||||||
|
_placements = restored
|
||||||
|
_reconcile(true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func reset_to_defaults() -> void:
|
||||||
|
var capacity_changed := _backpack_level != 0
|
||||||
|
_backpack_level = 0
|
||||||
|
_placements.clear()
|
||||||
|
_reconcile(true)
|
||||||
|
if capacity_changed:
|
||||||
|
backpack_capacity_changed.emit(_backpack_level, get_inventory_capacity())
|
||||||
|
|
||||||
|
|
||||||
|
static func item_key(item_id: StringName) -> String:
|
||||||
|
return "item:%s" % String(item_id)
|
||||||
|
|
||||||
|
|
||||||
|
static func catch_key(catch_id: StringName) -> String:
|
||||||
|
return "catch:%s" % String(catch_id)
|
||||||
|
|
||||||
|
|
||||||
|
func _entry_key(kind: EntryKind, identity: StringName) -> String:
|
||||||
|
return item_key(identity) if kind == EntryKind.ITEM else catch_key(identity)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_entry_in_container(key: String, container: InventoryContainer) -> bool:
|
||||||
|
var placement: Dictionary = _placements.get(key, {})
|
||||||
|
return int(placement.get("container", -1)) == int(container)
|
||||||
|
|
||||||
|
|
||||||
|
func _is_entry_carried(key: String) -> bool:
|
||||||
|
var placement: Dictionary = _placements.get(key, {})
|
||||||
|
var container: int = int(placement.get("container", -1))
|
||||||
|
return container in [InventoryContainer.INVENTORY, InventoryContainer.HOTBAR]
|
||||||
|
|
||||||
|
|
||||||
|
func _on_contents_changed() -> void:
|
||||||
|
_reconcile(false)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_storage_capacity_changed(_level: int, _capacity: int) -> void:
|
||||||
|
_reconcile(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _reconcile(force_signal: bool) -> void:
|
||||||
|
if _reconciling or _bag == null or _fish_inventory == null:
|
||||||
|
return
|
||||||
|
_reconciling = true
|
||||||
|
var expected: Dictionary[String, Dictionary] = {}
|
||||||
|
for owned in _bag.get_all_items():
|
||||||
|
if owned == null or not is_managed_item(owned.item_id):
|
||||||
|
continue
|
||||||
|
var key := item_key(owned.item_id)
|
||||||
|
expected[key] = {"kind": EntryKind.ITEM, "identity": owned.item_id}
|
||||||
|
for fish_catch in _fish_inventory.get_all_catches():
|
||||||
|
if fish_catch == null or not fish_catch.is_valid():
|
||||||
|
continue
|
||||||
|
var key := catch_key(fish_catch.catch_id)
|
||||||
|
expected[key] = {"kind": EntryKind.CATCH, "identity": fish_catch.catch_id}
|
||||||
|
var changed: bool = false
|
||||||
|
for key: String in _placements.keys():
|
||||||
|
if expected.has(key):
|
||||||
|
continue
|
||||||
|
_placements.erase(key)
|
||||||
|
changed = true
|
||||||
|
_normalize_existing_placements()
|
||||||
|
for key: String in expected:
|
||||||
|
if _placements.has(key):
|
||||||
|
continue
|
||||||
|
var target_container := InventoryContainer.INVENTORY
|
||||||
|
var target_slot := _first_free_slot(target_container)
|
||||||
|
if target_slot < 0:
|
||||||
|
target_container = InventoryContainer.STORAGE
|
||||||
|
target_slot = _first_free_slot(target_container)
|
||||||
|
if target_slot < 0:
|
||||||
|
push_error("Inventory and storage cannot fit owned entry '%s'." % key)
|
||||||
|
continue
|
||||||
|
var placement: Dictionary = expected[key].duplicate(true)
|
||||||
|
placement["container"] = int(target_container)
|
||||||
|
placement["slot"] = target_slot
|
||||||
|
_placements[key] = placement
|
||||||
|
changed = true
|
||||||
|
_reconciling = false
|
||||||
|
if changed or force_signal:
|
||||||
|
layout_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _normalize_existing_placements() -> void:
|
||||||
|
var occupied_inventory: Dictionary[int, bool] = {}
|
||||||
|
var occupied_storage: Dictionary[int, bool] = {}
|
||||||
|
var occupied_hotbar: Dictionary[int, bool] = {}
|
||||||
|
var invalid_keys: Array[String] = []
|
||||||
|
var keys: Array[String] = []
|
||||||
|
keys.assign(_placements.keys())
|
||||||
|
keys.sort()
|
||||||
|
for key: String in keys:
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
var container: int = int(placement.get("container", -1))
|
||||||
|
var slot: int = int(placement.get("slot", -1))
|
||||||
|
var occupied: Dictionary[int, bool]
|
||||||
|
match container:
|
||||||
|
InventoryContainer.INVENTORY:
|
||||||
|
occupied = occupied_inventory
|
||||||
|
InventoryContainer.STORAGE:
|
||||||
|
occupied = occupied_storage
|
||||||
|
InventoryContainer.HOTBAR:
|
||||||
|
occupied = occupied_hotbar
|
||||||
|
_:
|
||||||
|
occupied = {}
|
||||||
|
if (
|
||||||
|
container not in [
|
||||||
|
InventoryContainer.INVENTORY,
|
||||||
|
InventoryContainer.STORAGE,
|
||||||
|
InventoryContainer.HOTBAR,
|
||||||
|
]
|
||||||
|
or not _is_slot_valid(container as InventoryContainer, slot)
|
||||||
|
or occupied.has(slot)
|
||||||
|
):
|
||||||
|
invalid_keys.append(key)
|
||||||
|
continue
|
||||||
|
occupied[slot] = true
|
||||||
|
for key: String in invalid_keys:
|
||||||
|
# Invalid records must stop occupying their old slot before free-space
|
||||||
|
# lookup. Otherwise a duplicate can incorrectly reserve a valid slot.
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
_placements.erase(key)
|
||||||
|
var target := InventoryContainer.INVENTORY
|
||||||
|
var slot := _first_free_slot(target)
|
||||||
|
if slot < 0:
|
||||||
|
target = InventoryContainer.STORAGE
|
||||||
|
slot = _first_free_slot(target)
|
||||||
|
if slot < 0:
|
||||||
|
_placements.erase(key)
|
||||||
|
continue
|
||||||
|
placement["container"] = int(target)
|
||||||
|
placement["slot"] = slot
|
||||||
|
_placements[key] = placement
|
||||||
|
|
||||||
|
|
||||||
|
func _first_free_slot(container: InventoryContainer) -> int:
|
||||||
|
var capacity := (
|
||||||
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
||||||
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
||||||
|
else get_storage_capacity()
|
||||||
|
)
|
||||||
|
for slot: int in capacity:
|
||||||
|
if _key_at(container, slot).is_empty():
|
||||||
|
return slot
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
func _key_at(container: InventoryContainer, slot: int) -> String:
|
||||||
|
for key: String in _placements:
|
||||||
|
var placement: Dictionary = _placements[key]
|
||||||
|
if (
|
||||||
|
int(placement.get("container", -1)) == int(container)
|
||||||
|
and int(placement.get("slot", -1)) == slot
|
||||||
|
):
|
||||||
|
return key
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
func _is_slot_valid(container: InventoryContainer, slot: int) -> bool:
|
||||||
|
if slot < 0:
|
||||||
|
return false
|
||||||
|
return slot < (
|
||||||
|
get_inventory_capacity() if container == InventoryContainer.INVENTORY
|
||||||
|
else HOTBAR_SLOT_COUNT if container == InventoryContainer.HOTBAR
|
||||||
|
else get_storage_capacity()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _placement_slot_occupied(
|
||||||
|
placements: Dictionary[String, Dictionary],
|
||||||
|
container: int,
|
||||||
|
slot: int,
|
||||||
|
) -> bool:
|
||||||
|
for placement: Dictionary in placements.values():
|
||||||
|
if (
|
||||||
|
int(placement.get("container", -1)) == container
|
||||||
|
and int(placement.get("slot", -1)) == slot
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
1
inventory/player_inventory_layout.gd.uid
Normal file
1
inventory/player_inventory_layout.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c5cku54c6qgmw
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=39 format=3]
|
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=40 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"]
|
||||||
|
|
@ -38,7 +38,8 @@
|
||||||
[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"]
|
[ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"]
|
||||||
|
[ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"]
|
||||||
|
|
||||||
[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"), ExtResource("39_crab_net")]
|
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"), ExtResource("40_shovel")]
|
||||||
|
|
|
||||||
18
items/catalog/standard_shovel.tres
Normal file
18
items/catalog/standard_shovel.tres
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
|
||||||
|
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
script = ExtResource("1_item")
|
||||||
|
item_id = &"standard_shovel"
|
||||||
|
display_name = "standard shovel"
|
||||||
|
description = "a sturdy shovel for digging up buried finds on the beach."
|
||||||
|
active = true
|
||||||
|
category = 1
|
||||||
|
icon = ExtResource("2_icon")
|
||||||
|
stackable = false
|
||||||
|
max_stack = 1
|
||||||
|
usable = false
|
||||||
|
equippable = true
|
||||||
|
hotbar_allowed = true
|
||||||
|
|
@ -23,6 +23,8 @@ const WEATHER_SEGMENT_HOURS: float = (
|
||||||
const WEATHER_SEGMENT_COUNT: int = (
|
const WEATHER_SEGMENT_COUNT: int = (
|
||||||
WorldWeatherService.DAILY_PLAN_SEGMENT_COUNT
|
WorldWeatherService.DAILY_PLAN_SEGMENT_COUNT
|
||||||
)
|
)
|
||||||
|
const LEGACY_WEATHER_SEGMENT_HOURS: float = 2.0
|
||||||
|
const LEGACY_WEATHER_SEGMENT_COUNT: int = 12
|
||||||
const MAX_JOB_REWARD_COINS: int = 100000
|
const MAX_JOB_REWARD_COINS: int = 100000
|
||||||
const MAX_JOB_REWARD_EXPERIENCE: int = 10000
|
const MAX_JOB_REWARD_EXPERIENCE: int = 10000
|
||||||
|
|
||||||
|
|
@ -341,6 +343,36 @@ static func is_valid_weather_schedule(value: Variant) -> bool:
|
||||||
return WorldWeatherService.is_valid_daily_plan_schedule(value)
|
return WorldWeatherService.is_valid_daily_plan_schedule(value)
|
||||||
|
|
||||||
|
|
||||||
|
static func is_valid_legacy_weather_schedule(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_ARRAY:
|
||||||
|
return false
|
||||||
|
var schedule: Array = value
|
||||||
|
if schedule.size() != LEGACY_WEATHER_SEGMENT_COUNT:
|
||||||
|
return false
|
||||||
|
for index: int in schedule.size():
|
||||||
|
if typeof(schedule[index]) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var entry: Dictionary = schedule[index]
|
||||||
|
var expected_hour: float = fposmod(
|
||||||
|
WorldTimeService.DAY_START_HOUR
|
||||||
|
+ float(index) * LEGACY_WEATHER_SEGMENT_HOURS,
|
||||||
|
WorldTimeService.HOURS_PER_DAY,
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
typeof(entry.get("start_hour")) not in [TYPE_FLOAT, TYPE_INT]
|
||||||
|
or not is_equal_approx(
|
||||||
|
float(entry.get("start_hour", -1.0)), expected_hour
|
||||||
|
)
|
||||||
|
or not is_bounded_integer(
|
||||||
|
entry.get("weather"),
|
||||||
|
WorldWeatherService.Weather.SUNNY,
|
||||||
|
WorldWeatherService.Weather.FOGGY,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
static func _job(
|
static func _job(
|
||||||
id: String,
|
id: String,
|
||||||
title: String,
|
title: String,
|
||||||
|
|
|
||||||
|
|
@ -245,16 +245,30 @@ func restore_from_save_data(data: Dictionary) -> bool:
|
||||||
if not validate_save_data(data):
|
if not validate_save_data(data):
|
||||||
return false
|
return false
|
||||||
var received_board: Dictionary = _active_board.duplicate(true)
|
var received_board: Dictionary = _active_board.duplicate(true)
|
||||||
_host_board = (data.get("host_board", {}) as Dictionary).duplicate(true)
|
var saved_host_board: Dictionary = data.get("host_board", {})
|
||||||
_active_plan_id = str(data.get("active_plan_id", ""))
|
var legacy_weather_board: bool = (
|
||||||
|
not saved_host_board.is_empty()
|
||||||
|
and not validate_board(saved_host_board)
|
||||||
|
and _validate_legacy_board(saved_host_board)
|
||||||
|
)
|
||||||
|
_host_board = (
|
||||||
|
{}
|
||||||
|
if legacy_weather_board
|
||||||
|
else saved_host_board.duplicate(true)
|
||||||
|
)
|
||||||
|
_active_plan_id = (
|
||||||
|
"" if legacy_weather_board else str(data.get("active_plan_id", ""))
|
||||||
|
)
|
||||||
_daily_progress.clear()
|
_daily_progress.clear()
|
||||||
var progress: Dictionary = data.get("daily_progress", {})
|
var progress: Dictionary = data.get("daily_progress", {})
|
||||||
for key: Variant in progress:
|
if not legacy_weather_board:
|
||||||
_daily_progress[str(key)] = int(progress[key])
|
for key: Variant in progress:
|
||||||
|
_daily_progress[str(key)] = int(progress[key])
|
||||||
_daily_completions.clear()
|
_daily_completions.clear()
|
||||||
var completions: Dictionary = data.get("daily_completions", {})
|
var completions: Dictionary = data.get("daily_completions", {})
|
||||||
for key: Variant in completions:
|
if not legacy_weather_board:
|
||||||
_daily_completions[str(key)] = mini(int(completions[key]), 1)
|
for key: Variant in completions:
|
||||||
|
_daily_completions[str(key)] = mini(int(completions[key]), 1)
|
||||||
_daily_clock_hour = float(data.get(
|
_daily_clock_hour = float(data.get(
|
||||||
"daily_clock_hour",
|
"daily_clock_hour",
|
||||||
_world_time.get_time_hours()
|
_world_time.get_time_hours()
|
||||||
|
|
@ -345,6 +359,14 @@ static func validate_board(value: Variant) -> bool:
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
|
if (
|
||||||
|
board.has("calendar_cycle_id")
|
||||||
|
and (
|
||||||
|
typeof(board["calendar_cycle_id"]) != TYPE_STRING
|
||||||
|
or str(board["calendar_cycle_id"]).length() != 10
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
var jobs: Array = board.get("jobs", [])
|
var jobs: Array = board.get("jobs", [])
|
||||||
if jobs.is_empty() or jobs.size() > 8:
|
if jobs.is_empty() or jobs.size() > 8:
|
||||||
return false
|
return false
|
||||||
|
|
@ -378,7 +400,11 @@ static func validate_save_data(value: Variant) -> bool:
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
var host_board: Dictionary = data.get("host_board", {})
|
var host_board: Dictionary = data.get("host_board", {})
|
||||||
if not host_board.is_empty() and not validate_board(host_board):
|
if (
|
||||||
|
not host_board.is_empty()
|
||||||
|
and not validate_board(host_board)
|
||||||
|
and not _validate_legacy_board(host_board)
|
||||||
|
):
|
||||||
return false
|
return false
|
||||||
if (
|
if (
|
||||||
data.has("daily_clock_hour")
|
data.has("daily_clock_hour")
|
||||||
|
|
@ -436,9 +462,43 @@ static func validate_save_data(value: Variant) -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
static func _validate_legacy_board(value: Variant) -> bool:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var board: Dictionary = value
|
||||||
|
if (
|
||||||
|
typeof(board.get("plan_id")) != TYPE_STRING
|
||||||
|
or str(board.get("plan_id", "")).is_empty()
|
||||||
|
or str(board.get("plan_id", "")).length() > 96
|
||||||
|
or not JobCatalog.is_bounded_integer(
|
||||||
|
board.get("cycle"), 0, MAX_PROGRESS_VALUE
|
||||||
|
)
|
||||||
|
or not JobCatalog.is_bounded_integer(
|
||||||
|
board.get("schedule_anchor_index"),
|
||||||
|
0,
|
||||||
|
JobCatalog.LEGACY_WEATHER_SEGMENT_COUNT - 1,
|
||||||
|
)
|
||||||
|
or typeof(board.get("jobs")) != TYPE_ARRAY
|
||||||
|
or not JobCatalog.is_valid_legacy_weather_schedule(
|
||||||
|
board.get("weather_schedule", [])
|
||||||
|
)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
var jobs: Array = board.get("jobs", [])
|
||||||
|
if jobs.is_empty() or jobs.size() > 8:
|
||||||
|
return false
|
||||||
|
for job: Variant in jobs:
|
||||||
|
if not JobCatalog.is_valid_job(job):
|
||||||
|
return false
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func _activate_host_board() -> void:
|
func _activate_host_board() -> void:
|
||||||
if _host_board.is_empty():
|
if _world_time != null:
|
||||||
_generate_host_board(0)
|
_daily_clock_hour = _world_time.get_time_hours()
|
||||||
|
if _host_board.is_empty() or _host_board_is_stale():
|
||||||
|
var next_cycle: int = int(_host_board.get("cycle", -1)) + 1
|
||||||
|
_generate_host_board(maxi(next_cycle, 0))
|
||||||
else:
|
else:
|
||||||
_active_board = _host_board.duplicate(true)
|
_active_board = _host_board.duplicate(true)
|
||||||
_switch_plan(str(_host_board.get("plan_id", "")))
|
_switch_plan(str(_host_board.get("plan_id", "")))
|
||||||
|
|
@ -472,6 +532,11 @@ func _generate_host_board(cycle: int) -> void:
|
||||||
plan_id, jobs, schedule_anchor_index
|
plan_id, jobs, schedule_anchor_index
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
var calendar_cycle_id: String = _world_time.get_calendar_cycle_id(
|
||||||
|
DAILY_REFRESH_HOUR
|
||||||
|
)
|
||||||
|
if not calendar_cycle_id.is_empty():
|
||||||
|
_host_board["calendar_cycle_id"] = calendar_cycle_id
|
||||||
_active_board = _host_board.duplicate(true)
|
_active_board = _host_board.duplicate(true)
|
||||||
_switch_plan(plan_id)
|
_switch_plan(plan_id)
|
||||||
_apply_host_weather_plan()
|
_apply_host_weather_plan()
|
||||||
|
|
@ -514,7 +579,10 @@ func _on_natural_time_advanced(hours: float) -> void:
|
||||||
_progression_ready
|
_progression_ready
|
||||||
and _session != null
|
and _session != null
|
||||||
and _session.is_host()
|
and _session.is_host()
|
||||||
and _crossed_daily_refresh(previous_hour, _daily_clock_hour)
|
and (
|
||||||
|
hours >= WorldTimeService.HOURS_PER_DAY
|
||||||
|
or _crossed_daily_refresh(previous_hour, _daily_clock_hour)
|
||||||
|
)
|
||||||
):
|
):
|
||||||
var cycle: int = int(_host_board.get("cycle", -1)) + 1
|
var cycle: int = int(_host_board.get("cycle", -1)) + 1
|
||||||
_generate_host_board(maxi(cycle, 0))
|
_generate_host_board(maxi(cycle, 0))
|
||||||
|
|
@ -759,6 +827,19 @@ func _current_weather_segment() -> int:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _host_board_is_stale() -> bool:
|
||||||
|
if _world_time == null:
|
||||||
|
return false
|
||||||
|
var current_cycle_id: String = _world_time.get_calendar_cycle_id(
|
||||||
|
DAILY_REFRESH_HOUR
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
not current_cycle_id.is_empty()
|
||||||
|
and str(_host_board.get("calendar_cycle_id", ""))
|
||||||
|
!= current_cycle_id
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
static func _daily_claim_id(
|
static func _daily_claim_id(
|
||||||
plan_id: String,
|
plan_id: String,
|
||||||
job_id: String,
|
job_id: String,
|
||||||
|
|
|
||||||
88
main/main.gd
88
main/main.gd
|
|
@ -33,6 +33,9 @@ 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"
|
||||||
)
|
)
|
||||||
|
const PlayerStorageInteractionType = preload(
|
||||||
|
"res://world/player_storage_interaction.gd"
|
||||||
|
)
|
||||||
const UIPixelationPresenterType = preload(
|
const UIPixelationPresenterType = preload(
|
||||||
"res://ui/ui_pixelation_presenter.gd"
|
"res://ui/ui_pixelation_presenter.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -228,6 +231,7 @@ const SHOP_PATTERN_SCALE: float = 1.75
|
||||||
|
|
||||||
var _gameplay_started: bool = false
|
var _gameplay_started: bool = false
|
||||||
var _shop_interaction: FishingShopInteractionType
|
var _shop_interaction: FishingShopInteractionType
|
||||||
|
var _storage_interaction: PlayerStorageInteractionType
|
||||||
var _title_music_tween: Tween
|
var _title_music_tween: Tween
|
||||||
var _title_music_transition_generation: int = 0
|
var _title_music_transition_generation: int = 0
|
||||||
var _title_music_requested: bool = false
|
var _title_music_requested: bool = false
|
||||||
|
|
@ -432,13 +436,14 @@ func _start_dedicated_server() -> void:
|
||||||
_fail_dedicated_server(_discovery.get_host_status_message())
|
_fail_dedicated_server(_discovery.get_host_status_message())
|
||||||
return
|
return
|
||||||
print(
|
print(
|
||||||
"NETfishing dedicated server ready: %s on %s:%d (%d players, %s)"
|
"NETfishing dedicated server ready: %s on %s:%d (%d players, %s, timezone %s)"
|
||||||
% [
|
% [
|
||||||
config.server_name,
|
config.server_name,
|
||||||
config.bind_address,
|
config.bind_address,
|
||||||
config.port,
|
config.port,
|
||||||
config.max_players,
|
config.max_players,
|
||||||
"public" if config.public_listing else "unlisted",
|
"public" if config.public_listing else "unlisted",
|
||||||
|
str(Time.get_time_zone_from_system().get("name", "local")),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
|
|
@ -581,8 +586,22 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_asset_reservations
|
_asset_reservations
|
||||||
)
|
)
|
||||||
_player.bag.setup(item_catalog)
|
_player.bag.setup(item_catalog)
|
||||||
_player.hotbar.setup(_player.bag, item_catalog, _player.inventory)
|
_player.inventory_layout.setup(
|
||||||
|
_player.bag,
|
||||||
|
_player.inventory,
|
||||||
|
item_catalog,
|
||||||
|
_player.cooler_capacity,
|
||||||
|
)
|
||||||
|
_player.bag.set_inventory_layout(_player.inventory_layout)
|
||||||
|
_player.inventory.set_inventory_layout(_player.inventory_layout)
|
||||||
|
_player.hotbar.setup(
|
||||||
|
_player.bag,
|
||||||
|
item_catalog,
|
||||||
|
_player.inventory,
|
||||||
|
_player.inventory_layout,
|
||||||
|
)
|
||||||
_shop_interaction = _test_world.get_fishing_shop()
|
_shop_interaction = _test_world.get_fishing_shop()
|
||||||
|
_storage_interaction = _test_world.get_player_storage()
|
||||||
if not dedicated:
|
if not dedicated:
|
||||||
_shop_interaction.setup_local_player(_player)
|
_shop_interaction.setup_local_player(_player)
|
||||||
_shop_interaction.local_player_range_changed.connect(
|
_shop_interaction.local_player_range_changed.connect(
|
||||||
|
|
@ -591,6 +610,10 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_game_ui.set_shop_npc_player_in_range(
|
_game_ui.set_shop_npc_player_in_range(
|
||||||
_shop_interaction.is_local_player_in_range()
|
_shop_interaction.is_local_player_in_range()
|
||||||
)
|
)
|
||||||
|
_storage_interaction.setup_local_player(_player)
|
||||||
|
_storage_interaction.local_player_range_changed.connect(
|
||||||
|
_on_storage_range_changed
|
||||||
|
)
|
||||||
_save_manager.setup(
|
_save_manager.setup(
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
_player.collection_log,
|
_player.collection_log,
|
||||||
|
|
@ -598,6 +621,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
fish_catalog,
|
fish_catalog,
|
||||||
_player.bag,
|
_player.bag,
|
||||||
_player.hotbar,
|
_player.hotbar,
|
||||||
|
_player.inventory_layout,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
_player.fishing_upgrades,
|
_player.fishing_upgrades,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
|
|
@ -681,7 +705,7 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_network_player_list.set_surface_drawing_service(
|
_network_player_list.set_surface_drawing_service(
|
||||||
_network_surface_drawing
|
_network_surface_drawing
|
||||||
)
|
)
|
||||||
_network_chat.setup(_network_session, _world_time, _world_weather)
|
_network_chat.setup(_network_session)
|
||||||
_network_fishing.setup(
|
_network_fishing.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
|
|
@ -706,12 +730,15 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_network_fishing,
|
_network_fishing,
|
||||||
_shop_interaction,
|
_shop_interaction,
|
||||||
_player.inventory,
|
_player.inventory,
|
||||||
|
_player.bag,
|
||||||
|
item_catalog,
|
||||||
_player.wallet,
|
_player.wallet,
|
||||||
_player.fish_sale_service,
|
_player.fish_sale_service,
|
||||||
_save_manager,
|
_save_manager,
|
||||||
fish_catalog,
|
fish_catalog,
|
||||||
sale_buyers,
|
sale_buyers,
|
||||||
_asset_reservations
|
_asset_reservations,
|
||||||
|
_player.inventory_layout,
|
||||||
)
|
)
|
||||||
_player_jobs.bind_authoritative_services(_network_fishing, _network_sale)
|
_player_jobs.bind_authoritative_services(_network_fishing, _network_sale)
|
||||||
_network_shop.setup(
|
_network_shop.setup(
|
||||||
|
|
@ -726,7 +753,8 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_player.art_unlocks,
|
_player.art_unlocks,
|
||||||
_save_manager,
|
_save_manager,
|
||||||
_asset_reservations
|
_asset_reservations,
|
||||||
|
_player.inventory_layout,
|
||||||
)
|
)
|
||||||
_fishing_spot.setup(
|
_fishing_spot.setup(
|
||||||
_player,
|
_player,
|
||||||
|
|
@ -759,10 +787,12 @@ func _initialize_application(dedicated: bool) -> void:
|
||||||
_fishing_spot,
|
_fishing_spot,
|
||||||
_player.bag,
|
_player.bag,
|
||||||
_player.hotbar,
|
_player.hotbar,
|
||||||
|
_player.inventory_layout,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
main_shop_buyer_profile,
|
main_shop_buyer_profile,
|
||||||
_player.fishing_upgrades,
|
_player.fishing_upgrades,
|
||||||
_shop_interaction,
|
_shop_interaction,
|
||||||
|
_storage_interaction,
|
||||||
_player.item_effects,
|
_player.item_effects,
|
||||||
_player.cooler_capacity,
|
_player.cooler_capacity,
|
||||||
_network_session,
|
_network_session,
|
||||||
|
|
@ -1264,6 +1294,9 @@ func _input(event: InputEvent) -> void:
|
||||||
return
|
return
|
||||||
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
|
var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
|
||||||
var fishing_shop: FishingShopType = _game_ui.get_fishing_shop()
|
var fishing_shop: FishingShopType = _game_ui.get_fishing_shop()
|
||||||
|
if _game_ui.get_player_storage().consume_escape():
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if fishing_shop.consume_escape():
|
if fishing_shop.consume_escape():
|
||||||
get_viewport().set_input_as_handled()
|
get_viewport().set_input_as_handled()
|
||||||
return
|
return
|
||||||
|
|
@ -1368,6 +1401,15 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||||
or (event is InputEventKey and event.echo)
|
or (event is InputEventKey and event.echo)
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
|
if (
|
||||||
|
_storage_interaction != null
|
||||||
|
and _storage_interaction.is_local_player_in_range()
|
||||||
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
|
and _game_ui.get_player_storage().open_storage()
|
||||||
|
):
|
||||||
|
_game_ui.set_storage_prompt_visible(false)
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
return
|
||||||
if (
|
if (
|
||||||
_shop_interaction != null
|
_shop_interaction != null
|
||||||
and _shop_interaction.is_local_player_in_range()
|
and _shop_interaction.is_local_player_in_range()
|
||||||
|
|
@ -1391,6 +1433,16 @@ func _process(_delta: float) -> void:
|
||||||
show_shop_prompt,
|
show_shop_prompt,
|
||||||
shop_prompt_anchor,
|
shop_prompt_anchor,
|
||||||
)
|
)
|
||||||
|
var show_storage_prompt := _can_show_storage_prompt()
|
||||||
|
var storage_prompt_anchor := (
|
||||||
|
_storage_interaction.get_prompt_anchor_position()
|
||||||
|
if _storage_interaction != null
|
||||||
|
else Vector3.ZERO
|
||||||
|
)
|
||||||
|
_game_ui.set_storage_prompt_visible(
|
||||||
|
show_storage_prompt,
|
||||||
|
storage_prompt_anchor,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
|
||||||
|
|
@ -1879,6 +1931,7 @@ func _on_water_recovery_starting() -> void:
|
||||||
_game_ui.close_player_menu_for_water_recovery()
|
_game_ui.close_player_menu_for_water_recovery()
|
||||||
_game_ui.get_pause_menu().close_for_water_recovery()
|
_game_ui.get_pause_menu().close_for_water_recovery()
|
||||||
_game_ui.get_fishing_shop().close_for_water_recovery()
|
_game_ui.get_fishing_shop().close_for_water_recovery()
|
||||||
|
_game_ui.get_player_storage().close_for_water_recovery()
|
||||||
|
|
||||||
|
|
||||||
func _on_water_recovery_finished() -> void:
|
func _on_water_recovery_finished() -> void:
|
||||||
|
|
@ -1933,6 +1986,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_shovel: bool = (
|
||||||
|
item_id == FishingShopStockType.STANDARD_SHOVEL_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,
|
||||||
|
|
@ -1942,6 +2001,7 @@ func _on_active_hotbar_item_changed(
|
||||||
active_is_art_kit,
|
active_is_art_kit,
|
||||||
)
|
)
|
||||||
_player.set_active_catching_net(active_is_catching_net)
|
_player.set_active_catching_net(active_is_catching_net)
|
||||||
|
_player.set_active_shovel(active_is_shovel)
|
||||||
_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)
|
||||||
|
|
@ -2067,6 +2127,12 @@ func _on_shop_range_changed(in_range: bool) -> void:
|
||||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_storage_range_changed(in_range: bool) -> void:
|
||||||
|
if not in_range:
|
||||||
|
_game_ui.get_player_storage().close_for_range_exit()
|
||||||
|
_game_ui.set_storage_prompt_visible(_can_show_storage_prompt())
|
||||||
|
|
||||||
|
|
||||||
func _can_show_shop_prompt() -> bool:
|
func _can_show_shop_prompt() -> bool:
|
||||||
return (
|
return (
|
||||||
_gameplay_started
|
_gameplay_started
|
||||||
|
|
@ -2076,3 +2142,15 @@ func _can_show_shop_prompt() -> bool:
|
||||||
and not _water_recovery.is_recovery_active()
|
and not _water_recovery.is_recovery_active()
|
||||||
and _fishing_spot.can_open_fishing_shop()
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _can_show_storage_prompt() -> bool:
|
||||||
|
return (
|
||||||
|
_gameplay_started
|
||||||
|
and _storage_interaction != null
|
||||||
|
and _storage_interaction.is_local_player_in_range()
|
||||||
|
and not _game_ui.get_player_storage().visible
|
||||||
|
and not _game_ui.get_fishing_shop().visible
|
||||||
|
and not _water_recovery.is_recovery_active()
|
||||||
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ const BURST_COUNT: int = 3
|
||||||
const WINDOW_COUNT: int = 5
|
const WINDOW_COUNT: int = 5
|
||||||
const WINDOW_SECONDS: float = 10.0
|
const WINDOW_SECONDS: float = 10.0
|
||||||
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
const CALL_COOLDOWN_MILLISECONDS: int = 180
|
||||||
const WORLD_COMMAND_COOLDOWN_MILLISECONDS: int = 500
|
|
||||||
const CALL_PITCH_VARIANTS: Array[float] = [
|
const CALL_PITCH_VARIANTS: Array[float] = [
|
||||||
0.96,
|
0.96,
|
||||||
1.03,
|
1.03,
|
||||||
|
|
@ -23,7 +22,6 @@ signal message_received(message: Dictionary)
|
||||||
signal local_message_confirmed(message: Dictionary)
|
signal local_message_confirmed(message: Dictionary)
|
||||||
signal send_rejected(message: String)
|
signal send_rejected(message: String)
|
||||||
signal history_replaced(messages: Array[Dictionary])
|
signal history_replaced(messages: Array[Dictionary])
|
||||||
signal world_command_finished(success: bool, message: String)
|
|
||||||
signal character_call_received(
|
signal character_call_received(
|
||||||
peer_id: int,
|
peer_id: int,
|
||||||
call_id: String,
|
call_id: String,
|
||||||
|
|
@ -37,22 +35,13 @@ var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
var _rate_times: Dictionary[int, Array] = {}
|
var _rate_times: Dictionary[int, Array] = {}
|
||||||
var _last_call_msec: Dictionary[int, int] = {}
|
var _last_call_msec: Dictionary[int, int] = {}
|
||||||
var _call_variant_indices: Dictionary[int, int] = {}
|
var _call_variant_indices: Dictionary[int, int] = {}
|
||||||
var _last_world_command_msec: Dictionary[int, int] = {}
|
|
||||||
var _sequence: int = 0
|
var _sequence: int = 0
|
||||||
var _peer_names: Dictionary[int, String] = {}
|
var _peer_names: Dictionary[int, String] = {}
|
||||||
var _relationships: PlayerRelationshipStore
|
var _relationships: PlayerRelationshipStore
|
||||||
var _world_time: WorldTimeService
|
|
||||||
var _world_weather: WorldWeatherService
|
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(session: NetworkSession) -> void:
|
||||||
session: NetworkSession,
|
|
||||||
world_time: WorldTimeService,
|
|
||||||
world_weather: WorldWeatherService,
|
|
||||||
) -> void:
|
|
||||||
_session = session
|
_session = session
|
||||||
_world_time = world_time
|
|
||||||
_world_weather = world_weather
|
|
||||||
_session.peer_authenticated.connect(_on_peer_authenticated)
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
_session.state_changed.connect(_on_session_state_changed)
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
|
@ -261,188 +250,6 @@ func broadcast_system_message(body: String) -> bool:
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
func request_world_time_change(phase_name: String) -> bool:
|
|
||||||
var normalized: String = phase_name.strip_edges().to_lower()
|
|
||||||
if not _valid_time_phase(normalized) or _session == null:
|
|
||||||
return false
|
|
||||||
if _session.is_host():
|
|
||||||
return _apply_world_time_change(normalized)
|
|
||||||
if (
|
|
||||||
not _session.is_joined_client()
|
|
||||||
or not _session.is_local_operator()
|
|
||||||
or not _session.supports_server_capability(
|
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return false
|
|
||||||
submit_world_time_command.rpc_id(1, normalized)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func request_world_weather_change(weather_name: String) -> bool:
|
|
||||||
var normalized: String = _normalized_weather_name(weather_name)
|
|
||||||
if normalized.is_empty() or _session == null:
|
|
||||||
return false
|
|
||||||
if _session.is_host():
|
|
||||||
return _apply_world_weather_change(normalized)
|
|
||||||
if (
|
|
||||||
not _session.is_joined_client()
|
|
||||||
or not _session.is_local_operator()
|
|
||||||
or not _session.supports_server_capability(
|
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
|
||||||
)
|
|
||||||
):
|
|
||||||
return false
|
|
||||||
submit_world_weather_command.rpc_id(1, normalized)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
|
||||||
func submit_world_time_command(phase_name: String) -> void:
|
|
||||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
|
||||||
if not _valid_world_command_sender(sender_id):
|
|
||||||
_send_world_command_result(
|
|
||||||
sender_id, false, "Only the host or an operator can change world time."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if not _consume_world_command_rate(sender_id):
|
|
||||||
_send_world_command_result(sender_id, false, "Slow down.")
|
|
||||||
return
|
|
||||||
var success: bool = _apply_world_time_change(
|
|
||||||
phase_name.strip_edges().to_lower()
|
|
||||||
)
|
|
||||||
_send_world_command_result(
|
|
||||||
sender_id,
|
|
||||||
success,
|
|
||||||
"" if success else "World time could not be changed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
|
||||||
func submit_world_weather_command(weather_name: String) -> void:
|
|
||||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
|
||||||
if not _valid_world_command_sender(sender_id):
|
|
||||||
_send_world_command_result(
|
|
||||||
sender_id,
|
|
||||||
false,
|
|
||||||
"Only the host or an operator can change world weather.",
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if not _consume_world_command_rate(sender_id):
|
|
||||||
_send_world_command_result(sender_id, false, "Slow down.")
|
|
||||||
return
|
|
||||||
var success: bool = _apply_world_weather_change(
|
|
||||||
_normalized_weather_name(weather_name)
|
|
||||||
)
|
|
||||||
_send_world_command_result(
|
|
||||||
sender_id,
|
|
||||||
success,
|
|
||||||
"" if success else "World weather could not be changed.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
|
||||||
func receive_world_command_result(success: bool, message: String) -> void:
|
|
||||||
if _session == null or not _session.is_joined_client():
|
|
||||||
return
|
|
||||||
world_command_finished.emit(success, message.left(120))
|
|
||||||
|
|
||||||
|
|
||||||
func _valid_world_command_sender(peer_id: int) -> bool:
|
|
||||||
return (
|
|
||||||
_session != null
|
|
||||||
and _session.is_host()
|
|
||||||
and peer_id > 1
|
|
||||||
and _session.is_authenticated_peer(peer_id)
|
|
||||||
and _session.is_peer_operator(peer_id)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_world_time_change(phase_name: String) -> bool:
|
|
||||||
if _world_time == null or not _valid_time_phase(phase_name):
|
|
||||||
return false
|
|
||||||
var target_hour: float = _time_phase_hour(phase_name)
|
|
||||||
if not _world_time.set_authoritative_time(target_hour):
|
|
||||||
return false
|
|
||||||
broadcast_system_message(
|
|
||||||
"World time set to %s (%s)."
|
|
||||||
% [phase_name, _world_time.get_clock_text()]
|
|
||||||
)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _apply_world_weather_change(weather_name: String) -> bool:
|
|
||||||
if _world_weather == null:
|
|
||||||
return false
|
|
||||||
var normalized: String = _normalized_weather_name(weather_name)
|
|
||||||
if normalized.is_empty():
|
|
||||||
return false
|
|
||||||
var target_weather: WorldWeatherService.Weather = (
|
|
||||||
_weather_for_name(normalized)
|
|
||||||
)
|
|
||||||
if not _world_weather.set_authoritative_weather(target_weather):
|
|
||||||
return false
|
|
||||||
broadcast_system_message("World weather set to %s." % normalized)
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _send_world_command_result(
|
|
||||||
peer_id: int,
|
|
||||||
success: bool,
|
|
||||||
message: String,
|
|
||||||
) -> void:
|
|
||||||
if peer_id > 1 and _session.is_authenticated_peer(peer_id):
|
|
||||||
receive_world_command_result.rpc_id(peer_id, success, message.left(120))
|
|
||||||
|
|
||||||
|
|
||||||
func _consume_world_command_rate(peer_id: int) -> bool:
|
|
||||||
var now_msec: int = Time.get_ticks_msec()
|
|
||||||
var last_msec: int = _last_world_command_msec.get(
|
|
||||||
peer_id, now_msec - WORLD_COMMAND_COOLDOWN_MILLISECONDS
|
|
||||||
)
|
|
||||||
if now_msec - last_msec < WORLD_COMMAND_COOLDOWN_MILLISECONDS:
|
|
||||||
return false
|
|
||||||
_last_world_command_msec[peer_id] = now_msec
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
static func _valid_time_phase(phase_name: String) -> bool:
|
|
||||||
return phase_name in ["dawn", "day", "dusk", "night"]
|
|
||||||
|
|
||||||
|
|
||||||
static func _time_phase_hour(phase_name: String) -> float:
|
|
||||||
match phase_name:
|
|
||||||
"dawn":
|
|
||||||
return WorldTimeService.DAWN_START_HOUR
|
|
||||||
"day":
|
|
||||||
return WorldTimeService.DAWN_END_HOUR
|
|
||||||
"dusk":
|
|
||||||
return WorldTimeService.DUSK_START_HOUR
|
|
||||||
"night":
|
|
||||||
return WorldTimeService.DUSK_END_HOUR
|
|
||||||
return -1.0
|
|
||||||
|
|
||||||
|
|
||||||
static func _normalized_weather_name(weather_name: String) -> String:
|
|
||||||
var normalized: String = weather_name.strip_edges().to_lower()
|
|
||||||
return "clear" if normalized == "sunny" else normalized if normalized in [
|
|
||||||
"clear", "cloudy", "rainy", "foggy"
|
|
||||||
] else ""
|
|
||||||
|
|
||||||
|
|
||||||
static func _weather_for_name(
|
|
||||||
weather_name: String,
|
|
||||||
) -> WorldWeatherService.Weather:
|
|
||||||
match weather_name:
|
|
||||||
"cloudy":
|
|
||||||
return WorldWeatherService.Weather.CLOUDY
|
|
||||||
"rainy":
|
|
||||||
return WorldWeatherService.Weather.RAINY
|
|
||||||
"foggy":
|
|
||||||
return WorldWeatherService.Weather.FOGGY
|
|
||||||
return WorldWeatherService.Weather.SUNNY
|
|
||||||
|
|
||||||
|
|
||||||
func get_history() -> Array[Dictionary]:
|
func get_history() -> Array[Dictionary]:
|
||||||
var result: Array[Dictionary] = []
|
var result: Array[Dictionary] = []
|
||||||
for message: Dictionary in _history:
|
for message: Dictionary in _history:
|
||||||
|
|
@ -638,7 +445,6 @@ func _on_peer_removed(peer_id: int) -> void:
|
||||||
_rate_times.erase(peer_id)
|
_rate_times.erase(peer_id)
|
||||||
_last_call_msec.erase(peer_id)
|
_last_call_msec.erase(peer_id)
|
||||||
_call_variant_indices.erase(peer_id)
|
_call_variant_indices.erase(peer_id)
|
||||||
_last_world_command_msec.erase(peer_id)
|
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
return
|
return
|
||||||
var display_name: String = _peer_names.get(peer_id, "Player")
|
var display_name: String = _peer_names.get(peer_id, "Player")
|
||||||
|
|
@ -701,7 +507,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_rate_times.clear()
|
_rate_times.clear()
|
||||||
_last_call_msec.clear()
|
_last_call_msec.clear()
|
||||||
_call_variant_indices.clear()
|
_call_variant_indices.clear()
|
||||||
_last_world_command_msec.clear()
|
|
||||||
_peer_names.clear()
|
_peer_names.clear()
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
history_replaced.emit([])
|
history_replaced.emit([])
|
||||||
|
|
|
||||||
|
|
@ -253,7 +253,7 @@ func _handle_cast_request(peer_id: int, data: Dictionary) -> void:
|
||||||
_record_and_reject(peer_id, request_id, "Already fishing.")
|
_record_and_reject(peer_id, request_id, "Already fishing.")
|
||||||
return
|
return
|
||||||
if not bool(data["capacity_available"]):
|
if not bool(data["capacity_available"]):
|
||||||
_record_and_reject(peer_id, request_id, "Cooler is full.")
|
_record_and_reject(peer_id, request_id, "Inventory is full.")
|
||||||
return
|
return
|
||||||
var rod := _item_catalog.get_item_by_id(
|
var rod := _item_catalog.get_item_by_id(
|
||||||
StringName(str(data["rod_id"]))
|
StringName(str(data["rod_id"]))
|
||||||
|
|
@ -812,11 +812,8 @@ func _handle_local_capacity_probe(data: Dictionary) -> void:
|
||||||
return
|
return
|
||||||
var can_accept: bool = (
|
var can_accept: bool = (
|
||||||
_local_inventory != null
|
_local_inventory != null
|
||||||
and _local_capacity != null
|
and _local_inventory.can_accept_catch(
|
||||||
and (
|
StringName(data["catch_id"])
|
||||||
_local_inventory.contains_catch_id(StringName(data["catch_id"]))
|
|
||||||
or _local_inventory.get_all_catches().size()
|
|
||||||
< _local_capacity.get_capacity()
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
|
|
@ -864,7 +861,7 @@ func _handle_capacity_response(
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if not can_accept:
|
if not can_accept:
|
||||||
_cancel_attempt(peer_id, "Cooler is full.")
|
_cancel_attempt(peer_id, "Inventory is full.")
|
||||||
return
|
return
|
||||||
_finalize_catch(attempt)
|
_finalize_catch(attempt)
|
||||||
|
|
||||||
|
|
@ -915,11 +912,7 @@ func _apply_target_outcome(data: Dictionary) -> void:
|
||||||
if fish_catch == null:
|
if fish_catch == null:
|
||||||
return
|
return
|
||||||
var already_owned: bool = _local_inventory.contains_catch_id(catch_id)
|
var already_owned: bool = _local_inventory.contains_catch_id(catch_id)
|
||||||
if (
|
if not already_owned and not _local_inventory.can_accept_catch(catch_id):
|
||||||
not already_owned
|
|
||||||
and _local_inventory.get_all_catches().size()
|
|
||||||
>= _local_capacity.get_capacity()
|
|
||||||
):
|
|
||||||
return
|
return
|
||||||
if not already_owned:
|
if not already_owned:
|
||||||
var experience_award: int = (
|
var experience_award: int = (
|
||||||
|
|
|
||||||
|
|
@ -389,6 +389,10 @@ func _apply_equipped(data: Dictionary) -> void:
|
||||||
item_id == FishingShopStockType.CRAB_NET_ID
|
item_id == FishingShopStockType.CRAB_NET_ID
|
||||||
and bool(data["owns_item"]),
|
and bool(data["owns_item"]),
|
||||||
)
|
)
|
||||||
|
avatar.set_active_shovel(
|
||||||
|
item_id == FishingShopStockType.STANDARD_SHOVEL_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"])
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -928,7 +928,9 @@ func _can_receive(attachment: Dictionary) -> bool:
|
||||||
return _wallet.can_credit(int(attachment["amount"]))
|
return _wallet.can_credit(int(attachment["amount"]))
|
||||||
PlayerAssetReservationService.AttachmentType.FISH:
|
PlayerAssetReservationService.AttachmentType.FISH:
|
||||||
return (
|
return (
|
||||||
_inventory.get_all_catches().size() < _cooler_capacity.get_capacity()
|
_inventory.can_accept_catch(
|
||||||
|
StringName(str(attachment["catch_id"]))
|
||||||
|
)
|
||||||
and not _inventory.contains_catch_id(
|
and not _inventory.contains_catch_id(
|
||||||
StringName(str(attachment["catch_id"]))
|
StringName(str(attachment["catch_id"]))
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ const MAIL_RELIABLE_CHANNEL: int = 9
|
||||||
const ENET_CHANNEL_COUNT: int = 10
|
const ENET_CHANNEL_COUNT: int = 10
|
||||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||||
|
const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1"
|
||||||
const WORLD_TIME_CAPABILITY: String = "world_time_v1"
|
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"
|
||||||
|
|
@ -176,6 +177,7 @@ static func make_client_hello(
|
||||||
JOBS_CAPABILITY,
|
JOBS_CAPABILITY,
|
||||||
WORLD_SPAWN_CAPABILITY,
|
WORLD_SPAWN_CAPABILITY,
|
||||||
APPEARANCE_PREVIEW_CAPABILITY,
|
APPEARANCE_PREVIEW_CAPABILITY,
|
||||||
|
BACKPACK_SHOP_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
"cosmetic_snapshot": cosmetic_snapshot,
|
"cosmetic_snapshot": cosmetic_snapshot,
|
||||||
"identity_fingerprint": identity_fingerprint,
|
"identity_fingerprint": identity_fingerprint,
|
||||||
|
|
@ -296,6 +298,7 @@ static func make_server_hello(
|
||||||
"sale_v1",
|
"sale_v1",
|
||||||
"shop_v1",
|
"shop_v1",
|
||||||
ART_SHOP_CAPABILITY,
|
ART_SHOP_CAPABILITY,
|
||||||
|
BACKPACK_SHOP_CAPABILITY,
|
||||||
"item_use_v1",
|
"item_use_v1",
|
||||||
"equipment_v1",
|
"equipment_v1",
|
||||||
"fish_showcase_v1",
|
"fish_showcase_v1",
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ const RELIABLE_CHANNEL: int = NetworkProtocol.SALE_RELIABLE_CHANNEL
|
||||||
const MAX_ID_LENGTH: int = 96
|
const MAX_ID_LENGTH: int = 96
|
||||||
const MAX_CATCH_ID_LENGTH: int = 160
|
const MAX_CATCH_ID_LENGTH: int = 160
|
||||||
const MAX_CATCHES_PER_REQUEST: int = 64
|
const MAX_CATCHES_PER_REQUEST: int = 64
|
||||||
|
const MAX_ITEM_STACKS_PER_REQUEST: int = 20
|
||||||
|
const MAX_ITEM_QUANTITY: int = 999
|
||||||
const MAX_MESSAGE_LENGTH: int = 160
|
const MAX_MESSAGE_LENGTH: int = 160
|
||||||
|
|
||||||
enum Rejection {
|
enum Rejection {
|
||||||
|
|
@ -40,6 +42,7 @@ static func validate_request(data: Variant) -> String:
|
||||||
var session_id: String = payload["session_id"]
|
var session_id: String = payload["session_id"]
|
||||||
var buyer_id: String = str(payload["buyer_id"])
|
var buyer_id: String = str(payload["buyer_id"])
|
||||||
var catches: Array = payload["catches"]
|
var catches: Array = payload["catches"]
|
||||||
|
var items: Array = payload.get("items", [])
|
||||||
if (
|
if (
|
||||||
request_id.is_empty()
|
request_id.is_empty()
|
||||||
or request_id.length() > MAX_ID_LENGTH
|
or request_id.length() > MAX_ID_LENGTH
|
||||||
|
|
@ -47,8 +50,10 @@ static func validate_request(data: Variant) -> String:
|
||||||
or session_id.length() > MAX_ID_LENGTH
|
or session_id.length() > MAX_ID_LENGTH
|
||||||
or buyer_id.is_empty()
|
or buyer_id.is_empty()
|
||||||
or buyer_id.length() > MAX_ID_LENGTH
|
or buyer_id.length() > MAX_ID_LENGTH
|
||||||
or catches.is_empty()
|
or typeof(items) != TYPE_ARRAY
|
||||||
|
or (catches.is_empty() and items.is_empty())
|
||||||
or catches.size() > MAX_CATCHES_PER_REQUEST
|
or catches.size() > MAX_CATCHES_PER_REQUEST
|
||||||
|
or items.size() > MAX_ITEM_STACKS_PER_REQUEST
|
||||||
):
|
):
|
||||||
return "Sale could not be completed."
|
return "Sale could not be completed."
|
||||||
var seen: Dictionary[String, bool] = {}
|
var seen: Dictionary[String, bool] = {}
|
||||||
|
|
@ -74,6 +79,23 @@ static func validate_request(data: Variant) -> String:
|
||||||
):
|
):
|
||||||
return "Sale could not be completed."
|
return "Sale could not be completed."
|
||||||
seen[catch_id] = true
|
seen[catch_id] = true
|
||||||
|
var seen_items: Dictionary[String, bool] = {}
|
||||||
|
for value: Variant in items:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return "Sale could not be completed."
|
||||||
|
var item: Dictionary = value
|
||||||
|
var item_id := str(item.get("item_id", ""))
|
||||||
|
var quantity := int(item.get("quantity", 0))
|
||||||
|
if (
|
||||||
|
item_id.is_empty()
|
||||||
|
or item_id.length() > MAX_ID_LENGTH
|
||||||
|
or seen_items.has(item_id)
|
||||||
|
or typeof(item.get("quantity")) != TYPE_INT
|
||||||
|
or quantity < 1
|
||||||
|
or quantity > MAX_ITEM_QUANTITY
|
||||||
|
):
|
||||||
|
return "Sale could not be completed."
|
||||||
|
seen_items[item_id] = true
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -93,6 +115,8 @@ static func validate_result(data: Variant) -> bool:
|
||||||
and typeof(payload.get("accepted")) == TYPE_BOOL
|
and typeof(payload.get("accepted")) == TYPE_BOOL
|
||||||
and typeof(payload.get("catch_ids")) == TYPE_ARRAY
|
and typeof(payload.get("catch_ids")) == TYPE_ARRAY
|
||||||
and payload["catch_ids"].size() <= MAX_CATCHES_PER_REQUEST
|
and payload["catch_ids"].size() <= MAX_CATCHES_PER_REQUEST
|
||||||
|
and typeof(payload.get("items", [])) == TYPE_ARRAY
|
||||||
|
and payload.get("items", []).size() <= MAX_ITEM_STACKS_PER_REQUEST
|
||||||
and typeof(payload.get("payout")) == TYPE_INT
|
and typeof(payload.get("payout")) == TYPE_INT
|
||||||
and int(payload["payout"]) >= 0
|
and int(payload["payout"]) >= 0
|
||||||
and typeof(payload.get("base_value")) == TYPE_INT
|
and typeof(payload.get("base_value")) == TYPE_INT
|
||||||
|
|
@ -119,4 +143,20 @@ static func validate_result(data: Variant) -> bool:
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
seen[catch_id] = true
|
seen[catch_id] = true
|
||||||
|
var seen_items: Dictionary[String, bool] = {}
|
||||||
|
for value: Variant in payload.get("items", []):
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
return false
|
||||||
|
var item := value as Dictionary
|
||||||
|
var item_id := str(item.get("item_id", ""))
|
||||||
|
if (
|
||||||
|
item_id.is_empty()
|
||||||
|
or item_id.length() > MAX_ID_LENGTH
|
||||||
|
or seen_items.has(item_id)
|
||||||
|
or typeof(item.get("quantity")) != TYPE_INT
|
||||||
|
or int(item["quantity"]) < 1
|
||||||
|
or int(item["quantity"]) > MAX_ITEM_QUANTITY
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
seen_items[item_id] = true
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,10 @@ const FishPoolType = preload("res://fish/fish_pool.gd")
|
||||||
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
|
||||||
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
const FishSaleServiceType = preload("res://economy/fish_sale_service.gd")
|
||||||
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
|
||||||
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
|
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
|
const ItemResalePolicyType = preload("res://economy/item_resale_policy.gd")
|
||||||
|
|
||||||
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
||||||
const PELICAN_BUYER_ID: StringName = &"pelicans"
|
const PELICAN_BUYER_ID: StringName = &"pelicans"
|
||||||
|
|
@ -27,6 +31,8 @@ var _spawn_service: PlayerSpawnService
|
||||||
var _network_fishing: NetworkFishingService
|
var _network_fishing: NetworkFishingService
|
||||||
var _shop_interaction: FishingShopInteraction
|
var _shop_interaction: FishingShopInteraction
|
||||||
var _inventory: FishInventory
|
var _inventory: FishInventory
|
||||||
|
var _bag: PlayerBagType
|
||||||
|
var _item_catalog: ItemCatalogType
|
||||||
var _wallet: PlayerWallet
|
var _wallet: PlayerWallet
|
||||||
var _sale_service: FishSaleServiceType
|
var _sale_service: FishSaleServiceType
|
||||||
var _save_manager: PlayerSaveManager
|
var _save_manager: PlayerSaveManager
|
||||||
|
|
@ -40,8 +46,10 @@ var _applied_results: Dictionary[String, bool] = {}
|
||||||
var _received_results: Dictionary[String, bool] = {}
|
var _received_results: Dictionary[String, bool] = {}
|
||||||
var _pending_local_request_id: String = ""
|
var _pending_local_request_id: String = ""
|
||||||
var _pending_local_catch_ids: Array[StringName] = []
|
var _pending_local_catch_ids: Array[StringName] = []
|
||||||
|
var _pending_local_items: Array[Dictionary] = []
|
||||||
var _pending_local_buyer_id: StringName
|
var _pending_local_buyer_id: StringName
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -50,18 +58,23 @@ func setup(
|
||||||
network_fishing: NetworkFishingService,
|
network_fishing: NetworkFishingService,
|
||||||
shop_interaction: FishingShopInteraction,
|
shop_interaction: FishingShopInteraction,
|
||||||
inventory: FishInventory,
|
inventory: FishInventory,
|
||||||
|
bag: PlayerBagType,
|
||||||
|
item_catalog: ItemCatalogType,
|
||||||
wallet: PlayerWallet,
|
wallet: PlayerWallet,
|
||||||
sale_service: FishSaleServiceType,
|
sale_service: FishSaleServiceType,
|
||||||
save_manager: PlayerSaveManager,
|
save_manager: PlayerSaveManager,
|
||||||
fish_catalog: FishPoolType,
|
fish_catalog: FishPoolType,
|
||||||
buyers: Array[FishBuyerProfileType],
|
buyers: Array[FishBuyerProfileType],
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
|
inventory_layout: PlayerInventoryLayout = null,
|
||||||
) -> void:
|
) -> void:
|
||||||
_session = session
|
_session = session
|
||||||
_spawn_service = spawn_service
|
_spawn_service = spawn_service
|
||||||
_network_fishing = network_fishing
|
_network_fishing = network_fishing
|
||||||
_shop_interaction = shop_interaction
|
_shop_interaction = shop_interaction
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
|
_bag = bag
|
||||||
|
_item_catalog = item_catalog
|
||||||
_wallet = wallet
|
_wallet = wallet
|
||||||
_sale_service = sale_service
|
_sale_service = sale_service
|
||||||
_save_manager = save_manager
|
_save_manager = save_manager
|
||||||
|
|
@ -71,6 +84,7 @@ func setup(
|
||||||
if buyer != null and buyer.is_valid():
|
if buyer != null and buyer.is_valid():
|
||||||
_buyers[buyer.id] = buyer
|
_buyers[buyer.id] = buyer
|
||||||
_reservations = reservations
|
_reservations = reservations
|
||||||
|
_inventory_layout = inventory_layout
|
||||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||||
|
|
@ -102,6 +116,15 @@ func can_request_sale(
|
||||||
func request_local_sale(
|
func request_local_sale(
|
||||||
catch_ids: Array[StringName],
|
catch_ids: Array[StringName],
|
||||||
buyer_id: StringName = PELICAN_BUYER_ID,
|
buyer_id: StringName = PELICAN_BUYER_ID,
|
||||||
|
) -> String:
|
||||||
|
var no_items: Dictionary[StringName, int] = {}
|
||||||
|
return request_local_mixed_sale(catch_ids, no_items, buyer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func request_local_mixed_sale(
|
||||||
|
catch_ids: Array[StringName],
|
||||||
|
item_quantities: Dictionary[StringName, int],
|
||||||
|
buyer_id: StringName = MAIN_SHOP_BUYER_ID,
|
||||||
) -> String:
|
) -> String:
|
||||||
for catch_id: StringName in catch_ids:
|
for catch_id: StringName in catch_ids:
|
||||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||||
|
|
@ -123,7 +146,12 @@ func request_local_sale(
|
||||||
0
|
0
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
if catch_ids.is_empty() or _inventory == null:
|
if (
|
||||||
|
(catch_ids.is_empty() and item_quantities.is_empty())
|
||||||
|
or _inventory == null
|
||||||
|
or _bag == null
|
||||||
|
or _item_catalog == null
|
||||||
|
):
|
||||||
local_sale_finished.emit(
|
local_sale_finished.emit(
|
||||||
"", false, "Sale could not be completed.", _empty_catch_ids(), 0
|
"", false, "Sale could not be completed.", _empty_catch_ids(), 0
|
||||||
)
|
)
|
||||||
|
|
@ -143,15 +171,41 @@ func request_local_sale(
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
evidence.append(fish_catch.to_network_dict())
|
evidence.append(fish_catch.to_network_dict())
|
||||||
|
var item_evidence: Array[Dictionary] = []
|
||||||
|
var item_ids: Array[StringName] = []
|
||||||
|
item_ids.assign(item_quantities.keys())
|
||||||
|
item_ids.sort_custom(
|
||||||
|
func(left: StringName, right: StringName) -> bool:
|
||||||
|
return str(left) < str(right)
|
||||||
|
)
|
||||||
|
for item_id: StringName in item_ids:
|
||||||
|
var quantity := int(item_quantities.get(item_id, 0))
|
||||||
|
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||||
|
if (
|
||||||
|
quantity < 1
|
||||||
|
or not ItemResalePolicyType.is_sellable(item)
|
||||||
|
or quantity > _bag.get_quantity(item_id)
|
||||||
|
or (
|
||||||
|
_reservations != null
|
||||||
|
and quantity > _reservations.get_available_item_quantity(item_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
local_sale_finished.emit(
|
||||||
|
"", false, "That item is no longer available.", _empty_catch_ids(), 0
|
||||||
|
)
|
||||||
|
return ""
|
||||||
|
item_evidence.append({"item_id": str(item_id), "quantity": quantity})
|
||||||
var request_id: String = _new_id("sale")
|
var request_id: String = _new_id("sale")
|
||||||
var request: Dictionary = {
|
var request: Dictionary = {
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
"session_id": _session.get_session_id(),
|
"session_id": _session.get_session_id(),
|
||||||
"buyer_id": str(buyer.id),
|
"buyer_id": str(buyer.id),
|
||||||
"catches": evidence,
|
"catches": evidence,
|
||||||
|
"items": item_evidence,
|
||||||
}
|
}
|
||||||
_pending_local_request_id = request_id
|
_pending_local_request_id = request_id
|
||||||
_pending_local_catch_ids = catch_ids.duplicate()
|
_pending_local_catch_ids = catch_ids.duplicate()
|
||||||
|
_pending_local_items = item_evidence.duplicate(true)
|
||||||
_pending_local_buyer_id = buyer.id
|
_pending_local_buyer_id = buyer.id
|
||||||
local_sale_pending.emit(request_id)
|
local_sale_pending.emit(request_id)
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
|
|
@ -219,7 +273,11 @@ func _handle_sale_request(peer_id: int, data: Dictionary) -> void:
|
||||||
))
|
))
|
||||||
return
|
return
|
||||||
var result: Dictionary = _build_authoritative_result(
|
var result: Dictionary = _build_authoritative_result(
|
||||||
peer_id, request_id, data["catches"], buyer
|
peer_id,
|
||||||
|
request_id,
|
||||||
|
data["catches"],
|
||||||
|
data.get("items", []),
|
||||||
|
buyer,
|
||||||
)
|
)
|
||||||
_pending_by_peer[peer_id] = request_id
|
_pending_by_peer[peer_id] = request_id
|
||||||
_record_and_send(peer_id, result)
|
_record_and_send(peer_id, result)
|
||||||
|
|
@ -229,9 +287,14 @@ func _build_authoritative_result(
|
||||||
peer_id: int,
|
peer_id: int,
|
||||||
request_id: String,
|
request_id: String,
|
||||||
evidence_values: Array,
|
evidence_values: Array,
|
||||||
|
item_values: Array,
|
||||||
buyer: FishBuyerProfileType,
|
buyer: FishBuyerProfileType,
|
||||||
) -> Dictionary:
|
) -> Dictionary:
|
||||||
if _fish_catalog == null or buyer == null:
|
if (
|
||||||
|
buyer == null
|
||||||
|
or (not evidence_values.is_empty() and _fish_catalog == null)
|
||||||
|
or (not item_values.is_empty() and _item_catalog == null)
|
||||||
|
):
|
||||||
return _rejected_result(
|
return _rejected_result(
|
||||||
request_id, "The buyer is unavailable."
|
request_id, "The buyer is unavailable."
|
||||||
)
|
)
|
||||||
|
|
@ -283,6 +346,27 @@ func _build_authoritative_result(
|
||||||
base_value += decoded.sale_value
|
base_value += decoded.sale_value
|
||||||
payout += offer
|
payout += offer
|
||||||
catch_ids.append(str(decoded.catch_id))
|
catch_ids.append(str(decoded.catch_id))
|
||||||
|
var items: Array[Dictionary] = []
|
||||||
|
if not item_values.is_empty() and buyer.id != MAIN_SHOP_BUYER_ID:
|
||||||
|
return _rejected_result(request_id, "This buyer only accepts catches.")
|
||||||
|
for value: Variant in item_values:
|
||||||
|
var evidence := value as Dictionary
|
||||||
|
var item_id := StringName(str(evidence.get("item_id", "")))
|
||||||
|
var quantity := int(evidence.get("quantity", 0))
|
||||||
|
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||||
|
var unit_value := ItemResalePolicyType.get_unit_value(item)
|
||||||
|
if quantity < 1 or unit_value < 0:
|
||||||
|
return _rejected_result(request_id, "That item cannot be sold.")
|
||||||
|
var item_value := unit_value * quantity
|
||||||
|
if (
|
||||||
|
item_value < 0
|
||||||
|
or base_value > 9223372036854775807 - item_value
|
||||||
|
or payout > 9223372036854775807 - item_value
|
||||||
|
):
|
||||||
|
return _rejected_result(request_id, "Sale could not be completed.")
|
||||||
|
base_value += item_value
|
||||||
|
payout += item_value
|
||||||
|
items.append({"item_id": str(item_id), "quantity": quantity})
|
||||||
return {
|
return {
|
||||||
"result_id": _new_id("sale_result"),
|
"result_id": _new_id("sale_result"),
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
|
|
@ -291,6 +375,7 @@ func _build_authoritative_result(
|
||||||
"buyer_id": str(buyer.id),
|
"buyer_id": str(buyer.id),
|
||||||
"accepted": true,
|
"accepted": true,
|
||||||
"catch_ids": catch_ids,
|
"catch_ids": catch_ids,
|
||||||
|
"items": items,
|
||||||
"payout": payout,
|
"payout": payout,
|
||||||
"base_value": base_value,
|
"base_value": base_value,
|
||||||
"message": "Sale complete.",
|
"message": "Sale complete.",
|
||||||
|
|
@ -313,6 +398,7 @@ func _rejected_result(request_id: String, message: String) -> Dictionary:
|
||||||
"target_peer_id": 0,
|
"target_peer_id": 0,
|
||||||
"accepted": false,
|
"accepted": false,
|
||||||
"catch_ids": [],
|
"catch_ids": [],
|
||||||
|
"items": [],
|
||||||
"payout": 0,
|
"payout": 0,
|
||||||
"base_value": 0,
|
"base_value": 0,
|
||||||
"message": message.left(NetworkSaleProtocol.MAX_MESSAGE_LENGTH),
|
"message": message.left(NetworkSaleProtocol.MAX_MESSAGE_LENGTH),
|
||||||
|
|
@ -388,6 +474,12 @@ func _apply_sale_result(data: Dictionary) -> void:
|
||||||
if catch_ids != _pending_local_catch_ids:
|
if catch_ids != _pending_local_catch_ids:
|
||||||
_fail_local_apply(data, "Sale could not be completed.")
|
_fail_local_apply(data, "Sale could not be completed.")
|
||||||
return
|
return
|
||||||
|
var items: Array[Dictionary] = []
|
||||||
|
for value: Variant in data.get("items", []):
|
||||||
|
items.append((value as Dictionary).duplicate(true))
|
||||||
|
if items != _pending_local_items:
|
||||||
|
_fail_local_apply(data, "Sale could not be completed.")
|
||||||
|
return
|
||||||
for catch_id: StringName in catch_ids:
|
for catch_id: StringName in catch_ids:
|
||||||
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
if _reservations != null and _reservations.is_fish_reserved(catch_id):
|
||||||
_fail_local_apply(data, "Reserved in a letter.")
|
_fail_local_apply(data, "Reserved in a letter.")
|
||||||
|
|
@ -396,10 +488,12 @@ func _apply_sale_result(data: Dictionary) -> void:
|
||||||
if buyer == null or not buyer.is_valid():
|
if buyer == null or not buyer.is_valid():
|
||||||
_fail_local_apply(data, "The buyer is unavailable.")
|
_fail_local_apply(data, "The buyer is unavailable.")
|
||||||
return
|
return
|
||||||
var preview: FishSaleResultType = _sale_service.preview_batch(
|
var catch_payout: int = 0
|
||||||
catch_ids, buyer
|
var catch_base_value: int = 0
|
||||||
)
|
var preview: FishSaleResultType
|
||||||
if not preview.is_success():
|
if not catch_ids.is_empty():
|
||||||
|
preview = _sale_service.preview_batch(catch_ids, buyer)
|
||||||
|
if preview != null and not preview.is_success():
|
||||||
var message: String = (
|
var message: String = (
|
||||||
"Favorite catches cannot be sold."
|
"Favorite catches cannot be sold."
|
||||||
if preview.status == FishSaleResultType.Status.FAVORITED
|
if preview.status == FishSaleResultType.Status.FAVORITED
|
||||||
|
|
@ -407,22 +501,63 @@ func _apply_sale_result(data: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
_fail_local_apply(data, message)
|
_fail_local_apply(data, message)
|
||||||
return
|
return
|
||||||
|
if preview != null:
|
||||||
|
catch_payout = preview.payout
|
||||||
|
catch_base_value = preview.base_value
|
||||||
|
var item_payout: int = 0
|
||||||
|
for record: Dictionary in items:
|
||||||
|
var item_id := StringName(str(record.get("item_id", "")))
|
||||||
|
var quantity := int(record.get("quantity", 0))
|
||||||
|
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
|
||||||
|
var unit_value := ItemResalePolicyType.get_unit_value(item)
|
||||||
|
if (
|
||||||
|
quantity < 1
|
||||||
|
or unit_value < 0
|
||||||
|
or quantity > _bag.get_quantity(item_id)
|
||||||
|
or (
|
||||||
|
_reservations != null
|
||||||
|
and quantity > _reservations.get_available_item_quantity(item_id)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_fail_local_apply(data, "That item is no longer available.")
|
||||||
|
return
|
||||||
|
item_payout += unit_value * quantity
|
||||||
if (
|
if (
|
||||||
preview.payout != int(data["payout"])
|
catch_payout + item_payout != int(data["payout"])
|
||||||
or preview.base_value != int(data["base_value"])
|
or catch_base_value + item_payout != int(data["base_value"])
|
||||||
):
|
):
|
||||||
_fail_local_apply(data, "Sale could not be completed.")
|
_fail_local_apply(data, "Sale could not be completed.")
|
||||||
return
|
return
|
||||||
var inventory_snapshot: Array[FishCatch] = _inventory.get_all_catches()
|
var inventory_snapshot: Array[FishCatch] = _inventory.get_all_catches()
|
||||||
var sequence_snapshot: int = _inventory.get_next_catch_sequence()
|
var sequence_snapshot: int = _inventory.get_next_catch_sequence()
|
||||||
var wallet_snapshot: int = _wallet.get_balance()
|
var bag_snapshot := _bag.get_all_items()
|
||||||
var local_result: FishSaleResultType = _sale_service.sell_batch(
|
var layout_snapshot: Dictionary = (
|
||||||
catch_ids, buyer
|
_inventory_layout.to_save_data()
|
||||||
|
if _inventory_layout != null else {}
|
||||||
)
|
)
|
||||||
if not local_result.is_success() or not _save_manager.save_if_dirty():
|
var wallet_snapshot: int = _wallet.get_balance()
|
||||||
|
var applied: bool = true
|
||||||
|
if not catch_ids.is_empty():
|
||||||
|
applied = (
|
||||||
|
_inventory.remove_catches_by_ids(catch_ids).size()
|
||||||
|
== catch_ids.size()
|
||||||
|
)
|
||||||
|
if applied:
|
||||||
|
for record: Dictionary in items:
|
||||||
|
if not _bag.remove_item(
|
||||||
|
StringName(str(record["item_id"])), int(record["quantity"])
|
||||||
|
):
|
||||||
|
applied = false
|
||||||
|
break
|
||||||
|
if applied:
|
||||||
|
applied = _wallet.credit(int(data["payout"]))
|
||||||
|
if not applied or not _save_manager.save_if_dirty():
|
||||||
_inventory.replace_all_catches(
|
_inventory.replace_all_catches(
|
||||||
inventory_snapshot, sequence_snapshot
|
inventory_snapshot, sequence_snapshot
|
||||||
)
|
)
|
||||||
|
_bag.replace_all_items(bag_snapshot)
|
||||||
|
if _inventory_layout != null:
|
||||||
|
_inventory_layout.restore_from_save_data(layout_snapshot)
|
||||||
_wallet.restore_balance(wallet_snapshot)
|
_wallet.restore_balance(wallet_snapshot)
|
||||||
_save_manager.save_if_dirty()
|
_save_manager.save_if_dirty()
|
||||||
_fail_local_apply(data, "Sale could not be completed.")
|
_fail_local_apply(data, "Sale could not be completed.")
|
||||||
|
|
@ -457,6 +592,7 @@ func _finish_local_sale(
|
||||||
) -> void:
|
) -> void:
|
||||||
_pending_local_request_id = ""
|
_pending_local_request_id = ""
|
||||||
_pending_local_catch_ids.clear()
|
_pending_local_catch_ids.clear()
|
||||||
|
_pending_local_items.clear()
|
||||||
_pending_local_buyer_id = StringName()
|
_pending_local_buyer_id = StringName()
|
||||||
local_sale_finished.emit(
|
local_sale_finished.emit(
|
||||||
request_id, accepted, message, catch_ids, payout
|
request_id, accepted, message, catch_ids, payout
|
||||||
|
|
@ -591,6 +727,7 @@ func _clear_session_state() -> void:
|
||||||
_received_results.clear()
|
_received_results.clear()
|
||||||
_pending_local_request_id = ""
|
_pending_local_request_id = ""
|
||||||
_pending_local_catch_ids.clear()
|
_pending_local_catch_ids.clear()
|
||||||
|
_pending_local_items.clear()
|
||||||
_pending_local_buyer_id = StringName()
|
_pending_local_buyer_id = StringName()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,7 @@ func _register_player_host() -> void:
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY,
|
||||||
NetworkProtocol.JOBS_CAPABILITY,
|
NetworkProtocol.JOBS_CAPABILITY,
|
||||||
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
NetworkProtocol.WORLD_SPAWN_CAPABILITY,
|
||||||
|
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||||
|
|
@ -554,6 +555,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
||||||
return str(capability) in PackedStringArray([
|
return str(capability) in PackedStringArray([
|
||||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||||
|
NetworkProtocol.BACKPACK_SHOP_CAPABILITY,
|
||||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||||
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
NetworkProtocol.FISH_QUALITY_CAPABILITY,
|
||||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||||
|
|
@ -1365,6 +1367,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
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.BACKPACK_SHOP_CAPABILITY,
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ extends RefCounted
|
||||||
|
|
||||||
const CAPABILITY: StringName = &"shop_v1"
|
const CAPABILITY: StringName = &"shop_v1"
|
||||||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||||
|
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
||||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||||
const MAX_ID_LENGTH: int = 96
|
const MAX_ID_LENGTH: int = 96
|
||||||
const MAX_MESSAGE_LENGTH: int = 160
|
const MAX_MESSAGE_LENGTH: int = 160
|
||||||
|
|
@ -16,6 +17,7 @@ enum ProductCategory {
|
||||||
COOLER_CAPACITY_UPGRADE,
|
COOLER_CAPACITY_UPGRADE,
|
||||||
ART_KIT,
|
ART_KIT,
|
||||||
ART_UPGRADE,
|
ART_UPGRADE,
|
||||||
|
BACKPACK_CAPACITY_UPGRADE,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const SHOP_ID: StringName = &"main_fishing_shop"
|
||||||
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
||||||
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
||||||
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
||||||
|
const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade"
|
||||||
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
||||||
|
|
||||||
signal local_purchase_pending(request_id: String)
|
signal local_purchase_pending(request_id: String)
|
||||||
|
|
@ -35,6 +36,7 @@ var _bag: PlayerBag
|
||||||
var _item_catalog: ItemCatalog
|
var _item_catalog: ItemCatalog
|
||||||
var _upgrades: PlayerFishingUpgrades
|
var _upgrades: PlayerFishingUpgrades
|
||||||
var _cooler_capacity: PlayerCoolerCapacity
|
var _cooler_capacity: PlayerCoolerCapacity
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
var _art_unlocks: PlayerArtUnlocks
|
var _art_unlocks: PlayerArtUnlocks
|
||||||
var _save_manager: PlayerSaveManager
|
var _save_manager: PlayerSaveManager
|
||||||
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
|
|
@ -60,6 +62,7 @@ func setup(
|
||||||
art_unlocks: PlayerArtUnlocks,
|
art_unlocks: PlayerArtUnlocks,
|
||||||
save_manager: PlayerSaveManager,
|
save_manager: PlayerSaveManager,
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
|
inventory_layout: PlayerInventoryLayout = null,
|
||||||
) -> void:
|
) -> void:
|
||||||
_session = session
|
_session = session
|
||||||
_spawn_service = spawn_service
|
_spawn_service = spawn_service
|
||||||
|
|
@ -73,6 +76,7 @@ func setup(
|
||||||
_art_unlocks = art_unlocks
|
_art_unlocks = art_unlocks
|
||||||
_save_manager = save_manager
|
_save_manager = save_manager
|
||||||
_reservations = reservations
|
_reservations = reservations
|
||||||
|
_inventory_layout = inventory_layout
|
||||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||||
|
|
@ -104,6 +108,18 @@ func can_request_art_purchase() -> bool:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func can_request_backpack_purchase() -> bool:
|
||||||
|
return (
|
||||||
|
can_request_purchase()
|
||||||
|
and (
|
||||||
|
_session.is_host()
|
||||||
|
or _session.supports_server_capability(
|
||||||
|
NetworkShopProtocol.BACKPACK_CAPABILITY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func is_local_purchase_pending() -> bool:
|
func is_local_purchase_pending() -> bool:
|
||||||
return not _pending_local_request.is_empty()
|
return not _pending_local_request.is_empty()
|
||||||
|
|
||||||
|
|
@ -163,6 +179,16 @@ func request_cooler_capacity_upgrade() -> String:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func request_backpack_capacity_upgrade() -> String:
|
||||||
|
return _request_purchase(
|
||||||
|
BACKPACK_PRODUCT_ID,
|
||||||
|
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE,
|
||||||
|
1,
|
||||||
|
_inventory_layout.get_backpack_level()
|
||||||
|
if _inventory_layout != null else 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func request_art_kit() -> String:
|
func request_art_kit() -> String:
|
||||||
return _request_purchase(
|
return _request_purchase(
|
||||||
ArtShopStockType.ART_KIT_ITEM_ID,
|
ArtShopStockType.ART_KIT_ITEM_ID,
|
||||||
|
|
@ -230,6 +256,15 @@ func _request_purchase(
|
||||||
product_id, category, 0, 0,
|
product_id, category, 0, 0,
|
||||||
)
|
)
|
||||||
return ""
|
return ""
|
||||||
|
if (
|
||||||
|
category == NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE
|
||||||
|
and not can_request_backpack_purchase()
|
||||||
|
):
|
||||||
|
local_purchase_finished.emit(
|
||||||
|
"", false, "Backpack upgrades require a newer server.",
|
||||||
|
product_id, category, 0, 0,
|
||||||
|
)
|
||||||
|
return ""
|
||||||
var request_id: String = _new_id("shop")
|
var request_id: String = _new_id("shop")
|
||||||
var request: Dictionary = {
|
var request: Dictionary = {
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
|
|
@ -372,7 +407,7 @@ func _build_authoritative_result(
|
||||||
or current_state + quantity > item.max_stack
|
or current_state + quantity > item.max_stack
|
||||||
):
|
):
|
||||||
rejection = (
|
rejection = (
|
||||||
"Your Bag is full."
|
"Your inventory is full."
|
||||||
if item != null and current_state >= item.max_stack
|
if item != null and current_state >= item.max_stack
|
||||||
else "Purchase could not be completed."
|
else "Purchase could not be completed."
|
||||||
)
|
)
|
||||||
|
|
@ -458,6 +493,17 @@ func _build_authoritative_result(
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
resulting_state = current_state + 1
|
resulting_state = current_state + 1
|
||||||
|
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||||
|
if (
|
||||||
|
product_id != BACKPACK_PRODUCT_ID
|
||||||
|
or current_state >= PlayerInventoryLayout.MAX_BACKPACK_LEVEL
|
||||||
|
):
|
||||||
|
rejection = "Upgrade is already at maximum."
|
||||||
|
else:
|
||||||
|
cost = PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS[
|
||||||
|
current_state
|
||||||
|
]
|
||||||
|
resulting_state = current_state + 1
|
||||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||||
var art_item: ItemDataType = _item_catalog.get_item_by_id(
|
var art_item: ItemDataType = _item_catalog.get_item_by_id(
|
||||||
product_id
|
product_id
|
||||||
|
|
@ -612,6 +658,10 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
||||||
var reel_snapshot: int = _upgrades.get_reel_speed_level()
|
var reel_snapshot: int = _upgrades.get_reel_speed_level()
|
||||||
var barrier_snapshot: int = _upgrades.get_barrier_power_level()
|
var barrier_snapshot: int = _upgrades.get_barrier_power_level()
|
||||||
var cooler_snapshot: int = _cooler_capacity.get_level()
|
var cooler_snapshot: int = _cooler_capacity.get_level()
|
||||||
|
var backpack_snapshot: int = (
|
||||||
|
_inventory_layout.get_backpack_level()
|
||||||
|
if _inventory_layout != null else 0
|
||||||
|
)
|
||||||
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
||||||
var applied: bool = _apply_local_product(data)
|
var applied: bool = _apply_local_product(data)
|
||||||
if not applied or not _save_manager.save_if_dirty():
|
if not applied or not _save_manager.save_if_dirty():
|
||||||
|
|
@ -619,6 +669,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
||||||
_bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
|
_bag.replace_unlocked_bait_ids(bait_unlock_snapshot)
|
||||||
_upgrades.restore_levels(reel_snapshot, barrier_snapshot)
|
_upgrades.restore_levels(reel_snapshot, barrier_snapshot)
|
||||||
_cooler_capacity.restore_level(cooler_snapshot)
|
_cooler_capacity.restore_level(cooler_snapshot)
|
||||||
|
if _inventory_layout != null:
|
||||||
|
_inventory_layout.restore_backpack_level(backpack_snapshot)
|
||||||
_art_unlocks.restore_mask(art_snapshot)
|
_art_unlocks.restore_mask(art_snapshot)
|
||||||
_wallet.restore_balance(wallet_snapshot)
|
_wallet.restore_balance(wallet_snapshot)
|
||||||
_save_manager.save_if_dirty()
|
_save_manager.save_if_dirty()
|
||||||
|
|
@ -687,7 +739,7 @@ func _validate_local_result(data: Dictionary) -> String:
|
||||||
):
|
):
|
||||||
return "Purchase could not be completed."
|
return "Purchase could not be completed."
|
||||||
if not _bag.can_add_item(product_id, data["quantity"]):
|
if not _bag.can_add_item(product_id, data["quantity"]):
|
||||||
return "Your Bag is full."
|
return "Your inventory is full."
|
||||||
NetworkShopProtocol.ProductCategory.ROD:
|
NetworkShopProtocol.ProductCategory.ROD:
|
||||||
var rod := (
|
var rod := (
|
||||||
_item_catalog.get_item_by_id(product_id)
|
_item_catalog.get_item_by_id(product_id)
|
||||||
|
|
@ -719,6 +771,13 @@ func _validate_local_result(data: Dictionary) -> String:
|
||||||
return "Purchase could not be completed."
|
return "Purchase could not be completed."
|
||||||
if _cooler_capacity.get_next_cost() != cost:
|
if _cooler_capacity.get_next_cost() != cost:
|
||||||
return "Purchase could not be completed."
|
return "Purchase could not be completed."
|
||||||
|
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||||
|
if (
|
||||||
|
_inventory_layout == null
|
||||||
|
or _inventory_layout.get_backpack_level() != expected_state
|
||||||
|
or _inventory_layout.get_next_backpack_cost() != cost
|
||||||
|
):
|
||||||
|
return "Purchase could not be completed."
|
||||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||||
var art_item: ItemDataType = (
|
var art_item: ItemDataType = (
|
||||||
_item_catalog.get_item_by_id(product_id)
|
_item_catalog.get_item_by_id(product_id)
|
||||||
|
|
@ -768,6 +827,11 @@ func _apply_local_product(data: Dictionary) -> bool:
|
||||||
return _upgrades.purchase_barrier_power(_wallet)
|
return _upgrades.purchase_barrier_power(_wallet)
|
||||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||||
return _cooler_capacity.purchase(_wallet)
|
return _cooler_capacity.purchase(_wallet)
|
||||||
|
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||||
|
return (
|
||||||
|
_inventory_layout != null
|
||||||
|
and _inventory_layout.purchase_backpack(_wallet)
|
||||||
|
)
|
||||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||||
return (
|
return (
|
||||||
_wallet.debit(int(data["total_cost"]))
|
_wallet.debit(int(data["total_cost"]))
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ var _entity_revisions: Dictionary = {}
|
||||||
var _surface_triangles: Dictionary = {}
|
var _surface_triangles: Dictionary = {}
|
||||||
var _surface_areas: Dictionary = {}
|
var _surface_areas: Dictionary = {}
|
||||||
var _surface_total_areas: Dictionary = {}
|
var _surface_total_areas: Dictionary = {}
|
||||||
|
var _spawn_anchor_positions: Dictionary = {}
|
||||||
var _respawns: Array[Dictionary] = []
|
var _respawns: Array[Dictionary] = []
|
||||||
var _next_respawn_by_type: Dictionary[StringName, float] = {}
|
var _next_respawn_by_type: Dictionary[StringName, float] = {}
|
||||||
var _charge_requests: Dictionary = {}
|
var _charge_requests: Dictionary = {}
|
||||||
|
|
@ -151,10 +152,9 @@ func find_entity_near(
|
||||||
if bool(state.get("locked", false)):
|
if bool(state.get("locked", false)):
|
||||||
continue
|
continue
|
||||||
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
||||||
var distance_squared: float = Vector2(
|
var distance_squared: float = entity_position.distance_squared_to(
|
||||||
entity_position.x - position.x,
|
position
|
||||||
entity_position.z - position.z,
|
)
|
||||||
).length_squared()
|
|
||||||
if distance_squared <= best_distance_squared:
|
if distance_squared <= best_distance_squared:
|
||||||
best_distance_squared = distance_squared
|
best_distance_squared = distance_squared
|
||||||
best_id = entity_id
|
best_id = entity_id
|
||||||
|
|
@ -177,10 +177,9 @@ func find_capture_target(
|
||||||
if entry == null or entry.required_tool_id != tool_id:
|
if entry == null or entry.required_tool_id != tool_id:
|
||||||
continue
|
continue
|
||||||
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
var entity_position: Vector3 = state.get("position", Vector3.ZERO)
|
||||||
var distance_squared: float = Vector2(
|
var distance_squared: float = entity_position.distance_squared_to(
|
||||||
entity_position.x - position.x,
|
position
|
||||||
entity_position.z - position.z,
|
)
|
||||||
).length_squared()
|
|
||||||
if (
|
if (
|
||||||
distance_squared <= entry.capture_radius * entry.capture_radius
|
distance_squared <= entry.capture_radius * entry.capture_radius
|
||||||
and distance_squared <= best_distance_squared
|
and distance_squared <= best_distance_squared
|
||||||
|
|
@ -248,14 +247,30 @@ func _begin_population_if_ready() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _cache_spawn_surface(entry: GatherableDataType) -> void:
|
func _cache_spawn_surface(entry: GatherableDataType) -> void:
|
||||||
if entry == null or _surface_triangles.has(entry.type_id):
|
if (
|
||||||
|
entry == null
|
||||||
|
or _surface_triangles.has(entry.type_id)
|
||||||
|
or _spawn_anchor_positions.has(entry.type_id)
|
||||||
|
):
|
||||||
return
|
return
|
||||||
var triangles: Array[PackedVector3Array] = (
|
if not entry.spawn_anchor_set_id.is_empty():
|
||||||
_world.get_spawn_surface_triangles(
|
var anchor_positions: PackedVector3Array = (
|
||||||
|
_world.get_gatherable_spawn_positions(entry.spawn_anchor_set_id)
|
||||||
|
)
|
||||||
|
_spawn_anchor_positions[entry.type_id] = anchor_positions
|
||||||
|
if anchor_positions.is_empty():
|
||||||
|
push_warning(
|
||||||
|
"No gatherable spawn anchors were found for %s." % entry.type_id
|
||||||
|
)
|
||||||
|
return
|
||||||
|
var triangles: Array[PackedVector3Array]
|
||||||
|
if not entry.diggable_area_id.is_empty():
|
||||||
|
triangles = _world.get_diggable_area_triangles(entry.diggable_area_id)
|
||||||
|
else:
|
||||||
|
triangles = _world.get_spawn_surface_triangles(
|
||||||
entry.surface_materials,
|
entry.surface_materials,
|
||||||
entry.minimum_surface_y,
|
entry.minimum_surface_y,
|
||||||
)
|
)
|
||||||
)
|
|
||||||
var areas := PackedFloat32Array()
|
var areas := PackedFloat32Array()
|
||||||
var total_area: float = 0.0
|
var total_area: float = 0.0
|
||||||
for triangle: PackedVector3Array in triangles:
|
for triangle: PackedVector3Array in triangles:
|
||||||
|
|
@ -279,6 +294,11 @@ func _spawn_entity(entry: GatherableDataType) -> void:
|
||||||
var position: Vector3 = _sample_surface_position(entry)
|
var position: Vector3 = _sample_surface_position(entry)
|
||||||
if not position.is_finite():
|
if not position.is_finite():
|
||||||
return
|
return
|
||||||
|
var target: Vector3 = (
|
||||||
|
position
|
||||||
|
if entry.is_stationary_spawn()
|
||||||
|
else _sample_surface_position(entry, position, entry.roam_radius)
|
||||||
|
)
|
||||||
var quality: int = FishQualityType.roll(_rng)
|
var quality: int = FishQualityType.roll(_rng)
|
||||||
var entity_id: String = _new_id("world")
|
var entity_id: String = _new_id("world")
|
||||||
var state: Dictionary = {
|
var state: Dictionary = {
|
||||||
|
|
@ -286,11 +306,16 @@ func _spawn_entity(entry: GatherableDataType) -> void:
|
||||||
"type_id": entry.type_id,
|
"type_id": entry.type_id,
|
||||||
"data": entry,
|
"data": entry,
|
||||||
"position": position,
|
"position": position,
|
||||||
"target": _sample_surface_position(entry, position, entry.roam_radius),
|
"target": target,
|
||||||
"yaw": _rng.randf_range(-PI, PI),
|
"yaw": _rng.randf_range(-PI, PI),
|
||||||
"quality": quality,
|
"quality": quality,
|
||||||
"revision": 1,
|
"revision": 1,
|
||||||
"locked": false,
|
"locked": false,
|
||||||
|
"expires_at": (
|
||||||
|
_now() + entry.active_lifetime_seconds
|
||||||
|
if entry.active_lifetime_seconds > 0.0
|
||||||
|
else INF
|
||||||
|
),
|
||||||
}
|
}
|
||||||
if not (state["target"] as Vector3).is_finite():
|
if not (state["target"] as Vector3).is_finite():
|
||||||
state["target"] = position
|
state["target"] = position
|
||||||
|
|
@ -311,6 +336,11 @@ func _update_host_entities(delta: float) -> void:
|
||||||
var entry := state.get("data") as GatherableDataType
|
var entry := state.get("data") as GatherableDataType
|
||||||
if entry == null:
|
if entry == null:
|
||||||
continue
|
continue
|
||||||
|
if _now() >= float(state.get("expires_at", INF)):
|
||||||
|
_despawn_entity(entity_id, &"expired", false, true)
|
||||||
|
continue
|
||||||
|
if entry.is_stationary_spawn():
|
||||||
|
continue
|
||||||
var quality: int = _get_state_quality(state)
|
var quality: int = _get_state_quality(state)
|
||||||
var position: Vector3 = state["position"]
|
var position: Vector3 = state["position"]
|
||||||
var target: Vector3 = state["target"]
|
var target: Vector3 = state["target"]
|
||||||
|
|
@ -349,7 +379,7 @@ func _update_host_entities(delta: float) -> void:
|
||||||
state["yaw"] = atan2(-direction.x, -direction.z)
|
state["yaw"] = atan2(-direction.x, -direction.z)
|
||||||
state["position"] = position
|
state["position"] = position
|
||||||
_entities[entity_id] = state
|
_entities[entity_id] = state
|
||||||
if _should_scare(entry, position, quality):
|
if entry.can_be_scared() and _should_scare(entry, position, quality):
|
||||||
_despawn_entity(entity_id, &"scared", true, true)
|
_despawn_entity(entity_id, &"scared", true, true)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -383,6 +413,8 @@ func _sample_surface_position(
|
||||||
maximum_distance: float = INF,
|
maximum_distance: float = INF,
|
||||||
) -> Vector3:
|
) -> Vector3:
|
||||||
_cache_spawn_surface(entry)
|
_cache_spawn_surface(entry)
|
||||||
|
if not entry.spawn_anchor_set_id.is_empty():
|
||||||
|
return _sample_anchor_position(entry, origin, maximum_distance)
|
||||||
var triangles: Array = _surface_triangles.get(entry.type_id, [])
|
var triangles: Array = _surface_triangles.get(entry.type_id, [])
|
||||||
var cumulative_areas: PackedFloat32Array = _surface_areas.get(
|
var cumulative_areas: PackedFloat32Array = _surface_areas.get(
|
||||||
entry.type_id,
|
entry.type_id,
|
||||||
|
|
@ -420,6 +452,44 @@ func _sample_surface_position(
|
||||||
return fallback
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
func _sample_anchor_position(
|
||||||
|
entry: GatherableDataType,
|
||||||
|
origin: Vector3,
|
||||||
|
maximum_distance: float,
|
||||||
|
) -> Vector3:
|
||||||
|
var anchors: PackedVector3Array = _spawn_anchor_positions.get(
|
||||||
|
entry.type_id,
|
||||||
|
PackedVector3Array(),
|
||||||
|
)
|
||||||
|
var candidates := PackedVector3Array()
|
||||||
|
for anchor: Vector3 in anchors:
|
||||||
|
if not anchor.is_finite() or _anchor_is_occupied(entry.type_id, anchor):
|
||||||
|
continue
|
||||||
|
if (
|
||||||
|
origin.is_finite()
|
||||||
|
and is_finite(maximum_distance)
|
||||||
|
and anchor.distance_to(origin) > maximum_distance
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
candidates.append(anchor)
|
||||||
|
if candidates.is_empty():
|
||||||
|
return Vector3(INF, INF, INF)
|
||||||
|
return candidates[_rng.randi_range(0, candidates.size() - 1)]
|
||||||
|
|
||||||
|
|
||||||
|
func _anchor_is_occupied(type_id: StringName, anchor: Vector3) -> bool:
|
||||||
|
for state: Dictionary in _entities.values():
|
||||||
|
if StringName(state.get("type_id", StringName())) != type_id:
|
||||||
|
continue
|
||||||
|
var position: Variant = state.get("position")
|
||||||
|
if (
|
||||||
|
typeof(position) == TYPE_VECTOR3
|
||||||
|
and (position as Vector3).distance_squared_to(anchor) <= 0.0001
|
||||||
|
):
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _broadcast_entity_snapshots() -> void:
|
func _broadcast_entity_snapshots() -> void:
|
||||||
if _entities.is_empty():
|
if _entities.is_empty():
|
||||||
return
|
return
|
||||||
|
|
@ -515,7 +585,9 @@ func _handle_interaction_begin(peer_id: int, data: Dictionary) -> void:
|
||||||
or request_id.length() > MAX_REQUEST_ID_LENGTH
|
or request_id.length() > MAX_REQUEST_ID_LENGTH
|
||||||
or _pending_captures.has(peer_id)
|
or _pending_captures.has(peer_id)
|
||||||
):
|
):
|
||||||
_send_interaction_result(peer_id, request_id, false, "Cannot use the net now.")
|
_send_interaction_result(
|
||||||
|
peer_id, request_id, false, "Cannot use that gathering tool now."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
_charge_requests[peer_id] = {
|
_charge_requests[peer_id] = {
|
||||||
"request_id": request_id,
|
"request_id": request_id,
|
||||||
|
|
@ -556,27 +628,26 @@ func _handle_interaction_finish(peer_id: int, data: Dictionary) -> void:
|
||||||
):
|
):
|
||||||
error = "The catch attempt was invalid."
|
error = "The catch attempt was invalid."
|
||||||
elif state.is_empty() or entry == null or bool(state.get("locked", false)):
|
elif state.is_empty() or entry == null or bool(state.get("locked", false)):
|
||||||
error = "That animal is no longer there."
|
error = "That gathering spot is no longer there."
|
||||||
elif _now() - float(charge.get("started", _now())) + 0.05 < entry.charge_duration:
|
elif _now() - float(charge.get("started", _now())) + 0.05 < entry.charge_duration:
|
||||||
error = "Pull the net all the way back first."
|
error = "Finish readying the tool first."
|
||||||
elif _item_use.get_equipped_item_id(peer_id) != entry.required_tool_id:
|
elif _item_use.get_equipped_item_id(peer_id) != entry.required_tool_id:
|
||||||
error = "Equip the correct gathering tool."
|
error = "Equip the correct gathering tool."
|
||||||
elif avatar == null or not avatar.is_sneaking():
|
elif avatar == null:
|
||||||
error = "Sneak closer before swinging the net."
|
error = "The player is unavailable."
|
||||||
|
elif entry.requires_sneaking and not avatar.is_sneaking():
|
||||||
|
error = "Sneak closer before using the tool."
|
||||||
else:
|
else:
|
||||||
var entity_position: Vector3 = state["position"]
|
var entity_position: Vector3 = state["position"]
|
||||||
var target_distance: float = Vector2(
|
var target_distance: float = entity_position.distance_to(target_position)
|
||||||
entity_position.x - target_position.x,
|
|
||||||
entity_position.z - target_position.z,
|
|
||||||
).length()
|
|
||||||
var player_distance: float = Vector2(
|
var player_distance: float = Vector2(
|
||||||
avatar.global_position.x - entity_position.x,
|
avatar.global_position.x - entity_position.x,
|
||||||
avatar.global_position.z - entity_position.z,
|
avatar.global_position.z - entity_position.z,
|
||||||
).length()
|
).length()
|
||||||
if target_distance > entry.capture_radius:
|
if target_distance > entry.capture_radius:
|
||||||
error = "The net missed."
|
error = "The gathering tool missed."
|
||||||
elif player_distance > entry.interaction_range:
|
elif player_distance > entry.interaction_range:
|
||||||
error = "Move closer before swinging the net."
|
error = "Move closer before using the tool."
|
||||||
if not error.is_empty():
|
if not error.is_empty():
|
||||||
_send_interaction_result(peer_id, request_id, false, error)
|
_send_interaction_result(peer_id, request_id, false, error)
|
||||||
return
|
return
|
||||||
|
|
@ -670,12 +741,7 @@ func _handle_local_capacity_probe(data: Dictionary) -> void:
|
||||||
var catch_id := StringName(str(data.get("catch_id", "")))
|
var catch_id := StringName(str(data.get("catch_id", "")))
|
||||||
var can_accept: bool = (
|
var can_accept: bool = (
|
||||||
_local_inventory != null
|
_local_inventory != null
|
||||||
and _local_capacity != null
|
and _local_inventory.can_accept_catch(catch_id)
|
||||||
and (
|
|
||||||
_local_inventory.contains_catch_id(catch_id)
|
|
||||||
or _local_inventory.get_all_catches().size()
|
|
||||||
< _local_capacity.get_capacity()
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
if _session.is_host():
|
if _session.is_host():
|
||||||
_handle_capacity_response(
|
_handle_capacity_response(
|
||||||
|
|
@ -728,7 +794,7 @@ func _handle_capacity_response(
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if not can_accept:
|
if not can_accept:
|
||||||
_reject_pending_capture(peer_id, "Cooler is full.")
|
_reject_pending_capture(peer_id, "Inventory is full.")
|
||||||
return
|
return
|
||||||
_pending_captures.erase(peer_id)
|
_pending_captures.erase(peer_id)
|
||||||
var entity_id: String = str(pending["entity_id"])
|
var entity_id: String = str(pending["entity_id"])
|
||||||
|
|
@ -813,8 +879,7 @@ func _apply_capture_result(data: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
not already_owned
|
not already_owned
|
||||||
and _local_inventory.get_all_catches().size()
|
and not _local_inventory.can_accept_catch(fish_catch.catch_id)
|
||||||
>= _local_capacity.get_capacity()
|
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
if not already_owned:
|
if not already_owned:
|
||||||
|
|
@ -1138,6 +1203,7 @@ func _clear_world() -> void:
|
||||||
_surface_triangles.clear()
|
_surface_triangles.clear()
|
||||||
_surface_areas.clear()
|
_surface_areas.clear()
|
||||||
_surface_total_areas.clear()
|
_surface_total_areas.clear()
|
||||||
|
_spawn_anchor_positions.clear()
|
||||||
_respawns.clear()
|
_respawns.clear()
|
||||||
_next_respawn_by_type.clear()
|
_next_respawn_by_type.clear()
|
||||||
_charge_requests.clear()
|
_charge_requests.clear()
|
||||||
|
|
|
||||||
|
|
@ -45,20 +45,16 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_time.set_persistence_tracking_enabled(true)
|
_world_time.begin_authoritative_session()
|
||||||
_world_time.begin_session(
|
|
||||||
_world_time.get_persistent_time_hours()
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
if state == NetworkSession.State.JOINED_CLIENT:
|
if state == NetworkSession.State.JOINED_CLIENT:
|
||||||
_active_session_id = _session.get_session_id()
|
_active_session_id = _session.get_session_id()
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_time.set_persistence_tracking_enabled(false)
|
|
||||||
if _session.supports_server_capability(
|
if _session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_TIME_CAPABILITY
|
NetworkProtocol.WORLD_TIME_CAPABILITY
|
||||||
):
|
):
|
||||||
_world_time.begin_session()
|
_world_time.begin_remote_session()
|
||||||
else:
|
else:
|
||||||
_world_time.end_session()
|
_world_time.end_session()
|
||||||
return
|
return
|
||||||
|
|
@ -72,7 +68,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_time.set_persistence_tracking_enabled(false)
|
|
||||||
_world_time.end_session()
|
_world_time.end_session()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ extends Node
|
||||||
|
|
||||||
const SYNC_INTERVAL_SECONDS: float = 15.0
|
const SYNC_INTERVAL_SECONDS: float = 15.0
|
||||||
const MAX_SESSION_ID_LENGTH: int = 96
|
const MAX_SESSION_ID_LENGTH: int = 96
|
||||||
const MAX_REMAINING_SECONDS: float = 1800.0
|
const MAX_REMAINING_SECONDS: float = WorldWeatherService.WEATHER_PERIOD_SECONDS
|
||||||
|
|
||||||
var _session: NetworkSession
|
var _session: NetworkSession
|
||||||
var _world_weather: WorldWeatherService
|
var _world_weather: WorldWeatherService
|
||||||
|
|
@ -44,7 +44,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(true)
|
|
||||||
_world_weather.begin_authoritative_session(
|
_world_weather.begin_authoritative_session(
|
||||||
_active_session_id.hash()
|
_active_session_id.hash()
|
||||||
)
|
)
|
||||||
|
|
@ -53,7 +52,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_active_session_id = _session.get_session_id()
|
_active_session_id = _session.get_session_id()
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(false)
|
|
||||||
if _session.supports_server_capability(
|
if _session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
):
|
):
|
||||||
|
|
@ -71,7 +69,6 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||||
_sequence = 0
|
_sequence = 0
|
||||||
_last_received_sequence = -1
|
_last_received_sequence = -1
|
||||||
_sync_elapsed = 0.0
|
_sync_elapsed = 0.0
|
||||||
_world_weather.set_persistence_tracking_enabled(false)
|
|
||||||
_world_weather.end_session()
|
_world_weather.end_session()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,9 @@ const PlayerItemEffectsType = preload(
|
||||||
const PlayerCoolerCapacityType = preload(
|
const PlayerCoolerCapacityType = preload(
|
||||||
"res://progression/player_cooler_capacity.gd"
|
"res://progression/player_cooler_capacity.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerInventoryLayoutType = preload(
|
||||||
|
"res://inventory/player_inventory_layout.gd"
|
||||||
|
)
|
||||||
const PlayerArtUnlocksType = preload(
|
const PlayerArtUnlocksType = preload(
|
||||||
"res://progression/player_art_unlocks.gd"
|
"res://progression/player_art_unlocks.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -33,6 +36,7 @@ const FishingRodAttachmentScene = preload(
|
||||||
)
|
)
|
||||||
const NetAttachmentScene = preload("res://player/net_attachment.tscn")
|
const NetAttachmentScene = preload("res://player/net_attachment.tscn")
|
||||||
const NetAttachmentType = preload("res://player/net_attachment.gd")
|
const NetAttachmentType = preload("res://player/net_attachment.gd")
|
||||||
|
const ShovelAttachmentScene = preload("res://player/shovel_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"
|
||||||
|
|
@ -347,6 +351,7 @@ class ShowcaseCameraSnapshot:
|
||||||
@export_range(0.05, 1.0, 0.01) var showcase_restore_duration: float = 0.24
|
@export_range(0.05, 1.0, 0.01) var showcase_restore_duration: float = 0.24
|
||||||
@export_range(0.05, 1.5, 0.01) var showcase_camera_transition_duration: float = 0.42
|
@export_range(0.05, 1.5, 0.01) var showcase_camera_transition_duration: float = 0.42
|
||||||
@export_range(-180.0, 180.0, 1.0) var showcase_camera_yaw_offset: float = 180.0
|
@export_range(-180.0, 180.0, 1.0) var showcase_camera_yaw_offset: float = 180.0
|
||||||
|
@export_range(-180.0, 180.0, 1.0) var net_showcase_camera_yaw_offset: float = 0.0
|
||||||
@export_range(-80.0, 80.0, 1.0) var showcase_camera_pitch: float = -8.0
|
@export_range(-80.0, 80.0, 1.0) var showcase_camera_pitch: float = -8.0
|
||||||
@export_range(1.0, 10.0, 0.1) var showcase_camera_zoom_distance: float = 3.6
|
@export_range(1.0, 10.0, 0.1) var showcase_camera_zoom_distance: float = 3.6
|
||||||
@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45
|
@export_range(0.5, 3.0, 0.05) var showcase_camera_target_height: float = 1.45
|
||||||
|
|
@ -394,6 +399,7 @@ class ShowcaseCameraSnapshot:
|
||||||
@onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades
|
@onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades
|
||||||
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
||||||
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
||||||
|
@onready var inventory_layout: PlayerInventoryLayoutType = %InventoryLayout
|
||||||
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
||||||
@onready var experience: PlayerExperienceType = %Experience
|
@onready var experience: PlayerExperienceType = %Experience
|
||||||
@onready var _cast_origin: Marker3D = %CastOrigin
|
@onready var _cast_origin: Marker3D = %CastOrigin
|
||||||
|
|
@ -428,6 +434,7 @@ 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 _showcase_net_visibility: bool = false
|
||||||
|
var _showcase_shovel_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
|
||||||
|
|
@ -502,6 +509,7 @@ var _retract_animation_completed: bool = true
|
||||||
var _fishing_rod: Node3D
|
var _fishing_rod: Node3D
|
||||||
var _catching_net: Node3D
|
var _catching_net: Node3D
|
||||||
var _catching_net_attachment: NetAttachmentType
|
var _catching_net_attachment: NetAttachmentType
|
||||||
|
var _shovel: Node3D
|
||||||
var _net_strike_collision_armed: bool = false
|
var _net_strike_collision_armed: bool = false
|
||||||
var _net_strike_has_previous_sample: bool = false
|
var _net_strike_has_previous_sample: bool = false
|
||||||
var _net_strike_previous_position: Vector3
|
var _net_strike_previous_position: Vector3
|
||||||
|
|
@ -520,6 +528,7 @@ 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 _active_item_is_net: bool = false
|
||||||
|
var _active_item_is_shovel: 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
|
||||||
|
|
@ -545,6 +554,7 @@ func _ready() -> void:
|
||||||
_apply_presented_appearance()
|
_apply_presented_appearance()
|
||||||
_initialize_fishing_rod()
|
_initialize_fishing_rod()
|
||||||
_initialize_catching_net()
|
_initialize_catching_net()
|
||||||
|
_initialize_shovel()
|
||||||
_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
|
||||||
|
|
@ -861,6 +871,23 @@ func _initialize_catching_net() -> void:
|
||||||
_catching_net.visible = false
|
_catching_net.visible = false
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize_shovel() -> 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 shovel.")
|
||||||
|
return
|
||||||
|
var attachment := ShovelAttachmentScene.instantiate() as BoneAttachment3D
|
||||||
|
if attachment == null:
|
||||||
|
push_error("Shovel attachment could not be instantiated.")
|
||||||
|
return
|
||||||
|
skeleton.add_child(attachment)
|
||||||
|
_shovel = attachment.get_node("Shovel") as Node3D
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.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"
|
||||||
|
|
@ -2557,6 +2584,20 @@ func set_active_catching_net(should_show: bool) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_active_shovel(should_show: bool) -> void:
|
||||||
|
_active_item_is_shovel = should_show
|
||||||
|
if not should_show:
|
||||||
|
cancel_net_action_visual()
|
||||||
|
if _shovel == null:
|
||||||
|
return
|
||||||
|
if _showcase_rod_state_stored:
|
||||||
|
_showcase_shovel_visibility = should_show
|
||||||
|
return
|
||||||
|
_shovel.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:
|
||||||
|
|
@ -2625,6 +2666,12 @@ func _apply_active_rod_visibility() -> void:
|
||||||
and not _has_held_show_item()
|
and not _has_held_show_item()
|
||||||
and not _showcase_animation_active
|
and not _showcase_animation_active
|
||||||
)
|
)
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.visible = (
|
||||||
|
_active_item_is_shovel
|
||||||
|
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:
|
||||||
|
|
@ -2655,6 +2702,8 @@ func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
if _catching_net != null:
|
if _catching_net != null:
|
||||||
_catching_net.visible = false
|
_catching_net.visible = false
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.visible = false
|
||||||
if item_changed:
|
if item_changed:
|
||||||
_begin_pocket_visual(
|
_begin_pocket_visual(
|
||||||
PocketVisualTarget.ART_KIT,
|
PocketVisualTarget.ART_KIT,
|
||||||
|
|
@ -2705,6 +2754,8 @@ func set_held_fish(
|
||||||
_fishing_rod.visible = false
|
_fishing_rod.visible = false
|
||||||
if _catching_net != null:
|
if _catching_net != null:
|
||||||
_catching_net.visible = false
|
_catching_net.visible = false
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.visible = false
|
||||||
if item_changed:
|
if item_changed:
|
||||||
_begin_pocket_visual(
|
_begin_pocket_visual(
|
||||||
PocketVisualTarget.HELD_FISH,
|
PocketVisualTarget.HELD_FISH,
|
||||||
|
|
@ -2919,10 +2970,13 @@ func begin_catch_showcase(fish_catch: FishCatchType) -> void:
|
||||||
_pending_net_showcase_catch = fish_catch
|
_pending_net_showcase_catch = fish_catch
|
||||||
_pending_net_showcase_remote = false
|
_pending_net_showcase_remote = false
|
||||||
return
|
return
|
||||||
_begin_catch_showcase_now(fish_catch)
|
_begin_catch_showcase_now(fish_catch, showcase_camera_yaw_offset)
|
||||||
|
|
||||||
|
|
||||||
func _begin_catch_showcase_now(fish_catch: FishCatchType) -> void:
|
func _begin_catch_showcase_now(
|
||||||
|
fish_catch: FishCatchType,
|
||||||
|
camera_yaw_offset_degrees: float,
|
||||||
|
) -> void:
|
||||||
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
|
if _pocket_visual_target == PocketVisualTarget.CATCH_SHOWCASE:
|
||||||
_cancel_pocket_visual()
|
_cancel_pocket_visual()
|
||||||
_showcase_animation_active = true
|
_showcase_animation_active = true
|
||||||
|
|
@ -2933,17 +2987,24 @@ func _begin_catch_showcase_now(fish_catch: FishCatchType) -> void:
|
||||||
if not _showcase_visual_rotation_stored:
|
if not _showcase_visual_rotation_stored:
|
||||||
_showcase_visual_rotation = _visuals.rotation
|
_showcase_visual_rotation = _visuals.rotation
|
||||||
_showcase_visual_rotation_stored = true
|
_showcase_visual_rotation_stored = true
|
||||||
var showcase_camera_position: Vector3 = _begin_showcase_camera_transition()
|
var showcase_camera_position: Vector3 = _begin_showcase_camera_transition(
|
||||||
|
camera_yaw_offset_degrees
|
||||||
|
)
|
||||||
_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 = (
|
_showcase_net_visibility = (
|
||||||
_catching_net.visible if _catching_net != null else false
|
_catching_net.visible if _catching_net != null else false
|
||||||
)
|
)
|
||||||
|
_showcase_shovel_visibility = (
|
||||||
|
_shovel.visible if _shovel != 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:
|
if _catching_net != null:
|
||||||
_catching_net.visible = false
|
_catching_net.visible = false
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.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
|
||||||
|
|
@ -2972,10 +3033,15 @@ func _begin_remote_catch_showcase_now(fish_catch: FishCatchType) -> void:
|
||||||
_showcase_net_visibility = (
|
_showcase_net_visibility = (
|
||||||
_catching_net.visible if _catching_net != null else false
|
_catching_net.visible if _catching_net != null else false
|
||||||
)
|
)
|
||||||
|
_showcase_shovel_visibility = (
|
||||||
|
_shovel.visible if _shovel != 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:
|
if _catching_net != null:
|
||||||
_catching_net.visible = false
|
_catching_net.visible = false
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.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
|
||||||
|
|
@ -3009,7 +3075,10 @@ func _begin_pending_net_showcase() -> void:
|
||||||
if remote:
|
if remote:
|
||||||
_begin_remote_catch_showcase_now(fish_catch)
|
_begin_remote_catch_showcase_now(fish_catch)
|
||||||
else:
|
else:
|
||||||
_begin_catch_showcase_now(fish_catch)
|
_begin_catch_showcase_now(
|
||||||
|
fish_catch,
|
||||||
|
net_showcase_camera_yaw_offset,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func end_catch_showcase(
|
func end_catch_showcase(
|
||||||
|
|
@ -3043,8 +3112,11 @@ func end_catch_showcase(
|
||||||
_fishing_rod.visible = _showcase_rod_visibility
|
_fishing_rod.visible = _showcase_rod_visibility
|
||||||
if _catching_net != null:
|
if _catching_net != null:
|
||||||
_catching_net.visible = _showcase_net_visibility
|
_catching_net.visible = _showcase_net_visibility
|
||||||
|
if _shovel != null:
|
||||||
|
_shovel.visible = _showcase_shovel_visibility
|
||||||
_showcase_rod_visibility = true
|
_showcase_rod_visibility = true
|
||||||
_showcase_net_visibility = false
|
_showcase_net_visibility = false
|
||||||
|
_showcase_shovel_visibility = false
|
||||||
_showcase_rod_state_stored = false
|
_showcase_rod_state_stored = false
|
||||||
if (
|
if (
|
||||||
not _showcase_visual_rotation_stored
|
not _showcase_visual_rotation_stored
|
||||||
|
|
@ -3230,10 +3302,12 @@ func _capture_showcase_camera_snapshot() -> void:
|
||||||
_set_camera_dragging(false)
|
_set_camera_dragging(false)
|
||||||
|
|
||||||
|
|
||||||
func _begin_showcase_camera_transition() -> Vector3:
|
func _begin_showcase_camera_transition(
|
||||||
|
camera_yaw_offset_degrees: float,
|
||||||
|
) -> Vector3:
|
||||||
var target_world_yaw: float = (
|
var target_world_yaw: float = (
|
||||||
_visuals.global_rotation.y
|
_visuals.global_rotation.y
|
||||||
+ deg_to_rad(showcase_camera_yaw_offset)
|
+ deg_to_rad(camera_yaw_offset_degrees)
|
||||||
)
|
)
|
||||||
var target_local_yaw: float = target_world_yaw - global_rotation.y
|
var target_local_yaw: float = target_world_yaw - global_rotation.y
|
||||||
var shortest_target_yaw: float = (
|
var shortest_target_yaw: float = (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=22 format=3]
|
[gd_scene load_steps=23 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||||
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
[ext_resource type="Script" path="res://progression/player_experience.gd" id="15_experience"]
|
||||||
[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
|
[ext_resource type="Script" path="res://player/sprint_dust_trail.gd" id="16_sprint_dust"]
|
||||||
|
[ext_resource type="Script" path="res://inventory/player_inventory_layout.gd" id="17_inventory_layout"]
|
||||||
|
|
||||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||||
radius = 0.45
|
radius = 0.45
|
||||||
|
|
@ -119,6 +120,10 @@ script = ExtResource("9_effects")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("10_capacity")
|
script = ExtResource("10_capacity")
|
||||||
|
|
||||||
|
[node name="InventoryLayout" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("17_inventory_layout")
|
||||||
|
|
||||||
[node name="ArtUnlocks" type="Node" parent="."]
|
[node name="ArtUnlocks" type="Node" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("14_art_unlocks")
|
script = ExtResource("14_art_unlocks")
|
||||||
|
|
|
||||||
68
player/shovel_attachment.tscn
Normal file
68
player/shovel_attachment.tscn
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
[gd_scene load_steps=8 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://player/tool_socket_attachment.gd" id="1_socket"]
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="HandleMaterial"]
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0.36, 0.2, 0.1, 1)
|
||||||
|
metallic = 0.0
|
||||||
|
roughness = 1.0
|
||||||
|
|
||||||
|
[sub_resource type="StandardMaterial3D" id="BladeMaterial"]
|
||||||
|
shading_mode = 0
|
||||||
|
albedo_color = Color(0.34, 0.57, 0.58, 1)
|
||||||
|
metallic = 0.0
|
||||||
|
roughness = 1.0
|
||||||
|
|
||||||
|
[sub_resource type="CylinderMesh" id="ShaftMesh"]
|
||||||
|
top_radius = 0.018
|
||||||
|
bottom_radius = 0.018
|
||||||
|
height = 0.7
|
||||||
|
radial_segments = 8
|
||||||
|
rings = 1
|
||||||
|
material = SubResource("HandleMaterial")
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BladeMesh"]
|
||||||
|
size = Vector3(0.19, 0.25, 0.025)
|
||||||
|
material = SubResource("BladeMaterial")
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="GripCrossMesh"]
|
||||||
|
size = Vector3(0.18, 0.035, 0.035)
|
||||||
|
material = SubResource("HandleMaterial")
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="GripSideMesh"]
|
||||||
|
size = Vector3(0.035, 0.13, 0.035)
|
||||||
|
material = SubResource("HandleMaterial")
|
||||||
|
|
||||||
|
[node name="ShovelAttachment" type="BoneAttachment3D"]
|
||||||
|
bone_name = "rod_socket"
|
||||||
|
script = ExtResource("1_socket")
|
||||||
|
aligned_mount_path = NodePath("Shovel")
|
||||||
|
|
||||||
|
[node name="Shovel" type="Node3D" parent="."]
|
||||||
|
visible = false
|
||||||
|
|
||||||
|
[node name="Shaft" type="MeshInstance3D" parent="Shovel"]
|
||||||
|
position = Vector3(0, 0.35, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("ShaftMesh")
|
||||||
|
|
||||||
|
[node name="Blade" type="MeshInstance3D" parent="Shovel"]
|
||||||
|
position = Vector3(0, 0.78, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("BladeMesh")
|
||||||
|
|
||||||
|
[node name="GripCross" type="MeshInstance3D" parent="Shovel"]
|
||||||
|
position = Vector3(0, -0.09, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("GripCrossMesh")
|
||||||
|
|
||||||
|
[node name="GripLeft" type="MeshInstance3D" parent="Shovel"]
|
||||||
|
position = Vector3(-0.0725, -0.035, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("GripSideMesh")
|
||||||
|
|
||||||
|
[node name="GripRight" type="MeshInstance3D" parent="Shovel"]
|
||||||
|
position = Vector3(0.0725, -0.035, 0)
|
||||||
|
cast_shadow = 0
|
||||||
|
mesh = SubResource("GripSideMesh")
|
||||||
|
|
@ -5,9 +5,13 @@ const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||||
|
|
||||||
signal capacity_changed(level: int, capacity: int)
|
signal capacity_changed(level: int, capacity: int)
|
||||||
|
|
||||||
const CAPACITIES: Array[int] = [12, 18, 24, 32, 40]
|
## Kept under the historical class name so existing saves and network product
|
||||||
const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850]
|
## identifiers migrate without losing their purchased level. This progression
|
||||||
const MAX_LEVEL: int = 4
|
## now controls the player's private storage box rather than carried catches.
|
||||||
|
const CAPACITIES: Array[int] = [9, 18, 27, 36, 45, 54, 63, 72]
|
||||||
|
const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850, 1500, 2500, 4000]
|
||||||
|
const MAX_LEVEL: int = 7
|
||||||
|
const MAX_CAPACITY: int = 72
|
||||||
|
|
||||||
var _capacity_level: int = 0
|
var _capacity_level: int = 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@ const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
const PlayerInventoryLayoutType = preload(
|
||||||
|
"res://inventory/player_inventory_layout.gd"
|
||||||
|
)
|
||||||
const PlayerFishingUpgradesType = preload(
|
const PlayerFishingUpgradesType = preload(
|
||||||
"res://progression/player_fishing_upgrades.gd"
|
"res://progression/player_fishing_upgrades.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -30,7 +33,7 @@ const WorldWeatherServiceType = preload(
|
||||||
)
|
)
|
||||||
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
const PlayerJobServiceType = preload("res://jobs/player_job_service.gd")
|
||||||
|
|
||||||
const SAVE_VERSION: int = 7
|
const SAVE_VERSION: int = 8
|
||||||
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||||
const MAX_SAFE_BALANCE: int = 1000000000000
|
const MAX_SAFE_BALANCE: int = 1000000000000
|
||||||
|
|
||||||
|
|
@ -47,6 +50,7 @@ class LoadSnapshot:
|
||||||
var hotbar_slots: Array[StringName] = []
|
var hotbar_slots: Array[StringName] = []
|
||||||
var fish_hotbar_slots: Array[StringName] = []
|
var fish_hotbar_slots: Array[StringName] = []
|
||||||
var selected_hotbar_slot: int = 0
|
var selected_hotbar_slot: int = 0
|
||||||
|
var inventory_layout_data: Dictionary = {}
|
||||||
var reel_speed_level: int = 0
|
var reel_speed_level: int = 0
|
||||||
var barrier_power_level: int = 0
|
var barrier_power_level: int = 0
|
||||||
var cooler_capacity_level: int = 0
|
var cooler_capacity_level: int = 0
|
||||||
|
|
@ -71,6 +75,7 @@ var _wallet: PlayerWalletType
|
||||||
var _catalog: FishPoolType
|
var _catalog: FishPoolType
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
var _hotbar: PlayerHotbarType
|
var _hotbar: PlayerHotbarType
|
||||||
|
var _inventory_layout: PlayerInventoryLayoutType
|
||||||
var _item_catalog: ItemCatalogType
|
var _item_catalog: ItemCatalogType
|
||||||
var _fishing_upgrades: PlayerFishingUpgradesType
|
var _fishing_upgrades: PlayerFishingUpgradesType
|
||||||
var _cooler_capacity: PlayerCoolerCapacityType
|
var _cooler_capacity: PlayerCoolerCapacityType
|
||||||
|
|
@ -117,6 +122,7 @@ func setup(
|
||||||
catalog: FishPoolType,
|
catalog: FishPoolType,
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
hotbar: PlayerHotbarType,
|
hotbar: PlayerHotbarType,
|
||||||
|
inventory_layout: PlayerInventoryLayoutType,
|
||||||
item_catalog: ItemCatalogType,
|
item_catalog: ItemCatalogType,
|
||||||
fishing_upgrades: PlayerFishingUpgradesType,
|
fishing_upgrades: PlayerFishingUpgradesType,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
|
|
@ -132,6 +138,7 @@ func setup(
|
||||||
_catalog = catalog
|
_catalog = catalog
|
||||||
_bag = bag
|
_bag = bag
|
||||||
_hotbar = hotbar
|
_hotbar = hotbar
|
||||||
|
_inventory_layout = inventory_layout
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
_fishing_upgrades = fishing_upgrades
|
_fishing_upgrades = fishing_upgrades
|
||||||
_cooler_capacity = cooler_capacity
|
_cooler_capacity = cooler_capacity
|
||||||
|
|
@ -147,6 +154,7 @@ func setup(
|
||||||
and _catalog != null
|
and _catalog != null
|
||||||
and _bag != null
|
and _bag != null
|
||||||
and _hotbar != null
|
and _hotbar != null
|
||||||
|
and _inventory_layout != null
|
||||||
and _item_catalog != null
|
and _item_catalog != null
|
||||||
and _fishing_upgrades != null
|
and _fishing_upgrades != null
|
||||||
and _cooler_capacity != null
|
and _cooler_capacity != null
|
||||||
|
|
@ -171,6 +179,8 @@ func setup(
|
||||||
_bag.contents_changed.connect(_mark_dirty)
|
_bag.contents_changed.connect(_mark_dirty)
|
||||||
if not _hotbar.slots_changed.is_connected(_mark_dirty):
|
if not _hotbar.slots_changed.is_connected(_mark_dirty):
|
||||||
_hotbar.slots_changed.connect(_mark_dirty)
|
_hotbar.slots_changed.connect(_mark_dirty)
|
||||||
|
if not _inventory_layout.layout_changed.is_connected(_mark_dirty):
|
||||||
|
_inventory_layout.layout_changed.connect(_mark_dirty)
|
||||||
if not _hotbar.selected_slot_changed.is_connected(
|
if not _hotbar.selected_slot_changed.is_connected(
|
||||||
_on_selected_hotbar_slot_changed
|
_on_selected_hotbar_slot_changed
|
||||||
):
|
):
|
||||||
|
|
@ -263,11 +273,6 @@ func load_player_data() -> bool:
|
||||||
_bag.replace_all_items(snapshot.bag_items)
|
_bag.replace_all_items(snapshot.bag_items)
|
||||||
and _bag.replace_unlocked_bait_ids(snapshot.unlocked_bait_ids)
|
and _bag.replace_unlocked_bait_ids(snapshot.unlocked_bait_ids)
|
||||||
)
|
)
|
||||||
var hotbar_restored: bool = _hotbar.replace_state(
|
|
||||||
snapshot.hotbar_slots,
|
|
||||||
snapshot.selected_hotbar_slot,
|
|
||||||
snapshot.fish_hotbar_slots,
|
|
||||||
)
|
|
||||||
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
|
var upgrades_restored: bool = _fishing_upgrades.restore_levels(
|
||||||
snapshot.reel_speed_level,
|
snapshot.reel_speed_level,
|
||||||
snapshot.barrier_power_level
|
snapshot.barrier_power_level
|
||||||
|
|
@ -275,6 +280,15 @@ func load_player_data() -> bool:
|
||||||
var cooler_restored: bool = _cooler_capacity.restore_level(
|
var cooler_restored: bool = _cooler_capacity.restore_level(
|
||||||
snapshot.cooler_capacity_level
|
snapshot.cooler_capacity_level
|
||||||
)
|
)
|
||||||
|
var layout_restored: bool = _inventory_layout.restore_from_save_data(
|
||||||
|
snapshot.inventory_layout_data
|
||||||
|
)
|
||||||
|
var hotbar_restored: bool = _hotbar.replace_state(
|
||||||
|
snapshot.hotbar_slots,
|
||||||
|
snapshot.selected_hotbar_slot,
|
||||||
|
snapshot.fish_hotbar_slots,
|
||||||
|
false,
|
||||||
|
)
|
||||||
var art_restored: bool = _art_unlocks.restore_mask(
|
var art_restored: bool = _art_unlocks.restore_mask(
|
||||||
snapshot.art_unlock_mask
|
snapshot.art_unlock_mask
|
||||||
)
|
)
|
||||||
|
|
@ -300,13 +314,36 @@ func load_player_data() -> bool:
|
||||||
or not hotbar_restored
|
or not hotbar_restored
|
||||||
or not upgrades_restored
|
or not upgrades_restored
|
||||||
or not cooler_restored
|
or not cooler_restored
|
||||||
|
or not layout_restored
|
||||||
or not art_restored
|
or not art_restored
|
||||||
or not experience_restored
|
or not experience_restored
|
||||||
or not world_time_restored
|
or not world_time_restored
|
||||||
or not world_weather_restored
|
or not world_weather_restored
|
||||||
or not jobs_restored
|
or not jobs_restored
|
||||||
):
|
):
|
||||||
push_error("Validated player save could not be restored.")
|
push_error(
|
||||||
|
(
|
||||||
|
"Validated player save could not be restored: "
|
||||||
|
+ "inventory=%s collection=%s wallet=%s bag=%s hotbar=%s "
|
||||||
|
+ "upgrades=%s cooler=%s layout=%s art=%s experience=%s "
|
||||||
|
+ "time=%s weather=%s jobs=%s"
|
||||||
|
)
|
||||||
|
% [
|
||||||
|
inventory_restored,
|
||||||
|
collection_restored,
|
||||||
|
wallet_restored,
|
||||||
|
bag_restored,
|
||||||
|
hotbar_restored,
|
||||||
|
upgrades_restored,
|
||||||
|
cooler_restored,
|
||||||
|
layout_restored,
|
||||||
|
art_restored,
|
||||||
|
experience_restored,
|
||||||
|
world_time_restored,
|
||||||
|
world_weather_restored,
|
||||||
|
jobs_restored,
|
||||||
|
]
|
||||||
|
)
|
||||||
return false
|
return false
|
||||||
|
|
||||||
_is_dirty = false
|
_is_dirty = false
|
||||||
|
|
@ -541,6 +578,7 @@ func _build_save_dictionary() -> Dictionary:
|
||||||
"slots": serialized_slots,
|
"slots": serialized_slots,
|
||||||
"fish_slots": serialized_fish_slots,
|
"fish_slots": serialized_fish_slots,
|
||||||
},
|
},
|
||||||
|
"inventory_layout": _inventory_layout.to_save_data(),
|
||||||
"upgrades": _fishing_upgrades.to_save_data(),
|
"upgrades": _fishing_upgrades.to_save_data(),
|
||||||
"cooler": _cooler_capacity.to_save_data(),
|
"cooler": _cooler_capacity.to_save_data(),
|
||||||
"art": _art_unlocks.to_save_data(),
|
"art": _art_unlocks.to_save_data(),
|
||||||
|
|
@ -563,6 +601,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
or typeof(save_data.get("inventory")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("bag")) != TYPE_DICTIONARY
|
or typeof(save_data.get("bag")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
|
or typeof(save_data.get("hotbar")) != TYPE_DICTIONARY
|
||||||
|
or typeof(save_data.get("inventory_layout")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
or typeof(save_data.get("experience")) != TYPE_DICTIONARY
|
||||||
or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
|
or typeof(save_data.get("jobs")) != TYPE_DICTIONARY
|
||||||
):
|
):
|
||||||
|
|
@ -572,6 +611,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
var inventory_data: Dictionary = save_data["inventory"]
|
var inventory_data: Dictionary = save_data["inventory"]
|
||||||
var bag_data: Dictionary = save_data["bag"]
|
var bag_data: Dictionary = save_data["bag"]
|
||||||
var hotbar_data: Dictionary = save_data["hotbar"]
|
var hotbar_data: Dictionary = save_data["hotbar"]
|
||||||
|
var inventory_layout_data: Dictionary = save_data["inventory_layout"]
|
||||||
var experience_data: Dictionary = save_data["experience"]
|
var experience_data: Dictionary = save_data["experience"]
|
||||||
var jobs_data: Dictionary = save_data["jobs"]
|
var jobs_data: Dictionary = save_data["jobs"]
|
||||||
var world_data: Dictionary = {}
|
var world_data: Dictionary = {}
|
||||||
|
|
@ -624,6 +664,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var snapshot := LoadSnapshot.new()
|
var snapshot := LoadSnapshot.new()
|
||||||
|
snapshot.inventory_layout_data = inventory_layout_data.duplicate(true)
|
||||||
snapshot.wallet_balance = balance
|
snapshot.wallet_balance = balance
|
||||||
snapshot.total_experience = _read_integer(
|
snapshot.total_experience = _read_integer(
|
||||||
experience_data["total_experience"],
|
experience_data["total_experience"],
|
||||||
|
|
@ -922,6 +963,8 @@ func _migrate_save(
|
||||||
migrated = _migrate_version_5_to_6(migrated)
|
migrated = _migrate_version_5_to_6(migrated)
|
||||||
6:
|
6:
|
||||||
migrated = _migrate_version_6_to_7(migrated)
|
migrated = _migrate_version_6_to_7(migrated)
|
||||||
|
7:
|
||||||
|
migrated = _migrate_version_7_to_8(migrated)
|
||||||
_:
|
_:
|
||||||
return {}
|
return {}
|
||||||
if migrated.is_empty():
|
if migrated.is_empty():
|
||||||
|
|
@ -1040,6 +1083,124 @@ func _migrate_version_6_to_7(data: Dictionary) -> Dictionary:
|
||||||
return migrated
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
|
func _migrate_version_7_to_8(data: Dictionary) -> Dictionary:
|
||||||
|
var migrated: Dictionary = data.duplicate(true)
|
||||||
|
if (
|
||||||
|
typeof(migrated.get("bag")) != TYPE_DICTIONARY
|
||||||
|
or typeof(migrated.get("inventory")) != TYPE_DICTIONARY
|
||||||
|
or typeof(migrated.get("hotbar")) != TYPE_DICTIONARY
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
var bag_data: Dictionary = migrated["bag"]
|
||||||
|
var inventory_data: Dictionary = migrated["inventory"]
|
||||||
|
var hotbar_data: Dictionary = migrated["hotbar"]
|
||||||
|
if (
|
||||||
|
typeof(bag_data.get("items")) != TYPE_ARRAY
|
||||||
|
or typeof(inventory_data.get("catches")) != TYPE_ARRAY
|
||||||
|
or typeof(hotbar_data.get("slots")) != TYPE_ARRAY
|
||||||
|
):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
var placements: Array[Dictionary] = []
|
||||||
|
var assigned: Dictionary[String, bool] = {}
|
||||||
|
var item_slots: Array = hotbar_data["slots"]
|
||||||
|
var fish_slots: Array = []
|
||||||
|
if typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY:
|
||||||
|
fish_slots = hotbar_data["fish_slots"]
|
||||||
|
for slot: int in PlayerHotbarType.SLOT_COUNT:
|
||||||
|
var item_id := StringName(
|
||||||
|
str(item_slots[slot]) if slot < item_slots.size() else ""
|
||||||
|
)
|
||||||
|
if not item_id.is_empty():
|
||||||
|
var item = _item_catalog.get_item_by_id(item_id)
|
||||||
|
if item != null and not item.is_bait() and not item.is_lure():
|
||||||
|
var key := PlayerInventoryLayoutType.item_key(item_id)
|
||||||
|
if not assigned.has(key):
|
||||||
|
placements.append({
|
||||||
|
"kind": PlayerInventoryLayoutType.EntryKind.ITEM,
|
||||||
|
"identity": String(item_id),
|
||||||
|
"container": PlayerInventoryLayoutType.InventoryContainer.HOTBAR,
|
||||||
|
"slot": slot,
|
||||||
|
})
|
||||||
|
assigned[key] = true
|
||||||
|
continue
|
||||||
|
var catch_id := StringName(
|
||||||
|
str(fish_slots[slot]) if slot < fish_slots.size() else ""
|
||||||
|
)
|
||||||
|
if catch_id.is_empty():
|
||||||
|
continue
|
||||||
|
var catch_key := PlayerInventoryLayoutType.catch_key(catch_id)
|
||||||
|
if assigned.has(catch_key):
|
||||||
|
continue
|
||||||
|
placements.append({
|
||||||
|
"kind": PlayerInventoryLayoutType.EntryKind.CATCH,
|
||||||
|
"identity": String(catch_id),
|
||||||
|
"container": PlayerInventoryLayoutType.InventoryContainer.HOTBAR,
|
||||||
|
"slot": slot,
|
||||||
|
})
|
||||||
|
assigned[catch_key] = true
|
||||||
|
|
||||||
|
var next_inventory_slot: int = 0
|
||||||
|
var next_storage_slot: int = 0
|
||||||
|
for value: Variant in bag_data["items"] as Array:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var item_id := StringName(str((value as Dictionary).get("item_id", "")))
|
||||||
|
var item = _item_catalog.get_item_by_id(item_id)
|
||||||
|
if item == null or item.is_bait() or item.is_lure():
|
||||||
|
continue
|
||||||
|
var key := PlayerInventoryLayoutType.item_key(item_id)
|
||||||
|
if assigned.has(key):
|
||||||
|
continue
|
||||||
|
var target := PlayerInventoryLayoutType.InventoryContainer.INVENTORY
|
||||||
|
var target_slot := next_inventory_slot
|
||||||
|
if next_inventory_slot < PlayerInventoryLayoutType.INVENTORY_CAPACITIES[0]:
|
||||||
|
next_inventory_slot += 1
|
||||||
|
else:
|
||||||
|
target = PlayerInventoryLayoutType.InventoryContainer.STORAGE
|
||||||
|
target_slot = next_storage_slot
|
||||||
|
next_storage_slot += 1
|
||||||
|
placements.append({
|
||||||
|
"kind": PlayerInventoryLayoutType.EntryKind.ITEM,
|
||||||
|
"identity": String(item_id),
|
||||||
|
"container": target,
|
||||||
|
"slot": target_slot,
|
||||||
|
})
|
||||||
|
assigned[key] = true
|
||||||
|
for value: Variant in inventory_data["catches"] as Array:
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var catch_id := StringName(
|
||||||
|
str((value as Dictionary).get("catch_id", ""))
|
||||||
|
)
|
||||||
|
if catch_id.is_empty():
|
||||||
|
continue
|
||||||
|
var key := PlayerInventoryLayoutType.catch_key(catch_id)
|
||||||
|
if assigned.has(key):
|
||||||
|
continue
|
||||||
|
var target := PlayerInventoryLayoutType.InventoryContainer.INVENTORY
|
||||||
|
var target_slot := next_inventory_slot
|
||||||
|
if next_inventory_slot < PlayerInventoryLayoutType.INVENTORY_CAPACITIES[0]:
|
||||||
|
next_inventory_slot += 1
|
||||||
|
else:
|
||||||
|
target = PlayerInventoryLayoutType.InventoryContainer.STORAGE
|
||||||
|
target_slot = next_storage_slot
|
||||||
|
next_storage_slot += 1
|
||||||
|
placements.append({
|
||||||
|
"kind": PlayerInventoryLayoutType.EntryKind.CATCH,
|
||||||
|
"identity": String(catch_id),
|
||||||
|
"container": target,
|
||||||
|
"slot": target_slot,
|
||||||
|
})
|
||||||
|
assigned[key] = true
|
||||||
|
migrated["inventory_layout"] = {
|
||||||
|
"backpack_level": 0,
|
||||||
|
"placements": placements,
|
||||||
|
}
|
||||||
|
migrated["save_version"] = 8
|
||||||
|
return migrated
|
||||||
|
|
||||||
|
|
||||||
func _mark_dirty() -> void:
|
func _mark_dirty() -> void:
|
||||||
if (
|
if (
|
||||||
_is_restoring
|
_is_restoring
|
||||||
|
|
@ -1148,9 +1309,10 @@ func _restore_defaults() -> void:
|
||||||
_bag.replace_all_items(default_items)
|
_bag.replace_all_items(default_items)
|
||||||
var default_unlocked_baits: Array[StringName] = []
|
var default_unlocked_baits: Array[StringName] = []
|
||||||
_bag.replace_unlocked_bait_ids(default_unlocked_baits)
|
_bag.replace_unlocked_bait_ids(default_unlocked_baits)
|
||||||
_hotbar.replace_state(default_slots, 0)
|
|
||||||
_fishing_upgrades.reset_to_defaults()
|
_fishing_upgrades.reset_to_defaults()
|
||||||
_cooler_capacity.reset_to_defaults()
|
_cooler_capacity.reset_to_defaults()
|
||||||
|
_inventory_layout.reset_to_defaults()
|
||||||
|
_hotbar.replace_state(default_slots, 0)
|
||||||
_art_unlocks.reset_to_defaults()
|
_art_unlocks.reset_to_defaults()
|
||||||
_experience.reset_to_defaults()
|
_experience.reset_to_defaults()
|
||||||
_world_time.restore_persistent_time_hours(
|
_world_time.restore_persistent_time_hours(
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ readonly -a QUICK_TESTS=(
|
||||||
"tests/controller_world_interaction_validation.gd"
|
"tests/controller_world_interaction_validation.gd"
|
||||||
"tests/controller_ui_navigation_validation.gd"
|
"tests/controller_ui_navigation_validation.gd"
|
||||||
"tests/dedicated_server_config_validation.gd"
|
"tests/dedicated_server_config_validation.gd"
|
||||||
|
"tests/digging_prototype_validation.gd"
|
||||||
"tests/exported_decal_hotfix_validation.gd"
|
"tests/exported_decal_hotfix_validation.gd"
|
||||||
"tests/file_dialog_controller_navigation_validation.gd"
|
"tests/file_dialog_controller_navigation_validation.gd"
|
||||||
"tests/fish_catalog_content_validation.gd"
|
"tests/fish_catalog_content_validation.gd"
|
||||||
|
|
@ -43,6 +44,8 @@ readonly -a QUICK_TESTS=(
|
||||||
"tests/tackle_order_validation.gd"
|
"tests/tackle_order_validation.gd"
|
||||||
"tests/terrain_blender_material_validation.gd"
|
"tests/terrain_blender_material_validation.gd"
|
||||||
"tests/texture_sampling_validation.gd"
|
"tests/texture_sampling_validation.gd"
|
||||||
|
"tests/tree_gathering_prototype_validation.gd"
|
||||||
|
"tests/unified_inventory_validation.gd"
|
||||||
"tests/world_time_validation.gd"
|
"tests/world_time_validation.gd"
|
||||||
"tests/world_spawn_protocol_validation.gd"
|
"tests/world_spawn_protocol_validation.gd"
|
||||||
"tests/world_weather_validation.gd"
|
"tests/world_weather_validation.gd"
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@ const VoiceProfilesType = preload(
|
||||||
)
|
)
|
||||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||||
const BagItemSpriteScene = preload(
|
|
||||||
"res://ui/components/bubble_menu/bag_item_sprite.tscn"
|
|
||||||
)
|
|
||||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
||||||
|
const ItemCatalogResource: ItemCatalog = preload(
|
||||||
|
"res://items/catalog/item_catalog.tres"
|
||||||
|
)
|
||||||
const PlayersPageType = preload("res://ui/players_page.gd")
|
const PlayersPageType = preload("res://ui/players_page.gd")
|
||||||
const TheNetPageType = preload("res://ui/the_net_page.gd")
|
const TheNetPageType = preload("res://ui/the_net_page.gd")
|
||||||
const ControllerMappingManagerType = preload(
|
const ControllerMappingManagerType = preload(
|
||||||
|
|
@ -1077,21 +1076,63 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
var menu := PlayerMenuScene.instantiate() as Control
|
var menu := PlayerMenuScene.instantiate() as Control
|
||||||
root.add_child(menu)
|
root.add_child(menu)
|
||||||
await process_frame
|
await process_frame
|
||||||
|
var inventory_state := Node.new()
|
||||||
|
root.add_child(inventory_state)
|
||||||
|
var bag := PlayerBag.new()
|
||||||
|
var catches := FishInventory.new()
|
||||||
|
var storage_capacity := PlayerCoolerCapacity.new()
|
||||||
|
var layout := PlayerInventoryLayout.new()
|
||||||
|
var hotbar := PlayerHotbarType.new()
|
||||||
|
for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
|
||||||
|
inventory_state.add_child(node)
|
||||||
|
bag.setup(ItemCatalogResource)
|
||||||
|
layout.setup(bag, catches, ItemCatalogResource, storage_capacity)
|
||||||
|
bag.set_inventory_layout(layout)
|
||||||
|
catches.set_inventory_layout(layout)
|
||||||
|
hotbar.setup(bag, ItemCatalogResource, catches, layout)
|
||||||
|
assert(bag.add_item(&"basic_fishing_rod", 1))
|
||||||
|
assert(bag.add_item(&"coffee", 1))
|
||||||
|
assert(layout.move_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
&"basic_fishing_rod",
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
|
9,
|
||||||
|
))
|
||||||
|
assert(hotbar.assign_item(0, &"coffee"))
|
||||||
|
menu.set("_bag", bag)
|
||||||
|
menu.set("_inventory", catches)
|
||||||
|
menu.set("_hotbar", hotbar)
|
||||||
|
menu.set("_inventory_layout", layout)
|
||||||
|
menu.set("_item_catalog", ItemCatalogResource)
|
||||||
|
var inventory_grid := menu.get("_general_inventory_grid") as GeneralInventoryGrid
|
||||||
|
inventory_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
catches,
|
||||||
|
hotbar,
|
||||||
|
ItemCatalogResource,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
|
)
|
||||||
menu.visible = true
|
menu.visible = true
|
||||||
menu.call(
|
menu.call(
|
||||||
"_show_section_immediate",
|
"_show_section_immediate",
|
||||||
PlayerMenuType.Section.COOLER,
|
PlayerMenuType.Section.BAG,
|
||||||
)
|
)
|
||||||
menu.call("_set_content_interactive", true)
|
menu.call("_set_content_interactive", true)
|
||||||
menu.call("_enter_inventory_tabs_zone")
|
menu.call("_enter_inventory_tabs_zone")
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
var cooler_tab := menu.get_node("%CoolerSubTab") as Button
|
var inventory_tab := menu.get_node("%BagSubTab") as Button
|
||||||
var equipment_tab := menu.get_node("%BagSubTab") as Button
|
var tackle_tab := menu.get_node("%TackleSubTab") as Button
|
||||||
var items_tab := menu.get_node("%ItemsSubTab") as Button
|
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == cooler_tab,
|
root.gui_get_focus_owner() == inventory_tab,
|
||||||
"Inventory tab zone did not begin on the active Cooler tab.",
|
"Inventory tab zone did not begin on the unified Inventory tab.",
|
||||||
|
)
|
||||||
|
_expect(inventory_tab.text == "Inventory", "Unified tab is not labelled Inventory.")
|
||||||
|
_expect(
|
||||||
|
not (menu.get_node("%CoolerSubTab") as Button).visible
|
||||||
|
and not (menu.get_node("%ItemsSubTab") as Button).visible,
|
||||||
|
"Legacy Cooler or Items tabs remain visible.",
|
||||||
)
|
)
|
||||||
|
|
||||||
var right := InputEventAction.new()
|
var right := InputEventAction.new()
|
||||||
|
|
@ -1103,8 +1144,8 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
)
|
)
|
||||||
await _wait_for_player_menu_page_transition(menu)
|
await _wait_for_player_menu_page_transition(menu)
|
||||||
_expect(
|
_expect(
|
||||||
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
menu.get("_current_section") == PlayerMenuType.Section.TACKLE_BOX,
|
||||||
"Controller Right did not switch from Cooler to Equipment.",
|
"Controller Right did not switch from Inventory to Tackle.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
menu.get("_controller_ownership")
|
menu.get("_controller_ownership")
|
||||||
|
|
@ -1112,7 +1153,7 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
"Changing Inventory tabs entered the item-content zone without A.",
|
"Changing Inventory tabs entered the item-content zone without A.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == equipment_tab,
|
root.gui_get_focus_owner() == tackle_tab,
|
||||||
"Changing Inventory tabs did not keep focus on the selected tab.",
|
"Changing Inventory tabs did not keep focus on the selected tab.",
|
||||||
)
|
)
|
||||||
var down := InputEventAction.new()
|
var down := InputEventAction.new()
|
||||||
|
|
@ -1124,59 +1165,33 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
)
|
)
|
||||||
await process_frame
|
await process_frame
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == equipment_tab,
|
root.gui_get_focus_owner() == tackle_tab,
|
||||||
"Controller Down escaped the Inventory tab zone before A.",
|
"Controller Down escaped the Inventory tab zone before A.",
|
||||||
)
|
)
|
||||||
|
|
||||||
_expect(
|
|
||||||
bool(menu.call("_handle_controller_ownership_input", right)),
|
|
||||||
"Equipment-to-Items navigation did not consume controller Right.",
|
|
||||||
)
|
|
||||||
await _wait_for_player_menu_page_transition(menu)
|
|
||||||
_expect(
|
|
||||||
root.gui_get_focus_owner() == items_tab,
|
|
||||||
"Items tab selection did not retain tab focus before A (focus=%s)."
|
|
||||||
% root.gui_get_focus_owner(),
|
|
||||||
)
|
|
||||||
_expect(
|
|
||||||
menu.get("_controller_ownership")
|
|
||||||
== PlayerMenuType.ControllerOwnership.INVENTORY_TABS,
|
|
||||||
"Items tab selection changed controller zones before A.",
|
|
||||||
)
|
|
||||||
var left := InputEventAction.new()
|
var left := InputEventAction.new()
|
||||||
left.action = &"ui_left"
|
left.action = &"ui_left"
|
||||||
left.pressed = true
|
left.pressed = true
|
||||||
_expect(
|
_expect(
|
||||||
bool(menu.call("_handle_controller_ownership_input", left)),
|
bool(menu.call("_handle_controller_ownership_input", left)),
|
||||||
"Items-to-Equipment navigation did not consume controller Left.",
|
"Tackle-to-Inventory navigation did not consume controller Left.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
menu.get("_current_section") == PlayerMenuType.Section.BAG
|
||||||
|
or bool(menu.get("_page_transitioning")),
|
||||||
|
"Tackle-to-Inventory input did not start the Inventory transition.",
|
||||||
)
|
)
|
||||||
await _wait_for_player_menu_page_transition(menu)
|
await _wait_for_player_menu_page_transition(menu)
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == equipment_tab,
|
root.gui_get_focus_owner() == inventory_tab,
|
||||||
"Returning to Equipment did not retain tab focus before A.",
|
"Returning to Inventory did not retain tab focus before A.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# Add three representative equipment entries so entering the content zone
|
|
||||||
# exercises the real five-column directional layout.
|
|
||||||
var item_field := menu.get_node("%BagItemField") as Control
|
|
||||||
var bag_nodes: Dictionary = menu.get("_bag_item_nodes")
|
|
||||||
var owned_items: Array[OwnedItemType] = []
|
|
||||||
for index: int in 3:
|
|
||||||
var owned := OwnedItemType.new()
|
|
||||||
owned.item_id = StringName("controller_test_item_%d" % index)
|
|
||||||
owned_items.append(owned)
|
|
||||||
var item_node := BagItemSpriteScene.instantiate() as Button
|
|
||||||
item_node.set("item_id", owned.item_id)
|
|
||||||
item_node.position = Vector2(60.0 + float(index) * 180.0, 40.0)
|
|
||||||
item_field.add_child(item_node)
|
|
||||||
bag_nodes[owned.item_id] = item_node
|
|
||||||
menu.set("_sorted_bag_items", owned_items)
|
|
||||||
menu.call("_set_content_interactive", true)
|
menu.call("_set_content_interactive", true)
|
||||||
menu.call("_configure_bag_item_focus")
|
menu.call("_configure_bag_item_focus")
|
||||||
for item_node: Control in bag_nodes.values():
|
var active_slots := inventory_grid.get_slots()
|
||||||
|
for slot: GeneralInventorySlot in active_slots:
|
||||||
_expect(
|
_expect(
|
||||||
item_node.focus_mode == Control.FOCUS_NONE,
|
slot.focus_mode == Control.FOCUS_NONE,
|
||||||
"Equipment content remained focusable while tabs owned the controller.",
|
"Inventory slots remained focusable while tabs owned the controller.",
|
||||||
)
|
)
|
||||||
|
|
||||||
var accept := InputEventJoypadButton.new()
|
var accept := InputEventJoypadButton.new()
|
||||||
|
|
@ -1193,34 +1208,45 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
)
|
)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
var first_item := bag_nodes[owned_items[0].item_id] as Control
|
_expect(
|
||||||
var second_item := bag_nodes[owned_items[1].item_id] as Control
|
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
||||||
for item_node: Control in bag_nodes.values():
|
"Entering Inventory contents changed section to %s."
|
||||||
|
% menu.get("_current_section"),
|
||||||
|
)
|
||||||
|
var first_item := active_slots[0] as Control
|
||||||
|
var second_item := active_slots[1] as Control
|
||||||
|
for slot: GeneralInventorySlot in active_slots:
|
||||||
_expect(
|
_expect(
|
||||||
item_node.focus_mode == Control.FOCUS_ALL,
|
slot.focus_mode == Control.FOCUS_ALL,
|
||||||
"Accepting Equipment did not enable its content focus zone.",
|
"Accepting Inventory did not enable its content focus zone.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == first_item,
|
root.gui_get_focus_owner() == first_item,
|
||||||
"Entering Equipment did not focus its first item.",
|
"Entering Inventory did not focus its first slot.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
first_item.focus_neighbor_right == first_item.get_path_to(second_item),
|
first_item.focus_neighbor_right == first_item.get_path_to(second_item),
|
||||||
"Equipment items do not provide horizontal controller navigation.",
|
"Inventory slots do not provide horizontal controller navigation.",
|
||||||
)
|
)
|
||||||
Input.parse_input_event(right)
|
Input.parse_input_event(right)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
_expect(
|
_expect(
|
||||||
root.gui_get_focus_owner() == second_item,
|
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
||||||
"Controller Right did not move between Equipment items.",
|
"Inventory slot navigation changed section to %s."
|
||||||
|
% menu.get("_current_section"),
|
||||||
)
|
)
|
||||||
right.pressed = false
|
_expect(
|
||||||
Input.parse_input_event(right)
|
root.gui_get_focus_owner() == second_item,
|
||||||
right.pressed = true
|
"Controller Right did not move between Inventory slots.",
|
||||||
|
)
|
||||||
|
var right_release := InputEventAction.new()
|
||||||
|
right_release.action = &"ui_right"
|
||||||
|
right_release.pressed = false
|
||||||
|
Input.parse_input_event(right_release)
|
||||||
_expect(
|
_expect(
|
||||||
bool(menu.call("consume_escape")),
|
bool(menu.call("consume_escape")),
|
||||||
"Global player-menu Back did not consume Equipment contents.",
|
"Global player-menu Back did not consume Inventory contents.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
menu.visible,
|
menu.visible,
|
||||||
|
|
@ -1233,90 +1259,102 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
)
|
)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
_expect(
|
||||||
|
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
||||||
|
"Returning from Inventory contents changed section to %s."
|
||||||
|
% menu.get("_current_section"),
|
||||||
|
)
|
||||||
_expect(
|
_expect(
|
||||||
bool(menu.call("_handle_controller_ownership_input", accept)),
|
bool(menu.call("_handle_controller_ownership_input", accept)),
|
||||||
"Inventory tab zone did not re-enter Equipment contents.",
|
"Inventory tab zone did not re-enter Inventory contents.",
|
||||||
)
|
)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
var notepad_source := active_slots[9]
|
||||||
var favorite := menu.get_node("%FavoriteBubble") as BaseButton
|
notepad_source.grab_focus()
|
||||||
var sell := menu.get_node("%SellBubble") as BaseButton
|
var context_press := InputEventJoypadButton.new()
|
||||||
var sell_all := menu.get_node("%SellAllBubble") as BaseButton
|
context_press.button_index = JOY_BUTTON_Y
|
||||||
for action: BaseButton in [favorite, sell, sell_all]:
|
context_press.pressed = true
|
||||||
action.disabled = false
|
|
||||||
action.focus_mode = Control.FOCUS_ALL
|
|
||||||
var notepad_actions: Array[BaseButton] = [favorite, sell, sell_all]
|
|
||||||
menu.call(
|
|
||||||
"_configure_controller_notepad_action_focus",
|
|
||||||
notepad_actions,
|
|
||||||
)
|
|
||||||
_expect(
|
_expect(
|
||||||
sell.get_node(sell.focus_neighbor_bottom) == sell_all,
|
bool(menu.call(
|
||||||
"Sell All is not reachable below Sell Fish in the notepad zone.",
|
"_handle_controller_ownership_input", context_press
|
||||||
)
|
)),
|
||||||
_expect(
|
"Controller Y did not open the selected Inventory notepad.",
|
||||||
sell_all.get_node(sell_all.focus_neighbor_top) == sell,
|
|
||||||
"Sell All does not return to the upper notepad actions.",
|
|
||||||
)
|
|
||||||
menu.set(
|
|
||||||
"_controller_ownership",
|
|
||||||
PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
|
|
||||||
)
|
|
||||||
menu.set("_controller_source_section", PlayerMenuType.Section.BAG)
|
|
||||||
menu.set("_controller_source_identity", owned_items[1].item_id)
|
|
||||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
|
||||||
_expect(
|
|
||||||
bool(menu.call("consume_escape")),
|
|
||||||
"Global player-menu Back did not consume the Inventory notepad zone.",
|
|
||||||
)
|
|
||||||
_expect(
|
|
||||||
menu.visible,
|
|
||||||
"Global player-menu Back closed Inventory from its notepad zone.",
|
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
menu.get("_controller_ownership")
|
menu.get("_controller_ownership")
|
||||||
|
== PlayerMenuType.ControllerOwnership.NOTEPAD_ACTIONS,
|
||||||
|
"Inventory notepad did not become the active controller zone.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
(menu.get_node("%BagDetailConstellation") as Control).visible,
|
||||||
|
"Inventory notepad remained hidden after controller Y.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
bool(menu.call("consume_escape")),
|
||||||
|
"Controller B did not close the Inventory notepad.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
not (menu.get_node("%BagDetailConstellation") as Control).visible
|
||||||
|
and menu.get("_controller_ownership")
|
||||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||||
"Global player-menu Back did not return the notepad to Inventory contents.",
|
"Closing the Inventory notepad did not restore the item zone.",
|
||||||
)
|
)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
|
var accept_release := InputEventJoypadButton.new()
|
||||||
var hotbar := PlayerHotbarType.new()
|
accept_release.button_index = JOY_BUTTON_A
|
||||||
menu.set("_hotbar", hotbar)
|
accept_release.pressed = false
|
||||||
var hotbar_slots: Array[StringName] = []
|
notepad_source.grab_focus()
|
||||||
hotbar_slots.resize(PlayerHotbarType.SLOT_COUNT)
|
menu.call("_handle_controller_ownership_input", accept)
|
||||||
hotbar_slots.fill(StringName())
|
menu.call("_handle_controller_ownership_input", accept_release)
|
||||||
hotbar.set("_slots", hotbar_slots)
|
_expect(
|
||||||
var hotbar_fish_slots: Array[StringName] = []
|
str(menu.get("_inventory_move_identity")) == "basic_fishing_rod",
|
||||||
hotbar_fish_slots.resize(PlayerHotbarType.SLOT_COUNT)
|
"Controller A did not pick up the focused Inventory item.",
|
||||||
hotbar_fish_slots.fill(StringName())
|
)
|
||||||
hotbar_fish_slots[0] = &"controller_test_fish"
|
var move_target := active_slots[10]
|
||||||
hotbar.set("_fish_slots", hotbar_fish_slots)
|
move_target.grab_focus()
|
||||||
|
menu.call("_handle_controller_ownership_input", accept)
|
||||||
|
menu.call("_handle_controller_ownership_input", accept_release)
|
||||||
|
_expect(
|
||||||
|
layout.get_key_at(
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY, 10
|
||||||
|
) == PlayerInventoryLayout.item_key(&"basic_fishing_rod"),
|
||||||
|
"Controller A did not place the picked-up item in its target slot.",
|
||||||
|
)
|
||||||
var management_requests: Array[int] = [0]
|
var management_requests: Array[int] = [0]
|
||||||
menu.controller_hotbar_management_requested.connect(
|
menu.controller_hotbar_management_requested.connect(
|
||||||
func(_initial_slot: int) -> void:
|
func(_initial_slot: int) -> void:
|
||||||
management_requests[0] += 1
|
management_requests[0] += 1
|
||||||
)
|
)
|
||||||
var hotbar_owned := OwnedItemType.new()
|
|
||||||
hotbar_owned.item_id = &"controller_hotbar_source"
|
|
||||||
var hotbar_source := BagItemSpriteScene.instantiate() as Button
|
|
||||||
hotbar_source.set("item_id", hotbar_owned.item_id)
|
|
||||||
hotbar_source.position = Vector2(60.0, 40.0)
|
|
||||||
item_field.add_child(hotbar_source)
|
|
||||||
bag_nodes[hotbar_owned.item_id] = hotbar_source
|
|
||||||
var hotbar_items: Array[OwnedItemType] = [hotbar_owned]
|
|
||||||
menu.set("_sorted_bag_items", hotbar_items)
|
|
||||||
menu.set(
|
menu.set(
|
||||||
"_controller_ownership",
|
"_controller_ownership",
|
||||||
PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
||||||
)
|
)
|
||||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
menu.call("_apply_inventory_controller_zone_focus_modes")
|
||||||
menu.call("_configure_bag_item_focus")
|
menu.call("_configure_bag_item_focus")
|
||||||
|
var hotbar_source := active_slots[10]
|
||||||
|
_expect(
|
||||||
|
menu.get("_current_section") == PlayerMenuType.Section.BAG,
|
||||||
|
"Hotbar navigation test left the unified Inventory section (%s)."
|
||||||
|
% menu.get("_current_section"),
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
hotbar_source.focus_mode == Control.FOCUS_ALL,
|
||||||
|
"Final active Inventory row was not controller-focusable.",
|
||||||
|
)
|
||||||
hotbar_source.grab_focus()
|
hotbar_source.grab_focus()
|
||||||
|
_expect(
|
||||||
|
root.gui_get_focus_owner() == hotbar_source,
|
||||||
|
"Final Inventory row did not accept controller focus before Hotbar entry.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
bool(menu.call("_controller_focus_is_on_last_inventory_row")),
|
||||||
|
"Focused Inventory slot was not recognized as part of the final row.",
|
||||||
|
)
|
||||||
_expect(
|
_expect(
|
||||||
bool(menu.call("_handle_controller_ownership_input", down)),
|
bool(menu.call("_handle_controller_ownership_input", down)),
|
||||||
"Down from the final Equipment row did not enter the hotbar zone.",
|
"Down from the final Inventory row did not enter the hotbar zone.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
menu.get("_controller_ownership")
|
menu.get("_controller_ownership")
|
||||||
|
|
@ -1358,9 +1396,10 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
"Hotbar management did not consume controller A.",
|
"Hotbar management did not consume controller A.",
|
||||||
)
|
)
|
||||||
_expect(
|
_expect(
|
||||||
hotbar.get_fish_catch_id(0).is_empty(),
|
hotbar.get_item_id(0).is_empty(),
|
||||||
"Controller A did not remove the selected fish hotbar assignment.",
|
"Controller A did not return the selected hotbar item to Inventory.",
|
||||||
)
|
)
|
||||||
|
_expect(layout.is_item_in_inventory(&"coffee"), "Cleared hotbar item was lost.")
|
||||||
_expect(
|
_expect(
|
||||||
bool(menu.call("consume_escape")),
|
bool(menu.call("consume_escape")),
|
||||||
"Global player-menu Back did not consume hotbar management.",
|
"Global player-menu Back did not consume hotbar management.",
|
||||||
|
|
@ -1376,8 +1415,8 @@ func _validate_inventory_tab_zone_transitions() -> void:
|
||||||
)
|
)
|
||||||
for _frame: int in 2:
|
for _frame: int in 2:
|
||||||
await process_frame
|
await process_frame
|
||||||
hotbar.free()
|
|
||||||
menu.queue_free()
|
menu.queue_free()
|
||||||
|
inventory_state.queue_free()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
91
tests/digging_prototype_validation.gd
Normal file
91
tests/digging_prototype_validation.gd
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const StarterIslandScene = preload(
|
||||||
|
"res://world/regions/starter_island_region.tscn"
|
||||||
|
)
|
||||||
|
const ShovelAttachmentScene = preload("res://player/shovel_attachment.tscn")
|
||||||
|
const ItemCatalogResource: ItemCatalog = preload(
|
||||||
|
"res://items/catalog/item_catalog.tres"
|
||||||
|
)
|
||||||
|
const Gatherables: GatherableCatalog = preload(
|
||||||
|
"res://gathering/catalog/gatherable_catalog.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_validate_catalog_content()
|
||||||
|
_validate_flat_shovel()
|
||||||
|
await _validate_beach_authoring()
|
||||||
|
print("Digging prototype validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_catalog_content() -> void:
|
||||||
|
var shovel: ItemData = ItemCatalogResource.get_available_item_by_id(
|
||||||
|
&"standard_shovel"
|
||||||
|
)
|
||||||
|
assert(shovel != null)
|
||||||
|
assert(shovel.category == ItemData.Category.TOOL)
|
||||||
|
assert(shovel.equippable and shovel.hotbar_allowed)
|
||||||
|
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
|
||||||
|
assert(clam != null and clam.is_available())
|
||||||
|
assert(clam.required_tool_id == shovel.item_id)
|
||||||
|
assert(clam.diggable_area_id == &"starter_beach")
|
||||||
|
assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
|
||||||
|
assert(not clam.requires_sneaking)
|
||||||
|
assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_flat_shovel() -> void:
|
||||||
|
var attachment := ShovelAttachmentScene.instantiate() as BoneAttachment3D
|
||||||
|
assert(attachment != null)
|
||||||
|
var shovel := attachment.get_node("Shovel") as Node3D
|
||||||
|
assert(shovel != null)
|
||||||
|
var meshes: Array[MeshInstance3D] = []
|
||||||
|
_collect_meshes(shovel, meshes)
|
||||||
|
assert(meshes.size() == 5)
|
||||||
|
for mesh_instance: MeshInstance3D in meshes:
|
||||||
|
assert(
|
||||||
|
mesh_instance.cast_shadow
|
||||||
|
== GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
)
|
||||||
|
var material := mesh_instance.mesh.surface_get_material(0) as StandardMaterial3D
|
||||||
|
assert(material != null)
|
||||||
|
assert(material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
|
||||||
|
assert(material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
|
||||||
|
assert(material.albedo_texture == null)
|
||||||
|
assert(is_equal_approx(material.albedo_color.a, 1.0))
|
||||||
|
attachment.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_beach_authoring() -> void:
|
||||||
|
var region := StarterIslandScene.instantiate() as WorldRegion
|
||||||
|
root.add_child(region)
|
||||||
|
await process_frame
|
||||||
|
var area: DiggableArea3D = region.get_diggable_area(&"starter_beach")
|
||||||
|
assert(area != null)
|
||||||
|
assert(area.terrain_source == NodePath("../../Terrain/Visual"))
|
||||||
|
assert(area.surface_materials.size() == 1)
|
||||||
|
assert(area.surface_materials[0] == &"sand")
|
||||||
|
var triangles: Array[PackedVector3Array] = area.get_surface_triangles()
|
||||||
|
assert(not triangles.is_empty())
|
||||||
|
for triangle: PackedVector3Array in triangles:
|
||||||
|
assert(triangle.size() == 3)
|
||||||
|
var center := (triangle[0] + triangle[1] + triangle[2]) / 3.0
|
||||||
|
assert(area.generation_bounds.has_point(Vector2(center.x, center.z)))
|
||||||
|
assert(center.y <= area.maximum_global_y + 0.001)
|
||||||
|
region.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _collect_meshes(
|
||||||
|
root_node: Node,
|
||||||
|
result: Array[MeshInstance3D],
|
||||||
|
) -> void:
|
||||||
|
for child: Node in root_node.get_children():
|
||||||
|
if child is MeshInstance3D:
|
||||||
|
result.append(child as MeshInstance3D)
|
||||||
|
_collect_meshes(child, result)
|
||||||
1
tests/digging_prototype_validation.gd.uid
Normal file
1
tests/digging_prototype_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c1dt44ieacnvb
|
||||||
|
|
@ -48,8 +48,8 @@ func _run() -> void:
|
||||||
as PlayerAssetReservationService
|
as PlayerAssetReservationService
|
||||||
)
|
)
|
||||||
assert(player != null)
|
assert(player != null)
|
||||||
assert(catalog != null and catalog.candidates.size() == 314)
|
assert(catalog != null and catalog.candidates.size() == 316)
|
||||||
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 54)
|
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 56)
|
||||||
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())
|
||||||
|
|
@ -62,7 +62,6 @@ func _run() -> void:
|
||||||
_test_multi_sale(player, catalog, sale_service)
|
_test_multi_sale(player, catalog, sale_service)
|
||||||
_test_reservations(player, catalog, sale_service, reservations)
|
_test_reservations(player, catalog, sale_service, reservations)
|
||||||
_test_anywhere_sale(player, catalog, sale_service)
|
_test_anywhere_sale(player, catalog, sale_service)
|
||||||
await _test_player_menu_sale(main, player, catalog, sale_service)
|
|
||||||
await _test_host_shop_sale(main, player, catalog, sale_service)
|
await _test_host_shop_sale(main, player, catalog, sale_service)
|
||||||
|
|
||||||
assert(session.set_host_open(true))
|
assert(session.set_host_open(true))
|
||||||
|
|
@ -78,6 +77,8 @@ func _run() -> void:
|
||||||
await _test_fishing_shop_sale_ui(
|
await _test_fishing_shop_sale_ui(
|
||||||
main, player, catalog, sale_service, reservations
|
main, player, catalog, sale_service, reservations
|
||||||
)
|
)
|
||||||
|
_test_host_backpack_purchase(player, shop_service)
|
||||||
|
_test_host_storage_purchase(player, shop_service)
|
||||||
assert(not sale_service.is_local_sale_pending())
|
assert(not sale_service.is_local_sale_pending())
|
||||||
assert(not shop_service.is_local_purchase_pending())
|
assert(not shop_service.is_local_purchase_pending())
|
||||||
|
|
||||||
|
|
@ -217,6 +218,17 @@ func _run_multiplayer_client() -> void:
|
||||||
print("Client shop result: ", _shop_result)
|
print("Client shop result: ", _shop_result)
|
||||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||||
assert(not shop_service.is_local_purchase_pending())
|
assert(not shop_service.is_local_purchase_pending())
|
||||||
|
var backpack_cost := player.inventory_layout.get_next_backpack_cost()
|
||||||
|
if player.wallet.get_balance() < backpack_cost:
|
||||||
|
assert(player.wallet.credit(
|
||||||
|
backpack_cost - player.wallet.get_balance()
|
||||||
|
))
|
||||||
|
_shop_result.clear()
|
||||||
|
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
|
||||||
|
while _shop_result.is_empty():
|
||||||
|
await process_frame
|
||||||
|
assert(bool(_shop_result[1]))
|
||||||
|
assert(player.inventory_layout.get_inventory_capacity() == 18)
|
||||||
var required_art_balance: int = (
|
var required_art_balance: int = (
|
||||||
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE
|
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE
|
||||||
)
|
)
|
||||||
|
|
@ -377,80 +389,39 @@ func _test_host_shop_sale(
|
||||||
)
|
)
|
||||||
var species_ids: Array[StringName] = []
|
var species_ids: Array[StringName] = []
|
||||||
var expected_payout: int = 0
|
var expected_payout: int = 0
|
||||||
|
var species_balance_before: int = player.wallet.get_balance()
|
||||||
for fish: FishData in catalog.candidates:
|
for fish: FishData in catalog.candidates:
|
||||||
if not fish.active:
|
if not fish.active:
|
||||||
continue
|
continue
|
||||||
var fish_catch := _make_catch(fish)
|
var fish_catch := _make_catch(fish)
|
||||||
player.inventory.add_catch(fish_catch)
|
assert(player.inventory.add_catch(fish_catch))
|
||||||
species_ids.append(fish_catch.catch_id)
|
species_ids.append(fish_catch.catch_id)
|
||||||
expected_payout += fish_catch.sale_value
|
expected_payout += fish_catch.sale_value
|
||||||
var species_balance_before: int = player.wallet.get_balance()
|
if species_ids.size() >= 5:
|
||||||
_assert_sale(
|
_assert_sale(
|
||||||
player,
|
player,
|
||||||
sale_service,
|
sale_service,
|
||||||
species_ids,
|
species_ids,
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||||
)
|
)
|
||||||
|
species_ids.clear()
|
||||||
|
if not species_ids.is_empty():
|
||||||
|
_assert_sale(
|
||||||
|
player,
|
||||||
|
sale_service,
|
||||||
|
species_ids,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
NetworkSaleService.MAIN_SHOP_BUYER_ID,
|
||||||
|
)
|
||||||
assert(
|
assert(
|
||||||
player.wallet.get_balance()
|
player.wallet.get_balance()
|
||||||
== species_balance_before + expected_payout
|
== species_balance_before + expected_payout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
func _test_player_menu_sale(
|
|
||||||
main: Node,
|
|
||||||
player: Player,
|
|
||||||
catalog: FishPool,
|
|
||||||
sale_service: NetworkSaleService,
|
|
||||||
) -> void:
|
|
||||||
var game_ui := main.get_node("%GameUI") as GameUI
|
|
||||||
var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu
|
|
||||||
var fish_catch := _make_catch(catalog.candidates[6])
|
|
||||||
player.inventory.add_catch(fish_catch)
|
|
||||||
player.global_position = Vector3(0.0, 3.95, 13.0)
|
|
||||||
player_menu.open_menu()
|
|
||||||
await create_timer(2.2).timeout
|
|
||||||
player_menu.call("_on_catch_card_pressed", fish_catch.catch_id)
|
|
||||||
await process_frame
|
|
||||||
var sell_action := player_menu.get_node("%SellBubble") as Button
|
|
||||||
var confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
|
||||||
var confirm_button := player_menu.get_node("%ConfirmSaleButton") as Button
|
|
||||||
var ui_viewport := main.get_node(
|
|
||||||
"UIPresentation/UIViewport"
|
|
||||||
) as SubViewport
|
|
||||||
assert(sell_action.visible and not sell_action.disabled)
|
|
||||||
assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP)
|
|
||||||
assert(ui_viewport != null)
|
|
||||||
await _activate_pointer_control(sell_action, ui_viewport)
|
|
||||||
await process_frame
|
|
||||||
assert(confirmation.visible)
|
|
||||||
assert(confirm_button.visible and not confirm_button.disabled)
|
|
||||||
assert(
|
|
||||||
confirmation.z_index
|
|
||||||
> (player_menu.get_node("%CoolerOuterWall") as Control).z_index
|
|
||||||
)
|
|
||||||
_sale_result.clear()
|
|
||||||
await _activate_pointer_control(confirm_button, ui_viewport)
|
|
||||||
await process_frame
|
|
||||||
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
|
||||||
assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
|
|
||||||
assert(not sale_service.is_local_sale_pending())
|
|
||||||
player_menu.close_menu()
|
|
||||||
await create_timer(2.2).timeout
|
|
||||||
assert(not player_menu.visible)
|
|
||||||
player_menu.open_menu()
|
|
||||||
await create_timer(2.2).timeout
|
|
||||||
assert(player_menu.visible)
|
|
||||||
assert(
|
|
||||||
not sell_action.disabled
|
|
||||||
or player.inventory.get_all_catches().is_empty()
|
|
||||||
)
|
|
||||||
player_menu.close_menu()
|
|
||||||
await create_timer(2.2).timeout
|
|
||||||
|
|
||||||
|
|
||||||
func _activate_pointer_control(
|
func _activate_pointer_control(
|
||||||
control: Control,
|
control: Control,
|
||||||
ui_viewport: SubViewport,
|
ui_viewport: SubViewport,
|
||||||
|
|
@ -547,6 +518,48 @@ func _test_host_rod_purchase(
|
||||||
assert(not shop_service.is_local_purchase_pending())
|
assert(not shop_service.is_local_purchase_pending())
|
||||||
|
|
||||||
|
|
||||||
|
func _test_host_backpack_purchase(
|
||||||
|
player: Player,
|
||||||
|
shop_service: NetworkShopService,
|
||||||
|
) -> void:
|
||||||
|
var layout := player.inventory_layout
|
||||||
|
assert(layout.get_inventory_capacity() == 9)
|
||||||
|
var total_cost: int = 0
|
||||||
|
for cost: int in PlayerInventoryLayout.BACKPACK_EXPANSION_COSTS:
|
||||||
|
total_cost += cost
|
||||||
|
if player.wallet.get_balance() < total_cost:
|
||||||
|
assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
|
||||||
|
var balance_before := player.wallet.get_balance()
|
||||||
|
for expected_capacity: int in [18, 27, 36]:
|
||||||
|
_shop_result.clear()
|
||||||
|
assert(not shop_service.request_backpack_capacity_upgrade().is_empty())
|
||||||
|
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||||
|
assert(layout.get_inventory_capacity() == expected_capacity)
|
||||||
|
assert(player.wallet.get_balance() == balance_before - total_cost)
|
||||||
|
assert(layout.get_next_backpack_cost() == -1)
|
||||||
|
|
||||||
|
|
||||||
|
func _test_host_storage_purchase(
|
||||||
|
player: Player,
|
||||||
|
shop_service: NetworkShopService,
|
||||||
|
) -> void:
|
||||||
|
var capacity := player.cooler_capacity
|
||||||
|
assert(capacity.get_capacity() == 9)
|
||||||
|
var total_cost: int = 0
|
||||||
|
for cost: int in PlayerCoolerCapacity.EXPANSION_COSTS:
|
||||||
|
total_cost += cost
|
||||||
|
if player.wallet.get_balance() < total_cost:
|
||||||
|
assert(player.wallet.credit(total_cost - player.wallet.get_balance()))
|
||||||
|
var balance_before := player.wallet.get_balance()
|
||||||
|
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
|
||||||
|
_shop_result.clear()
|
||||||
|
assert(not shop_service.request_cooler_capacity_upgrade().is_empty())
|
||||||
|
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||||
|
assert(capacity.get_capacity() == expected_capacity)
|
||||||
|
assert(player.wallet.get_balance() == balance_before - total_cost)
|
||||||
|
assert(capacity.get_next_cost() == -1)
|
||||||
|
|
||||||
|
|
||||||
func _test_fishing_shop_sale_ui(
|
func _test_fishing_shop_sale_ui(
|
||||||
main: Node,
|
main: Node,
|
||||||
player: Player,
|
player: Player,
|
||||||
|
|
@ -608,7 +621,11 @@ func _test_fishing_shop_sale_ui(
|
||||||
"QuantityBadge/Quantity"
|
"QuantityBadge/Quantity"
|
||||||
) as Label
|
) as Label
|
||||||
assert(tackle_quantity.text == expected_bait_supply)
|
assert(tackle_quantity.text == expected_bait_supply)
|
||||||
assert(tackle_bait_button.tooltip_text.contains(expected_bait_supply))
|
assert(
|
||||||
|
str(tackle_bait_button.get_meta(
|
||||||
|
&"inventory_context_text", ""
|
||||||
|
)).contains(expected_bait_supply)
|
||||||
|
)
|
||||||
var tackle_badge := tackle_quantity.get_parent() as Panel
|
var tackle_badge := tackle_quantity.get_parent() as Panel
|
||||||
assert(
|
assert(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
|
|
@ -654,7 +671,10 @@ func _test_fishing_shop_sale_ui(
|
||||||
assert((shop.get_node("%Upgrades") as Control).visible)
|
assert((shop.get_node("%Upgrades") as Control).visible)
|
||||||
assert(not (shop.get_node("%Supplies") as Control).visible)
|
assert(not (shop.get_node("%Supplies") as Control).visible)
|
||||||
for upgrade_name: String in [
|
for upgrade_name: String in [
|
||||||
"ReelPurchase", "BarrierPurchase", "CoolerPurchase"
|
"ReelPurchase",
|
||||||
|
"BarrierPurchase",
|
||||||
|
"CoolerPurchase",
|
||||||
|
"BackpackPurchase",
|
||||||
]:
|
]:
|
||||||
var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
|
var upgrade_button := shop.get_node("%%%s" % upgrade_name) as Button
|
||||||
assert(upgrade_button != null)
|
assert(upgrade_button != null)
|
||||||
|
|
@ -662,6 +682,15 @@ func _test_fishing_shop_sale_ui(
|
||||||
assert(upgrade_button.text.is_empty())
|
assert(upgrade_button.text.is_empty())
|
||||||
assert(upgrade_button.icon != null)
|
assert(upgrade_button.icon != null)
|
||||||
assert(upgrade_button.tooltip_text.contains("level"))
|
assert(upgrade_button.tooltip_text.contains("level"))
|
||||||
|
for placeholder_name: String in ["CoolerPurchase", "BackpackPurchase"]:
|
||||||
|
var placeholder_button := shop.get_node(
|
||||||
|
"%%%s" % placeholder_name
|
||||||
|
) as Button
|
||||||
|
assert(
|
||||||
|
placeholder_button.icon.resource_path.ends_with(
|
||||||
|
"/pictograms/x_light.png"
|
||||||
|
)
|
||||||
|
)
|
||||||
assert(not shop.has_node("%ReelLevel"))
|
assert(not shop.has_node("%ReelLevel"))
|
||||||
assert(not shop.has_node("%BarrierEffect"))
|
assert(not shop.has_node("%BarrierEffect"))
|
||||||
assert(not shop.has_node("%CoolerLevel"))
|
assert(not shop.has_node("%CoolerLevel"))
|
||||||
|
|
@ -674,7 +703,9 @@ func _test_fishing_shop_sale_ui(
|
||||||
"/shop/32_currency.png"
|
"/shop/32_currency.png"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
for cost_name: String in ["ReelCost", "BarrierCost", "CoolerCost"]:
|
for cost_name: String in [
|
||||||
|
"ReelCost", "BarrierCost", "CoolerCost", "BackpackCost"
|
||||||
|
]:
|
||||||
var cost_display := shop.get_node("%%%s" % cost_name) as CurrencyAmount
|
var cost_display := shop.get_node("%%%s" % cost_name) as CurrencyAmount
|
||||||
assert(cost_display != null)
|
assert(cost_display != null)
|
||||||
var cost_icon := cost_display.get_node("Icon") as TextureRect
|
var cost_icon := cost_display.get_node("Icon") as TextureRect
|
||||||
|
|
@ -690,7 +721,7 @@ func _test_fishing_shop_sale_ui(
|
||||||
var art_supplies_tab := shop_tabs[4] as Button
|
var art_supplies_tab := shop_tabs[4] as Button
|
||||||
assert(art_supplies_tab != null and art_supplies_tab.text == "Art Supplies")
|
assert(art_supplies_tab != null and art_supplies_tab.text == "Art Supplies")
|
||||||
var sell_mode := shop_tabs[5] as Button
|
var sell_mode := shop_tabs[5] as Button
|
||||||
assert(sell_mode != null and sell_mode.text == "Sell Fish")
|
assert(sell_mode != null and sell_mode.text == "Sell")
|
||||||
var equipment_tab := shop_tabs[3] as Button
|
var equipment_tab := shop_tabs[3] as Button
|
||||||
assert(equipment_tab != null and equipment_tab.text == "Equipment")
|
assert(equipment_tab != null and equipment_tab.text == "Equipment")
|
||||||
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
|
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
|
||||||
|
|
@ -881,44 +912,47 @@ func _test_fishing_shop_sale_ui(
|
||||||
assert((shop.get_node("%ShopPanel") as Control).visible)
|
assert((shop.get_node("%ShopPanel") as Control).visible)
|
||||||
assert(not (shop.get_node("ShopPanel/Margin/Layout/Body") as Control).visible)
|
assert(not (shop.get_node("ShopPanel/Margin/Layout/Body") as Control).visible)
|
||||||
assert(not (shop.get_node("%Feedback") as Control).visible)
|
assert(not (shop.get_node("%Feedback") as Control).visible)
|
||||||
var mounted_cooler := player_menu.get("_cooler_page") as Control
|
var sell_inventory := shop.get("_sell_inventory") as ShopSellInventory
|
||||||
assert(mounted_cooler != null and mounted_cooler.visible)
|
assert(sell_inventory != null and sell_inventory.visible)
|
||||||
|
var sell_tray_grid := sell_inventory.get("_tray_grid") as GridContainer
|
||||||
assert(
|
assert(
|
||||||
mounted_cooler.get_parent() == shop.get_node("%ShopCoolerMount")
|
sell_tray_grid.columns == PlayerInventoryLayout.INVENTORY_COLUMNS
|
||||||
)
|
)
|
||||||
var cooler_outer_wall := player_menu.get("_cooler_outer_wall") as Control
|
|
||||||
var water_surface := player_menu.get("_cooler_water_surface") as ColorRect
|
|
||||||
assert(cooler_outer_wall != null and cooler_outer_wall.visible)
|
|
||||||
assert(water_surface.visible and water_surface.material is ShaderMaterial)
|
|
||||||
var cooler_sort_option := player_menu.get("_cooler_sort_option") as Control
|
|
||||||
await _activate_pointer_control(cooler_sort_option, ui_viewport)
|
|
||||||
var cooler_choice_panel := cooler_sort_option.get("_choice_panel") as Control
|
|
||||||
assert(cooler_choice_panel.visible)
|
|
||||||
cooler_sort_option.call("close_choices")
|
|
||||||
assert(
|
assert(
|
||||||
StringName(
|
sell_tray_grid.get_theme_constant("h_separation")
|
||||||
(player_menu.get("_sale_buyer_override") as FishBuyerProfile).id
|
== GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
|
||||||
) == NetworkSaleService.MAIN_SHOP_BUYER_ID
|
|
||||||
)
|
)
|
||||||
var fish_nodes: Dictionary = player_menu.get("_fish_nodes")
|
|
||||||
var fish_button := fish_nodes.get(fish_catch.catch_id) as Button
|
|
||||||
var reserved_button := fish_nodes.get(reserved_catch.catch_id) as Button
|
|
||||||
assert(fish_button != null and fish_button.visible)
|
|
||||||
assert(reserved_button != null and reserved_button.visible)
|
|
||||||
await _activate_pointer_control(fish_button, ui_viewport)
|
|
||||||
var sell_button := player_menu.get("_sell_bubble") as Button
|
|
||||||
assert(sell_button.visible and not sell_button.disabled)
|
|
||||||
await _activate_pointer_control(sell_button, ui_viewport)
|
|
||||||
await process_frame
|
|
||||||
var confirmation := player_menu.get("_sale_confirmation") as Control
|
|
||||||
var confirm_button := player_menu.get("_confirm_sale_button") as Button
|
|
||||||
assert(confirmation.visible)
|
|
||||||
assert(
|
assert(
|
||||||
confirmation.z_index
|
sell_inventory.get_parent() == shop.get_node("%ShopCoolerMount")
|
||||||
> cooler_outer_wall.z_index
|
|
||||||
)
|
)
|
||||||
|
var staged: Dictionary = sell_inventory.get("_staged")
|
||||||
|
sell_inventory.call(
|
||||||
|
"_stage",
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
reserved_catch.catch_id,
|
||||||
|
)
|
||||||
|
assert(staged.is_empty())
|
||||||
|
sell_inventory.call(
|
||||||
|
"_stage",
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
fish_catch.catch_id,
|
||||||
|
)
|
||||||
|
assert(staged.has(PlayerInventoryLayout.catch_key(fish_catch.catch_id)))
|
||||||
|
assert((sell_inventory.get("_feedback") as Label).text.is_empty())
|
||||||
|
var source_grid := sell_inventory.get("_inventory_grid") as GeneralInventoryGrid
|
||||||
|
var staged_source_found := false
|
||||||
|
for source_slot: GeneralInventorySlot in source_grid.get_slots():
|
||||||
|
if source_slot.entry_identity == fish_catch.catch_id:
|
||||||
|
staged_source_found = bool(source_slot.get("_staged"))
|
||||||
|
break
|
||||||
|
assert(staged_source_found)
|
||||||
|
var feedback := sell_inventory.get("_feedback") as Label
|
||||||
|
var total_label := sell_inventory.get("_total_label") as Label
|
||||||
|
var sell_button := sell_inventory.get("_sell_button") as Button
|
||||||
|
assert(feedback.get_index() < total_label.get_parent().get_index())
|
||||||
|
assert(total_label.get_parent().get_index() < sell_button.get_index())
|
||||||
_sale_result.clear()
|
_sale_result.clear()
|
||||||
await _activate_pointer_control(confirm_button, ui_viewport)
|
sell_inventory.call("_submit_sale")
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
assert(not _sale_result.is_empty() and bool(_sale_result[1]))
|
||||||
assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
|
assert(not player.inventory.contains_catch_id(fish_catch.catch_id))
|
||||||
|
|
@ -929,7 +963,7 @@ func _test_fishing_shop_sale_ui(
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible)
|
assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible)
|
||||||
assert(not (shop.get_node("%ShopCoolerPage") as Control).visible)
|
assert(not (shop.get_node("%ShopCoolerPage") as Control).visible)
|
||||||
assert(not player_menu.is_shop_cooler_mounted())
|
assert(not sell_inventory.is_visible_in_tree())
|
||||||
shop.close_shop()
|
shop.close_shop()
|
||||||
await shop.menu_visibility_changed
|
await shop.menu_visibility_changed
|
||||||
assert(not shop.visible)
|
assert(not shop.visible)
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,17 @@ func _initialize() -> void:
|
||||||
assert(crab_net != null)
|
assert(crab_net != null)
|
||||||
assert(crab_net.icon != null)
|
assert(crab_net.icon != null)
|
||||||
assert(crab_net.icon.resource_path.ends_with("/equipment/temp_net.png"))
|
assert(crab_net.icon.resource_path.ends_with("/equipment/temp_net.png"))
|
||||||
|
var shovel: ItemDataType = ItemCatalogResource.get_available_item_by_id(
|
||||||
|
&"standard_shovel"
|
||||||
|
)
|
||||||
|
assert(shovel != null)
|
||||||
|
assert(shovel.category == ItemDataType.Category.TOOL)
|
||||||
|
assert(shovel.icon != null)
|
||||||
|
assert(shovel.equippable)
|
||||||
|
assert(shovel.hotbar_allowed)
|
||||||
|
assert(FishingShopStockType.get_price(&"standard_shovel") == 75)
|
||||||
|
assert(FishingShopStockType.get_stock_item_ids().has(&"standard_shovel"))
|
||||||
|
assert(FishingShopStockType.is_permanent_unlock(&"standard_shovel", shovel))
|
||||||
|
|
||||||
var wallet := PlayerWalletType.new()
|
var wallet := PlayerWalletType.new()
|
||||||
wallet.current_balance = 250
|
wallet.current_balance = 250
|
||||||
|
|
|
||||||
|
|
@ -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() == 314)
|
assert(Catalog.candidates.size() == 316)
|
||||||
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 == 54)
|
assert(active_count == 56)
|
||||||
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)
|
||||||
|
|
@ -455,6 +455,7 @@ func _validate_catches_and_authoritative_sale() -> void:
|
||||||
1,
|
1,
|
||||||
"catalog_sale_%d" % catch_sequence,
|
"catalog_sale_%d" % catch_sequence,
|
||||||
[loaded.to_network_dict()],
|
[loaded.to_network_dict()],
|
||||||
|
[],
|
||||||
PelicanBuyer,
|
PelicanBuyer,
|
||||||
)
|
)
|
||||||
assert(bool(sale_result.get("accepted", false)))
|
assert(bool(sale_result.get("accepted", false)))
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,7 @@ func _run() -> void:
|
||||||
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
var hotbar_data: Dictionary = (parsed as Dictionary)["hotbar"]
|
||||||
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY)
|
||||||
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id)
|
||||||
assert(int((parsed as Dictionary)["save_version"]) == 7)
|
assert(int((parsed as Dictionary)["save_version"]) == 8)
|
||||||
assert(
|
assert(
|
||||||
int((parsed as Dictionary)["experience"]["total_experience"])
|
int((parsed as Dictionary)["experience"]["total_experience"])
|
||||||
== 125
|
== 125
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,7 @@ func _validate_catch_round_trip_and_sale() -> void:
|
||||||
1,
|
1,
|
||||||
"quality_sale",
|
"quality_sale",
|
||||||
[network_data],
|
[network_data],
|
||||||
|
[],
|
||||||
PelicanBuyer,
|
PelicanBuyer,
|
||||||
)
|
)
|
||||||
assert(bool(accepted.get("accepted", false)))
|
assert(bool(accepted.get("accepted", false)))
|
||||||
|
|
@ -336,6 +337,7 @@ func _validate_catch_round_trip_and_sale() -> void:
|
||||||
1,
|
1,
|
||||||
"quality_sale_forged",
|
"quality_sale_forged",
|
||||||
[forged],
|
[forged],
|
||||||
|
[],
|
||||||
PelicanBuyer,
|
PelicanBuyer,
|
||||||
)
|
)
|
||||||
assert(not bool(rejected.get("accepted", false)))
|
assert(not bool(rejected.get("accepted", false)))
|
||||||
|
|
@ -431,7 +433,7 @@ func _validate_version_four_migration() -> void:
|
||||||
version_four,
|
version_four,
|
||||||
4,
|
4,
|
||||||
)
|
)
|
||||||
assert(int(migrated.get("save_version", -1)) == 7)
|
assert(int(migrated.get("save_version", -1)) == 8)
|
||||||
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0)
|
||||||
assert(
|
assert(
|
||||||
is_equal_approx(
|
is_equal_approx(
|
||||||
|
|
|
||||||
|
|
@ -288,9 +288,9 @@ func _validate_fur_color_ui(snapshot: Dictionary) -> void:
|
||||||
var picker_popup := picker.get_popup()
|
var picker_popup := picker.get_popup()
|
||||||
var picker_control := picker.get_picker()
|
var picker_control := picker.get_picker()
|
||||||
assert(color_panel != null and color_panel.size.x > 360.0)
|
assert(color_panel != null and color_panel.size.x > 360.0)
|
||||||
assert(channel_grid != null and channel_grid.columns == 4)
|
assert(channel_grid != null and channel_grid.columns == 2)
|
||||||
assert(channel_grid.get_child_count() == 4)
|
assert(channel_grid.get_child_count() == 4)
|
||||||
assert(palette_grid != null and palette_grid.columns == 10)
|
assert(palette_grid != null and palette_grid.columns == 6)
|
||||||
assert(palette_grid.get_child_count() == 30)
|
assert(palette_grid.get_child_count() == 30)
|
||||||
for option_button: Button in palette_grid.get_children():
|
for option_button: Button in palette_grid.get_children():
|
||||||
assert(
|
assert(
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||||
|
const HotbarScene = preload("res://ui/hotbar.tscn")
|
||||||
const UIReferencePresentationType = preload(
|
const UIReferencePresentationType = preload(
|
||||||
"res://ui/ui_reference_presentation.gd"
|
"res://ui/ui_reference_presentation.gd"
|
||||||
)
|
)
|
||||||
const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0)
|
const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0)
|
||||||
const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8)
|
const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8)
|
||||||
const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4)
|
const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4)
|
||||||
const INVENTORY_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
|
const LEGACY_COOLER_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0)
|
||||||
const NOTEPAD_HOST_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
|
const LEGACY_COOLER_NOTEPAD_RECT := Rect2(952.0, 166.0, 278.0, 484.0)
|
||||||
|
const INVENTORY_PANEL_RECT := Rect2(199.0, 166.0, 882.0, 484.0)
|
||||||
|
const NOTEPAD_HOST_RECT := Rect2(501.0, 166.0, 278.0, 484.0)
|
||||||
const NOTEPAD_ART_RECT := Rect2(
|
const NOTEPAD_ART_RECT := Rect2(
|
||||||
NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
|
NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION,
|
||||||
EXPECTED_ART_SIZE,
|
EXPECTED_ART_SIZE,
|
||||||
|
|
@ -57,6 +60,11 @@ func _run() -> void:
|
||||||
+ InventoryNotepad.NOTEPAD_ART_OFFSET
|
+ InventoryNotepad.NOTEPAD_ART_OFFSET
|
||||||
))
|
))
|
||||||
_validate_shared_inventory_geometry(player_menu)
|
_validate_shared_inventory_geometry(player_menu)
|
||||||
|
_validate_first_open_inventory(player_menu)
|
||||||
|
await _validate_inventory_grid_centering(player_menu, presentation_stage)
|
||||||
|
_validate_utility_page_geometry(player_menu)
|
||||||
|
await _validate_profile_content_bounds(player_menu)
|
||||||
|
await _validate_hotbar_centering(presentation_stage)
|
||||||
_validate_inventory_layering(player_menu)
|
_validate_inventory_layering(player_menu)
|
||||||
_validate_tackle_to_items_transition(player_menu)
|
_validate_tackle_to_items_transition(player_menu)
|
||||||
_validate_cooler_notepad_typography(player_menu)
|
_validate_cooler_notepad_typography(player_menu)
|
||||||
|
|
@ -103,23 +111,151 @@ func _apply_stage_layout(stage: Control, display_size: Vector2) -> void:
|
||||||
|
|
||||||
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
|
func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void:
|
||||||
for node_name: StringName in [
|
for node_name: StringName in [
|
||||||
&"CoolerOuterWall",
|
|
||||||
&"BagOuterWall",
|
&"BagOuterWall",
|
||||||
&"TackleMainPanel",
|
&"TackleMainPanel",
|
||||||
]:
|
]:
|
||||||
var main_panel := player_menu.get_node("%%%s" % node_name) as Control
|
var main_panel := player_menu.get_node("%%%s" % node_name) as Control
|
||||||
assert(main_panel != null)
|
assert(main_panel != null)
|
||||||
assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
|
assert(main_panel.position.is_equal_approx(INVENTORY_PANEL_RECT.position))
|
||||||
assert(main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size))
|
assert(
|
||||||
|
main_panel.size.is_equal_approx(INVENTORY_PANEL_RECT.size),
|
||||||
|
"%s was %s, expected %s" % [
|
||||||
|
node_name,
|
||||||
|
main_panel.size,
|
||||||
|
INVENTORY_PANEL_RECT.size,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control
|
||||||
|
var cooler_notepad := player_menu.get_node("%DetailConstellation") as Control
|
||||||
|
assert(cooler_panel.position.is_equal_approx(
|
||||||
|
LEGACY_COOLER_PANEL_RECT.position
|
||||||
|
))
|
||||||
|
assert(cooler_panel.size.is_equal_approx(LEGACY_COOLER_PANEL_RECT.size))
|
||||||
|
assert(cooler_notepad.position.is_equal_approx(
|
||||||
|
LEGACY_COOLER_NOTEPAD_RECT.position
|
||||||
|
))
|
||||||
|
assert(cooler_notepad.size.is_equal_approx(
|
||||||
|
LEGACY_COOLER_NOTEPAD_RECT.size
|
||||||
|
))
|
||||||
for node_name: StringName in [
|
for node_name: StringName in [
|
||||||
&"DetailConstellation",
|
|
||||||
&"BagDetailConstellation",
|
&"BagDetailConstellation",
|
||||||
&"TackleDetailPanel",
|
&"TackleDetailPanel",
|
||||||
]:
|
]:
|
||||||
var host := player_menu.get_node("%%%s" % node_name) as Control
|
var host := player_menu.get_node("%%%s" % node_name) as Control
|
||||||
assert(host != null)
|
assert(host != null)
|
||||||
assert(host.position.is_equal_approx(Vector2(952.0, 166.0)))
|
assert(host.position.is_equal_approx(NOTEPAD_HOST_RECT.position))
|
||||||
assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
|
assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE))
|
||||||
|
assert(is_equal_approx(INVENTORY_PANEL_RECT.get_center().x, 640.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_first_open_inventory(player_menu: PlayerMenu) -> void:
|
||||||
|
var legacy_slots: Array = player_menu.get("_bag_slot_nodes") as Array
|
||||||
|
assert(legacy_slots.is_empty())
|
||||||
|
var old_slot_nodes: Array[Node] = player_menu.find_children(
|
||||||
|
"*", "BagStorageSlot", true, false
|
||||||
|
)
|
||||||
|
assert(old_slot_nodes.is_empty())
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_inventory_grid_centering(
|
||||||
|
player_menu: PlayerMenu,
|
||||||
|
stage: Control,
|
||||||
|
) -> void:
|
||||||
|
var grid := player_menu.get("_general_inventory_grid") as Control
|
||||||
|
assert(grid != null)
|
||||||
|
grid.custom_minimum_size = Vector2(782.0, 342.0)
|
||||||
|
grid.size = grid.custom_minimum_size
|
||||||
|
player_menu.call("_layout_general_inventory_grid")
|
||||||
|
await process_frame
|
||||||
|
assert(is_equal_approx(grid.position.y, 39.0))
|
||||||
|
assert(
|
||||||
|
is_equal_approx(
|
||||||
|
grid.get_global_rect().get_center().x,
|
||||||
|
stage.get_global_rect().get_center().x,
|
||||||
|
),
|
||||||
|
"inventory center %s did not match stage center %s" % [
|
||||||
|
grid.get_global_rect().get_center().x,
|
||||||
|
stage.get_global_rect().get_center().x,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_utility_page_geometry(player_menu: PlayerMenu) -> void:
|
||||||
|
for page_name: StringName in [
|
||||||
|
&"TheNetPage",
|
||||||
|
&"MailPage",
|
||||||
|
&"ProfilePage",
|
||||||
|
&"PlayersPage",
|
||||||
|
]:
|
||||||
|
var page := player_menu.get_node("%%%s" % page_name) as Control
|
||||||
|
assert(page != null)
|
||||||
|
var shell := page.find_child(
|
||||||
|
"UtilityMainBox", true, false
|
||||||
|
) as Control
|
||||||
|
assert(shell != null)
|
||||||
|
assert(shell.position.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.position))
|
||||||
|
assert(shell.size.is_equal_approx(UtilityPageStyle.LAPTOP_RECT.size))
|
||||||
|
assert(is_equal_approx(shell.get_rect().get_center().x, 640.0))
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_profile_content_bounds(player_menu: PlayerMenu) -> void:
|
||||||
|
var profile := player_menu.get_node("%ProfilePage") as ProfilePage
|
||||||
|
assert(profile != null)
|
||||||
|
var menu_was_visible: bool = player_menu.visible
|
||||||
|
var profile_was_visible: bool = profile.visible
|
||||||
|
player_menu.visible = true
|
||||||
|
profile.visible = true
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
var option_list := profile.get("_option_list") as Control
|
||||||
|
var body := option_list.get_parent() as Control
|
||||||
|
var preview := profile.get("_preview") as Control
|
||||||
|
var shell := profile.find_child("UtilityMainBox", true, false) as Control
|
||||||
|
assert(option_list != null and body != null and preview != null and shell != null)
|
||||||
|
for category_id: String in ["fur_pattern", "voice"]:
|
||||||
|
profile.call("_select_category", category_id)
|
||||||
|
await process_frame
|
||||||
|
await process_frame
|
||||||
|
assert(
|
||||||
|
body.get_combined_minimum_size().x <= body.size.x,
|
||||||
|
"%s customization content overflowed its body: %s > %s" % [
|
||||||
|
category_id,
|
||||||
|
body.get_combined_minimum_size().x,
|
||||||
|
body.size.x,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
preview.get_global_rect().end.x
|
||||||
|
<= shell.get_global_rect().end.x,
|
||||||
|
"%s preview overflowed the utility content area" % category_id,
|
||||||
|
)
|
||||||
|
profile.visible = profile_was_visible
|
||||||
|
player_menu.visible = menu_was_visible
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_hotbar_centering(parent: Control) -> void:
|
||||||
|
var hotbar := HotbarScene.instantiate() as HotbarUI
|
||||||
|
parent.add_child(hotbar)
|
||||||
|
await process_frame
|
||||||
|
var presentation := hotbar.get_node(
|
||||||
|
"%HotbarPresentationScaleRoot"
|
||||||
|
) as Control
|
||||||
|
var field := hotbar.get_node("%BubbleField") as Control
|
||||||
|
hotbar.set_player_menu_context(true)
|
||||||
|
var displayed_center_x: float = (
|
||||||
|
presentation.position.x
|
||||||
|
+ field.get_rect().get_center().x * presentation.scale.x
|
||||||
|
)
|
||||||
|
assert(is_equal_approx(displayed_center_x, 640.0))
|
||||||
|
var displayed_top: float = (
|
||||||
|
presentation.position.y + field.position.y * presentation.scale.y
|
||||||
|
)
|
||||||
|
var displayed_bottom: float = (
|
||||||
|
displayed_top + field.size.y * presentation.scale.y
|
||||||
|
)
|
||||||
|
assert(displayed_top < INVENTORY_PANEL_RECT.end.y)
|
||||||
|
assert(displayed_bottom > INVENTORY_PANEL_RECT.end.y)
|
||||||
|
hotbar.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
|
func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
|
||||||
|
|
@ -136,13 +272,31 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
|
||||||
assert(cooler_panel != null)
|
assert(cooler_panel != null)
|
||||||
assert(tackle_panel != null)
|
assert(tackle_panel != null)
|
||||||
assert(inventory_tabs != null)
|
assert(inventory_tabs != null)
|
||||||
assert(items_tab != null and items_tab.text == "Items")
|
assert(items_tab != null and not items_tab.visible)
|
||||||
assert(bait_list != null and bait_list.columns == 3)
|
assert(not (player_menu.get_node("%CoolerSubTab") as Button).visible)
|
||||||
assert(lure_list != null and lure_list.columns == 3)
|
assert((player_menu.get_node("%BagSubTab") as Button).text == "Inventory")
|
||||||
|
assert(bait_list != null and bait_list.columns == 4)
|
||||||
|
assert(lure_list != null and lure_list.columns == 4)
|
||||||
assert(bait_list.get_theme_constant("h_separation") == 28)
|
assert(bait_list.get_theme_constant("h_separation") == 28)
|
||||||
assert(lure_list.get_theme_constant("h_separation") == 28)
|
assert(lure_list.get_theme_constant("h_separation") == 28)
|
||||||
assert(sale_confirmation.z_index > cooler_panel.z_index)
|
assert(sale_confirmation.z_index > cooler_panel.z_index)
|
||||||
assert(sale_confirmation.z_index > tackle_panel.z_index)
|
assert(sale_confirmation.z_index > tackle_panel.z_index)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
|
||||||
|
> bag_panel.z_index
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagModalBlocker") as Control).z_index
|
||||||
|
> bag_panel.z_index
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagDetailConstellation") as Control).z_index
|
||||||
|
> (player_menu.get_node("%BagModalBlocker") as Control).z_index
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%TackleDetailPanel") as Control).z_index
|
||||||
|
> tackle_panel.z_index
|
||||||
|
)
|
||||||
# The contextual Hotbar uses z=90 while the Player Menu is open.
|
# The contextual Hotbar uses z=90 while the Player Menu is open.
|
||||||
assert(sale_confirmation.z_index > 90)
|
assert(sale_confirmation.z_index > 90)
|
||||||
assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0)))
|
assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0)))
|
||||||
|
|
@ -159,25 +313,55 @@ func _validate_inventory_layering(player_menu: PlayerMenu) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
|
func _validate_tackle_to_items_transition(player_menu: PlayerMenu) -> void:
|
||||||
var empty_state := player_menu.get_node("%BagEmptyState") as Label
|
var inventory_notepad := player_menu.get_node(
|
||||||
player_menu.set("_bag_view", PlayerMenu.BagView.EQUIPMENT)
|
"%BagDetailBubble"
|
||||||
player_menu.call("_refresh_bag")
|
) as InventoryNotepad
|
||||||
assert(empty_state.text == "No equipment in your Bag.")
|
assert(inventory_notepad != null)
|
||||||
|
assert(inventory_notepad.title_text == "inventory notes")
|
||||||
player_menu.call(
|
player_menu.call(
|
||||||
"_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
|
"_show_section_immediate", PlayerMenu.Section.TACKLE_BOX
|
||||||
)
|
)
|
||||||
player_menu.call("_show_bag_view", PlayerMenu.BagView.CONSUMABLES)
|
player_menu.call("_show_inventory_tab", 0)
|
||||||
assert(
|
|
||||||
player_menu.get("_current_section") == PlayerMenu.Section.BAG
|
|
||||||
)
|
|
||||||
assert(
|
|
||||||
player_menu.get("_bag_view") == PlayerMenu.BagView.CONSUMABLES
|
|
||||||
)
|
|
||||||
assert(empty_state.text == "No items in your Bag.")
|
|
||||||
player_menu.call("_cancel_page_tween")
|
player_menu.call("_cancel_page_tween")
|
||||||
player_menu.call(
|
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
|
||||||
"_show_section_immediate", PlayerMenu.Section.COOLER
|
player_menu.call("_update_bag_detail")
|
||||||
|
assert(player_menu.get("_current_section") == PlayerMenu.Section.BAG)
|
||||||
|
assert(not (player_menu.get_node("%BagDetailConstellation") as Control).visible)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagSpriteDetailData") as Label).text
|
||||||
|
== "select an item for details."
|
||||||
)
|
)
|
||||||
|
player_menu.visible = true
|
||||||
|
player_menu.call("_set_content_interactive", true)
|
||||||
|
var no_actions: Array[BaseButton] = []
|
||||||
|
player_menu.call(
|
||||||
|
"_open_inventory_notepad",
|
||||||
|
PlayerMenu.Section.BAG,
|
||||||
|
StringName("modal-test"),
|
||||||
|
no_actions,
|
||||||
|
)
|
||||||
|
assert((player_menu.get_node("%BagDetailConstellation") as Control).visible)
|
||||||
|
assert((player_menu.get_node("%BagModalBlocker") as Control).visible)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagModalBlocker") as Control).mouse_filter
|
||||||
|
== Control.MOUSE_FILTER_STOP
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%InventoryTab") as Button).focus_mode
|
||||||
|
== Control.FOCUS_NONE
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
(player_menu.get_node("%BagSpriteDetailData") as Label).get_theme_font(
|
||||||
|
"font"
|
||||||
|
) == InventoryNotepad.NOTEPAD_FONT
|
||||||
|
)
|
||||||
|
player_menu.call(
|
||||||
|
"_release_controller_ownership", false, false
|
||||||
|
)
|
||||||
|
assert(not (player_menu.get_node("%BagModalBlocker") as Control).visible)
|
||||||
|
player_menu.call("_set_content_interactive", false)
|
||||||
|
player_menu.visible = false
|
||||||
|
player_menu.call("_cancel_page_tween")
|
||||||
|
|
||||||
|
|
||||||
func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
|
func _validate_cooler_notepad_typography(player_menu: PlayerMenu) -> void:
|
||||||
|
|
@ -314,17 +498,16 @@ func _capture_inventory_pages(player_menu: PlayerMenu) -> void:
|
||||||
return
|
return
|
||||||
player_menu.visible = true
|
player_menu.visible = true
|
||||||
var sections: Array[PlayerMenu.Section] = [
|
var sections: Array[PlayerMenu.Section] = [
|
||||||
PlayerMenu.Section.COOLER,
|
|
||||||
PlayerMenu.Section.BAG,
|
PlayerMenu.Section.BAG,
|
||||||
PlayerMenu.Section.TACKLE_BOX,
|
PlayerMenu.Section.TACKLE_BOX,
|
||||||
]
|
]
|
||||||
var suffixes: Array[String] = ["cooler", "equipment", "tackle"]
|
var suffixes: Array[String] = ["inventory", "tackle"]
|
||||||
for index: int in sections.size():
|
for index: int in sections.size():
|
||||||
player_menu.call("_show_section_immediate", sections[index])
|
player_menu.call("_show_section_immediate", sections[index])
|
||||||
await process_frame
|
await process_frame
|
||||||
await process_frame
|
await process_frame
|
||||||
await _save_capture(suffixes[index])
|
await _save_capture(suffixes[index])
|
||||||
player_menu.call("_show_section_immediate", PlayerMenu.Section.COOLER)
|
player_menu.call("_show_section_immediate", PlayerMenu.Section.BAG)
|
||||||
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control
|
||||||
var confirmation_message := player_menu.get_node(
|
var confirmation_message := player_menu.get_node(
|
||||||
"%ConfirmationMessage"
|
"%ConfirmationMessage"
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,7 @@
|
||||||
extends SceneTree
|
extends SceneTree
|
||||||
|
|
||||||
const BagItemSpriteType = preload(
|
const Catalog: ItemCatalog = preload("res://items/catalog/item_catalog.tres")
|
||||||
"res://ui/components/bubble_menu/bag_item_sprite.gd"
|
const Bluegill: FishData = preload("res://fish/species/bluegill/bluegill.tres")
|
||||||
)
|
|
||||||
const BagStorageSlotType = preload(
|
|
||||||
"res://ui/components/bubble_menu/bag_storage_slot.gd"
|
|
||||||
)
|
|
||||||
const Catalog: ItemCatalog = preload(
|
|
||||||
"res://items/catalog/item_catalog.tres"
|
|
||||||
)
|
|
||||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
|
||||||
const PlayerBagType = preload("res://inventory/player_bag.gd")
|
|
||||||
const PlayerHotbarType = preload("res://inventory/player_hotbar.gd")
|
|
||||||
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
|
||||||
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
|
||||||
|
|
||||||
const EQUIPMENT_IDS: Array[StringName] = [
|
|
||||||
&"basic_fishing_rod",
|
|
||||||
&"art_kit",
|
|
||||||
&"crab_net",
|
|
||||||
&"magnet",
|
|
||||||
]
|
|
||||||
const ITEM_IDS: Array[StringName] = [
|
|
||||||
&"coffee",
|
|
||||||
&"energy_drink",
|
|
||||||
&"snack",
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -33,140 +9,119 @@ func _initialize() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
root.size = Vector2i(1280, 720)
|
var state := Node.new()
|
||||||
var bag := PlayerBagType.new()
|
root.add_child(state)
|
||||||
|
var bag := PlayerBag.new()
|
||||||
|
var catches := FishInventory.new()
|
||||||
|
var storage_capacity := PlayerCoolerCapacity.new()
|
||||||
|
var layout := PlayerInventoryLayout.new()
|
||||||
|
var hotbar := PlayerHotbar.new()
|
||||||
|
for node: Node in [bag, catches, storage_capacity, layout, hotbar]:
|
||||||
|
state.add_child(node)
|
||||||
bag.setup(Catalog)
|
bag.setup(Catalog)
|
||||||
for item_id: StringName in EQUIPMENT_IDS + ITEM_IDS:
|
layout.setup(bag, catches, Catalog, storage_capacity)
|
||||||
assert(bag.add_item(item_id))
|
bag.set_inventory_layout(layout)
|
||||||
for index: int in EQUIPMENT_IDS.size():
|
catches.set_inventory_layout(layout)
|
||||||
assert(bag.get_storage_slot(EQUIPMENT_IDS[index]) == index)
|
hotbar.setup(bag, Catalog, catches, layout)
|
||||||
for index: int in ITEM_IDS.size():
|
|
||||||
assert(bag.get_storage_slot(ITEM_IDS[index]) == index)
|
|
||||||
|
|
||||||
assert(bag.move_item_to_storage_slot(&"basic_fishing_rod", 14))
|
assert(bag.add_item(&"basic_fishing_rod", 1))
|
||||||
assert(bag.get_storage_slot(&"basic_fishing_rod") == 14)
|
assert(bag.add_item(&"coffee", 3))
|
||||||
assert(bag.move_item_to_storage_slot(&"crab_net", 14))
|
var fish_catch := _make_bluegill()
|
||||||
assert(bag.get_storage_slot(&"crab_net") == 14)
|
assert(catches.add_catch(fish_catch))
|
||||||
assert(bag.get_storage_slot(&"basic_fishing_rod") == 2)
|
|
||||||
var saved_record: Dictionary = bag.get_owned_item(
|
|
||||||
&"crab_net"
|
|
||||||
).to_save_dict()
|
|
||||||
assert(int(saved_record.get("storage_slot", -1)) == 14)
|
|
||||||
|
|
||||||
var legacy_equipment := OwnedItemType.new()
|
var inventory_grid := GeneralInventoryGrid.new()
|
||||||
legacy_equipment.item_id = &"basic_fishing_rod"
|
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
|
||||||
var legacy_item := OwnedItemType.new()
|
root.add_child(inventory_grid)
|
||||||
legacy_item.item_id = &"coffee"
|
inventory_grid.setup(
|
||||||
var legacy_records: Array[OwnedItemType] = [
|
layout,
|
||||||
legacy_equipment,
|
bag,
|
||||||
legacy_item,
|
catches,
|
||||||
]
|
hotbar,
|
||||||
var legacy_bag := PlayerBagType.new()
|
Catalog,
|
||||||
legacy_bag.setup(Catalog)
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
assert(legacy_bag.replace_all_items(legacy_records))
|
|
||||||
assert(legacy_bag.get_storage_slot(&"basic_fishing_rod") == 0)
|
|
||||||
assert(legacy_bag.get_storage_slot(&"coffee") == 0)
|
|
||||||
legacy_bag.free()
|
|
||||||
|
|
||||||
var menu := PlayerMenuScene.instantiate() as PlayerMenu
|
|
||||||
root.add_child(menu)
|
|
||||||
await process_frame
|
|
||||||
menu.set("_bag", bag)
|
|
||||||
var hotbar := PlayerHotbarType.new()
|
|
||||||
hotbar.setup(bag, Catalog)
|
|
||||||
menu.set("_hotbar", hotbar)
|
|
||||||
menu.set("_item_catalog", Catalog)
|
|
||||||
menu.set("_bag_view", PlayerMenuType.BagView.EQUIPMENT)
|
|
||||||
menu.visible = true
|
|
||||||
menu.call("_show_section_immediate", PlayerMenuType.Section.BAG)
|
|
||||||
menu.call("_set_content_interactive", true)
|
|
||||||
menu.call("_refresh_bag")
|
|
||||||
await process_frame
|
|
||||||
_validate_grid(menu, EQUIPMENT_IDS.size())
|
|
||||||
|
|
||||||
var item_nodes: Dictionary = menu.get("_bag_item_nodes")
|
|
||||||
var crab_node := item_nodes.get(&"crab_net") as BagItemSpriteType
|
|
||||||
assert(crab_node != null)
|
|
||||||
menu.set(
|
|
||||||
"_controller_ownership",
|
|
||||||
PlayerMenuType.ControllerOwnership.ITEM_LIST,
|
|
||||||
)
|
)
|
||||||
menu.call("_apply_inventory_controller_zone_focus_modes")
|
var storage_grid := GeneralInventoryGrid.new()
|
||||||
crab_node.grab_focus()
|
root.add_child(storage_grid)
|
||||||
assert(bool(menu.call("_try_begin_controller_storage_placement")))
|
storage_grid.setup(
|
||||||
await process_frame
|
layout,
|
||||||
assert(
|
bag,
|
||||||
menu.get("_controller_ownership")
|
catches,
|
||||||
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
|
hotbar,
|
||||||
)
|
Catalog,
|
||||||
var slots: Array = menu.get("_bag_slot_nodes")
|
PlayerInventoryLayout.InventoryContainer.STORAGE,
|
||||||
var bottom_slot := slots[12] as BagStorageSlotType
|
|
||||||
bottom_slot.grab_focus()
|
|
||||||
var down := InputEventAction.new()
|
|
||||||
down.action = &"ui_down"
|
|
||||||
down.pressed = true
|
|
||||||
assert(bool(menu.call("_handle_controller_ownership_input", down)))
|
|
||||||
assert(
|
|
||||||
menu.get("_controller_ownership")
|
|
||||||
== PlayerMenuType.ControllerOwnership.HOTBAR_PLACEMENT
|
|
||||||
)
|
|
||||||
var up := InputEventAction.new()
|
|
||||||
up.action = &"ui_up"
|
|
||||||
up.pressed = true
|
|
||||||
assert(bool(menu.call("_handle_controller_ownership_input", up)))
|
|
||||||
assert(
|
|
||||||
menu.get("_controller_ownership")
|
|
||||||
== PlayerMenuType.ControllerOwnership.STORAGE_PLACEMENT
|
|
||||||
)
|
)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(root.gui_get_focus_owner() == bottom_slot)
|
|
||||||
assert(StringName(menu.get("_controller_storage_identity")) == &"crab_net")
|
var inventory_slots: Array = inventory_grid.get("_slots")
|
||||||
var target_slot := slots[7] as BagStorageSlotType
|
var storage_slots: Array = storage_grid.get("_slots")
|
||||||
target_slot.grab_focus()
|
assert(inventory_slots.size() == 36)
|
||||||
menu.call("_confirm_controller_storage_placement")
|
assert(inventory_grid.get_slots().size() == 9)
|
||||||
await process_frame
|
assert(inventory_grid.custom_minimum_size == Vector2(782.0, 342.0))
|
||||||
assert(bag.get_storage_slot(&"crab_net") == 7)
|
assert(storage_capacity.get_capacity() == 9)
|
||||||
assert(
|
assert(storage_slots.size() == 72)
|
||||||
menu.get("_controller_ownership")
|
assert(storage_grid.get_slots().size() == 9)
|
||||||
== PlayerMenuType.ControllerOwnership.ITEM_LIST
|
assert((storage_slots[0] as Control).custom_minimum_size == Vector2(52.0, 52.0))
|
||||||
|
assert((storage_slots[9] as GeneralInventorySlot).disabled)
|
||||||
|
|
||||||
|
var coffee_slot := _find_slot(inventory_slots, &"coffee")
|
||||||
|
assert(coffee_slot != null)
|
||||||
|
(inventory_slots[7] as GeneralInventorySlot).call(
|
||||||
|
"_drop_data",
|
||||||
|
Vector2.ZERO,
|
||||||
|
{"kind": "bag_item", "item_id": "coffee"},
|
||||||
)
|
)
|
||||||
|
assert(int(layout.get_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM, &"coffee"
|
||||||
|
).get("slot", -1)) == 7)
|
||||||
|
assert(coffee_slot.entry_identity.is_empty())
|
||||||
|
|
||||||
menu.call("_show_bag_view", PlayerMenuType.BagView.CONSUMABLES)
|
(storage_slots[0] as GeneralInventorySlot).call(
|
||||||
await process_frame
|
"_drop_data",
|
||||||
_validate_grid(menu, ITEM_IDS.size())
|
Vector2.ZERO,
|
||||||
menu.call("_on_bag_item_dropped", &"coffee", 12)
|
{"kind": "cooler_fish", "catch_id": String(fish_catch.catch_id)},
|
||||||
await process_frame
|
)
|
||||||
assert(bag.get_storage_slot(&"coffee") == 12)
|
assert(layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH, fish_catch.catch_id
|
||||||
|
) == PlayerInventoryLayout.InventoryContainer.STORAGE)
|
||||||
|
|
||||||
|
assert(hotbar.assign_item(0, &"basic_fishing_rod"))
|
||||||
|
assert(not layout.is_item_in_inventory(&"basic_fishing_rod"))
|
||||||
|
(inventory_slots[8] as GeneralInventorySlot).call(
|
||||||
|
"_drop_data",
|
||||||
|
Vector2.ZERO,
|
||||||
|
{"kind": "hotbar_slot", "slot_index": 0},
|
||||||
|
)
|
||||||
|
assert(hotbar.get_item_id(0).is_empty())
|
||||||
|
assert(layout.is_item_in_inventory(&"basic_fishing_rod"))
|
||||||
|
assert(int(layout.get_entry(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM, &"basic_fishing_rod"
|
||||||
|
).get("slot", -1)) == 8)
|
||||||
|
|
||||||
menu.queue_free()
|
|
||||||
hotbar.free()
|
|
||||||
bag.free()
|
|
||||||
await process_frame
|
|
||||||
print("Inventory storage validation: PASS")
|
print("Inventory storage validation: PASS")
|
||||||
|
state.free()
|
||||||
|
inventory_grid.free()
|
||||||
|
storage_grid.free()
|
||||||
quit()
|
quit()
|
||||||
|
|
||||||
|
|
||||||
func _validate_grid(menu: PlayerMenu, expected_items: int) -> void:
|
func _make_bluegill() -> FishCatch:
|
||||||
var slots: Array = menu.get("_bag_slot_nodes")
|
var fish_catch := FishCatch.new()
|
||||||
assert(slots.size() == PlayerBagType.MIN_STORAGE_SLOT_COUNT)
|
fish_catch.fish = Bluegill
|
||||||
var seen_positions: Dictionary[Vector2, bool] = {}
|
fish_catch.fish_id = Bluegill.id
|
||||||
for index: int in slots.size():
|
fish_catch.catch_id = &"bluegill:inventory_storage_test"
|
||||||
var slot := slots[index] as BagStorageSlotType
|
fish_catch.weight_lb = Bluegill.get_minimum_weight()
|
||||||
assert(slot != null)
|
fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
|
||||||
assert(slot.storage_slot_index == index)
|
fish_catch.weight_lb
|
||||||
assert(not seen_positions.has(slot.position))
|
|
||||||
seen_positions[slot.position] = true
|
|
||||||
var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
|
|
||||||
assert(normal != null)
|
|
||||||
assert(normal.bg_color.a >= 0.7)
|
|
||||||
assert((slots[1] as Control).position.x > (slots[0] as Control).position.x)
|
|
||||||
assert((slots[5] as Control).position.y > (slots[0] as Control).position.y)
|
|
||||||
var item_nodes := menu.get("_bag_item_nodes") as Dictionary
|
|
||||||
assert(
|
|
||||||
item_nodes.size() == expected_items,
|
|
||||||
"expected %d visible bag items, found %d: %s" % [
|
|
||||||
expected_items,
|
|
||||||
item_nodes.size(),
|
|
||||||
str(item_nodes.keys()),
|
|
||||||
],
|
|
||||||
)
|
)
|
||||||
|
fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
return fish_catch
|
||||||
|
|
||||||
|
|
||||||
|
func _find_slot(slots: Array, identity: StringName) -> GeneralInventorySlot:
|
||||||
|
for candidate: Variant in slots:
|
||||||
|
var slot := candidate as GeneralInventorySlot
|
||||||
|
if slot != null and slot.entry_identity == identity:
|
||||||
|
return slot
|
||||||
|
return null
|
||||||
|
|
|
||||||
|
|
@ -203,7 +203,7 @@ func _run() -> void:
|
||||||
save_file.close()
|
save_file.close()
|
||||||
assert(typeof(parsed) == TYPE_DICTIONARY)
|
assert(typeof(parsed) == TYPE_DICTIONARY)
|
||||||
var save_data: Dictionary = parsed
|
var save_data: Dictionary = parsed
|
||||||
assert(int(save_data.get("save_version", -1)) == 7)
|
assert(int(save_data.get("save_version", -1)) == 8)
|
||||||
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
assert(PlayerJobService.validate_save_data(save_data.get("jobs", {})))
|
||||||
|
|
||||||
_validate_pause_session_switch(main, session)
|
_validate_pause_session_switch(main, session)
|
||||||
|
|
|
||||||
|
|
@ -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() == 314)
|
assert(catalog.candidates.size() == 316)
|
||||||
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
|
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
|
||||||
assert(active_species.size() == 54)
|
assert(active_species.size() == 56)
|
||||||
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() == 54)
|
assert(player.inventory.get_all_catches().size() == 56)
|
||||||
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() == 314)
|
assert(CatalogResource.candidates.size() == 316)
|
||||||
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
|
var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates)
|
||||||
assert(ordered.size() == 54)
|
assert(ordered.size() == 56)
|
||||||
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:
|
||||||
|
|
@ -113,6 +113,10 @@ func _validate_page() -> void:
|
||||||
LogbookCatalog.Category.OTHER,
|
LogbookCatalog.Category.OTHER,
|
||||||
])
|
])
|
||||||
assert(category_tabs.size() == category_tab_categories.size())
|
assert(category_tabs.size() == category_tab_categories.size())
|
||||||
|
assert(
|
||||||
|
LogbookCatalog.category_label(LogbookCatalog.Category.OTHER)
|
||||||
|
== "Insects"
|
||||||
|
)
|
||||||
for tab_node: Variant in category_tabs:
|
for tab_node: Variant in category_tabs:
|
||||||
var category_tab := tab_node as Button
|
var category_tab := tab_node as Button
|
||||||
assert(category_tab.size == LogbookPage.LOGBOOK_TAB_SIZE)
|
assert(category_tab.size == LogbookPage.LOGBOOK_TAB_SIZE)
|
||||||
|
|
@ -203,16 +207,18 @@ func _validate_page() -> void:
|
||||||
|
|
||||||
page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
|
page.call("_select_category", LogbookCatalog.Category.SHELLFISH)
|
||||||
await create_timer(0.25).timeout
|
await create_timer(0.25).timeout
|
||||||
assert((page.get("_entry_buttons") as Dictionary).size() == 1)
|
assert((page.get("_entry_buttons") as Dictionary).size() == 2)
|
||||||
assert(
|
assert(
|
||||||
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
|
(page.get("_entry_buttons") as Dictionary).has(&"unknown_3906")
|
||||||
)
|
)
|
||||||
|
assert(
|
||||||
|
(page.get("_entry_buttons") as Dictionary).has(&"unknown_6406")
|
||||||
|
)
|
||||||
page.call("_select_category", LogbookCatalog.Category.OTHER)
|
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).size() == 1)
|
||||||
assert(
|
assert(
|
||||||
(page.get("_empty_state") as Label).text
|
(page.get("_entry_buttons") as Dictionary).has(&"unknown_8001")
|
||||||
== "No entries available."
|
|
||||||
)
|
)
|
||||||
page.call("_select_category", LogbookCatalog.Category.FRESH_WATER)
|
page.call("_select_category", LogbookCatalog.Category.FRESH_WATER)
|
||||||
await create_timer(0.25).timeout
|
await create_timer(0.25).timeout
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func _validate_save_migration() -> void:
|
||||||
version_five,
|
version_five,
|
||||||
5,
|
5,
|
||||||
)
|
)
|
||||||
assert(int(migrated.get("save_version", -1)) == 7)
|
assert(int(migrated.get("save_version", -1)) == 8)
|
||||||
var experience_data: Dictionary = migrated.get("experience", {})
|
var experience_data: Dictionary = migrated.get("experience", {})
|
||||||
assert(int(experience_data.get("total_experience", -1)) == 0)
|
assert(int(experience_data.get("total_experience", -1)) == 0)
|
||||||
var world_data: Dictionary = migrated.get("world", {})
|
var world_data: Dictionary = migrated.get("world", {})
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,14 @@ func _run() -> void:
|
||||||
assert(StringName(player.get("_animation_action_id")).is_empty())
|
assert(StringName(player.get("_animation_action_id")).is_empty())
|
||||||
assert(animation_player.current_animation == &"idle")
|
assert(animation_player.current_animation == &"idle")
|
||||||
Input.action_press(&"sneak")
|
Input.action_press(&"sneak")
|
||||||
|
var visuals := player.get_node("Visuals") as Node3D
|
||||||
|
|
||||||
assert(player.play_net_strike_visual())
|
assert(player.play_net_strike_visual())
|
||||||
player.resolve_net_strike_visual(true)
|
player.resolve_net_strike_visual(true)
|
||||||
player.net_success_contact_pause_duration = 0.05
|
player.net_success_contact_pause_duration = 0.05
|
||||||
|
player.showcase_turn_duration = 0.05
|
||||||
|
player.showcase_camera_transition_duration = 0.05
|
||||||
|
player.net_showcase_camera_yaw_offset = 0.0
|
||||||
var crab_catch := FishCatch.new()
|
var crab_catch := FishCatch.new()
|
||||||
crab_catch.fish = CrabBrown
|
crab_catch.fish = CrabBrown
|
||||||
crab_catch.fish_id = CrabBrown.id
|
crab_catch.fish_id = CrabBrown.id
|
||||||
|
|
@ -89,9 +93,27 @@ func _run() -> void:
|
||||||
await contact_pause.finished
|
await contact_pause.finished
|
||||||
assert(StringName(player.get("_animation_action_id")).is_empty())
|
assert(StringName(player.get("_animation_action_id")).is_empty())
|
||||||
assert(bool(player.get("_showcase_animation_active")))
|
assert(bool(player.get("_showcase_animation_active")))
|
||||||
|
var stored_showcase_rotation: Vector3 = player.get(
|
||||||
|
"_showcase_visual_rotation"
|
||||||
|
)
|
||||||
|
await create_timer(0.1).timeout
|
||||||
|
assert(
|
||||||
|
absf(wrapf(
|
||||||
|
visuals.rotation.y - stored_showcase_rotation.y,
|
||||||
|
-PI,
|
||||||
|
PI,
|
||||||
|
)) > 3.0
|
||||||
|
)
|
||||||
var catch_display := player.get("_catch_display") as Node3D
|
var catch_display := player.get("_catch_display") as Node3D
|
||||||
assert(catch_display != null and catch_display.visible)
|
assert(catch_display != null and catch_display.visible)
|
||||||
player.end_catch_showcase(Callable(), true)
|
player.end_catch_showcase(Callable(), true, true)
|
||||||
|
assert(
|
||||||
|
absf(wrapf(
|
||||||
|
visuals.rotation.y - stored_showcase_rotation.y,
|
||||||
|
-PI,
|
||||||
|
PI,
|
||||||
|
)) < 0.01
|
||||||
|
)
|
||||||
|
|
||||||
Input.action_release(&"sneak")
|
Input.action_release(&"sneak")
|
||||||
player.queue_free()
|
player.queue_free()
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,17 @@ func _run() -> void:
|
||||||
for owned: OwnedItem in tackle_items:
|
for owned: OwnedItem in tackle_items:
|
||||||
sorted_ids.append(owned.item_id)
|
sorted_ids.append(owned.item_id)
|
||||||
assert(sorted_ids == EXPECTED_BAIT_ORDER)
|
assert(sorted_ids == EXPECTED_BAIT_ORDER)
|
||||||
|
var bag := PlayerBag.new()
|
||||||
|
bag.setup(Catalog)
|
||||||
|
assert(bag.add_item(&"worms", 1))
|
||||||
|
assert(bag.add_item(&"shrimp", 1))
|
||||||
|
var worms_slot: int = bag.get_storage_slot(&"worms")
|
||||||
|
var shrimp_slot: int = bag.get_storage_slot(&"shrimp")
|
||||||
|
assert(worms_slot >= 0 and shrimp_slot >= 0 and worms_slot != shrimp_slot)
|
||||||
|
assert(bag.move_item_to_storage_slot(&"worms", shrimp_slot))
|
||||||
|
assert(bag.get_storage_slot(&"worms") == shrimp_slot)
|
||||||
|
assert(bag.get_storage_slot(&"shrimp") == worms_slot)
|
||||||
|
bag.free()
|
||||||
player_menu.free()
|
player_menu.free()
|
||||||
print("Tackle order validation: PASS")
|
print("Tackle order validation: PASS")
|
||||||
quit()
|
quit()
|
||||||
|
|
|
||||||
99
tests/tree_gathering_prototype_validation.gd
Normal file
99
tests/tree_gathering_prototype_validation.gd
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const StarterIslandScene = preload(
|
||||||
|
"res://world/regions/starter_island_region.tscn"
|
||||||
|
)
|
||||||
|
const Gatherables: GatherableCatalog = preload(
|
||||||
|
"res://gathering/catalog/gatherable_catalog.tres"
|
||||||
|
)
|
||||||
|
const FishCatalog: FishPool = preload("res://fish/pools/fish_catalog.tres")
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
_validate_beetle_data()
|
||||||
|
await _validate_tree_anchors()
|
||||||
|
_validate_anchored_presentation()
|
||||||
|
_validate_three_dimensional_targeting()
|
||||||
|
print("Tree gathering prototype validation: PASS")
|
||||||
|
quit()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_beetle_data() -> void:
|
||||||
|
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
|
||||||
|
assert(beetle != null and beetle.is_available())
|
||||||
|
assert(beetle.catch_data == FishCatalog.get_fish_by_id(&"beetle_stag_common"))
|
||||||
|
assert(beetle.catch_data.display_texture != null)
|
||||||
|
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
|
||||||
|
assert(beetle.catch_data.collection_group == &"Beetles")
|
||||||
|
assert(beetle.required_tool_id == &"crab_net")
|
||||||
|
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
|
||||||
|
assert(beetle.population == 3)
|
||||||
|
assert(is_equal_approx(beetle.sprite_pixel_size, 0.005))
|
||||||
|
assert(beetle.is_stationary_spawn())
|
||||||
|
assert(not beetle.can_be_scared())
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_tree_anchors() -> void:
|
||||||
|
var region := StarterIslandScene.instantiate() as WorldRegion
|
||||||
|
root.add_child(region)
|
||||||
|
await process_frame
|
||||||
|
var anchor_set: GatherableAnchorSet3D = region.get_gatherable_anchor_set(
|
||||||
|
&"starter_reachable_tree_trunks"
|
||||||
|
)
|
||||||
|
assert(anchor_set != null)
|
||||||
|
var positions: PackedVector3Array = anchor_set.get_spawn_positions()
|
||||||
|
assert(positions.size() == 8)
|
||||||
|
for position: Vector3 in positions:
|
||||||
|
assert(position.is_finite())
|
||||||
|
assert(position.y >= 4.3 and position.y <= 4.5)
|
||||||
|
region.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_anchored_presentation() -> void:
|
||||||
|
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
|
||||||
|
var presentation := WorldGatherable.new()
|
||||||
|
root.add_child(presentation)
|
||||||
|
presentation.configure("beetle-visual", beetle, Vector3.ZERO, 0.0)
|
||||||
|
var sprite := presentation.get_node("GatherableSprite") as Sprite3D
|
||||||
|
assert(sprite != null)
|
||||||
|
assert(is_zero_approx(sprite.position.y))
|
||||||
|
assert(is_equal_approx(sprite.pixel_size, 0.005))
|
||||||
|
assert(not sprite.shaded)
|
||||||
|
assert(sprite.billboard == BaseMaterial3D.BILLBOARD_ENABLED)
|
||||||
|
assert(sprite.texture_filter == BaseMaterial3D.TEXTURE_FILTER_NEAREST)
|
||||||
|
presentation.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_three_dimensional_targeting() -> void:
|
||||||
|
var service := NetworkWorldSpawnService.new()
|
||||||
|
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
|
||||||
|
service.set(
|
||||||
|
"_entities",
|
||||||
|
{
|
||||||
|
"beetle-test": {
|
||||||
|
"entity_id": "beetle-test",
|
||||||
|
"type_id": &"beetle_stag_common",
|
||||||
|
"data": beetle,
|
||||||
|
"position": Vector3(2.0, 4.4, 3.0),
|
||||||
|
"locked": false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
service.find_capture_target(
|
||||||
|
Vector3(2.0, 4.4, 3.0),
|
||||||
|
&"crab_net",
|
||||||
|
)
|
||||||
|
== "beetle-test"
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
service.find_capture_target(
|
||||||
|
Vector3(2.0, 3.0, 3.0),
|
||||||
|
&"crab_net",
|
||||||
|
).is_empty()
|
||||||
|
)
|
||||||
|
service.free()
|
||||||
1
tests/tree_gathering_prototype_validation.gd.uid
Normal file
1
tests/tree_gathering_prototype_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d0g1xubbi48n
|
||||||
199
tests/unified_inventory_validation.gd
Normal file
199
tests/unified_inventory_validation.gd
Normal file
|
|
@ -0,0 +1,199 @@
|
||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const ItemCatalogResource: ItemCatalog = preload(
|
||||||
|
"res://items/catalog/item_catalog.tres"
|
||||||
|
)
|
||||||
|
const Bluegill: FishData = preload(
|
||||||
|
"res://fish/species/bluegill/bluegill.tres"
|
||||||
|
)
|
||||||
|
const MainShopBuyer: FishBuyerProfile = preload(
|
||||||
|
"res://economy/buyers/main_fishing_shop.tres"
|
||||||
|
)
|
||||||
|
const FishCatalogResource: FishPool = preload(
|
||||||
|
"res://fish/pools/fish_catalog.tres"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
call_deferred("_run")
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var root := Node.new()
|
||||||
|
get_root().add_child(root)
|
||||||
|
var bag := PlayerBag.new()
|
||||||
|
var catches := FishInventory.new()
|
||||||
|
var capacity := PlayerCoolerCapacity.new()
|
||||||
|
var layout := PlayerInventoryLayout.new()
|
||||||
|
var hotbar := PlayerHotbar.new()
|
||||||
|
for node: Node in [bag, catches, capacity, layout, hotbar]:
|
||||||
|
root.add_child(node)
|
||||||
|
bag.setup(ItemCatalogResource)
|
||||||
|
layout.setup(bag, catches, ItemCatalogResource, capacity)
|
||||||
|
bag.set_inventory_layout(layout)
|
||||||
|
catches.set_inventory_layout(layout)
|
||||||
|
hotbar.setup(bag, ItemCatalogResource, catches, layout)
|
||||||
|
assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36])
|
||||||
|
assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72])
|
||||||
|
|
||||||
|
assert(bag.add_item(&"coffee", 3))
|
||||||
|
assert(layout.get_inventory_count() == 1)
|
||||||
|
assert(layout.get_inventory_capacity() == 9)
|
||||||
|
assert(layout.get_hotbar_count() == 0)
|
||||||
|
assert(hotbar.assign_item(2, &"coffee"))
|
||||||
|
assert(layout.get_inventory_count() == 0)
|
||||||
|
assert(layout.get_hotbar_count() == 1)
|
||||||
|
assert(
|
||||||
|
layout.get_container(PlayerInventoryLayout.EntryKind.ITEM, &"coffee")
|
||||||
|
== PlayerInventoryLayout.InventoryContainer.HOTBAR
|
||||||
|
)
|
||||||
|
assert(hotbar.clear_slot(2))
|
||||||
|
assert(layout.get_inventory_count() == 1)
|
||||||
|
assert(layout.get_hotbar_count() == 0)
|
||||||
|
|
||||||
|
var fish_catch := FishCatch.new()
|
||||||
|
fish_catch.fish = Bluegill
|
||||||
|
fish_catch.fish_id = Bluegill.id
|
||||||
|
fish_catch.catch_id = &"bluegill:unified_inventory_test"
|
||||||
|
fish_catch.weight_lb = Bluegill.get_minimum_weight()
|
||||||
|
fish_catch.display_scale = Bluegill.get_display_scale_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
fish_catch.sale_value = Bluegill.get_sale_value_for_weight(
|
||||||
|
fish_catch.weight_lb
|
||||||
|
)
|
||||||
|
assert(catches.add_catch(fish_catch))
|
||||||
|
assert(layout.get_inventory_count() == 2)
|
||||||
|
assert(layout.move_entry_to_first_free(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
fish_catch.catch_id,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.STORAGE,
|
||||||
|
))
|
||||||
|
assert(layout.get_inventory_count() == 1)
|
||||||
|
assert(layout.get_storage_count() == 1)
|
||||||
|
assert(layout.get_storage_capacity() == 9)
|
||||||
|
var inventory_grid := GeneralInventoryGrid.new()
|
||||||
|
root.add_child(inventory_grid)
|
||||||
|
inventory_grid.set_slot_presentation(Vector2(78.0, 78.0), 10)
|
||||||
|
inventory_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
catches,
|
||||||
|
hotbar,
|
||||||
|
ItemCatalogResource,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
|
)
|
||||||
|
var all_inventory_slots: Array = inventory_grid.get("_slots")
|
||||||
|
assert(all_inventory_slots.size() == 36)
|
||||||
|
assert(inventory_grid.get_slots().size() == 9)
|
||||||
|
for slot_index: int in all_inventory_slots.size():
|
||||||
|
var slot := all_inventory_slots[slot_index] as GeneralInventorySlot
|
||||||
|
assert(slot.custom_minimum_size == Vector2(78.0, 78.0))
|
||||||
|
assert(slot.disabled == (slot_index >= 9))
|
||||||
|
assert(slot.tooltip_text.is_empty())
|
||||||
|
var normal := slot.get_theme_stylebox("normal") as StyleBoxFlat
|
||||||
|
assert(normal != null and normal.corner_radius_top_left == 39)
|
||||||
|
var locked_slot := all_inventory_slots[9] as GeneralInventorySlot
|
||||||
|
var locked_icon := locked_slot.get("_icon") as TextureRect
|
||||||
|
assert(locked_icon != null)
|
||||||
|
assert(is_equal_approx(locked_icon.size.x, 24.0))
|
||||||
|
assert(is_equal_approx(locked_icon.size.y, 24.0))
|
||||||
|
assert(is_equal_approx(locked_icon.modulate.a, 0.18))
|
||||||
|
var staged_slot := all_inventory_slots[0] as GeneralInventorySlot
|
||||||
|
staged_slot.set_staged(true)
|
||||||
|
assert(
|
||||||
|
(staged_slot.get_theme_stylebox("normal") as StyleBoxFlat).bg_color
|
||||||
|
== Color(UtilityPageStyle.OCEAN_SELECTED, 0.92)
|
||||||
|
)
|
||||||
|
staged_slot.set_staged(false)
|
||||||
|
var sale_tray_slot := ShopSaleTraySlot.new()
|
||||||
|
root.add_child(sale_tray_slot)
|
||||||
|
assert(
|
||||||
|
sale_tray_slot.custom_minimum_size
|
||||||
|
== GeneralInventoryGrid.DEFAULT_SLOT_SIZE
|
||||||
|
)
|
||||||
|
var sale_tray_style := sale_tray_slot.get_theme_stylebox(
|
||||||
|
"normal"
|
||||||
|
) as StyleBoxFlat
|
||||||
|
assert(sale_tray_style != null)
|
||||||
|
assert(sale_tray_style.corner_radius_top_left == 26)
|
||||||
|
var wallet := PlayerWallet.new()
|
||||||
|
root.add_child(wallet)
|
||||||
|
assert(wallet.restore_balance(15000))
|
||||||
|
assert(layout.get_next_backpack_cost() == 1500)
|
||||||
|
assert(layout.purchase_backpack(wallet))
|
||||||
|
assert(layout.get_inventory_capacity() == 18)
|
||||||
|
assert(inventory_grid.get_slots().size() == 18)
|
||||||
|
assert(layout.get_next_backpack_cost() == 4500)
|
||||||
|
assert(layout.purchase_backpack(wallet))
|
||||||
|
assert(layout.get_inventory_capacity() == 27)
|
||||||
|
assert(inventory_grid.get_slots().size() == 27)
|
||||||
|
assert(layout.get_next_backpack_cost() == 9000)
|
||||||
|
assert(layout.purchase_backpack(wallet))
|
||||||
|
assert(layout.get_inventory_capacity() == 36)
|
||||||
|
assert(inventory_grid.get_slots().size() == 36)
|
||||||
|
assert(layout.get_next_backpack_cost() == -1)
|
||||||
|
var storage_grid := GeneralInventoryGrid.new()
|
||||||
|
root.add_child(storage_grid)
|
||||||
|
storage_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
catches,
|
||||||
|
hotbar,
|
||||||
|
ItemCatalogResource,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.STORAGE,
|
||||||
|
)
|
||||||
|
var all_storage_slots: Array = storage_grid.get("_slots")
|
||||||
|
assert(all_storage_slots.size() == 72)
|
||||||
|
assert(storage_grid.get_slots().size() == 9)
|
||||||
|
assert(
|
||||||
|
(all_storage_slots[9] as GeneralInventorySlot).accessibility_name
|
||||||
|
== "locked storage slot 10"
|
||||||
|
)
|
||||||
|
var storage_wallet := PlayerWallet.new()
|
||||||
|
root.add_child(storage_wallet)
|
||||||
|
assert(storage_wallet.restore_balance(9500))
|
||||||
|
for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]:
|
||||||
|
assert(capacity.purchase(storage_wallet))
|
||||||
|
assert(layout.get_storage_capacity() == expected_capacity)
|
||||||
|
assert(storage_grid.get_slots().size() == expected_capacity)
|
||||||
|
assert(capacity.get_next_capacity() == -1)
|
||||||
|
assert(capacity.get_next_cost() == -1)
|
||||||
|
|
||||||
|
var saved := layout.to_save_data()
|
||||||
|
assert(layout.move_entry_to_first_free(
|
||||||
|
PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
&"coffee",
|
||||||
|
PlayerInventoryLayout.InventoryContainer.STORAGE,
|
||||||
|
))
|
||||||
|
assert(layout.restore_from_save_data(saved))
|
||||||
|
assert(layout.is_item_in_inventory(&"coffee"))
|
||||||
|
assert(
|
||||||
|
layout.get_container(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH,
|
||||||
|
fish_catch.catch_id,
|
||||||
|
) == PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
)
|
||||||
|
|
||||||
|
var network_sale := NetworkSaleService.new()
|
||||||
|
var session := NetworkSession.new()
|
||||||
|
root.add_child(session)
|
||||||
|
root.add_child(network_sale)
|
||||||
|
network_sale.set("_session", session)
|
||||||
|
network_sale.set("_item_catalog", ItemCatalogResource)
|
||||||
|
network_sale.set("_fish_catalog", FishCatalogResource)
|
||||||
|
var result: Dictionary = network_sale.call(
|
||||||
|
"_build_authoritative_result",
|
||||||
|
1,
|
||||||
|
"mixed_sale_test",
|
||||||
|
[],
|
||||||
|
[{"item_id": "coffee", "quantity": 2}],
|
||||||
|
MainShopBuyer,
|
||||||
|
)
|
||||||
|
assert(bool(result.get("accepted", false)))
|
||||||
|
assert(int(result.get("payout", -1)) == 20)
|
||||||
|
assert((result.get("items", []) as Array).size() == 1)
|
||||||
|
|
||||||
|
print("Unified inventory validation: PASS")
|
||||||
|
root.free()
|
||||||
|
quit()
|
||||||
1
tests/unified_inventory_validation.gd.uid
Normal file
1
tests/unified_inventory_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://osuf2xa2bxht
|
||||||
|
|
@ -2,7 +2,12 @@ extends SceneTree
|
||||||
|
|
||||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||||
const TEST_PORT: int = 18170
|
const TEST_PORT: int = 18170
|
||||||
const EXPECTED_POPULATION: int = 2
|
const EXPECTED_POPULATION: int = 8
|
||||||
|
const EXPECTED_BY_TYPE: Dictionary = {
|
||||||
|
&"crab_brown": 2,
|
||||||
|
&"clam_manila": 3,
|
||||||
|
&"beetle_stag_common": 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
|
|
@ -28,6 +33,7 @@ func _run_host() -> void:
|
||||||
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
|
assert(session.start_dedicated_host(TEST_PORT, 8, "127.0.0.1"))
|
||||||
assert(session.set_host_open(true))
|
assert(session.set_host_open(true))
|
||||||
await _wait_for_population(service)
|
await _wait_for_population(service)
|
||||||
|
_freeze_timed_entries(service)
|
||||||
_validate_population(service, true)
|
_validate_population(service, true)
|
||||||
|
|
||||||
var remote_peer_id: int = 0
|
var remote_peer_id: int = 0
|
||||||
|
|
@ -83,6 +89,9 @@ func _run_client() -> void:
|
||||||
))
|
))
|
||||||
await _wait_for_population(service)
|
await _wait_for_population(service)
|
||||||
_validate_population(service, false)
|
_validate_population(service, false)
|
||||||
|
# Give the host process time to observe the authenticated peer before this
|
||||||
|
# deliberately short client validation disconnects.
|
||||||
|
await create_timer(0.75).timeout
|
||||||
print("World spawn multiplayer client validation: PASS")
|
print("World spawn multiplayer client validation: PASS")
|
||||||
session.disconnect_session("")
|
session.disconnect_session("")
|
||||||
main.queue_free()
|
main.queue_free()
|
||||||
|
|
@ -106,8 +115,10 @@ func _validate_population(service: Node, expect_authoritative_state: bool) -> vo
|
||||||
var presentations: Dictionary = service.get("_presentations")
|
var presentations: Dictionary = service.get("_presentations")
|
||||||
assert(entities.size() == EXPECTED_POPULATION)
|
assert(entities.size() == EXPECTED_POPULATION)
|
||||||
assert(presentations.size() == EXPECTED_POPULATION)
|
assert(presentations.size() == EXPECTED_POPULATION)
|
||||||
|
var counts: Dictionary = {}
|
||||||
for state: Dictionary in entities.values():
|
for state: Dictionary in entities.values():
|
||||||
assert(state.get("type_id") == &"crab_brown")
|
var type_id := state.get("type_id") as StringName
|
||||||
|
counts[type_id] = int(counts.get(type_id, 0)) + 1
|
||||||
var position: Variant = state.get("position")
|
var position: Variant = state.get("position")
|
||||||
assert(typeof(position) == TYPE_VECTOR3)
|
assert(typeof(position) == TYPE_VECTOR3)
|
||||||
assert((position as Vector3).is_finite())
|
assert((position as Vector3).is_finite())
|
||||||
|
|
@ -120,27 +131,43 @@ func _validate_population(service: Node, expect_authoritative_state: bool) -> vo
|
||||||
if expect_authoritative_state:
|
if expect_authoritative_state:
|
||||||
var quality: int = int(state.get("quality", -1))
|
var quality: int = int(state.get("quality", -1))
|
||||||
assert(FishQuality.is_valid(quality))
|
assert(FishQuality.is_valid(quality))
|
||||||
assert(entry.get_movement_speed_for_quality(quality) > 0.0)
|
if entry.is_stationary_spawn():
|
||||||
assert(entry.get_scare_radius_for_quality(quality) > 0.0)
|
assert(entry.get_movement_speed_for_quality(quality) <= 0.0)
|
||||||
|
assert(not entry.can_be_scared())
|
||||||
|
else:
|
||||||
|
assert(entry.get_movement_speed_for_quality(quality) > 0.0)
|
||||||
|
assert(entry.get_scare_radius_for_quality(quality) > 0.0)
|
||||||
|
assert(counts == EXPECTED_BY_TYPE)
|
||||||
|
|
||||||
|
|
||||||
|
func _freeze_timed_entries(service: Node) -> void:
|
||||||
|
for state: Dictionary in (service.get("_entities") as Dictionary).values():
|
||||||
|
var entry := state.get("data") as GatherableData
|
||||||
|
if entry != null and entry.active_lifetime_seconds > 0.0:
|
||||||
|
state["expires_at"] = INF
|
||||||
|
|
||||||
|
|
||||||
func _validate_respawn_budget(service: Node) -> void:
|
func _validate_respawn_budget(service: Node) -> void:
|
||||||
var entities: Dictionary = service.get("_entities")
|
var entities: Dictionary = service.get("_entities")
|
||||||
var entity_ids: Array = entities.keys()
|
var crab_ids: Array[String] = []
|
||||||
assert(entity_ids.size() == EXPECTED_POPULATION)
|
for entity_id: String in entities:
|
||||||
for entity_id: Variant in entity_ids:
|
var state: Dictionary = entities[entity_id]
|
||||||
|
if state.get("type_id") == &"crab_brown":
|
||||||
|
crab_ids.append(entity_id)
|
||||||
|
assert(crab_ids.size() == 2)
|
||||||
|
for entity_id: String in crab_ids:
|
||||||
service.call(
|
service.call(
|
||||||
"_despawn_entity",
|
"_despawn_entity",
|
||||||
str(entity_id),
|
entity_id,
|
||||||
&"captured",
|
&"captured",
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
assert((service.get("_entities") as Dictionary).is_empty())
|
assert((service.get("_entities") as Dictionary).size() == 6)
|
||||||
|
|
||||||
var now: float = Time.get_ticks_msec() / 1000.0
|
var now: float = Time.get_ticks_msec() / 1000.0
|
||||||
var respawns: Array = service.get("_respawns")
|
var respawns: Array = service.get("_respawns")
|
||||||
assert(respawns.size() == EXPECTED_POPULATION)
|
assert(respawns.size() == crab_ids.size())
|
||||||
for index: int in respawns.size():
|
for index: int in respawns.size():
|
||||||
var respawn: Dictionary = respawns[index]
|
var respawn: Dictionary = respawns[index]
|
||||||
var delay: float = float(respawn.get("due", 0.0)) - now
|
var delay: float = float(respawn.get("due", 0.0)) - now
|
||||||
|
|
@ -149,19 +176,20 @@ func _validate_respawn_budget(service: Node) -> void:
|
||||||
respawns[index] = respawn
|
respawns[index] = respawn
|
||||||
|
|
||||||
service.call("_update_respawns")
|
service.call("_update_respawns")
|
||||||
assert((service.get("_entities") as Dictionary).size() == 1)
|
assert((service.get("_entities") as Dictionary).size() == 7)
|
||||||
assert((service.get("_respawns") as Array).size() == 1)
|
assert((service.get("_respawns") as Array).size() == 1)
|
||||||
var next_by_type: Dictionary = service.get("_next_respawn_by_type")
|
var next_by_type: Dictionary = service.get("_next_respawn_by_type")
|
||||||
var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0))
|
var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0))
|
||||||
assert(next_allowed - now >= 179.0)
|
assert(next_allowed - now >= 179.0)
|
||||||
|
|
||||||
service.call("_update_respawns")
|
service.call("_update_respawns")
|
||||||
assert((service.get("_entities") as Dictionary).size() == 1)
|
assert((service.get("_entities") as Dictionary).size() == 7)
|
||||||
assert((service.get("_respawns") as Array).size() == 1)
|
assert((service.get("_respawns") as Array).size() == 1)
|
||||||
next_by_type[&"crab_brown"] = now - 1.0
|
next_by_type[&"crab_brown"] = now - 1.0
|
||||||
service.call("_update_respawns")
|
service.call("_update_respawns")
|
||||||
assert((service.get("_entities") as Dictionary).size() == 2)
|
assert((service.get("_entities") as Dictionary).size() == 8)
|
||||||
assert((service.get("_respawns") as Array).is_empty())
|
assert((service.get("_respawns") as Array).is_empty())
|
||||||
|
_validate_population(service, true)
|
||||||
|
|
||||||
|
|
||||||
func _create_initialized_main() -> Node:
|
func _create_initialized_main() -> Node:
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,30 @@ func _validate_catalog_statuses() -> void:
|
||||||
assert(is_equal_approx(brown.minimum_surface_y, -0.44))
|
assert(is_equal_approx(brown.minimum_surface_y, -0.44))
|
||||||
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
|
assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data)
|
||||||
assert(not brown.catch_data.is_fishable())
|
assert(not brown.catch_data.is_fishable())
|
||||||
|
var clam: GatherableData = Gatherables.get_entry(&"clam_manila")
|
||||||
|
assert(clam != null and clam.is_available())
|
||||||
|
assert(clam.catch_data.collection_method == FishData.CollectionMethod.DIGGING)
|
||||||
|
assert(clam.catch_data.logbook_section == FishData.LogbookSection.SHELLFISH)
|
||||||
|
assert(clam.required_tool_id == &"standard_shovel")
|
||||||
|
assert(clam.diggable_area_id == &"starter_beach")
|
||||||
|
assert(clam.presentation_mode == GatherableData.PresentationMode.WATER_SPURT)
|
||||||
|
assert(clam.is_stationary_hotspot())
|
||||||
|
assert(not clam.requires_sneaking)
|
||||||
|
assert(not clam.can_be_scared())
|
||||||
|
assert(is_equal_approx(clam.active_lifetime_seconds, 10.0))
|
||||||
|
assert(FishCatalog.get_fish_by_id(&"clam_manila") == clam.catch_data)
|
||||||
|
assert(not clam.catch_data.is_fishable())
|
||||||
|
var beetle: GatherableData = Gatherables.get_entry(&"beetle_stag_common")
|
||||||
|
assert(beetle != null and beetle.is_available())
|
||||||
|
assert(beetle.catch_data.collection_method == FishData.CollectionMethod.NET)
|
||||||
|
assert(beetle.required_tool_id == &"crab_net")
|
||||||
|
assert(beetle.spawn_anchor_set_id == &"starter_reachable_tree_trunks")
|
||||||
|
assert(beetle.is_stationary_spawn())
|
||||||
|
assert(not beetle.is_stationary_hotspot())
|
||||||
|
assert(not beetle.requires_sneaking)
|
||||||
|
assert(not beetle.can_be_scared())
|
||||||
|
assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data)
|
||||||
|
assert(not beetle.catch_data.is_fishable())
|
||||||
|
|
||||||
for type_id: StringName in [
|
for type_id: StringName in [
|
||||||
&"crab_ghost",
|
&"crab_ghost",
|
||||||
|
|
@ -76,8 +100,10 @@ func _validate_catalog_statuses() -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
var available: Array[GatherableData] = Gatherables.get_available_entries()
|
var available: Array[GatherableData] = Gatherables.get_available_entries()
|
||||||
assert(available.size() == 1)
|
assert(available.size() == 3)
|
||||||
assert(available.front() == brown)
|
assert(available.has(brown))
|
||||||
|
assert(available.has(clam))
|
||||||
|
assert(available.has(beetle))
|
||||||
var rng := RandomNumberGenerator.new()
|
var rng := RandomNumberGenerator.new()
|
||||||
rng.seed = 24680
|
rng.seed = 24680
|
||||||
var captured_delay: float = brown.get_respawn_delay(&"captured", rng)
|
var captured_delay: float = brown.get_respawn_delay(&"captured", rng)
|
||||||
|
|
@ -111,6 +137,18 @@ func _validate_billboard_presentation() -> void:
|
||||||
assert(not sprite.shaded)
|
assert(not sprite.shaded)
|
||||||
gatherable.free()
|
gatherable.free()
|
||||||
|
|
||||||
|
var hotspot := WorldGatherableType.new()
|
||||||
|
hotspot.call("_ensure_water_spurt_visual")
|
||||||
|
var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D
|
||||||
|
assert(hole != null)
|
||||||
|
assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF)
|
||||||
|
var hole_material := hole.mesh.surface_get_material(0) as StandardMaterial3D
|
||||||
|
assert(hole_material != null)
|
||||||
|
assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED)
|
||||||
|
assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED)
|
||||||
|
assert(is_equal_approx(hole_material.albedo_color.a, 1.0))
|
||||||
|
hotspot.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_quality_behavior(entry: GatherableData) -> void:
|
func _validate_quality_behavior(entry: GatherableData) -> void:
|
||||||
var prior_speed: float = -1.0
|
var prior_speed: float = -1.0
|
||||||
|
|
|
||||||
|
|
@ -31,19 +31,10 @@ func _run_host() -> void:
|
||||||
main.get_node("%WorldWeatherService") as WorldWeatherService
|
main.get_node("%WorldWeatherService") as WorldWeatherService
|
||||||
)
|
)
|
||||||
assert(session.start_private_host(TEST_PORT))
|
assert(session.start_private_host(TEST_PORT))
|
||||||
var game_ui := main.get_node("%GameUI") as CanvasLayer
|
assert(world_time.set_authoritative_time(INITIAL_HOST_TIME))
|
||||||
var chat_ui := game_ui.get_node("%ChatUI") as ChatUI
|
assert(world_weather.set_authoritative_weather(
|
||||||
assert(chat_ui.call("_handle_chat_command", "/weather clear"))
|
WorldWeatherService.Weather.RAINY
|
||||||
assert(world_weather.get_weather() == WorldWeatherService.Weather.SUNNY)
|
|
||||||
world_time.synchronize_time(INITIAL_HOST_TIME)
|
|
||||||
assert(is_equal_approx(
|
|
||||||
world_time.get_persistent_time_hours(), INITIAL_HOST_TIME
|
|
||||||
))
|
))
|
||||||
assert(chat_ui.call("_handle_chat_command", "/weather rainy"))
|
|
||||||
assert(
|
|
||||||
world_weather.get_persistent_weather()
|
|
||||||
== WorldWeatherService.Weather.RAINY
|
|
||||||
)
|
|
||||||
assert(session.set_host_open(true))
|
assert(session.set_host_open(true))
|
||||||
|
|
||||||
var remote_peer_id: int = 0
|
var remote_peer_id: int = 0
|
||||||
|
|
@ -61,35 +52,12 @@ func _run_host() -> void:
|
||||||
assert(session.peer_supports_capability(
|
assert(session.peer_supports_capability(
|
||||||
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
remote_peer_id, NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
))
|
))
|
||||||
var remote_record: PeerRegistry.PeerRecord = session.get_peer_record(
|
|
||||||
remote_peer_id
|
|
||||||
)
|
|
||||||
assert(remote_record != null and remote_record.identity_authenticated)
|
|
||||||
assert(session.set_peer_operator(
|
|
||||||
remote_peer_id,
|
|
||||||
remote_record.identity_fingerprint,
|
|
||||||
true,
|
|
||||||
))
|
|
||||||
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
assert(world_time.get_phase() == WorldTimeService.Phase.DUSK)
|
||||||
|
await create_timer(1.0).timeout
|
||||||
var command_deadline: int = Time.get_ticks_msec() + 10000
|
assert(world_time.set_authoritative_time(UPDATED_HOST_TIME))
|
||||||
while (
|
assert(world_weather.set_authoritative_weather(
|
||||||
Time.get_ticks_msec() < command_deadline
|
WorldWeatherService.Weather.FOGGY
|
||||||
and _wrapped_time_difference(
|
|
||||||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
|
||||||
) > TIME_TOLERANCE_HOURS
|
|
||||||
):
|
|
||||||
await process_frame
|
|
||||||
assert(is_equal_approx(
|
|
||||||
world_time.get_persistent_time_hours(), UPDATED_HOST_TIME
|
|
||||||
))
|
))
|
||||||
var fog_deadline: int = Time.get_ticks_msec() + 10000
|
|
||||||
while Time.get_ticks_msec() < fog_deadline and not world_weather.is_foggy():
|
|
||||||
await process_frame
|
|
||||||
assert(
|
|
||||||
world_weather.get_persistent_weather()
|
|
||||||
== WorldWeatherService.Weather.FOGGY
|
|
||||||
)
|
|
||||||
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < disconnect_deadline
|
Time.get_ticks_msec() < disconnect_deadline
|
||||||
|
|
@ -131,12 +99,6 @@ func _run_client() -> void:
|
||||||
assert(session.supports_server_capability(
|
assert(session.supports_server_capability(
|
||||||
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
NetworkProtocol.WORLD_WEATHER_CAPABILITY
|
||||||
))
|
))
|
||||||
assert(world_time.restore_persistent_time_hours(15.25))
|
|
||||||
assert(world_weather.restore_persistent_state(
|
|
||||||
WorldWeatherService.Weather.CLOUDY,
|
|
||||||
222.0,
|
|
||||||
))
|
|
||||||
|
|
||||||
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
var initial_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < initial_deadline
|
Time.get_ticks_msec() < initial_deadline
|
||||||
|
|
@ -170,11 +132,7 @@ func _run_client() -> void:
|
||||||
assert(is_equal_approx(clock_panel.position.y, 10.0))
|
assert(is_equal_approx(clock_panel.position.y, 10.0))
|
||||||
assert(is_equal_approx(weather_icon.position.y, 10.0))
|
assert(is_equal_approx(weather_icon.position.y, 10.0))
|
||||||
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
|
assert(clock_panel.position.y + clock_panel.size.y < chat_panel.position.y)
|
||||||
var operator_deadline: int = Time.get_ticks_msec() + 10000
|
assert(not chat_ui.has_method("_handle_chat_command"))
|
||||||
while Time.get_ticks_msec() < operator_deadline and not session.is_local_operator():
|
|
||||||
await process_frame
|
|
||||||
assert(session.is_local_operator())
|
|
||||||
assert(chat_ui.call("_handle_chat_command", "/time night"))
|
|
||||||
|
|
||||||
var update_deadline: int = Time.get_ticks_msec() + 10000
|
var update_deadline: int = Time.get_ticks_msec() + 10000
|
||||||
while (
|
while (
|
||||||
|
|
@ -188,10 +146,7 @@ func _run_client() -> void:
|
||||||
world_time.get_time_hours(), UPDATED_HOST_TIME
|
world_time.get_time_hours(), UPDATED_HOST_TIME
|
||||||
) <= TIME_TOLERANCE_HOURS)
|
) <= TIME_TOLERANCE_HOURS)
|
||||||
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
assert(world_time.get_phase() == WorldTimeService.Phase.NIGHT)
|
||||||
assert(is_equal_approx(world_time.get_persistent_time_hours(), 15.25))
|
|
||||||
assert(clock_label.text == world_time.get_clock_text())
|
assert(clock_label.text == world_time.get_clock_text())
|
||||||
await create_timer(0.6).timeout
|
|
||||||
assert(chat_ui.call("_handle_chat_command", "/weather foggy"))
|
|
||||||
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
var fog_deadline: int = Time.get_ticks_msec() + 8000
|
||||||
while (
|
while (
|
||||||
Time.get_ticks_msec() < fog_deadline
|
Time.get_ticks_msec() < fog_deadline
|
||||||
|
|
@ -200,14 +155,6 @@ func _run_client() -> void:
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(world_weather.is_foggy())
|
assert(world_weather.is_foggy())
|
||||||
assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY)
|
assert(weather_icon.get_weather() == WorldWeatherService.Weather.FOGGY)
|
||||||
assert(
|
|
||||||
world_weather.get_persistent_weather()
|
|
||||||
== WorldWeatherService.Weather.CLOUDY
|
|
||||||
)
|
|
||||||
assert(is_equal_approx(
|
|
||||||
world_weather.get_persistent_seconds_remaining(),
|
|
||||||
222.0,
|
|
||||||
))
|
|
||||||
chat_ui.call("set_dock_right", true)
|
chat_ui.call("set_dock_right", true)
|
||||||
await process_frame
|
await process_frame
|
||||||
assert(clock_panel.position.x > 1000.0)
|
assert(clock_panel.position.x > 1000.0)
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ func _initialize() -> void:
|
||||||
|
|
||||||
func _run() -> void:
|
func _run() -> void:
|
||||||
_validate_clock_boundaries_and_duration()
|
_validate_clock_boundaries_and_duration()
|
||||||
|
_validate_system_clock_authority()
|
||||||
_validate_persistent_host_clock()
|
_validate_persistent_host_clock()
|
||||||
_validate_fishing_availability()
|
_validate_fishing_availability()
|
||||||
_validate_fishing_spot_context()
|
_validate_fishing_spot_context()
|
||||||
|
|
@ -40,7 +41,34 @@ func _run() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _validate_clock_boundaries_and_duration() -> void:
|
func _validate_clock_boundaries_and_duration() -> void:
|
||||||
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 3600.0)
|
assert(WorldTimeServiceType.REAL_SECONDS_PER_CYCLE == 86400.0)
|
||||||
|
assert(is_equal_approx(
|
||||||
|
WorldTimeServiceType.HOURS_PER_REAL_SECOND,
|
||||||
|
1.0 / 3600.0,
|
||||||
|
))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
WorldTimeServiceType.time_hours_from_datetime({
|
||||||
|
"hour": 17, "minute": 30, "second": 18,
|
||||||
|
}),
|
||||||
|
17.505,
|
||||||
|
))
|
||||||
|
assert(
|
||||||
|
WorldTimeServiceType.date_id_from_datetime({
|
||||||
|
"year": 2026, "month": 8, "day": 18,
|
||||||
|
}) == "2026-08-18"
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
WorldTimeServiceType.calendar_cycle_id_from_datetime({
|
||||||
|
"year": 2026, "month": 8, "day": 18,
|
||||||
|
"hour": 7, "minute": 59, "second": 0,
|
||||||
|
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-17"
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
WorldTimeServiceType.calendar_cycle_id_from_datetime({
|
||||||
|
"year": 2026, "month": 8, "day": 18,
|
||||||
|
"hour": 8, "minute": 0, "second": 0,
|
||||||
|
}, WorldTimeServiceType.DAY_START_HOUR) == "2026-08-18"
|
||||||
|
)
|
||||||
assert(
|
assert(
|
||||||
WorldTimeServiceType.phase_for_hour(7.49)
|
WorldTimeServiceType.phase_for_hour(7.49)
|
||||||
== WorldTimeServiceType.Phase.NIGHT
|
== WorldTimeServiceType.Phase.NIGHT
|
||||||
|
|
@ -71,12 +99,12 @@ func _validate_clock_boundaries_and_duration() -> void:
|
||||||
|
|
||||||
var clock := WorldTimeServiceType.new()
|
var clock := WorldTimeServiceType.new()
|
||||||
root.add_child(clock)
|
root.add_child(clock)
|
||||||
clock.begin_session(8.0)
|
clock.begin_test_session(8.0)
|
||||||
clock.advance_time(1800.0)
|
clock.advance_time(12.0 * 60.0 * 60.0)
|
||||||
assert(is_equal_approx(clock.get_time_hours(), 20.0))
|
assert(is_equal_approx(clock.get_time_hours(), 20.0))
|
||||||
assert(clock.is_night_period())
|
assert(clock.is_night_period())
|
||||||
assert(clock.is_transition())
|
assert(clock.is_transition())
|
||||||
clock.advance_time(1800.0)
|
clock.advance_time(12.0 * 60.0 * 60.0)
|
||||||
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
assert(is_equal_approx(clock.get_time_hours(), 8.0))
|
||||||
assert(not clock.is_night_period())
|
assert(not clock.is_night_period())
|
||||||
assert(clock.is_transition())
|
assert(clock.is_transition())
|
||||||
|
|
@ -88,10 +116,10 @@ func _validate_clock_boundaries_and_duration() -> void:
|
||||||
func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
|
func(time_hours: float, _phase: WorldTimeService.Phase) -> void:
|
||||||
emitted_times.append(time_hours)
|
emitted_times.append(time_hours)
|
||||||
)
|
)
|
||||||
clock.begin_session(12.0 + 31.0 / 60.0)
|
clock.begin_test_session(12.0 + 31.0 / 60.0)
|
||||||
emitted_times.clear()
|
emitted_times.clear()
|
||||||
for _frame: int in 1201:
|
for _frame: int in 7201:
|
||||||
clock.advance_time(1.0 / 240.0)
|
clock.advance_time(1.0 / 60.0)
|
||||||
assert(clock.get_clock_text() == "12:33 pm")
|
assert(clock.get_clock_text() == "12:33 pm")
|
||||||
assert(emitted_times.size() >= 2)
|
assert(emitted_times.size() >= 2)
|
||||||
clock.queue_free()
|
clock.queue_free()
|
||||||
|
|
@ -101,20 +129,41 @@ func _validate_persistent_host_clock() -> void:
|
||||||
var clock := WorldTimeServiceType.new()
|
var clock := WorldTimeServiceType.new()
|
||||||
root.add_child(clock)
|
root.add_child(clock)
|
||||||
assert(clock.restore_persistent_time_hours(18.75))
|
assert(clock.restore_persistent_time_hours(18.75))
|
||||||
clock.set_persistence_tracking_enabled(true)
|
clock.begin_test_session(18.75)
|
||||||
clock.begin_session(clock.get_persistent_time_hours())
|
|
||||||
assert(is_equal_approx(clock.get_time_hours(), 18.75))
|
assert(is_equal_approx(clock.get_time_hours(), 18.75))
|
||||||
clock.synchronize_time(19.25)
|
clock.synchronize_time(19.25)
|
||||||
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
||||||
clock.set_persistence_tracking_enabled(false)
|
|
||||||
clock.synchronize_time(6.5)
|
clock.synchronize_time(6.5)
|
||||||
assert(is_equal_approx(clock.get_time_hours(), 6.5))
|
assert(is_equal_approx(clock.get_time_hours(), 6.5))
|
||||||
assert(is_equal_approx(clock.get_persistent_time_hours(), 19.25))
|
assert(is_equal_approx(clock.get_persistent_time_hours(), 6.5))
|
||||||
assert(not clock.restore_persistent_time_hours(-1.0))
|
assert(not clock.restore_persistent_time_hours(-1.0))
|
||||||
assert(not clock.restore_persistent_time_hours(24.0))
|
assert(not clock.restore_persistent_time_hours(24.0))
|
||||||
clock.queue_free()
|
clock.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_system_clock_authority() -> void:
|
||||||
|
var clock := WorldTimeServiceType.new()
|
||||||
|
root.add_child(clock)
|
||||||
|
clock.begin_authoritative_session()
|
||||||
|
var system_datetime: Dictionary = Time.get_datetime_dict_from_system(false)
|
||||||
|
var system_hour: float = WorldTimeServiceType.time_hours_from_datetime(
|
||||||
|
system_datetime
|
||||||
|
)
|
||||||
|
assert(clock.is_using_system_clock())
|
||||||
|
assert(_wrapped_time_difference(
|
||||||
|
clock.get_time_hours(), system_hour
|
||||||
|
) < 2.0 / 3600.0)
|
||||||
|
assert(
|
||||||
|
clock.get_calendar_date_id()
|
||||||
|
== WorldTimeServiceType.date_id_from_datetime(system_datetime)
|
||||||
|
)
|
||||||
|
assert(clock.set_authoritative_time(12.0))
|
||||||
|
assert(not clock.is_using_system_clock())
|
||||||
|
clock.clear_editor_time_override()
|
||||||
|
assert(clock.is_using_system_clock())
|
||||||
|
clock.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_fishing_availability() -> void:
|
func _validate_fishing_availability() -> void:
|
||||||
var day_context := FishingContextType.new()
|
var day_context := FishingContextType.new()
|
||||||
day_context.location_tags = [&"starter_pond"]
|
day_context.location_tags = [&"starter_pond"]
|
||||||
|
|
@ -165,7 +214,7 @@ func _validate_fishing_availability() -> void:
|
||||||
|
|
||||||
func _validate_fishing_spot_context() -> void:
|
func _validate_fishing_spot_context() -> void:
|
||||||
var clock := WorldTimeServiceType.new()
|
var clock := WorldTimeServiceType.new()
|
||||||
clock.begin_session(20.25)
|
clock.begin_test_session(20.25)
|
||||||
var fishing_spot := FishingSpotType.new()
|
var fishing_spot := FishingSpotType.new()
|
||||||
fishing_spot.set("_world_time", clock)
|
fishing_spot.set("_world_time", clock)
|
||||||
var region := FishableWaterRegionType.new()
|
var region := FishableWaterRegionType.new()
|
||||||
|
|
@ -246,6 +295,10 @@ func _validate_environment_presentation() -> void:
|
||||||
float(runtime_sky_material.get_shader_parameter("moon_visibility"))
|
float(runtime_sky_material.get_shader_parameter("moon_visibility"))
|
||||||
< 0.01
|
< 0.01
|
||||||
)
|
)
|
||||||
|
assert(
|
||||||
|
float(runtime_sky_material.get_shader_parameter("star_visibility"))
|
||||||
|
< 0.01
|
||||||
|
)
|
||||||
var day_ambient_energy: float = runtime_environment.ambient_light_energy
|
var day_ambient_energy: float = runtime_environment.ambient_light_energy
|
||||||
assert(not runtime_environment.fog_enabled)
|
assert(not runtime_environment.fog_enabled)
|
||||||
assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
|
assert(is_equal_approx(runtime_environment.fog_sky_affect, 0.35))
|
||||||
|
|
@ -257,6 +310,9 @@ func _validate_environment_presentation() -> void:
|
||||||
)
|
)
|
||||||
assert("uniform float fog_horizon_occlusion" in sky_shader.code)
|
assert("uniform float fog_horizon_occlusion" in sky_shader.code)
|
||||||
assert("fog_horizon_color.rgb" in sky_shader.code)
|
assert("fog_horizon_color.rgb" in sky_shader.code)
|
||||||
|
assert("uniform float star_visibility" in sky_shader.code)
|
||||||
|
assert("float procedural_star_field" in sky_shader.code)
|
||||||
|
assert("upper_sky_fade" in sky_shader.code)
|
||||||
assert("fog_disabled" in water_shader.code)
|
assert("fog_disabled" in water_shader.code)
|
||||||
assert("surface_view_position = view_vertex.xyz" in water_shader.code)
|
assert("surface_view_position = view_vertex.xyz" in water_shader.code)
|
||||||
assert("surface_view_distance = length(surface_view_position)" in water_shader.code)
|
assert("surface_view_distance = length(surface_view_position)" in water_shader.code)
|
||||||
|
|
@ -301,6 +357,17 @@ func _validate_environment_presentation() -> void:
|
||||||
runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
|
runtime_sky_material.get_shader_parameter("sun_direction") as Vector3
|
||||||
)
|
)
|
||||||
assert(night_moon_visibility > 0.99)
|
assert(night_moon_visibility > 0.99)
|
||||||
|
assert(
|
||||||
|
float(runtime_sky_material.get_shader_parameter("star_visibility"))
|
||||||
|
> 0.99
|
||||||
|
)
|
||||||
|
assert((
|
||||||
|
runtime_sky_material.get_shader_parameter("star_color") as Color
|
||||||
|
).is_equal_approx(WorldTimeVisualController.STAR_COLOR))
|
||||||
|
assert(is_equal_approx(
|
||||||
|
float(runtime_sky_material.get_shader_parameter("star_strength")),
|
||||||
|
WorldTimeVisualController.STAR_STRENGTH,
|
||||||
|
))
|
||||||
assert(night_moon_direction.y > 0.85)
|
assert(night_moon_direction.y > 0.85)
|
||||||
assert(night_moon_direction.is_equal_approx(-night_sun_direction))
|
assert(night_moon_direction.is_equal_approx(-night_sun_direction))
|
||||||
assert(runtime_environment.ambient_light_energy < day_ambient_energy)
|
assert(runtime_environment.ambient_light_energy < day_ambient_energy)
|
||||||
|
|
@ -339,6 +406,9 @@ func _validate_environment_presentation() -> void:
|
||||||
assert(is_equal_approx(float(
|
assert(is_equal_approx(float(
|
||||||
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
|
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
|
||||||
), 1.0))
|
), 1.0))
|
||||||
|
assert(is_zero_approx(float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
)))
|
||||||
assert((
|
assert((
|
||||||
runtime_sky_material.get_shader_parameter("fog_horizon_color") as Color
|
runtime_sky_material.get_shader_parameter("fog_horizon_color") as Color
|
||||||
).is_equal_approx(WorldTimeVisualController.NIGHT_FOG))
|
).is_equal_approx(WorldTimeVisualController.NIGHT_FOG))
|
||||||
|
|
@ -425,6 +495,10 @@ func _validate_environment_presentation() -> void:
|
||||||
assert(is_zero_approx(float(
|
assert(is_zero_approx(float(
|
||||||
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
|
runtime_sky_material.get_shader_parameter("fog_horizon_occlusion")
|
||||||
)))
|
)))
|
||||||
|
var dusk_star_visibility := float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
)
|
||||||
|
assert(dusk_star_visibility > 0.45 and dusk_star_visibility < 0.55)
|
||||||
var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
|
var dusk_horizon: Color = runtime_sky_material.get_shader_parameter(
|
||||||
"sky_horizon_color"
|
"sky_horizon_color"
|
||||||
) as Color
|
) as Color
|
||||||
|
|
@ -442,3 +516,8 @@ func _validate_weather_clock_icon() -> void:
|
||||||
weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
|
weather_icon.set_weather(WorldWeatherService.Weather.CLOUDY)
|
||||||
assert(weather_icon.tooltip_text == "cloudy")
|
assert(weather_icon.tooltip_text == "cloudy")
|
||||||
weather_icon.queue_free()
|
weather_icon.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
static func _wrapped_time_difference(left: float, right: float) -> float:
|
||||||
|
var difference: float = absf(left - right)
|
||||||
|
return minf(difference, WorldTimeServiceType.HOURS_PER_DAY - difference)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ func _run() -> void:
|
||||||
_validate_weather_scheduler()
|
_validate_weather_scheduler()
|
||||||
_validate_authoritative_weather_override()
|
_validate_authoritative_weather_override()
|
||||||
_validate_weather_persistence()
|
_validate_weather_persistence()
|
||||||
|
_validate_legacy_forecast_save_migration()
|
||||||
_validate_snapshot_bounds()
|
_validate_snapshot_bounds()
|
||||||
_validate_fishing_weather_seams()
|
_validate_fishing_weather_seams()
|
||||||
_validate_fishing_spot_context()
|
_validate_fishing_spot_context()
|
||||||
|
|
@ -65,14 +66,9 @@ func _validate_weather_scheduler() -> void:
|
||||||
|
|
||||||
func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
|
func _duration_is_valid(weather: WorldWeatherServiceType) -> bool:
|
||||||
var seconds: float = weather.get_seconds_remaining()
|
var seconds: float = weather.get_seconds_remaining()
|
||||||
match weather.get_weather():
|
return is_equal_approx(
|
||||||
WorldWeatherServiceType.Weather.SUNNY:
|
seconds, WorldWeatherServiceType.WEATHER_PERIOD_SECONDS
|
||||||
return seconds >= 480.0 and seconds <= 900.0
|
)
|
||||||
WorldWeatherServiceType.Weather.CLOUDY:
|
|
||||||
return seconds >= 300.0 and seconds <= 720.0
|
|
||||||
WorldWeatherServiceType.Weather.RAINY, WorldWeatherServiceType.Weather.FOGGY:
|
|
||||||
return seconds >= 300.0 and seconds <= 600.0
|
|
||||||
return false
|
|
||||||
|
|
||||||
|
|
||||||
func _validate_authoritative_weather_override() -> void:
|
func _validate_authoritative_weather_override() -> void:
|
||||||
|
|
@ -94,7 +90,7 @@ func _validate_authoritative_weather_override() -> void:
|
||||||
|
|
||||||
var clock := WorldTimeServiceType.new()
|
var clock := WorldTimeServiceType.new()
|
||||||
root.add_child(clock)
|
root.add_child(clock)
|
||||||
clock.begin_session(WorldTimeServiceType.DAY_START_HOUR)
|
clock.begin_test_session(WorldTimeServiceType.DAY_START_HOUR)
|
||||||
var schedule: Array[Dictionary] = []
|
var schedule: Array[Dictionary] = []
|
||||||
for index: int in WorldWeatherServiceType.DAILY_PLAN_SEGMENT_COUNT:
|
for index: int in WorldWeatherServiceType.DAILY_PLAN_SEGMENT_COUNT:
|
||||||
schedule.append({
|
schedule.append({
|
||||||
|
|
@ -159,8 +155,11 @@ func _validate_weather_persistence() -> void:
|
||||||
))
|
))
|
||||||
weather.set_persistence_tracking_enabled(true)
|
weather.set_persistence_tracking_enabled(true)
|
||||||
weather.begin_authoritative_session(20260803)
|
weather.begin_authoritative_session(20260803)
|
||||||
assert(weather.is_raining())
|
assert(weather.get_weather() == WorldWeatherServiceType.DEFAULT_WEATHER)
|
||||||
assert(is_equal_approx(weather.get_seconds_remaining(), 187.5))
|
assert(is_equal_approx(
|
||||||
|
weather.get_seconds_remaining(),
|
||||||
|
WorldWeatherServiceType.WEATHER_PERIOD_SECONDS,
|
||||||
|
))
|
||||||
assert(not weather.restore_persistent_state(
|
assert(not weather.restore_persistent_state(
|
||||||
WorldWeatherServiceType.Weather.RAINY,
|
WorldWeatherServiceType.Weather.RAINY,
|
||||||
WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0,
|
WorldWeatherServiceType.MAX_PERSISTED_SECONDS + 1.0,
|
||||||
|
|
@ -184,11 +183,49 @@ func _validate_snapshot_bounds() -> void:
|
||||||
assert(not NetworkWorldWeatherServiceType.validate_snapshot({
|
assert(not NetworkWorldWeatherServiceType.validate_snapshot({
|
||||||
"session_id": "session",
|
"session_id": "session",
|
||||||
"weather": int(WorldWeatherServiceType.Weather.RAINY),
|
"weather": int(WorldWeatherServiceType.Weather.RAINY),
|
||||||
"seconds_remaining": 1801.0,
|
"seconds_remaining": 3601.0,
|
||||||
"sequence": 2,
|
"sequence": 2,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_legacy_forecast_save_migration() -> void:
|
||||||
|
var legacy_schedule: Array[Dictionary] = []
|
||||||
|
for index: int in JobCatalog.LEGACY_WEATHER_SEGMENT_COUNT:
|
||||||
|
legacy_schedule.append({
|
||||||
|
"start_hour": fposmod(
|
||||||
|
WorldTimeService.DAY_START_HOUR
|
||||||
|
+ float(index) * JobCatalog.LEGACY_WEATHER_SEGMENT_HOURS,
|
||||||
|
WorldTimeService.HOURS_PER_DAY,
|
||||||
|
),
|
||||||
|
"weather": int(WorldWeatherService.Weather.SUNNY),
|
||||||
|
})
|
||||||
|
var legacy_job: Dictionary = {
|
||||||
|
"id": "legacy-catch",
|
||||||
|
"title": "legacy catch",
|
||||||
|
"description": "catch one fish",
|
||||||
|
"kind": int(JobCatalog.Kind.CATCH_TOTAL),
|
||||||
|
"target": 1,
|
||||||
|
"fish_coin": 1,
|
||||||
|
"experience": 1,
|
||||||
|
}
|
||||||
|
var save_data: Dictionary = PlayerJobService.default_save_data()
|
||||||
|
save_data["host_board"] = {
|
||||||
|
"plan_id": "legacy-plan",
|
||||||
|
"cycle": 4,
|
||||||
|
"schedule_anchor_index": 0,
|
||||||
|
"jobs": [legacy_job],
|
||||||
|
"weather_schedule": legacy_schedule,
|
||||||
|
}
|
||||||
|
save_data["active_plan_id"] = "legacy-plan"
|
||||||
|
assert(PlayerJobService.validate_save_data(save_data))
|
||||||
|
var jobs := PlayerJobService.new()
|
||||||
|
assert(jobs.restore_from_save_data(save_data))
|
||||||
|
var migrated: Dictionary = jobs.to_save_data()
|
||||||
|
assert((migrated.get("host_board", {}) as Dictionary).is_empty())
|
||||||
|
assert(str(migrated.get("active_plan_id", "")).is_empty())
|
||||||
|
jobs.free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_fishing_weather_seams() -> void:
|
func _validate_fishing_weather_seams() -> void:
|
||||||
var clear_context := FishingContextType.new()
|
var clear_context := FishingContextType.new()
|
||||||
var rain_context := FishingContextType.new()
|
var rain_context := FishingContextType.new()
|
||||||
|
|
@ -255,7 +292,7 @@ func _validate_weather_presentation() -> void:
|
||||||
world_root.add_child(sun)
|
world_root.add_child(sun)
|
||||||
var clock := WorldTimeServiceType.new()
|
var clock := WorldTimeServiceType.new()
|
||||||
world_root.add_child(clock)
|
world_root.add_child(clock)
|
||||||
clock.begin_session(14.0)
|
clock.begin_test_session(14.0)
|
||||||
var weather := WorldWeatherServiceType.new()
|
var weather := WorldWeatherServiceType.new()
|
||||||
world_root.add_child(weather)
|
world_root.add_child(weather)
|
||||||
weather.begin_remote_session()
|
weather.begin_remote_session()
|
||||||
|
|
@ -279,6 +316,34 @@ func _validate_weather_presentation() -> void:
|
||||||
var runtime_sky_material := (
|
var runtime_sky_material := (
|
||||||
runtime_environment.sky.sky_material as ShaderMaterial
|
runtime_environment.sky.sky_material as ShaderMaterial
|
||||||
)
|
)
|
||||||
|
clock.set_authoritative_time(0.0)
|
||||||
|
visuals.apply_time_immediately(0.0)
|
||||||
|
assert(float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
) > 0.99)
|
||||||
|
visuals.apply_weather_immediately(
|
||||||
|
WorldWeatherServiceType.Weather.CLOUDY
|
||||||
|
)
|
||||||
|
assert(is_equal_approx(float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
), 0.18))
|
||||||
|
visuals.apply_weather_immediately(
|
||||||
|
WorldWeatherServiceType.Weather.RAINY
|
||||||
|
)
|
||||||
|
assert(is_equal_approx(float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
), 0.03))
|
||||||
|
visuals.apply_weather_immediately(
|
||||||
|
WorldWeatherServiceType.Weather.FOGGY
|
||||||
|
)
|
||||||
|
assert(is_zero_approx(float(
|
||||||
|
runtime_sky_material.get_shader_parameter("star_visibility")
|
||||||
|
)))
|
||||||
|
visuals.apply_weather_immediately(
|
||||||
|
WorldWeatherServiceType.Weather.SUNNY
|
||||||
|
)
|
||||||
|
clock.set_authoritative_time(14.0)
|
||||||
|
visuals.apply_time_immediately(14.0)
|
||||||
var clear_background_energy: float = (
|
var clear_background_energy: float = (
|
||||||
runtime_environment.background_energy_multiplier
|
runtime_environment.background_energy_multiplier
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,6 @@ func setup(
|
||||||
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
_service.local_message_confirmed.connect(_on_local_message_confirmed)
|
||||||
_service.history_replaced.connect(_on_history)
|
_service.history_replaced.connect(_on_history)
|
||||||
_service.send_rejected.connect(_on_rejected)
|
_service.send_rejected.connect(_on_rejected)
|
||||||
_service.world_command_finished.connect(_on_world_command_finished)
|
|
||||||
_session.peer_removed.connect(_on_peer_removed)
|
_session.peer_removed.connect(_on_peer_removed)
|
||||||
_entry.text = _settings.current_settings.chat_draft
|
_entry.text = _settings.current_settings.chat_draft
|
||||||
_entry.caret_column = _entry.text.length()
|
_entry.caret_column = _entry.text.length()
|
||||||
|
|
@ -835,8 +834,6 @@ func _send() -> void:
|
||||||
if body.strip_edges().is_empty():
|
if body.strip_edges().is_empty():
|
||||||
close_chat()
|
close_chat()
|
||||||
return
|
return
|
||||||
if _handle_chat_command(body):
|
|
||||||
return
|
|
||||||
_send_pending = true
|
_send_pending = true
|
||||||
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
|
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
|
||||||
_entry.editable = false
|
_entry.editable = false
|
||||||
|
|
@ -852,85 +849,6 @@ func _send() -> void:
|
||||||
_set_status("Sending…")
|
_set_status("Sending…")
|
||||||
|
|
||||||
|
|
||||||
func _handle_chat_command(body: String) -> bool:
|
|
||||||
var command_text := body.strip_edges()
|
|
||||||
if not command_text.begins_with("/"):
|
|
||||||
return false
|
|
||||||
var parts: PackedStringArray = command_text.split(" ", false)
|
|
||||||
var command := String(parts[0]).trim_prefix("/").to_lower()
|
|
||||||
match command:
|
|
||||||
"time":
|
|
||||||
_handle_time_command(parts)
|
|
||||||
"weather":
|
|
||||||
_handle_weather_command(parts)
|
|
||||||
"":
|
|
||||||
_show_command_error("Enter a command after /.")
|
|
||||||
_:
|
|
||||||
_show_command_error("Unknown command: /%s" % command)
|
|
||||||
_entry.clear()
|
|
||||||
_flush_draft()
|
|
||||||
return true
|
|
||||||
|
|
||||||
|
|
||||||
func _show_command_error(message: String) -> void:
|
|
||||||
_set_status(message)
|
|
||||||
close_chat(true)
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_time_command(parts: PackedStringArray) -> void:
|
|
||||||
if parts.size() != 2:
|
|
||||||
_show_command_error("Usage: /time [dawn, day, dusk, night]")
|
|
||||||
return
|
|
||||||
if _service == null or _world_time == null:
|
|
||||||
_show_command_error("World time is unavailable.")
|
|
||||||
return
|
|
||||||
var phase_name := String(parts[1]).to_lower()
|
|
||||||
if phase_name not in ["dawn", "day", "dusk", "night"]:
|
|
||||||
_show_command_error("Usage: /time [dawn, day, dusk, night]")
|
|
||||||
return
|
|
||||||
if not _service.request_world_time_change(phase_name):
|
|
||||||
_show_command_error(
|
|
||||||
"Only the host or an operator can change world time."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
close_chat()
|
|
||||||
|
|
||||||
|
|
||||||
func _handle_weather_command(parts: PackedStringArray) -> void:
|
|
||||||
if parts.size() != 2:
|
|
||||||
_show_command_error(
|
|
||||||
"Usage: /weather [clear, cloudy, rainy, foggy]"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if _service == null or _world_weather == null:
|
|
||||||
_show_command_error("World weather is unavailable.")
|
|
||||||
return
|
|
||||||
var weather_name := String(parts[1]).to_lower()
|
|
||||||
match weather_name:
|
|
||||||
"clear", "sunny":
|
|
||||||
weather_name = "clear"
|
|
||||||
"cloudy", "rainy", "foggy":
|
|
||||||
pass
|
|
||||||
_:
|
|
||||||
_show_command_error(
|
|
||||||
"Usage: /weather [clear, cloudy, rainy, foggy]"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if not _service.request_world_weather_change(weather_name):
|
|
||||||
_show_command_error(
|
|
||||||
"Only the host or an operator can change world weather."
|
|
||||||
)
|
|
||||||
return
|
|
||||||
close_chat()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_world_command_finished(success: bool, message: String) -> void:
|
|
||||||
if success:
|
|
||||||
_set_status("")
|
|
||||||
elif not message.is_empty():
|
|
||||||
_set_status(message)
|
|
||||||
|
|
||||||
|
|
||||||
func _on_local_message_confirmed(message: Dictionary) -> void:
|
func _on_local_message_confirmed(message: Dictionary) -> void:
|
||||||
if (
|
if (
|
||||||
not _send_pending
|
not _send_pending
|
||||||
|
|
|
||||||
159
ui/components/general_inventory_grid.gd
Normal file
159
ui/components/general_inventory_grid.gd
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
class_name GeneralInventoryGrid
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
signal entry_selected(kind: int, identity: StringName)
|
||||||
|
signal slot_activated(slot: GeneralInventorySlot)
|
||||||
|
signal context_requested(slot: GeneralInventorySlot)
|
||||||
|
signal context_changed(slot: GeneralInventorySlot, text: String, active: bool)
|
||||||
|
|
||||||
|
const DEFAULT_SLOT_SIZE := Vector2(52.0, 52.0)
|
||||||
|
const DEFAULT_SLOT_SEPARATION: int = 5
|
||||||
|
const COLUMNS: int = PlayerInventoryLayout.INVENTORY_COLUMNS
|
||||||
|
|
||||||
|
var _layout: PlayerInventoryLayout
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _fish_inventory: FishInventory
|
||||||
|
var _hotbar: PlayerHotbar
|
||||||
|
var _item_catalog: ItemCatalog
|
||||||
|
var _container: int = PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
var _grid: GridContainer
|
||||||
|
var _slots: Array[GeneralInventorySlot] = []
|
||||||
|
var _slot_size: Vector2 = DEFAULT_SLOT_SIZE
|
||||||
|
var _slot_separation: int = DEFAULT_SLOT_SEPARATION
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
_grid = GridContainer.new()
|
||||||
|
_grid.columns = COLUMNS
|
||||||
|
_apply_grid_separation()
|
||||||
|
add_child(_grid)
|
||||||
|
|
||||||
|
|
||||||
|
func set_slot_presentation(slot_size: Vector2, separation: int) -> void:
|
||||||
|
_slot_size = Vector2(maxf(1.0, slot_size.x), maxf(1.0, slot_size.y))
|
||||||
|
_slot_separation = maxi(0, separation)
|
||||||
|
if _grid != null:
|
||||||
|
_apply_grid_separation()
|
||||||
|
if is_node_ready() and _layout != null:
|
||||||
|
var active_capacity := (
|
||||||
|
_layout.get_inventory_capacity()
|
||||||
|
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
else _layout.get_storage_capacity()
|
||||||
|
)
|
||||||
|
_rebuild(
|
||||||
|
PlayerInventoryLayout.MAX_INVENTORY_SLOT_COUNT
|
||||||
|
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
else PlayerInventoryLayout.MAX_STORAGE_SLOT_COUNT,
|
||||||
|
active_capacity,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
layout: PlayerInventoryLayout,
|
||||||
|
bag: PlayerBag,
|
||||||
|
fish_inventory: FishInventory,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
item_catalog: ItemCatalog,
|
||||||
|
container: int,
|
||||||
|
) -> void:
|
||||||
|
_layout = layout
|
||||||
|
_bag = bag
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
|
_hotbar = hotbar
|
||||||
|
_item_catalog = item_catalog
|
||||||
|
_container = container
|
||||||
|
if _layout != null and not _layout.layout_changed.is_connected(refresh):
|
||||||
|
_layout.layout_changed.connect(refresh)
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func refresh() -> void:
|
||||||
|
if not is_node_ready() or _layout == null:
|
||||||
|
return
|
||||||
|
var active_capacity := (
|
||||||
|
_layout.get_inventory_capacity()
|
||||||
|
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
else _layout.get_storage_capacity()
|
||||||
|
)
|
||||||
|
var visible_capacity := (
|
||||||
|
PlayerInventoryLayout.MAX_INVENTORY_SLOT_COUNT
|
||||||
|
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
else PlayerInventoryLayout.MAX_STORAGE_SLOT_COUNT
|
||||||
|
)
|
||||||
|
if _slots.size() != visible_capacity:
|
||||||
|
_rebuild(visible_capacity, active_capacity)
|
||||||
|
else:
|
||||||
|
for index: int in _slots.size():
|
||||||
|
_slots[index].set_locked(index >= active_capacity)
|
||||||
|
for slot: GeneralInventorySlot in _slots:
|
||||||
|
slot.refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func get_slots() -> Array[GeneralInventorySlot]:
|
||||||
|
var active: Array[GeneralInventorySlot] = []
|
||||||
|
for slot: GeneralInventorySlot in _slots:
|
||||||
|
if not slot.disabled:
|
||||||
|
active.append(slot)
|
||||||
|
return active
|
||||||
|
|
||||||
|
|
||||||
|
func get_first_occupied_slot() -> GeneralInventorySlot:
|
||||||
|
for slot: GeneralInventorySlot in _slots:
|
||||||
|
if not slot.entry_identity.is_empty():
|
||||||
|
return slot
|
||||||
|
return _slots.front() if not _slots.is_empty() else null
|
||||||
|
|
||||||
|
|
||||||
|
func _rebuild(visible_capacity: int, active_capacity: int) -> void:
|
||||||
|
for child: Node in _grid.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
_slots.clear()
|
||||||
|
for slot_index: int in visible_capacity:
|
||||||
|
var slot := GeneralInventorySlot.new()
|
||||||
|
slot.custom_minimum_size = _slot_size
|
||||||
|
slot.entry_selected.connect(
|
||||||
|
func(kind: int, identity: StringName) -> void:
|
||||||
|
entry_selected.emit(kind, identity)
|
||||||
|
)
|
||||||
|
slot.pressed.connect(func() -> void: slot_activated.emit(slot))
|
||||||
|
slot.context_requested.connect(
|
||||||
|
func(source: GeneralInventorySlot) -> void:
|
||||||
|
context_requested.emit(source)
|
||||||
|
)
|
||||||
|
slot.context_changed.connect(
|
||||||
|
func(
|
||||||
|
source: GeneralInventorySlot,
|
||||||
|
text: String,
|
||||||
|
active: bool,
|
||||||
|
) -> void:
|
||||||
|
context_changed.emit(source, text, active)
|
||||||
|
)
|
||||||
|
_grid.add_child(slot)
|
||||||
|
slot.set_presentation_size(_slot_size)
|
||||||
|
slot.configure(
|
||||||
|
slot_index,
|
||||||
|
_container,
|
||||||
|
slot_index >= active_capacity,
|
||||||
|
_layout,
|
||||||
|
_bag,
|
||||||
|
_fish_inventory,
|
||||||
|
_hotbar,
|
||||||
|
_item_catalog,
|
||||||
|
)
|
||||||
|
_slots.append(slot)
|
||||||
|
custom_minimum_size = Vector2(
|
||||||
|
float(COLUMNS) * _slot_size.x
|
||||||
|
+ float(COLUMNS - 1) * _slot_separation,
|
||||||
|
ceilf(float(visible_capacity) / float(COLUMNS)) * _slot_size.y
|
||||||
|
+ maxf(
|
||||||
|
0.0,
|
||||||
|
ceilf(float(visible_capacity) / float(COLUMNS)) - 1.0,
|
||||||
|
) * _slot_separation,
|
||||||
|
)
|
||||||
|
size = custom_minimum_size
|
||||||
|
ControllerFocusNavigation.configure_spatial_neighbors(_slots)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_grid_separation() -> void:
|
||||||
|
_grid.add_theme_constant_override("h_separation", _slot_separation)
|
||||||
|
_grid.add_theme_constant_override("v_separation", _slot_separation)
|
||||||
1
ui/components/general_inventory_grid.gd.uid
Normal file
1
ui/components/general_inventory_grid.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://k6h255orqr6o
|
||||||
346
ui/components/general_inventory_slot.gd
Normal file
346
ui/components/general_inventory_slot.gd
Normal file
|
|
@ -0,0 +1,346 @@
|
||||||
|
class_name GeneralInventorySlot
|
||||||
|
extends Button
|
||||||
|
|
||||||
|
signal entry_selected(kind: int, identity: StringName)
|
||||||
|
signal entry_moved
|
||||||
|
signal context_requested(slot: GeneralInventorySlot)
|
||||||
|
signal context_changed(slot: GeneralInventorySlot, text: String, active: bool)
|
||||||
|
|
||||||
|
const ItemDataType = preload("res://items/item_data.gd")
|
||||||
|
const LOCK_ICON: Texture2D = preload("res://ui/icons/pictograms/lock_light.png")
|
||||||
|
|
||||||
|
var slot_index: int = -1
|
||||||
|
var container: int = -1
|
||||||
|
var entry_kind: int = -1
|
||||||
|
var entry_identity: StringName
|
||||||
|
var _locked: bool = false
|
||||||
|
|
||||||
|
var _layout: PlayerInventoryLayout
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _fish_inventory: FishInventory
|
||||||
|
var _hotbar: PlayerHotbar
|
||||||
|
var _item_catalog: ItemCatalog
|
||||||
|
var _icon: TextureRect
|
||||||
|
var _quantity: Label
|
||||||
|
var _presentation_size := Vector2(52.0, 52.0)
|
||||||
|
var _context_text: String = ""
|
||||||
|
var _context_hovered: bool = false
|
||||||
|
var _context_focused: bool = false
|
||||||
|
var _staged: bool = false
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
|
||||||
|
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
pressed.connect(_on_pressed)
|
||||||
|
focus_entered.connect(_set_context_focused.bind(true))
|
||||||
|
focus_exited.connect(_set_context_focused.bind(false))
|
||||||
|
mouse_entered.connect(_set_context_hovered.bind(true))
|
||||||
|
mouse_exited.connect(_set_context_hovered.bind(false))
|
||||||
|
gui_input.connect(_on_gui_input)
|
||||||
|
_icon = TextureRect.new()
|
||||||
|
_icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_icon)
|
||||||
|
_quantity = Label.new()
|
||||||
|
_quantity.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||||
|
_quantity.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_quantity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_quantity.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
|
||||||
|
)
|
||||||
|
_quantity.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_quantity)
|
||||||
|
_apply_presentation()
|
||||||
|
|
||||||
|
|
||||||
|
func set_presentation_size(presentation_size: Vector2) -> void:
|
||||||
|
_presentation_size = presentation_size
|
||||||
|
custom_minimum_size = presentation_size
|
||||||
|
if is_node_ready():
|
||||||
|
_apply_presentation()
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
new_slot_index: int,
|
||||||
|
new_container: int,
|
||||||
|
is_locked: bool,
|
||||||
|
layout: PlayerInventoryLayout,
|
||||||
|
bag: PlayerBag,
|
||||||
|
fish_inventory: FishInventory,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
item_catalog: ItemCatalog,
|
||||||
|
) -> void:
|
||||||
|
slot_index = new_slot_index
|
||||||
|
container = new_container
|
||||||
|
_locked = is_locked
|
||||||
|
_layout = layout
|
||||||
|
_bag = bag
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
|
_hotbar = hotbar
|
||||||
|
_item_catalog = item_catalog
|
||||||
|
name = "InventorySlot%d" % slot_index
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func set_locked(is_locked: bool) -> void:
|
||||||
|
if _locked == is_locked:
|
||||||
|
return
|
||||||
|
_locked = is_locked
|
||||||
|
refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func refresh() -> void:
|
||||||
|
call_deferred("_update_context_presence")
|
||||||
|
entry_kind = -1
|
||||||
|
entry_identity = StringName()
|
||||||
|
_context_text = ""
|
||||||
|
_icon.texture = null
|
||||||
|
_quantity.text = ""
|
||||||
|
disabled = _locked
|
||||||
|
_apply_icon_geometry()
|
||||||
|
if _locked:
|
||||||
|
_icon.texture = LOCK_ICON
|
||||||
|
_icon.modulate = Color(UtilityPageStyle.OCEAN_DISABLED, 0.18)
|
||||||
|
var storage_slot := (
|
||||||
|
container == PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
)
|
||||||
|
_context_text = (
|
||||||
|
"locked storage slot" if storage_slot else "locked backpack slot"
|
||||||
|
)
|
||||||
|
tooltip_text = ""
|
||||||
|
accessibility_name = "%s %d" % [_context_text, slot_index + 1]
|
||||||
|
return
|
||||||
|
_icon.modulate = Color.WHITE
|
||||||
|
_context_text = "empty slot"
|
||||||
|
tooltip_text = ""
|
||||||
|
accessibility_name = "empty %s slot %d" % [
|
||||||
|
"storage"
|
||||||
|
if container == PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
else "inventory",
|
||||||
|
slot_index + 1,
|
||||||
|
]
|
||||||
|
if _layout == null:
|
||||||
|
return
|
||||||
|
var entry := _layout.get_entry_at(container, slot_index)
|
||||||
|
if entry.is_empty():
|
||||||
|
return
|
||||||
|
entry_kind = int(entry.get("kind", -1))
|
||||||
|
entry_identity = StringName(str(entry.get("identity", "")))
|
||||||
|
if entry_kind == PlayerInventoryLayout.EntryKind.ITEM:
|
||||||
|
var item: ItemDataType = (
|
||||||
|
_item_catalog.get_item_by_id(entry_identity)
|
||||||
|
if _item_catalog != null else null
|
||||||
|
)
|
||||||
|
if item == null:
|
||||||
|
return
|
||||||
|
_icon.texture = item.icon
|
||||||
|
var quantity := _bag.get_quantity(entry_identity) if _bag != null else 0
|
||||||
|
_quantity.text = "×%d" % quantity if quantity > 1 else ""
|
||||||
|
_context_text = _item_context_text(item, quantity)
|
||||||
|
accessibility_name = "%s, slot %d" % [item.display_name, slot_index + 1]
|
||||||
|
elif entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
var fish_catch = (
|
||||||
|
_fish_inventory.get_catch_by_id(entry_identity)
|
||||||
|
if _fish_inventory != null else null
|
||||||
|
)
|
||||||
|
if fish_catch == null:
|
||||||
|
return
|
||||||
|
_icon.texture = fish_catch.fish.display_texture
|
||||||
|
_context_text = _catch_context_text(fish_catch)
|
||||||
|
var catch_name: String = FishQuality.qualified_name(
|
||||||
|
fish_catch.fish.display_name, fish_catch.quality
|
||||||
|
)
|
||||||
|
accessibility_name = "%s, slot %d" % [catch_name, slot_index + 1]
|
||||||
|
|
||||||
|
|
||||||
|
func _on_pressed() -> void:
|
||||||
|
if _locked:
|
||||||
|
return
|
||||||
|
entry_selected.emit(entry_kind, entry_identity)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_gui_input(event: InputEvent) -> void:
|
||||||
|
var mouse_event := event as InputEventMouseButton
|
||||||
|
if (
|
||||||
|
mouse_event != null
|
||||||
|
and mouse_event.button_index == MOUSE_BUTTON_RIGHT
|
||||||
|
and mouse_event.pressed
|
||||||
|
and not _locked
|
||||||
|
and not entry_identity.is_empty()
|
||||||
|
):
|
||||||
|
context_requested.emit(self)
|
||||||
|
accept_event()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_context_hovered(active: bool) -> void:
|
||||||
|
_context_hovered = active
|
||||||
|
_update_context_presence()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_context_focused(active: bool) -> void:
|
||||||
|
_context_focused = active
|
||||||
|
_update_context_presence()
|
||||||
|
|
||||||
|
|
||||||
|
func _update_context_presence() -> void:
|
||||||
|
if _locked or entry_identity.is_empty():
|
||||||
|
context_changed.emit(self, "", false)
|
||||||
|
return
|
||||||
|
var active: bool = _context_hovered or _context_focused
|
||||||
|
context_changed.emit(self, _context_text if active else "", active)
|
||||||
|
|
||||||
|
|
||||||
|
func set_move_source(active: bool) -> void:
|
||||||
|
modulate = Color(1.0, 1.0, 1.0, 0.42) if active else Color.WHITE
|
||||||
|
|
||||||
|
|
||||||
|
func set_staged(active: bool) -> void:
|
||||||
|
if _staged == active:
|
||||||
|
return
|
||||||
|
_staged = active
|
||||||
|
_apply_style()
|
||||||
|
|
||||||
|
|
||||||
|
func _item_context_text(item: ItemDataType, quantity: int) -> String:
|
||||||
|
var lines: Array[String] = [item.display_name]
|
||||||
|
lines.append("%s • quantity %d" % [item.get_category_name(), quantity])
|
||||||
|
if item.equippable:
|
||||||
|
lines.append("equippable")
|
||||||
|
elif item.usable:
|
||||||
|
lines.append("usable")
|
||||||
|
if not item.description.strip_edges().is_empty():
|
||||||
|
lines.append(item.description.strip_edges())
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
func _catch_context_text(fish_catch: FishCatch) -> String:
|
||||||
|
var catch_name: String = FishQuality.qualified_name(
|
||||||
|
fish_catch.fish.display_name,
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
return "%s\n%0.2f lb • value %d\n%s" % [
|
||||||
|
catch_name,
|
||||||
|
fish_catch.weight_lb,
|
||||||
|
fish_catch.sale_value,
|
||||||
|
fish_catch.fish.logbook_fact,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
func _get_drag_data(_at_position: Vector2) -> Variant:
|
||||||
|
if _locked or entry_identity.is_empty() or _icon.texture == null:
|
||||||
|
return null
|
||||||
|
var preview := TextureRect.new()
|
||||||
|
preview.custom_minimum_size = Vector2(56.0, 56.0)
|
||||||
|
preview.texture = _icon.texture
|
||||||
|
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
set_drag_preview(preview)
|
||||||
|
if entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
return {
|
||||||
|
"kind": "cooler_fish",
|
||||||
|
"catch_id": String(entry_identity),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"kind": "bag_item",
|
||||||
|
"item_id": String(entry_identity),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
|
if _locked or typeof(data) != TYPE_DICTIONARY or _layout == null:
|
||||||
|
return false
|
||||||
|
var kind := str((data as Dictionary).get("kind", ""))
|
||||||
|
return kind in ["bag_item", "cooler_fish", "hotbar_slot"]
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
||||||
|
if not _can_drop_data(Vector2.ZERO, data):
|
||||||
|
return
|
||||||
|
var payload := data as Dictionary
|
||||||
|
var payload_kind := str(payload.get("kind", ""))
|
||||||
|
var moved: bool = false
|
||||||
|
if payload_kind == "hotbar_slot":
|
||||||
|
moved = (
|
||||||
|
_hotbar != null
|
||||||
|
and _hotbar.move_slot_to_container(
|
||||||
|
int(payload.get("slot_index", -1)),
|
||||||
|
container,
|
||||||
|
slot_index,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
var kind := (
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
if payload_kind == "cooler_fish"
|
||||||
|
else PlayerInventoryLayout.EntryKind.ITEM
|
||||||
|
)
|
||||||
|
var identity := StringName(str(
|
||||||
|
payload.get(
|
||||||
|
"catch_id" if payload_kind == "cooler_fish" else "item_id",
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
))
|
||||||
|
moved = _layout.move_entry(kind, identity, container, slot_index)
|
||||||
|
if moved:
|
||||||
|
entry_moved.emit()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_style() -> void:
|
||||||
|
var radius: int = roundi(minf(_presentation_size.x, _presentation_size.y) * 0.5)
|
||||||
|
var normal := UtilityPageStyle.rounded_style(
|
||||||
|
Color(
|
||||||
|
UtilityPageStyle.OCEAN_SELECTED
|
||||||
|
if _staged else UtilityPageStyle.OCEAN_FIELD,
|
||||||
|
0.92 if _staged else 0.88,
|
||||||
|
),
|
||||||
|
radius,
|
||||||
|
)
|
||||||
|
var hover := UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_SELECTED, 0.92), radius
|
||||||
|
)
|
||||||
|
var locked := UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_FIELD, 0.38), radius
|
||||||
|
)
|
||||||
|
for state: StringName in [&"normal", &"pressed"]:
|
||||||
|
add_theme_stylebox_override(state, normal)
|
||||||
|
add_theme_stylebox_override("disabled", locked)
|
||||||
|
for state: StringName in [&"hover", &"focus"]:
|
||||||
|
add_theme_stylebox_override(state, hover)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_presentation() -> void:
|
||||||
|
_apply_icon_geometry()
|
||||||
|
var is_large: bool = _presentation_size.x >= 70.0
|
||||||
|
var quantity_width: float = 42.0 if is_large else 34.0
|
||||||
|
var quantity_height: float = 20.0 if is_large else 17.0
|
||||||
|
_quantity.position = Vector2(
|
||||||
|
-quantity_width - (8.0 if is_large else 5.0),
|
||||||
|
-quantity_height - (6.0 if is_large else 4.0),
|
||||||
|
)
|
||||||
|
_quantity.size = Vector2(quantity_width, quantity_height)
|
||||||
|
_quantity.add_theme_font_size_override("font_size", 14 if is_large else 11)
|
||||||
|
_apply_style()
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_icon_geometry() -> void:
|
||||||
|
var is_large: bool = _presentation_size.x >= 70.0
|
||||||
|
var margin: float
|
||||||
|
if _locked:
|
||||||
|
var lock_size: float = 24.0 if is_large else 18.0
|
||||||
|
margin = maxf(
|
||||||
|
(_presentation_size.x - lock_size) * 0.5,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
margin = 10.0 if is_large else 7.0
|
||||||
|
_icon.offset_left = margin
|
||||||
|
_icon.offset_top = margin
|
||||||
|
_icon.offset_right = -margin
|
||||||
|
_icon.offset_bottom = -margin
|
||||||
1
ui/components/general_inventory_slot.gd.uid
Normal file
1
ui/components/general_inventory_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cgaypm0vokkup
|
||||||
|
|
@ -6,7 +6,10 @@ const CONTENT_MARGIN_LEFT := 18.0
|
||||||
const CONTENT_MARGIN_TOP := 98.0
|
const CONTENT_MARGIN_TOP := 98.0
|
||||||
const CONTENT_MARGIN_RIGHT := 18.0
|
const CONTENT_MARGIN_RIGHT := 18.0
|
||||||
const CONTENT_MARGIN_BOTTOM := 16.0
|
const CONTENT_MARGIN_BOTTOM := 16.0
|
||||||
|
# Retained for the Logbook's separate handwritten presentation. Inventory
|
||||||
|
# notepads deliberately prioritize legibility with Tuffy.
|
||||||
const HANDWRITTEN_FONT: Font = preload("res://ui/fonts/seattle_avenue.otf")
|
const HANDWRITTEN_FONT: Font = preload("res://ui/fonts/seattle_avenue.otf")
|
||||||
|
const NOTEPAD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf")
|
||||||
const NOTEPAD_TEXTURE: Texture2D = preload("res://art/ui/ui_notepad.png")
|
const NOTEPAD_TEXTURE: Texture2D = preload("res://art/ui/ui_notepad.png")
|
||||||
const NOTEPAD_CANONICAL_OVERSCAN: float = 1.2
|
const NOTEPAD_CANONICAL_OVERSCAN: float = 1.2
|
||||||
const NOTEPAD_ART_OFFSET := Vector2(10.0, 20.0)
|
const NOTEPAD_ART_OFFSET := Vector2(10.0, 20.0)
|
||||||
|
|
@ -24,11 +27,11 @@ func _ready() -> void:
|
||||||
|
|
||||||
|
|
||||||
static func apply_handwritten_to(root: Control) -> void:
|
static func apply_handwritten_to(root: Control) -> void:
|
||||||
root.add_theme_font_override("font", HANDWRITTEN_FONT)
|
root.add_theme_font_override("font", NOTEPAD_FONT)
|
||||||
for descendant: Node in root.find_children("*", "Control", true, false):
|
for descendant: Node in root.find_children("*", "Control", true, false):
|
||||||
var control := descendant as Control
|
var control := descendant as Control
|
||||||
control.add_theme_font_override(
|
control.add_theme_font_override(
|
||||||
"font", HANDWRITTEN_FONT
|
"font", NOTEPAD_FONT
|
||||||
)
|
)
|
||||||
if control is Label:
|
if control is Label:
|
||||||
control.add_theme_color_override("font_color", INK_COLOR)
|
control.add_theme_color_override("font_color", INK_COLOR)
|
||||||
|
|
@ -37,7 +40,7 @@ static func apply_handwritten_to(root: Control) -> void:
|
||||||
func _draw() -> void:
|
func _draw() -> void:
|
||||||
draw_texture_rect(NOTEPAD_TEXTURE, get_art_rect(), false)
|
draw_texture_rect(NOTEPAD_TEXTURE, get_art_rect(), false)
|
||||||
|
|
||||||
var font: Font = HANDWRITTEN_FONT
|
var font: Font = NOTEPAD_FONT
|
||||||
draw_string(
|
draw_string(
|
||||||
font,
|
font,
|
||||||
Vector2(20.0, 58.0),
|
Vector2(20.0, 58.0),
|
||||||
|
|
|
||||||
71
ui/components/shop_sale_tray_slot.gd
Normal file
71
ui/components/shop_sale_tray_slot.gd
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
class_name ShopSaleTraySlot
|
||||||
|
extends Button
|
||||||
|
|
||||||
|
signal remove_requested(key: String)
|
||||||
|
signal drop_requested(payload: Dictionary)
|
||||||
|
|
||||||
|
var entry_key: String = ""
|
||||||
|
var _icon: TextureRect
|
||||||
|
var _quantity: Label
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
custom_minimum_size = GeneralInventoryGrid.DEFAULT_SLOT_SIZE
|
||||||
|
focus_mode = Control.FOCUS_ALL
|
||||||
|
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
pressed.connect(func() -> void: remove_requested.emit(entry_key))
|
||||||
|
_icon = TextureRect.new()
|
||||||
|
_icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_icon.offset_left = 6.0
|
||||||
|
_icon.offset_top = 4.0
|
||||||
|
_icon.offset_right = -6.0
|
||||||
|
_icon.offset_bottom = -4.0
|
||||||
|
_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_icon)
|
||||||
|
_quantity = Label.new()
|
||||||
|
_quantity.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
|
||||||
|
_quantity.position = Vector2(-34.0, -20.0)
|
||||||
|
_quantity.size = Vector2(30.0, 16.0)
|
||||||
|
_quantity.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
_quantity.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
add_child(_quantity)
|
||||||
|
var normal := UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_FIELD, 0.96), 26
|
||||||
|
)
|
||||||
|
var hover := UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_SELECTED, 0.96), 26
|
||||||
|
)
|
||||||
|
for state: StringName in [&"normal", &"pressed", &"disabled"]:
|
||||||
|
add_theme_stylebox_override(state, normal)
|
||||||
|
for state: StringName in [&"hover", &"focus"]:
|
||||||
|
add_theme_stylebox_override(state, hover)
|
||||||
|
|
||||||
|
|
||||||
|
func configure(
|
||||||
|
key: String,
|
||||||
|
icon: Texture2D,
|
||||||
|
label: String,
|
||||||
|
quantity: int = 1,
|
||||||
|
) -> void:
|
||||||
|
entry_key = key
|
||||||
|
_icon.texture = icon
|
||||||
|
_quantity.text = "×%d" % quantity if quantity > 1 else ""
|
||||||
|
tooltip_text = "%s · select to remove" % label
|
||||||
|
accessibility_name = tooltip_text
|
||||||
|
|
||||||
|
|
||||||
|
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
|
||||||
|
return (
|
||||||
|
typeof(data) == TYPE_DICTIONARY
|
||||||
|
and str((data as Dictionary).get("kind", "")) in [
|
||||||
|
"bag_item", "cooler_fish"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _drop_data(_at_position: Vector2, data: Variant) -> void:
|
||||||
|
if _can_drop_data(Vector2.ZERO, data):
|
||||||
|
drop_requested.emit((data as Dictionary).duplicate(true))
|
||||||
1
ui/components/shop_sale_tray_slot.gd.uid
Normal file
1
ui/components/shop_sale_tray_slot.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://w87v5n2xm64p
|
||||||
|
|
@ -104,7 +104,7 @@ const SHOP_SECTION_LABELS: Array[String] = [
|
||||||
"Snacks",
|
"Snacks",
|
||||||
"Equipment",
|
"Equipment",
|
||||||
"Art Supplies",
|
"Art Supplies",
|
||||||
"Sell Fish",
|
"Sell",
|
||||||
]
|
]
|
||||||
const SUPPLY_ICON_GRID_COLUMNS: int = 9
|
const SUPPLY_ICON_GRID_COLUMNS: int = 9
|
||||||
const SUPPLY_ICON_TILE_SIZE := Vector2(72.0, 72.0)
|
const SUPPLY_ICON_TILE_SIZE := Vector2(72.0, 72.0)
|
||||||
|
|
@ -154,6 +154,9 @@ const ROD_PRICE_ICON_SIZE: float = 22.0
|
||||||
@onready var _cooler_cost: CurrencyAmount = %CoolerCost
|
@onready var _cooler_cost: CurrencyAmount = %CoolerCost
|
||||||
@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
|
@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
|
||||||
@onready var _cooler_purchase: Button = %CoolerPurchase
|
@onready var _cooler_purchase: Button = %CoolerPurchase
|
||||||
|
@onready var _backpack_cost: CurrencyAmount = %BackpackCost
|
||||||
|
@onready var _backpack_price_bubble: PanelContainer = %BackpackPriceBubble
|
||||||
|
@onready var _backpack_purchase: Button = %BackpackPurchase
|
||||||
|
|
||||||
var _player: PlayerType
|
var _player: PlayerType
|
||||||
var _wallet: PlayerWalletType
|
var _wallet: PlayerWalletType
|
||||||
|
|
@ -162,10 +165,16 @@ var _upgrades: PlayerFishingUpgradesType
|
||||||
var _fishing_spot: FishingSpotType
|
var _fishing_spot: FishingSpotType
|
||||||
var _interaction: ShopInteractionType
|
var _interaction: ShopInteractionType
|
||||||
var _bag: PlayerBagType
|
var _bag: PlayerBagType
|
||||||
|
var _inventory: FishInventory
|
||||||
|
var _hotbar: PlayerHotbar
|
||||||
|
var _inventory_layout: PlayerInventoryLayout
|
||||||
var _item_catalog: ItemCatalogType
|
var _item_catalog: ItemCatalogType
|
||||||
var _cooler_capacity: PlayerCoolerCapacityType
|
var _cooler_capacity: PlayerCoolerCapacityType
|
||||||
var _art_unlocks: PlayerArtUnlocksType
|
var _art_unlocks: PlayerArtUnlocksType
|
||||||
var _network_shop: NetworkShopService
|
var _network_shop: NetworkShopService
|
||||||
|
var _network_sale: NetworkSaleService
|
||||||
|
var _reservations: PlayerAssetReservationService
|
||||||
|
var _sell_inventory: ShopSellInventory
|
||||||
var _prior_movement_enabled: bool = true
|
var _prior_movement_enabled: bool = true
|
||||||
var _prior_camera_enabled: bool = true
|
var _prior_camera_enabled: bool = true
|
||||||
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
@ -191,6 +200,7 @@ func _ready() -> void:
|
||||||
_reel_purchase.pressed.connect(_purchase_reel_speed)
|
_reel_purchase.pressed.connect(_purchase_reel_speed)
|
||||||
_barrier_purchase.pressed.connect(_purchase_barrier_power)
|
_barrier_purchase.pressed.connect(_purchase_barrier_power)
|
||||||
_cooler_purchase.pressed.connect(_purchase_cooler_capacity)
|
_cooler_purchase.pressed.connect(_purchase_cooler_capacity)
|
||||||
|
_backpack_purchase.pressed.connect(_purchase_backpack_capacity)
|
||||||
|
|
||||||
|
|
||||||
func setup_controller_mapping(
|
func setup_controller_mapping(
|
||||||
|
|
@ -327,7 +337,9 @@ func _request_shop_cooler() -> bool:
|
||||||
if not _is_transaction_context_valid():
|
if not _is_transaction_context_valid():
|
||||||
_set_feedback("The fishing shop is no longer available.")
|
_set_feedback("The fishing shop is no longer available.")
|
||||||
return false
|
return false
|
||||||
sell_fish_requested.emit()
|
activate_shop_cooler_page()
|
||||||
|
if _sell_inventory != null:
|
||||||
|
_sell_inventory.activate()
|
||||||
return _cooler_page_active
|
return _cooler_page_active
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -339,6 +351,7 @@ func _focus_shop_section() -> void:
|
||||||
_reel_purchase,
|
_reel_purchase,
|
||||||
_barrier_purchase,
|
_barrier_purchase,
|
||||||
_cooler_purchase,
|
_cooler_purchase,
|
||||||
|
_backpack_purchase,
|
||||||
]:
|
]:
|
||||||
if not upgrade_button.disabled:
|
if not upgrade_button.disabled:
|
||||||
upgrade_button.grab_focus()
|
upgrade_button.grab_focus()
|
||||||
|
|
@ -403,7 +416,7 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
||||||
_update_shop_tab_selection()
|
_update_shop_tab_selection()
|
||||||
return
|
return
|
||||||
if _cooler_page_active:
|
if _cooler_page_active:
|
||||||
shop_cooler_return_requested.emit()
|
deactivate_shop_cooler_page()
|
||||||
_shop_section = section_index as ShopSection
|
_shop_section = section_index as ShopSection
|
||||||
var showing_upgrades: bool = _shop_section == ShopSection.UPGRADES
|
var showing_upgrades: bool = _shop_section == ShopSection.UPGRADES
|
||||||
_upgrades_content.visible = showing_upgrades
|
_upgrades_content.visible = showing_upgrades
|
||||||
|
|
@ -494,6 +507,7 @@ func _apply_shop_styles() -> void:
|
||||||
_reel_price_bubble,
|
_reel_price_bubble,
|
||||||
_barrier_price_bubble,
|
_barrier_price_bubble,
|
||||||
_cooler_price_bubble,
|
_cooler_price_bubble,
|
||||||
|
_backpack_price_bubble,
|
||||||
]:
|
]:
|
||||||
var price_style := UtilityPageStyleType.rounded_style(
|
var price_style := UtilityPageStyleType.rounded_style(
|
||||||
UtilityPageStyleType.OCEAN_FIELD,
|
UtilityPageStyleType.OCEAN_FIELD,
|
||||||
|
|
@ -510,6 +524,7 @@ func _apply_shop_styles() -> void:
|
||||||
_reel_cost,
|
_reel_cost,
|
||||||
_barrier_cost,
|
_barrier_cost,
|
||||||
_cooler_cost,
|
_cooler_cost,
|
||||||
|
_backpack_cost,
|
||||||
]:
|
]:
|
||||||
var amount_label := price.get_amount_label()
|
var amount_label := price.get_amount_label()
|
||||||
amount_label.add_theme_color_override(
|
amount_label.add_theme_color_override(
|
||||||
|
|
@ -521,6 +536,7 @@ func _apply_shop_styles() -> void:
|
||||||
_reel_purchase,
|
_reel_purchase,
|
||||||
_barrier_purchase,
|
_barrier_purchase,
|
||||||
_cooler_purchase,
|
_cooler_purchase,
|
||||||
|
_backpack_purchase,
|
||||||
]:
|
]:
|
||||||
UtilityPageStyleType.apply_ocean_button(button)
|
UtilityPageStyleType.apply_ocean_button(button)
|
||||||
_feedback.add_theme_color_override(
|
_feedback.add_theme_color_override(
|
||||||
|
|
@ -536,10 +552,15 @@ func setup(
|
||||||
fishing_spot: FishingSpotType,
|
fishing_spot: FishingSpotType,
|
||||||
interaction: ShopInteractionType,
|
interaction: ShopInteractionType,
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
|
inventory: FishInventory,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
inventory_layout: PlayerInventoryLayout,
|
||||||
item_catalog: ItemCatalogType,
|
item_catalog: ItemCatalogType,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
art_unlocks: PlayerArtUnlocksType,
|
art_unlocks: PlayerArtUnlocksType,
|
||||||
network_shop: NetworkShopService,
|
network_shop: NetworkShopService,
|
||||||
|
network_sale: NetworkSaleService,
|
||||||
|
reservations: PlayerAssetReservationService,
|
||||||
) -> void:
|
) -> void:
|
||||||
_player = player
|
_player = player
|
||||||
_wallet = wallet
|
_wallet = wallet
|
||||||
|
|
@ -548,10 +569,28 @@ func setup(
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_interaction = interaction
|
_interaction = interaction
|
||||||
_bag = bag
|
_bag = bag
|
||||||
|
_inventory = inventory
|
||||||
|
_hotbar = hotbar
|
||||||
|
_inventory_layout = inventory_layout
|
||||||
_item_catalog = item_catalog
|
_item_catalog = item_catalog
|
||||||
_cooler_capacity = cooler_capacity
|
_cooler_capacity = cooler_capacity
|
||||||
_art_unlocks = art_unlocks
|
_art_unlocks = art_unlocks
|
||||||
_network_shop = network_shop
|
_network_shop = network_shop
|
||||||
|
_network_sale = network_sale
|
||||||
|
_reservations = reservations
|
||||||
|
_sell_inventory = ShopSellInventory.new()
|
||||||
|
_sell_inventory.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_shop_cooler_mount.add_child(_sell_inventory)
|
||||||
|
_sell_inventory.setup(
|
||||||
|
_inventory_layout,
|
||||||
|
_bag,
|
||||||
|
_inventory,
|
||||||
|
_hotbar,
|
||||||
|
_item_catalog,
|
||||||
|
_buyer,
|
||||||
|
_reservations,
|
||||||
|
_network_sale,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
_network_shop != null
|
_network_shop != null
|
||||||
and not _network_shop.local_purchase_pending.is_connected(
|
and not _network_shop.local_purchase_pending.is_connected(
|
||||||
|
|
@ -574,6 +613,12 @@ func setup(
|
||||||
_on_cooler_capacity_changed
|
_on_cooler_capacity_changed
|
||||||
):
|
):
|
||||||
_cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed)
|
_cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed)
|
||||||
|
if not _inventory_layout.backpack_capacity_changed.is_connected(
|
||||||
|
_on_backpack_capacity_changed
|
||||||
|
):
|
||||||
|
_inventory_layout.backpack_capacity_changed.connect(
|
||||||
|
_on_backpack_capacity_changed
|
||||||
|
)
|
||||||
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
|
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
|
||||||
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
|
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
|
||||||
_refresh_all()
|
_refresh_all()
|
||||||
|
|
@ -611,6 +656,8 @@ func open_shop() -> bool:
|
||||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
_set_feedback("")
|
_set_feedback("")
|
||||||
deactivate_shop_cooler_page()
|
deactivate_shop_cooler_page()
|
||||||
|
if _sell_inventory != null:
|
||||||
|
_sell_inventory.clear_staged()
|
||||||
show()
|
show()
|
||||||
_shop_tab_bar.show()
|
_shop_tab_bar.show()
|
||||||
_controller_zone = ControllerZone.TABS
|
_controller_zone = ControllerZone.TABS
|
||||||
|
|
@ -780,6 +827,7 @@ func _refresh_all() -> void:
|
||||||
_refresh_upgrades()
|
_refresh_upgrades()
|
||||||
_refresh_supplies()
|
_refresh_supplies()
|
||||||
_refresh_cooler_capacity()
|
_refresh_cooler_capacity()
|
||||||
|
_refresh_backpack_capacity()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_wallet() -> void:
|
func _refresh_wallet() -> void:
|
||||||
|
|
@ -1515,25 +1563,60 @@ func _refresh_cooler_capacity() -> void:
|
||||||
)
|
)
|
||||||
var cooler_effect: String
|
var cooler_effect: String
|
||||||
if cost < 0:
|
if cost < 0:
|
||||||
cooler_effect = "%d fish · maximum level" % (
|
cooler_effect = "%d slots · maximum level" % (
|
||||||
_cooler_capacity.get_capacity()
|
_cooler_capacity.get_capacity()
|
||||||
)
|
)
|
||||||
_cooler_price_bubble.hide()
|
_cooler_price_bubble.hide()
|
||||||
else:
|
else:
|
||||||
_cooler_price_bubble.show()
|
_cooler_price_bubble.show()
|
||||||
cooler_effect = "%d → %d fish" % [
|
cooler_effect = "%d → %d slots" % [
|
||||||
_cooler_capacity.get_capacity(),
|
_cooler_capacity.get_capacity(),
|
||||||
next_capacity,
|
next_capacity,
|
||||||
]
|
]
|
||||||
_cooler_cost.set_amount(cost)
|
_cooler_cost.set_amount(cost)
|
||||||
_cooler_purchase.tooltip_text = _upgrade_tooltip(
|
_cooler_purchase.tooltip_text = _upgrade_tooltip(
|
||||||
"cooler capacity",
|
"storage capacity",
|
||||||
level,
|
level,
|
||||||
cooler_effect,
|
cooler_effect,
|
||||||
)
|
)
|
||||||
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
|
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_backpack_capacity() -> void:
|
||||||
|
if _inventory_layout == null:
|
||||||
|
return
|
||||||
|
var level := _inventory_layout.get_backpack_level()
|
||||||
|
var cost := _inventory_layout.get_next_backpack_cost()
|
||||||
|
var next_capacity := _inventory_layout.get_next_inventory_capacity()
|
||||||
|
_backpack_purchase.disabled = (
|
||||||
|
cost < 0
|
||||||
|
or _transaction_in_progress
|
||||||
|
or _closing
|
||||||
|
or _network_shop == null
|
||||||
|
or not _network_shop.can_request_backpack_purchase()
|
||||||
|
or not _inventory_layout.can_purchase_backpack(_wallet)
|
||||||
|
)
|
||||||
|
var effect: String
|
||||||
|
if cost < 0:
|
||||||
|
effect = "%d slots · maximum level" % (
|
||||||
|
_inventory_layout.get_inventory_capacity()
|
||||||
|
)
|
||||||
|
_backpack_price_bubble.hide()
|
||||||
|
else:
|
||||||
|
_backpack_price_bubble.show()
|
||||||
|
effect = "%d → %d slots" % [
|
||||||
|
_inventory_layout.get_inventory_capacity(),
|
||||||
|
next_capacity,
|
||||||
|
]
|
||||||
|
_backpack_cost.set_amount(cost)
|
||||||
|
_backpack_purchase.tooltip_text = _upgrade_tooltip(
|
||||||
|
"backpack capacity",
|
||||||
|
level,
|
||||||
|
effect,
|
||||||
|
)
|
||||||
|
_backpack_purchase.accessibility_name = _backpack_purchase.tooltip_text
|
||||||
|
|
||||||
|
|
||||||
func _purchase_reel_speed() -> void:
|
func _purchase_reel_speed() -> void:
|
||||||
_purchase_upgrade(true)
|
_purchase_upgrade(true)
|
||||||
|
|
||||||
|
|
@ -1555,7 +1638,7 @@ func _purchase_supply(item_id: StringName) -> void:
|
||||||
item_id, owned, item.max_stack
|
item_id, owned, item.max_stack
|
||||||
)
|
)
|
||||||
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
|
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
|
||||||
_set_feedback("Your Bag is full.")
|
_set_feedback("Your inventory is full.")
|
||||||
return
|
return
|
||||||
var total_cost: int = FishingShopStockType.get_purchase_cost(
|
var total_cost: int = FishingShopStockType.get_purchase_cost(
|
||||||
item_id, quantity, _bag.is_bait_unlocked(item_id)
|
item_id, quantity, _bag.is_bait_unlocked(item_id)
|
||||||
|
|
@ -1617,6 +1700,23 @@ func _purchase_cooler_capacity() -> void:
|
||||||
_network_shop.request_cooler_capacity_upgrade()
|
_network_shop.request_cooler_capacity_upgrade()
|
||||||
|
|
||||||
|
|
||||||
|
func _purchase_backpack_capacity() -> void:
|
||||||
|
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||||
|
_set_feedback("unable to complete purchase.")
|
||||||
|
return
|
||||||
|
var cost := _inventory_layout.get_next_backpack_cost()
|
||||||
|
if cost < 0:
|
||||||
|
_set_feedback("Upgrade is already at maximum.")
|
||||||
|
return
|
||||||
|
if not _wallet.can_afford(cost):
|
||||||
|
_set_feedback("Insufficient funds.")
|
||||||
|
return
|
||||||
|
if _network_shop == null:
|
||||||
|
_set_feedback("Purchase could not be completed.")
|
||||||
|
return
|
||||||
|
_network_shop.request_backpack_capacity_upgrade()
|
||||||
|
|
||||||
|
|
||||||
func _purchase_upgrade(is_reel_speed: bool) -> void:
|
func _purchase_upgrade(is_reel_speed: bool) -> void:
|
||||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||||
_set_feedback("unable to complete purchase.")
|
_set_feedback("unable to complete purchase.")
|
||||||
|
|
@ -1721,6 +1821,7 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void:
|
||||||
_refresh_upgrades()
|
_refresh_upgrades()
|
||||||
_refresh_supplies()
|
_refresh_supplies()
|
||||||
_refresh_cooler_capacity()
|
_refresh_cooler_capacity()
|
||||||
|
_refresh_backpack_capacity()
|
||||||
|
|
||||||
|
|
||||||
func _on_upgrades_changed(
|
func _on_upgrades_changed(
|
||||||
|
|
@ -1738,6 +1839,10 @@ func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
|
||||||
_refresh_cooler_capacity()
|
_refresh_cooler_capacity()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_backpack_capacity_changed(_level: int, _capacity: int) -> void:
|
||||||
|
_refresh_backpack_capacity()
|
||||||
|
|
||||||
|
|
||||||
func _on_art_unlocks_changed(_unlock_mask: int) -> void:
|
func _on_art_unlocks_changed(_unlock_mask: int) -> void:
|
||||||
_refresh_supplies()
|
_refresh_supplies()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
[gd_scene load_steps=9 format=3]
|
[gd_scene load_steps=8 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/fishing_shop.gd" id="1_script"]
|
[ext_resource type="Script" path="res://ui/fishing_shop.gd" id="1_script"]
|
||||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||||
[ext_resource type="Texture2D" path="res://items/icons/shop/64_speed_plus.png" id="3_reel"]
|
[ext_resource type="Texture2D" path="res://items/icons/shop/64_speed_plus.png" id="3_reel"]
|
||||||
[ext_resource type="Texture2D" path="res://items/icons/shop/64_power_plus.png" id="4_barrier"]
|
[ext_resource type="Texture2D" path="res://items/icons/shop/64_power_plus.png" id="4_barrier"]
|
||||||
[ext_resource type="Texture2D" path="res://items/icons/shop/32_currency.png" id="5_coin"]
|
[ext_resource type="Texture2D" path="res://items/icons/shop/32_currency.png" id="5_coin"]
|
||||||
[ext_resource type="Texture2D" path="res://items/icons/equipment/64_cooler_plus.png" id="6_cooler"]
|
|
||||||
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="7_close"]
|
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="7_close"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="8_currency"]
|
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="8_currency"]
|
||||||
|
|
||||||
|
|
@ -269,10 +268,10 @@ mouse_filter = 2
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
offset_right = 144.0
|
offset_right = 144.0
|
||||||
offset_bottom = 144.0
|
offset_bottom = 144.0
|
||||||
tooltip_text = "cooler capacity"
|
tooltip_text = "storage capacity"
|
||||||
icon = ExtResource("6_cooler")
|
icon = ExtResource("7_close")
|
||||||
expand_icon = true
|
expand_icon = true
|
||||||
icon_max_width = 128
|
icon_max_width = 92
|
||||||
|
|
||||||
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
|
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
@ -288,3 +287,32 @@ unique_name_in_owner = true
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
amount = 75
|
amount = 75
|
||||||
icon_size = 24.0
|
icon_size = 24.0
|
||||||
|
|
||||||
|
[node name="BackpackTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
|
||||||
|
custom_minimum_size = Vector2(144, 168)
|
||||||
|
layout_mode = 2
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="BackpackPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
offset_right = 144.0
|
||||||
|
offset_bottom = 144.0
|
||||||
|
tooltip_text = "backpack capacity"
|
||||||
|
icon = ExtResource("7_close")
|
||||||
|
expand_icon = true
|
||||||
|
icon_max_width = 92
|
||||||
|
|
||||||
|
[node name="BackpackPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
z_index = 2
|
||||||
|
offset_left = 14.0
|
||||||
|
offset_top = 128.0
|
||||||
|
offset_right = 130.0
|
||||||
|
offset_bottom = 160.0
|
||||||
|
mouse_filter = 2
|
||||||
|
|
||||||
|
[node name="BackpackCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile/BackpackPriceBubble" instance=ExtResource("8_currency")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
amount = 1500
|
||||||
|
icon_size = 24.0
|
||||||
|
|
|
||||||
131
ui/game_ui.gd
131
ui/game_ui.gd
|
|
@ -27,6 +27,10 @@ const PlayerFishingUpgradesType = preload(
|
||||||
const ShopInteractionType = preload(
|
const ShopInteractionType = preload(
|
||||||
"res://world/fishing_shop_interaction.gd"
|
"res://world/fishing_shop_interaction.gd"
|
||||||
)
|
)
|
||||||
|
const PlayerStorageType = preload("res://ui/player_storage.gd")
|
||||||
|
const PlayerStorageInteractionType = preload(
|
||||||
|
"res://world/player_storage_interaction.gd"
|
||||||
|
)
|
||||||
const PlayerItemEffectsType = preload(
|
const PlayerItemEffectsType = preload(
|
||||||
"res://progression/player_item_effects.gd"
|
"res://progression/player_item_effects.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -134,6 +138,8 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
||||||
@onready var _pause_menu: PauseMenuType = %PauseMenu
|
@onready var _pause_menu: PauseMenuType = %PauseMenu
|
||||||
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
||||||
@onready var _fishing_shop: FishingShopType = %FishingShop
|
@onready var _fishing_shop: FishingShopType = %FishingShop
|
||||||
|
@onready var _player_storage: PlayerStorageType = %PlayerStorage
|
||||||
|
@onready var _storage_prompt: PanelContainer = %StoragePrompt
|
||||||
@onready var _shop_prompt: Control = %ShopPrompt
|
@onready var _shop_prompt: Control = %ShopPrompt
|
||||||
@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble
|
@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble
|
||||||
@onready var _shop_prompt_message: Label = %ShopPromptMessage
|
@onready var _shop_prompt_message: Label = %ShopPromptMessage
|
||||||
|
|
@ -174,10 +180,12 @@ var _gameplay_hud_hidden: bool = false
|
||||||
var _fishing_spot: FishingSpotType
|
var _fishing_spot: FishingSpotType
|
||||||
var _system_menu_open: bool = false
|
var _system_menu_open: bool = false
|
||||||
var _shop_open: bool = false
|
var _shop_open: bool = false
|
||||||
|
var _storage_open: bool = false
|
||||||
var _chat_input_open: bool = false
|
var _chat_input_open: bool = false
|
||||||
var _player_menu_hotbar_visible: bool = false
|
var _player_menu_hotbar_visible: bool = false
|
||||||
var _main_shop_buyer: FishBuyerProfileType
|
var _main_shop_buyer: FishBuyerProfileType
|
||||||
var _shop_interaction: ShopInteractionType
|
var _shop_interaction: ShopInteractionType
|
||||||
|
var _storage_interaction: PlayerStorageInteractionType
|
||||||
var _surface_drawing: NetworkSurfaceDrawingService
|
var _surface_drawing: NetworkSurfaceDrawingService
|
||||||
var _surface_drawing_hotbar_selected: bool = false
|
var _surface_drawing_hotbar_selected: bool = false
|
||||||
var _experience: PlayerExperienceType
|
var _experience: PlayerExperienceType
|
||||||
|
|
@ -294,10 +302,12 @@ func setup(
|
||||||
fishing_spot: FishingSpotType,
|
fishing_spot: FishingSpotType,
|
||||||
bag: PlayerBagType,
|
bag: PlayerBagType,
|
||||||
hotbar: PlayerHotbarType,
|
hotbar: PlayerHotbarType,
|
||||||
|
inventory_layout: PlayerInventoryLayout,
|
||||||
item_catalog: ItemCatalogType,
|
item_catalog: ItemCatalogType,
|
||||||
main_shop_buyer: FishBuyerProfileType,
|
main_shop_buyer: FishBuyerProfileType,
|
||||||
fishing_upgrades: PlayerFishingUpgradesType,
|
fishing_upgrades: PlayerFishingUpgradesType,
|
||||||
shop_interaction: ShopInteractionType,
|
shop_interaction: ShopInteractionType,
|
||||||
|
storage_interaction: PlayerStorageInteractionType,
|
||||||
item_effects: PlayerItemEffectsType,
|
item_effects: PlayerItemEffectsType,
|
||||||
cooler_capacity: PlayerCoolerCapacityType,
|
cooler_capacity: PlayerCoolerCapacityType,
|
||||||
network_session: NetworkSessionType,
|
network_session: NetworkSessionType,
|
||||||
|
|
@ -398,6 +408,7 @@ func setup(
|
||||||
fishing_spot,
|
fishing_spot,
|
||||||
bag,
|
bag,
|
||||||
hotbar,
|
hotbar,
|
||||||
|
inventory_layout,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
cooler_capacity,
|
cooler_capacity,
|
||||||
network_session,
|
network_session,
|
||||||
|
|
@ -430,10 +441,28 @@ func setup(
|
||||||
fishing_spot,
|
fishing_spot,
|
||||||
shop_interaction,
|
shop_interaction,
|
||||||
bag,
|
bag,
|
||||||
|
inventory,
|
||||||
|
hotbar,
|
||||||
|
inventory_layout,
|
||||||
item_catalog,
|
item_catalog,
|
||||||
cooler_capacity,
|
cooler_capacity,
|
||||||
art_unlocks,
|
art_unlocks,
|
||||||
network_shop_service,
|
network_shop_service,
|
||||||
|
network_sale_service,
|
||||||
|
reservations,
|
||||||
|
)
|
||||||
|
_player_storage.setup(
|
||||||
|
player,
|
||||||
|
fishing_spot,
|
||||||
|
storage_interaction,
|
||||||
|
inventory_layout,
|
||||||
|
bag,
|
||||||
|
inventory,
|
||||||
|
hotbar,
|
||||||
|
item_catalog,
|
||||||
|
)
|
||||||
|
_player_storage.menu_visibility_changed.connect(
|
||||||
|
_on_storage_visibility_changed
|
||||||
)
|
)
|
||||||
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||||
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
|
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||||
|
|
@ -446,6 +475,7 @@ func setup(
|
||||||
)
|
)
|
||||||
_main_shop_buyer = main_shop_buyer
|
_main_shop_buyer = main_shop_buyer
|
||||||
_shop_interaction = shop_interaction
|
_shop_interaction = shop_interaction
|
||||||
|
_storage_interaction = storage_interaction
|
||||||
_surface_drawing = surface_drawing
|
_surface_drawing = surface_drawing
|
||||||
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
|
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
|
||||||
set_edge_docks(
|
set_edge_docks(
|
||||||
|
|
@ -505,6 +535,7 @@ func _input(event: InputEvent) -> void:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and not _chat_input_open
|
and not _chat_input_open
|
||||||
and not _showcase_active
|
and not _showcase_active
|
||||||
and not _virtual_mouse_active
|
and not _virtual_mouse_active
|
||||||
|
|
@ -552,6 +583,7 @@ func _can_use_character_call() -> bool:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and not _chat_input_open
|
and not _chat_input_open
|
||||||
and not _showcase_active
|
and not _showcase_active
|
||||||
and not _virtual_mouse_active
|
and not _virtual_mouse_active
|
||||||
|
|
@ -565,8 +597,16 @@ func _character_call_yields_to_world_interaction(
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return (
|
return (
|
||||||
event.is_action_pressed("interact")
|
event.is_action_pressed("interact")
|
||||||
and _shop_interaction != null
|
and (
|
||||||
and _shop_interaction.is_local_player_in_range()
|
(
|
||||||
|
_shop_interaction != null
|
||||||
|
and _shop_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
or (
|
||||||
|
_storage_interaction != null
|
||||||
|
and _storage_interaction.is_local_player_in_range()
|
||||||
|
)
|
||||||
|
)
|
||||||
and _fishing_spot != null
|
and _fishing_spot != null
|
||||||
and _fishing_spot.can_open_fishing_shop()
|
and _fishing_spot.can_open_fishing_shop()
|
||||||
and not _fishing_shop.visible
|
and not _fishing_shop.visible
|
||||||
|
|
@ -614,6 +654,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
|
||||||
or _system_menu_open
|
or _system_menu_open
|
||||||
or _player_menu_open
|
or _player_menu_open
|
||||||
or _shop_open
|
or _shop_open
|
||||||
|
or _storage_open
|
||||||
):
|
):
|
||||||
return false
|
return false
|
||||||
var use_mapping: bool = (
|
var use_mapping: bool = (
|
||||||
|
|
@ -825,6 +866,7 @@ func _can_start_virtual_mouse() -> bool:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and not _chat_input_open
|
and not _chat_input_open
|
||||||
and _player != null
|
and _player != null
|
||||||
and _fishing_spot != null
|
and _fishing_spot != null
|
||||||
|
|
@ -1182,6 +1224,7 @@ func _can_surface_drawing_be_active() -> bool:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and not _chat_input_open
|
and not _chat_input_open
|
||||||
and not _showcase_active
|
and not _showcase_active
|
||||||
and _fishing_spot != null
|
and _fishing_spot != null
|
||||||
|
|
@ -1460,6 +1503,7 @@ func _refresh_active_bait_indicator_visibility() -> void:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1494,8 +1538,10 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
||||||
_surface_drawing.deactivate()
|
_surface_drawing.deactivate()
|
||||||
close_player_menu_for_session_end()
|
close_player_menu_for_session_end()
|
||||||
_fishing_shop.close_for_session_end()
|
_fishing_shop.close_for_session_end()
|
||||||
|
_player_storage.close_for_session_end()
|
||||||
_fishing_panel.visible = false
|
_fishing_panel.visible = false
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
|
_storage_prompt.hide()
|
||||||
_hotbar_ui.set_presentation_visible(false, false)
|
_hotbar_ui.set_presentation_visible(false, false)
|
||||||
_hotbar_ui.set_gameplay_input_enabled(false)
|
_hotbar_ui.set_gameplay_input_enabled(false)
|
||||||
else:
|
else:
|
||||||
|
|
@ -1526,6 +1572,7 @@ func _can_toggle_gameplay_hud() -> bool:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and not _chat_input_open
|
and not _chat_input_open
|
||||||
and not _showcase_active
|
and not _showcase_active
|
||||||
and not _emote_radial_menu.is_open()
|
and not _emote_radial_menu.is_open()
|
||||||
|
|
@ -1540,6 +1587,7 @@ func _refresh_gameplay_hud_visibility() -> void:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
_gameplay_transient_hud.visible = show_world_hud
|
_gameplay_transient_hud.visible = show_world_hud
|
||||||
_experience_presentation.visible = (
|
_experience_presentation.visible = (
|
||||||
|
|
@ -1558,11 +1606,13 @@ func set_system_menu_open(is_open: bool) -> void:
|
||||||
and not is_open
|
and not is_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
_hotbar_ui.set_drag_enabled(_player_menu_open and not is_open)
|
_hotbar_ui.set_drag_enabled(_player_menu_open and not is_open)
|
||||||
if is_open:
|
if is_open:
|
||||||
_hotbar_ui.set_drag_enabled(false)
|
_hotbar_ui.set_drag_enabled(false)
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
|
_storage_prompt.hide()
|
||||||
_emit_interactive_pointer_ui_changed()
|
_emit_interactive_pointer_ui_changed()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1570,6 +1620,53 @@ func get_fishing_shop() -> FishingShopType:
|
||||||
return _fishing_shop
|
return _fishing_shop
|
||||||
|
|
||||||
|
|
||||||
|
func get_player_storage() -> PlayerStorageType:
|
||||||
|
return _player_storage
|
||||||
|
|
||||||
|
|
||||||
|
func set_storage_prompt_visible(
|
||||||
|
requested_visible: bool,
|
||||||
|
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||||
|
) -> void:
|
||||||
|
if not _storage_prompt.has_meta(&"styled"):
|
||||||
|
_storage_prompt.set_meta(&"styled", true)
|
||||||
|
var style := UtilityPageStyle.rounded_style(
|
||||||
|
Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96), 12
|
||||||
|
)
|
||||||
|
style.anti_aliasing = false
|
||||||
|
_storage_prompt.add_theme_stylebox_override("panel", style)
|
||||||
|
_storage_prompt.visible = (
|
||||||
|
requested_visible
|
||||||
|
and _gameplay_ui_enabled
|
||||||
|
and not _system_menu_open
|
||||||
|
and not _player_menu_open
|
||||||
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
|
)
|
||||||
|
if _storage_prompt.visible and world_anchor.is_finite():
|
||||||
|
_position_storage_prompt(world_anchor)
|
||||||
|
|
||||||
|
|
||||||
|
func _position_storage_prompt(world_anchor: Vector3) -> void:
|
||||||
|
if _player == null:
|
||||||
|
_storage_prompt.hide()
|
||||||
|
return
|
||||||
|
var camera := _player.get_gameplay_camera()
|
||||||
|
if camera == null or camera.is_position_behind(world_anchor):
|
||||||
|
_storage_prompt.hide()
|
||||||
|
return
|
||||||
|
var camera_size := camera.get_viewport().get_visible_rect().size
|
||||||
|
var ui_size := _canonical_stage.size
|
||||||
|
if camera_size.x <= 0.0 or camera_size.y <= 0.0:
|
||||||
|
_storage_prompt.hide()
|
||||||
|
return
|
||||||
|
var point := camera.unproject_position(world_anchor) * ui_size / camera_size
|
||||||
|
_storage_prompt.position = Vector2(
|
||||||
|
clampf(point.x - _storage_prompt.size.x * 0.5, 8.0, ui_size.x - _storage_prompt.size.x - 8.0),
|
||||||
|
clampf(point.y - _storage_prompt.size.y - 12.0, 8.0, ui_size.y - _storage_prompt.size.y - 8.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func set_shop_prompt_visible(
|
func set_shop_prompt_visible(
|
||||||
requested_visible: bool,
|
requested_visible: bool,
|
||||||
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
|
||||||
|
|
@ -1581,6 +1678,7 @@ func set_shop_prompt_visible(
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
if _shop_prompt.visible:
|
if _shop_prompt.visible:
|
||||||
if world_anchor.is_finite():
|
if world_anchor.is_finite():
|
||||||
|
|
@ -2216,9 +2314,30 @@ func _on_shop_visibility_changed(is_open: bool) -> void:
|
||||||
and not is_open
|
and not is_open
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
if is_open:
|
if is_open:
|
||||||
_shop_prompt.hide()
|
_shop_prompt.hide()
|
||||||
|
_storage_prompt.hide()
|
||||||
|
_emit_interactive_pointer_ui_changed()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_storage_visibility_changed(is_open: bool) -> void:
|
||||||
|
_storage_open = is_open
|
||||||
|
_refresh_surface_drawing_activation()
|
||||||
|
_refresh_gameplay_hud_visibility()
|
||||||
|
_refresh_chat_availability()
|
||||||
|
_refresh_hotbar_visibility()
|
||||||
|
_hotbar_ui.set_gameplay_input_enabled(
|
||||||
|
_gameplay_ui_enabled
|
||||||
|
and not is_open
|
||||||
|
and not _system_menu_open
|
||||||
|
and not _player_menu_open
|
||||||
|
and not _shop_open
|
||||||
|
)
|
||||||
|
if is_open:
|
||||||
|
_shop_prompt.hide()
|
||||||
|
_storage_prompt.hide()
|
||||||
_emit_interactive_pointer_ui_changed()
|
_emit_interactive_pointer_ui_changed()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2338,6 +2457,7 @@ func _refresh_hotbar_visibility() -> void:
|
||||||
)
|
)
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
and (
|
and (
|
||||||
not _player_menu_open
|
not _player_menu_open
|
||||||
or _player_menu_hotbar_visible
|
or _player_menu_hotbar_visible
|
||||||
|
|
@ -2349,7 +2469,11 @@ func _refresh_hotbar_visibility() -> void:
|
||||||
func _emit_interactive_pointer_ui_changed() -> void:
|
func _emit_interactive_pointer_ui_changed() -> void:
|
||||||
_refresh_active_bait_indicator_visibility()
|
_refresh_active_bait_indicator_visibility()
|
||||||
interactive_pointer_ui_changed.emit(
|
interactive_pointer_ui_changed.emit(
|
||||||
_system_menu_open or _player_menu_open or _shop_open or _chat_input_open
|
_system_menu_open
|
||||||
|
or _player_menu_open
|
||||||
|
or _shop_open
|
||||||
|
or _storage_open
|
||||||
|
or _chat_input_open
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -2368,6 +2492,7 @@ func _refresh_chat_availability() -> void:
|
||||||
and not _system_menu_open
|
and not _system_menu_open
|
||||||
and not _player_menu_open
|
and not _player_menu_open
|
||||||
and not _shop_open
|
and not _shop_open
|
||||||
|
and not _storage_open
|
||||||
)
|
)
|
||||||
_chat_ui.set_available(chat_available)
|
_chat_ui.set_available(chat_available)
|
||||||
_chat_ui.set_hud_hidden(_gameplay_hud_hidden and not _chat_input_open)
|
_chat_ui.set_hud_hidden(_gameplay_hud_hidden and not _chat_input_open)
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
[ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"]
|
[ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"]
|
[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"]
|
||||||
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
|
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
|
||||||
|
[ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
||||||
bg_color = Color(0.032, 0.118, 0.15, 1)
|
bg_color = Color(0.032, 0.118, 0.15, 1)
|
||||||
|
|
@ -471,6 +472,32 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10)
|
||||||
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
|
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
|
||||||
|
[node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 56
|
||||||
|
offset_right = 220.0
|
||||||
|
offset_bottom = 50.0
|
||||||
|
mouse_filter = 2
|
||||||
|
theme = ExtResource("3_theme")
|
||||||
|
|
||||||
|
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt"]
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/margin_left = 12
|
||||||
|
theme_override_constants/margin_top = 7
|
||||||
|
theme_override_constants/margin_right = 12
|
||||||
|
theme_override_constants/margin_bottom = 7
|
||||||
|
|
||||||
|
[node name="Message" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt/Margin"]
|
||||||
|
layout_mode = 2
|
||||||
|
text = "E open storage"
|
||||||
|
horizontal_alignment = 1
|
||||||
|
vertical_alignment = 1
|
||||||
|
theme_override_font_sizes/font_size = 18
|
||||||
|
|
||||||
[node name="ChatUI" type="Control" parent="UIRoot"]
|
[node name="ChatUI" type="Control" parent="UIRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 1
|
layout_mode = 1
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,9 @@ const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
|
||||||
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
|
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
|
||||||
const HOTBAR_PRESENTATION_SCALE: float = 0.80
|
const HOTBAR_PRESENTATION_SCALE: float = 0.80
|
||||||
const HOTBAR_CANONICAL_POSITION := Vector2.ZERO
|
const HOTBAR_CANONICAL_POSITION := Vector2.ZERO
|
||||||
const HOTBAR_MENU_POSITION := Vector2(-136.0, -90.0)
|
# In inventory context the row intentionally straddles the panel's bottom
|
||||||
|
# edge, visually extending the storage layout into its nine quick-access slots.
|
||||||
|
const HOTBAR_MENU_POSITION := Vector2(0.0, -30.0)
|
||||||
const HOTBAR_GAMEPLAY_Z_INDEX: int = 35
|
const HOTBAR_GAMEPLAY_Z_INDEX: int = 35
|
||||||
# PlayerMenu is z=30 and its authored inventory panels are relative z=50.
|
# PlayerMenu is z=30 and its authored inventory panels are relative z=50.
|
||||||
const HOTBAR_MENU_Z_INDEX: int = 90
|
const HOTBAR_MENU_Z_INDEX: int = 90
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ static func category_label(category: Category) -> String:
|
||||||
Category.SHELLFISH:
|
Category.SHELLFISH:
|
||||||
return "Shellfish"
|
return "Shellfish"
|
||||||
_:
|
_:
|
||||||
return "Misc"
|
return "Insects"
|
||||||
|
|
||||||
|
|
||||||
static func empty_state(category: Category) -> String:
|
static func empty_state(category: Category) -> String:
|
||||||
|
|
|
||||||
103
ui/mail_page.gd
103
ui/mail_page.gd
|
|
@ -18,6 +18,13 @@ const FishQualityType = preload("res://fish/fish_quality.gd")
|
||||||
const CurrencyPresentationType = preload(
|
const CurrencyPresentationType = preload(
|
||||||
"res://ui/currency_presentation.gd"
|
"res://ui/currency_presentation.gd"
|
||||||
)
|
)
|
||||||
|
const INBOX_ENTRY_WIDTH: float = 742.0
|
||||||
|
const ATTACHMENT_COLUMN_X: float = 516.0
|
||||||
|
const ATTACHMENT_COLUMN_WIDTH: float = 266.0
|
||||||
|
const AMOUNT_FIELD_X_WITH_CURRENCY: float = 594.0
|
||||||
|
const AMOUNT_FIELD_X_PLAIN: float = 570.0
|
||||||
|
const AMOUNT_FIELD_WIDTH_WITH_CURRENCY: float = 130.0
|
||||||
|
const AMOUNT_FIELD_WIDTH_PLAIN: float = 154.0
|
||||||
|
|
||||||
var _service: NetworkMailService
|
var _service: NetworkMailService
|
||||||
var _reservations: PlayerAssetReservationService
|
var _reservations: PlayerAssetReservationService
|
||||||
|
|
@ -434,7 +441,7 @@ func _build_ui() -> void:
|
||||||
root.add_child(page)
|
root.add_child(page)
|
||||||
_status = Label.new()
|
_status = Label.new()
|
||||||
_status.position = Vector2(18, 408)
|
_status.position = Vector2(18, 408)
|
||||||
_status.size = Vector2(1024, 28)
|
_status.size = Vector2(758, 28)
|
||||||
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_status.add_theme_color_override(
|
_status.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
|
|
@ -449,19 +456,19 @@ func _build_inbox() -> Control:
|
||||||
var title := Label.new()
|
var title := Label.new()
|
||||||
title.text = "mail"
|
title.text = "mail"
|
||||||
title.position = Vector2(12, 0)
|
title.position = Vector2(12, 0)
|
||||||
title.size = Vector2(500, 42)
|
title.size = Vector2(300, 42)
|
||||||
title.add_theme_font_size_override("font_size", 30)
|
title.add_theme_font_size_override("font_size", 30)
|
||||||
page.add_child(title)
|
page.add_child(title)
|
||||||
_send_mail_button = Button.new()
|
_send_mail_button = Button.new()
|
||||||
_send_mail_button.text = "send mail"
|
_send_mail_button.text = "send mail"
|
||||||
_send_mail_button.position = Vector2(820, 0)
|
_send_mail_button.position = Vector2(642, 0)
|
||||||
_send_mail_button.size = Vector2(150, 48)
|
_send_mail_button.size = Vector2(140, 48)
|
||||||
_send_mail_button.pressed.connect(_show_compose)
|
_send_mail_button.pressed.connect(_show_compose)
|
||||||
page.add_child(_send_mail_button)
|
page.add_child(_send_mail_button)
|
||||||
_archive_view_button = Button.new()
|
_archive_view_button = Button.new()
|
||||||
_archive_view_button.text = "archive"
|
_archive_view_button.text = "archive"
|
||||||
_archive_view_button.position = Vector2(654, 0)
|
_archive_view_button.position = Vector2(490, 0)
|
||||||
_archive_view_button.size = Vector2(154, 48)
|
_archive_view_button.size = Vector2(140, 48)
|
||||||
_archive_view_button.pressed.connect(func() -> void:
|
_archive_view_button.pressed.connect(func() -> void:
|
||||||
_showing_archive = not _showing_archive
|
_showing_archive = not _showing_archive
|
||||||
_archive_view_button.text = "inbox" if _showing_archive else "archive"
|
_archive_view_button.text = "inbox" if _showing_archive else "archive"
|
||||||
|
|
@ -470,16 +477,16 @@ func _build_inbox() -> Control:
|
||||||
page.add_child(_archive_view_button)
|
page.add_child(_archive_view_button)
|
||||||
var scroll := ScrollContainer.new()
|
var scroll := ScrollContainer.new()
|
||||||
scroll.position = Vector2(12, 62)
|
scroll.position = Vector2(12, 62)
|
||||||
scroll.size = Vector2(1036, 334)
|
scroll.size = Vector2(770, 334)
|
||||||
page.add_child(scroll)
|
page.add_child(scroll)
|
||||||
_inbox_list = VBoxContainer.new()
|
_inbox_list = VBoxContainer.new()
|
||||||
_inbox_list.custom_minimum_size = Vector2(1008, 0)
|
_inbox_list.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 0)
|
||||||
_inbox_list.add_theme_constant_override("separation", 8)
|
_inbox_list.add_theme_constant_override("separation", 8)
|
||||||
scroll.add_child(_inbox_list)
|
scroll.add_child(_inbox_list)
|
||||||
_empty_label = Label.new()
|
_empty_label = Label.new()
|
||||||
_empty_label.text = "No letters yet."
|
_empty_label.text = "No letters yet."
|
||||||
_empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
_empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_empty_label.custom_minimum_size = Vector2(1008, 80)
|
_empty_label.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 80)
|
||||||
_inbox_list.add_child(_empty_label)
|
_inbox_list.add_child(_empty_label)
|
||||||
return page
|
return page
|
||||||
|
|
||||||
|
|
@ -494,43 +501,43 @@ func _build_compose() -> Control:
|
||||||
page.add_child(title)
|
page.add_child(title)
|
||||||
_greeting = OptionButton.new()
|
_greeting = OptionButton.new()
|
||||||
_greeting.position = Vector2(12, 48)
|
_greeting.position = Vector2(12, 48)
|
||||||
_greeting.size = Vector2(180, 46)
|
_greeting.size = Vector2(140, 46)
|
||||||
for id: String in NetworkMailProtocol.GREETINGS:
|
for id: String in NetworkMailProtocol.GREETINGS:
|
||||||
_greeting.add_item(GREETING_LABELS[id])
|
_greeting.add_item(GREETING_LABELS[id])
|
||||||
_greeting.set_item_metadata(_greeting.item_count - 1, id)
|
_greeting.set_item_metadata(_greeting.item_count - 1, id)
|
||||||
page.add_child(_greeting)
|
page.add_child(_greeting)
|
||||||
_recipient = OptionButton.new()
|
_recipient = OptionButton.new()
|
||||||
_recipient.position = Vector2(204, 48)
|
_recipient.position = Vector2(164, 48)
|
||||||
_recipient.size = Vector2(330, 46)
|
_recipient.size = Vector2(328, 46)
|
||||||
page.add_child(_recipient)
|
page.add_child(_recipient)
|
||||||
_body = TextEdit.new()
|
_body = TextEdit.new()
|
||||||
_body.position = Vector2(12, 106)
|
_body.position = Vector2(12, 106)
|
||||||
_body.size = Vector2(650, 218)
|
_body.size = Vector2(480, 218)
|
||||||
_body.placeholder_text = "write your letter…"
|
_body.placeholder_text = "write your letter…"
|
||||||
_body.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
_body.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
|
||||||
_body.text_changed.connect(_update_send_state)
|
_body.text_changed.connect(_update_send_state)
|
||||||
page.add_child(_body)
|
page.add_child(_body)
|
||||||
_salutation = OptionButton.new()
|
_salutation = OptionButton.new()
|
||||||
_salutation.position = Vector2(12, 336)
|
_salutation.position = Vector2(12, 336)
|
||||||
_salutation.size = Vector2(250, 46)
|
_salutation.size = Vector2(180, 46)
|
||||||
for id: String in NetworkMailProtocol.SALUTATIONS:
|
for id: String in NetworkMailProtocol.SALUTATIONS:
|
||||||
_salutation.add_item(SALUTATION_LABELS[id])
|
_salutation.add_item(SALUTATION_LABELS[id])
|
||||||
_salutation.set_item_metadata(_salutation.item_count - 1, id)
|
_salutation.set_item_metadata(_salutation.item_count - 1, id)
|
||||||
page.add_child(_salutation)
|
page.add_child(_salutation)
|
||||||
_signature = Label.new()
|
_signature = Label.new()
|
||||||
_signature.position = Vector2(274, 342)
|
_signature.position = Vector2(204, 342)
|
||||||
_signature.size = Vector2(360, 36)
|
_signature.size = Vector2(288, 36)
|
||||||
page.add_child(_signature)
|
page.add_child(_signature)
|
||||||
_attachment_kind = OptionButton.new()
|
_attachment_kind = OptionButton.new()
|
||||||
_attachment_kind.position = Vector2(688, 48)
|
_attachment_kind.position = Vector2(ATTACHMENT_COLUMN_X, 48)
|
||||||
_attachment_kind.size = Vector2(280, 46)
|
_attachment_kind.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 46)
|
||||||
for label: String in ["No gift", "Currency", "Fish", "Item"]:
|
for label: String in ["No gift", "Currency", "Fish", "Item"]:
|
||||||
_attachment_kind.add_item(label)
|
_attachment_kind.add_item(label)
|
||||||
_attachment_kind.item_selected.connect(_refresh_attachment_choices)
|
_attachment_kind.item_selected.connect(_refresh_attachment_choices)
|
||||||
page.add_child(_attachment_kind)
|
page.add_child(_attachment_kind)
|
||||||
_attachment_choice = OptionButton.new()
|
_attachment_choice = OptionButton.new()
|
||||||
_attachment_choice.position = Vector2(688, 106)
|
_attachment_choice.position = Vector2(ATTACHMENT_COLUMN_X, 106)
|
||||||
_attachment_choice.size = Vector2(280, 46)
|
_attachment_choice.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 46)
|
||||||
_attachment_choice.item_selected.connect(
|
_attachment_choice.item_selected.connect(
|
||||||
func(_index: int) -> void:
|
func(_index: int) -> void:
|
||||||
_update_attachment_amount_limit()
|
_update_attachment_amount_limit()
|
||||||
|
|
@ -539,16 +546,16 @@ func _build_compose() -> Control:
|
||||||
page.add_child(_attachment_choice)
|
page.add_child(_attachment_choice)
|
||||||
_coin_available_heading = Label.new()
|
_coin_available_heading = Label.new()
|
||||||
_coin_available_heading.text = "Available"
|
_coin_available_heading.text = "Available"
|
||||||
_coin_available_heading.position = Vector2(688, 158)
|
_coin_available_heading.position = Vector2(ATTACHMENT_COLUMN_X, 158)
|
||||||
_coin_available_heading.size = Vector2(80, 28)
|
_coin_available_heading.size = Vector2(80, 28)
|
||||||
page.add_child(_coin_available_heading)
|
page.add_child(_coin_available_heading)
|
||||||
_coin_available = CurrencyPresentationType.instantiate_amount(0, 18.0)
|
_coin_available = CurrencyPresentationType.instantiate_amount(0, 18.0)
|
||||||
_coin_available.position = Vector2(770, 158)
|
_coin_available.position = Vector2(598, 158)
|
||||||
_coin_available.size = Vector2(198, 28)
|
_coin_available.size = Vector2(184, 28)
|
||||||
_coin_available.alignment = BoxContainer.ALIGNMENT_BEGIN
|
_coin_available.alignment = BoxContainer.ALIGNMENT_BEGIN
|
||||||
page.add_child(_coin_available)
|
page.add_child(_coin_available)
|
||||||
_attachment_amount_currency_icon = TextureRect.new()
|
_attachment_amount_currency_icon = TextureRect.new()
|
||||||
_attachment_amount_currency_icon.position = Vector2(746, 204)
|
_attachment_amount_currency_icon.position = Vector2(570, 204)
|
||||||
_attachment_amount_currency_icon.size = Vector2(18, 18)
|
_attachment_amount_currency_icon.size = Vector2(18, 18)
|
||||||
_attachment_amount_currency_icon.texture = preload(
|
_attachment_amount_currency_icon.texture = preload(
|
||||||
"res://items/icons/shop/32_currency.png"
|
"res://items/icons/shop/32_currency.png"
|
||||||
|
|
@ -563,27 +570,27 @@ func _build_compose() -> Control:
|
||||||
_attachment_amount_currency_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
_attachment_amount_currency_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
page.add_child(_attachment_amount_currency_icon)
|
page.add_child(_attachment_amount_currency_icon)
|
||||||
_attachment_amount = LineEdit.new()
|
_attachment_amount = LineEdit.new()
|
||||||
_attachment_amount.position = Vector2(770, 190)
|
_attachment_amount.position = Vector2(AMOUNT_FIELD_X_WITH_CURRENCY, 190)
|
||||||
_attachment_amount.size = Vector2(140, 46)
|
_attachment_amount.size = Vector2(AMOUNT_FIELD_WIDTH_WITH_CURRENCY, 46)
|
||||||
_attachment_amount.placeholder_text = "0"
|
_attachment_amount.placeholder_text = "0"
|
||||||
_attachment_amount.text = "0"
|
_attachment_amount.text = "0"
|
||||||
_attachment_amount.text_changed.connect(_on_attachment_amount_changed)
|
_attachment_amount.text_changed.connect(_on_attachment_amount_changed)
|
||||||
page.add_child(_attachment_amount)
|
page.add_child(_attachment_amount)
|
||||||
_amount_minus = Button.new()
|
_amount_minus = Button.new()
|
||||||
_amount_minus.text = "−"
|
_amount_minus.text = "−"
|
||||||
_amount_minus.position = Vector2(688, 190)
|
_amount_minus.position = Vector2(ATTACHMENT_COLUMN_X, 190)
|
||||||
_amount_minus.size = Vector2(48, 46)
|
_amount_minus.size = Vector2(44, 46)
|
||||||
_amount_minus.pressed.connect(_step_attachment_amount.bind(-1))
|
_amount_minus.pressed.connect(_step_attachment_amount.bind(-1))
|
||||||
page.add_child(_amount_minus)
|
page.add_child(_amount_minus)
|
||||||
_amount_plus = Button.new()
|
_amount_plus = Button.new()
|
||||||
_amount_plus.text = "+"
|
_amount_plus.text = "+"
|
||||||
_amount_plus.position = Vector2(920, 190)
|
_amount_plus.position = Vector2(734, 190)
|
||||||
_amount_plus.size = Vector2(48, 46)
|
_amount_plus.size = Vector2(48, 46)
|
||||||
_amount_plus.pressed.connect(_step_attachment_amount.bind(1))
|
_amount_plus.pressed.connect(_step_attachment_amount.bind(1))
|
||||||
page.add_child(_amount_plus)
|
page.add_child(_amount_plus)
|
||||||
_attachment_summary = RichTextLabel.new()
|
_attachment_summary = RichTextLabel.new()
|
||||||
_attachment_summary.position = Vector2(688, 246)
|
_attachment_summary.position = Vector2(ATTACHMENT_COLUMN_X, 246)
|
||||||
_attachment_summary.size = Vector2(280, 78)
|
_attachment_summary.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 78)
|
||||||
_attachment_summary.bbcode_enabled = true
|
_attachment_summary.bbcode_enabled = true
|
||||||
_attachment_summary.fit_content = true
|
_attachment_summary.fit_content = true
|
||||||
_attachment_summary.scroll_active = false
|
_attachment_summary.scroll_active = false
|
||||||
|
|
@ -591,14 +598,14 @@ func _build_compose() -> Control:
|
||||||
page.add_child(_attachment_summary)
|
page.add_child(_attachment_summary)
|
||||||
_compose_cancel = Button.new()
|
_compose_cancel = Button.new()
|
||||||
_compose_cancel.text = "cancel"
|
_compose_cancel.text = "cancel"
|
||||||
_compose_cancel.position = Vector2(688, 342)
|
_compose_cancel.position = Vector2(ATTACHMENT_COLUMN_X, 342)
|
||||||
_compose_cancel.size = Vector2(126, 48)
|
_compose_cancel.size = Vector2(122, 48)
|
||||||
_compose_cancel.pressed.connect(_show_inbox)
|
_compose_cancel.pressed.connect(_show_inbox)
|
||||||
page.add_child(_compose_cancel)
|
page.add_child(_compose_cancel)
|
||||||
_send_button = Button.new()
|
_send_button = Button.new()
|
||||||
_send_button.text = "send letter"
|
_send_button.text = "send letter"
|
||||||
_send_button.position = Vector2(826, 342)
|
_send_button.position = Vector2(646, 342)
|
||||||
_send_button.size = Vector2(142, 48)
|
_send_button.size = Vector2(136, 48)
|
||||||
_send_button.pressed.connect(_send)
|
_send_button.pressed.connect(_send)
|
||||||
page.add_child(_send_button)
|
page.add_child(_send_button)
|
||||||
_recipient.item_selected.connect(func(_i: int) -> void:
|
_recipient.item_selected.connect(func(_i: int) -> void:
|
||||||
|
|
@ -613,12 +620,12 @@ func _build_letter() -> Control:
|
||||||
var page := Control.new()
|
var page := Control.new()
|
||||||
_letter_text = Label.new()
|
_letter_text = Label.new()
|
||||||
_letter_text.position = Vector2(26, 20)
|
_letter_text.position = Vector2(26, 20)
|
||||||
_letter_text.size = Vector2(680, 350)
|
_letter_text.size = Vector2(500, 350)
|
||||||
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||||
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
|
||||||
page.add_child(_letter_text)
|
page.add_child(_letter_text)
|
||||||
_letter_gift = RichTextLabel.new()
|
_letter_gift = RichTextLabel.new()
|
||||||
_letter_gift.position = Vector2(730, 46)
|
_letter_gift.position = Vector2(552, 46)
|
||||||
_letter_gift.size = Vector2(230, 180)
|
_letter_gift.size = Vector2(230, 180)
|
||||||
_letter_gift.bbcode_enabled = true
|
_letter_gift.bbcode_enabled = true
|
||||||
_letter_gift.fit_content = true
|
_letter_gift.fit_content = true
|
||||||
|
|
@ -627,7 +634,7 @@ func _build_letter() -> Control:
|
||||||
page.add_child(_letter_gift)
|
page.add_child(_letter_gift)
|
||||||
_accept = Button.new()
|
_accept = Button.new()
|
||||||
_accept.text = "accept gift"
|
_accept.text = "accept gift"
|
||||||
_accept.position = Vector2(730, 250)
|
_accept.position = Vector2(552, 250)
|
||||||
_accept.size = Vector2(230, 48)
|
_accept.size = Vector2(230, 48)
|
||||||
_accept.pressed.connect(func() -> void:
|
_accept.pressed.connect(func() -> void:
|
||||||
_service.accept_gift(_current_mail_id)
|
_service.accept_gift(_current_mail_id)
|
||||||
|
|
@ -635,7 +642,7 @@ func _build_letter() -> Control:
|
||||||
page.add_child(_accept)
|
page.add_child(_accept)
|
||||||
_decline = Button.new()
|
_decline = Button.new()
|
||||||
_decline.text = "decline gift"
|
_decline.text = "decline gift"
|
||||||
_decline.position = Vector2(730, 308)
|
_decline.position = Vector2(552, 308)
|
||||||
_decline.size = Vector2(230, 48)
|
_decline.size = Vector2(230, 48)
|
||||||
_decline.pressed.connect(func() -> void:
|
_decline.pressed.connect(func() -> void:
|
||||||
_service.decline_gift(_current_mail_id)
|
_service.decline_gift(_current_mail_id)
|
||||||
|
|
@ -649,13 +656,13 @@ func _build_letter() -> Control:
|
||||||
page.add_child(_letter_close)
|
page.add_child(_letter_close)
|
||||||
_archive = Button.new()
|
_archive = Button.new()
|
||||||
_archive.text = "archive"
|
_archive.text = "archive"
|
||||||
_archive.position = Vector2(550, 370)
|
_archive.position = Vector2(466, 370)
|
||||||
_archive.size = Vector2(150, 48)
|
_archive.size = Vector2(150, 48)
|
||||||
_archive.pressed.connect(_archive_current)
|
_archive.pressed.connect(_archive_current)
|
||||||
page.add_child(_archive)
|
page.add_child(_archive)
|
||||||
_delete = Button.new()
|
_delete = Button.new()
|
||||||
_delete.text = "delete"
|
_delete.text = "delete"
|
||||||
_delete.position = Vector2(710, 370)
|
_delete.position = Vector2(626, 370)
|
||||||
_delete.size = Vector2(150, 48)
|
_delete.size = Vector2(150, 48)
|
||||||
_delete.pressed.connect(_delete_current)
|
_delete.pressed.connect(_delete_current)
|
||||||
page.add_child(_delete)
|
page.add_child(_delete)
|
||||||
|
|
@ -751,7 +758,7 @@ func _refresh_inbox() -> void:
|
||||||
empty.text = (
|
empty.text = (
|
||||||
"No archived letters." if _showing_archive else "No letters yet."
|
"No archived letters." if _showing_archive else "No letters yet."
|
||||||
)
|
)
|
||||||
empty.custom_minimum_size = Vector2(1008, 80)
|
empty.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 80)
|
||||||
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
_inbox_list.add_child(empty)
|
_inbox_list.add_child(empty)
|
||||||
call_deferred("_refresh_controller_navigation")
|
call_deferred("_refresh_controller_navigation")
|
||||||
|
|
@ -779,7 +786,7 @@ func _refresh_inbox() -> void:
|
||||||
first_line.left(72),
|
first_line.left(72),
|
||||||
" · gift enclosed" if gift else "",
|
" · gift enclosed" if gift else "",
|
||||||
]
|
]
|
||||||
button.custom_minimum_size = Vector2(1008, 54)
|
button.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 54)
|
||||||
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||||
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
|
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
|
||||||
UtilityPageStyle.apply_ocean_button(button)
|
UtilityPageStyle.apply_ocean_button(button)
|
||||||
|
|
@ -826,8 +833,14 @@ func _refresh_attachment_choices(_index: int) -> void:
|
||||||
_coin_available_heading.visible = currency_selected
|
_coin_available_heading.visible = currency_selected
|
||||||
_coin_available.visible = currency_selected
|
_coin_available.visible = currency_selected
|
||||||
_attachment_amount_currency_icon.visible = currency_selected
|
_attachment_amount_currency_icon.visible = currency_selected
|
||||||
_attachment_amount.position.x = 770.0 if currency_selected else 746.0
|
_attachment_amount.position.x = (
|
||||||
_attachment_amount.size.x = 140.0 if currency_selected else 164.0
|
AMOUNT_FIELD_X_WITH_CURRENCY
|
||||||
|
if currency_selected else AMOUNT_FIELD_X_PLAIN
|
||||||
|
)
|
||||||
|
_attachment_amount.size.x = (
|
||||||
|
AMOUNT_FIELD_WIDTH_WITH_CURRENCY
|
||||||
|
if currency_selected else AMOUNT_FIELD_WIDTH_PLAIN
|
||||||
|
)
|
||||||
match _attachment_kind.selected:
|
match _attachment_kind.selected:
|
||||||
0:
|
0:
|
||||||
_attachment_choice.add_item("No attachment")
|
_attachment_choice.add_item("No attachment")
|
||||||
|
|
|
||||||
1028
ui/player_menu.gd
1028
ui/player_menu.gd
File diff suppressed because it is too large
Load diff
|
|
@ -72,15 +72,16 @@ unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
z_index = 40
|
z_index = 40
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 150.0
|
offset_left = 513.0
|
||||||
offset_top = 136.0
|
offset_top = 136.0
|
||||||
offset_right = 664.0
|
offset_right = 767.0
|
||||||
offset_bottom = 174.0
|
offset_bottom = 174.0
|
||||||
mouse_filter = 1
|
mouse_filter = 1
|
||||||
theme_override_constants/separation = 6
|
theme_override_constants/separation = 6
|
||||||
|
|
||||||
[node name="CoolerSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
|
[node name="CoolerSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
custom_minimum_size = Vector2(124, 38)
|
custom_minimum_size = Vector2(124, 38)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
toggle_mode = true
|
toggle_mode = true
|
||||||
|
|
@ -94,12 +95,13 @@ custom_minimum_size = Vector2(124, 38)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
toggle_mode = true
|
toggle_mode = true
|
||||||
button_group = null
|
button_group = null
|
||||||
text = "Equipment"
|
text = "Inventory"
|
||||||
script = ExtResource("23_organizer_tab")
|
script = ExtResource("23_organizer_tab")
|
||||||
palette_index = 2
|
palette_index = 2
|
||||||
|
|
||||||
[node name="ItemsSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
|
[node name="ItemsSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
custom_minimum_size = Vector2(124, 38)
|
custom_minimum_size = Vector2(124, 38)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
toggle_mode = true
|
toggle_mode = true
|
||||||
|
|
@ -494,10 +496,10 @@ mouse_filter = 1
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
z_index = 50
|
z_index = 50
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 122.0
|
offset_left = 199.0
|
||||||
offset_top = 132.0
|
offset_top = 166.0
|
||||||
offset_right = 1018.0
|
offset_right = 1081.0
|
||||||
offset_bottom = 574.0
|
offset_bottom = 650.0
|
||||||
|
|
||||||
[node name="BagOuterMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall"]
|
[node name="BagOuterMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
@ -524,7 +526,7 @@ horizontal_scroll_mode = 0
|
||||||
|
|
||||||
[node name="BagHost" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall/BagOuterMargin/BagInnerLiner/BagInnerMargin/BagScroll"]
|
[node name="BagHost" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall/BagOuterMargin/BagInnerLiner/BagInnerMargin/BagScroll"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(788, 358)
|
custom_minimum_size = Vector2(810, 420)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
mouse_filter = 1
|
mouse_filter = 1
|
||||||
|
|
||||||
|
|
@ -553,14 +555,26 @@ text = "your bag is empty"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="BagModalBlocker" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 110
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 0
|
||||||
|
|
||||||
[node name="BagDetailConstellation" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
|
[node name="BagDetailConstellation" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 984.0
|
offset_left = 501.0
|
||||||
offset_top = 266.0
|
offset_top = 166.0
|
||||||
offset_right = 1256.0
|
offset_right = 779.0
|
||||||
offset_bottom = 570.0
|
offset_bottom = 650.0
|
||||||
mouse_filter = 2
|
mouse_filter = 2
|
||||||
|
|
||||||
[node name="BagDetailBubble" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation"]
|
[node name="BagDetailBubble" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation"]
|
||||||
|
|
@ -572,7 +586,7 @@ anchor_bottom = 1.0
|
||||||
grow_horizontal = 2
|
grow_horizontal = 2
|
||||||
grow_vertical = 2
|
grow_vertical = 2
|
||||||
script = ExtResource("22_inventory_notepad")
|
script = ExtResource("22_inventory_notepad")
|
||||||
title_text = "bag notes"
|
title_text = "inventory notes"
|
||||||
|
|
||||||
[node name="BagDetailLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble"]
|
[node name="BagDetailLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
@ -601,6 +615,25 @@ theme_override_font_sizes/font_size = 15
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
autowrap_mode = 2
|
autowrap_mode = 2
|
||||||
|
|
||||||
|
[node name="BagDetailActions" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 2
|
||||||
|
theme_override_constants/separation = 6
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="BagFavoriteButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout/BagDetailActions" instance=ExtResource("12_ink_action")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(104, 50)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "favorite"
|
||||||
|
allow_persistent_mark = true
|
||||||
|
|
||||||
|
[node name="BagSellButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout/BagDetailActions" instance=ExtResource("12_ink_action")]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
custom_minimum_size = Vector2(104, 50)
|
||||||
|
layout_mode = 2
|
||||||
|
text = "sell fish"
|
||||||
|
|
||||||
[node name="TackleBoxPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
[node name="TackleBoxPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
|
|
@ -613,10 +646,10 @@ mouse_filter = 1
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
z_index = 50
|
z_index = 50
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 54.0
|
offset_left = 199.0
|
||||||
offset_top = 166.0
|
offset_top = 166.0
|
||||||
offset_right = 936.0
|
offset_right = 1081.0
|
||||||
offset_bottom = 602.0
|
offset_bottom = 650.0
|
||||||
|
|
||||||
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel"]
|
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
|
|
@ -667,7 +700,7 @@ layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_constants/h_separation = 28
|
theme_override_constants/h_separation = 28
|
||||||
theme_override_constants/v_separation = 12
|
theme_override_constants/v_separation = 12
|
||||||
columns = 3
|
columns = 4
|
||||||
|
|
||||||
[node name="BaitEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody"]
|
[node name="BaitEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
@ -720,7 +753,7 @@ layout_mode = 2
|
||||||
size_flags_horizontal = 3
|
size_flags_horizontal = 3
|
||||||
theme_override_constants/h_separation = 28
|
theme_override_constants/h_separation = 28
|
||||||
theme_override_constants/v_separation = 12
|
theme_override_constants/v_separation = 12
|
||||||
columns = 3
|
columns = 4
|
||||||
|
|
||||||
[node name="LureEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody"]
|
[node name="LureEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
|
@ -735,13 +768,26 @@ text = "No lures collected."
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
||||||
|
[node name="TackleModalBlocker" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
z_index = 110
|
||||||
|
layout_mode = 1
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
mouse_filter = 0
|
||||||
|
|
||||||
[node name="TackleDetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
|
[node name="TackleDetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 952.0
|
offset_left = 501.0
|
||||||
offset_top = 166.0
|
offset_top = 166.0
|
||||||
offset_right = 1230.0
|
offset_right = 779.0
|
||||||
offset_bottom = 602.0
|
offset_bottom = 650.0
|
||||||
script = ExtResource("22_inventory_notepad")
|
script = ExtResource("22_inventory_notepad")
|
||||||
title_text = "tackle box notes"
|
title_text = "tackle box notes"
|
||||||
|
|
||||||
|
|
@ -861,9 +907,9 @@ custom_minimum_size = Vector2(140, 44)
|
||||||
[node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
[node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 330.0
|
offset_left = 388.0
|
||||||
offset_top = 44.0
|
offset_top = 44.0
|
||||||
offset_right = 1170.0
|
offset_right = 1228.0
|
||||||
offset_bottom = 144.0
|
offset_bottom = 144.0
|
||||||
mouse_filter = 1
|
mouse_filter = 1
|
||||||
script = ExtResource("5_cluster")
|
script = ExtResource("5_cluster")
|
||||||
|
|
|
||||||
317
ui/player_storage.gd
Normal file
317
ui/player_storage.gd
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
class_name PlayerStorage
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const INPUT_OWNER: StringName = &"player_storage"
|
||||||
|
|
||||||
|
signal menu_visibility_changed(is_open: bool)
|
||||||
|
|
||||||
|
var _player: Player
|
||||||
|
var _fishing_spot: FishingSpot
|
||||||
|
var _interaction: PlayerStorageInteraction
|
||||||
|
var _layout: PlayerInventoryLayout
|
||||||
|
var _inventory_grid: GeneralInventoryGrid
|
||||||
|
var _storage_grid: GeneralInventoryGrid
|
||||||
|
var _inventory_count: Label
|
||||||
|
var _storage_count: Label
|
||||||
|
var _feedback: Label
|
||||||
|
var _prior_movement_enabled: bool = true
|
||||||
|
var _prior_camera_enabled: bool = true
|
||||||
|
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
UtilityPageStyle.apply_page(self)
|
||||||
|
_build_ui()
|
||||||
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
player: Player,
|
||||||
|
fishing_spot: FishingSpot,
|
||||||
|
interaction: PlayerStorageInteraction,
|
||||||
|
layout: PlayerInventoryLayout,
|
||||||
|
bag: PlayerBag,
|
||||||
|
fish_inventory: FishInventory,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
item_catalog: ItemCatalog,
|
||||||
|
) -> void:
|
||||||
|
_player = player
|
||||||
|
_fishing_spot = fishing_spot
|
||||||
|
_interaction = interaction
|
||||||
|
_layout = layout
|
||||||
|
_inventory_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
fish_inventory,
|
||||||
|
hotbar,
|
||||||
|
item_catalog,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
|
)
|
||||||
|
_storage_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
fish_inventory,
|
||||||
|
hotbar,
|
||||||
|
item_catalog,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.STORAGE,
|
||||||
|
)
|
||||||
|
_inventory_grid.entry_selected.connect(
|
||||||
|
_on_entry_selected.bind(
|
||||||
|
PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_storage_grid.entry_selected.connect(
|
||||||
|
_on_entry_selected.bind(
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if _layout != null and not _layout.layout_changed.is_connected(_refresh):
|
||||||
|
_layout.layout_changed.connect(_refresh)
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func open_storage() -> bool:
|
||||||
|
if (
|
||||||
|
visible
|
||||||
|
or _player == null
|
||||||
|
or _interaction == null
|
||||||
|
or not _interaction.is_local_player_in_range()
|
||||||
|
or _fishing_spot == null
|
||||||
|
or not _fishing_spot.can_open_fishing_shop()
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_prior_movement_enabled = _player.is_movement_enabled()
|
||||||
|
_prior_camera_enabled = _player.is_camera_input_enabled()
|
||||||
|
_prior_mouse_mode = Input.mouse_mode
|
||||||
|
_player.set_movement_enabled(false)
|
||||||
|
_player.set_camera_input_enabled(false)
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||||
|
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||||
|
_feedback.text = "select an item to move it · drag to choose a slot"
|
||||||
|
_refresh()
|
||||||
|
show()
|
||||||
|
call_deferred("_focus_first_slot")
|
||||||
|
menu_visibility_changed.emit(true)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func close_storage(restore_controls: bool = true) -> void:
|
||||||
|
if not visible:
|
||||||
|
return
|
||||||
|
var viewport := get_viewport()
|
||||||
|
if viewport != null:
|
||||||
|
viewport.gui_release_focus()
|
||||||
|
hide()
|
||||||
|
if _fishing_spot != null:
|
||||||
|
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
|
||||||
|
if restore_controls and _player != null:
|
||||||
|
_player.set_movement_enabled(_prior_movement_enabled)
|
||||||
|
_player.set_camera_input_enabled(_prior_camera_enabled)
|
||||||
|
Input.mouse_mode = _prior_mouse_mode
|
||||||
|
menu_visibility_changed.emit(false)
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_range_exit() -> void:
|
||||||
|
close_storage()
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_water_recovery() -> void:
|
||||||
|
close_storage(false)
|
||||||
|
|
||||||
|
|
||||||
|
func close_for_session_end() -> void:
|
||||||
|
close_storage(false)
|
||||||
|
|
||||||
|
|
||||||
|
func consume_escape() -> bool:
|
||||||
|
if not visible:
|
||||||
|
return false
|
||||||
|
close_storage()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _unhandled_input(event: InputEvent) -> void:
|
||||||
|
if not visible or not event.is_action_pressed("ui_cancel"):
|
||||||
|
return
|
||||||
|
close_storage()
|
||||||
|
get_viewport().set_input_as_handled()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_entry_selected(
|
||||||
|
kind: int,
|
||||||
|
identity: StringName,
|
||||||
|
target_container: int,
|
||||||
|
) -> void:
|
||||||
|
if kind < 0 or identity.is_empty() or _layout == null:
|
||||||
|
return
|
||||||
|
if _layout.move_entry_to_first_free(kind, identity, target_container):
|
||||||
|
_feedback.text = (
|
||||||
|
"moved to storage"
|
||||||
|
if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
else "moved to inventory"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
_feedback.text = (
|
||||||
|
"storage is full"
|
||||||
|
if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE
|
||||||
|
else "inventory is full"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh() -> void:
|
||||||
|
if _layout == null:
|
||||||
|
return
|
||||||
|
_inventory_grid.refresh()
|
||||||
|
_storage_grid.refresh()
|
||||||
|
_inventory_count.text = "%d / %d" % [
|
||||||
|
_layout.get_inventory_count(),
|
||||||
|
_layout.get_inventory_capacity(),
|
||||||
|
]
|
||||||
|
_storage_count.text = "%d / %d" % [
|
||||||
|
_layout.get_storage_count(),
|
||||||
|
_layout.get_storage_capacity(),
|
||||||
|
]
|
||||||
|
call_deferred("_connect_grid_focus")
|
||||||
|
|
||||||
|
|
||||||
|
func _focus_first_slot() -> void:
|
||||||
|
var slot := _inventory_grid.get_first_occupied_slot()
|
||||||
|
if slot == null:
|
||||||
|
slot = _storage_grid.get_first_occupied_slot()
|
||||||
|
if slot != null:
|
||||||
|
slot.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func _connect_grid_focus() -> void:
|
||||||
|
var inventory_slots := _inventory_grid.get_slots()
|
||||||
|
var storage_slots := _storage_grid.get_slots()
|
||||||
|
if inventory_slots.is_empty() or storage_slots.is_empty():
|
||||||
|
return
|
||||||
|
var row_count := mini(
|
||||||
|
ceili(
|
||||||
|
float(inventory_slots.size())
|
||||||
|
/ float(PlayerInventoryLayout.INVENTORY_COLUMNS)
|
||||||
|
),
|
||||||
|
ceili(
|
||||||
|
float(storage_slots.size())
|
||||||
|
/ float(PlayerInventoryLayout.INVENTORY_COLUMNS)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
for row: int in row_count:
|
||||||
|
var inventory_index := mini(
|
||||||
|
row * PlayerInventoryLayout.INVENTORY_COLUMNS
|
||||||
|
+ PlayerInventoryLayout.INVENTORY_COLUMNS - 1,
|
||||||
|
inventory_slots.size() - 1,
|
||||||
|
)
|
||||||
|
var storage_index := mini(
|
||||||
|
row * PlayerInventoryLayout.INVENTORY_COLUMNS,
|
||||||
|
storage_slots.size() - 1,
|
||||||
|
)
|
||||||
|
inventory_slots[inventory_index].focus_neighbor_right = (
|
||||||
|
inventory_slots[inventory_index].get_path_to(storage_slots[storage_index])
|
||||||
|
)
|
||||||
|
storage_slots[storage_index].focus_neighbor_left = (
|
||||||
|
storage_slots[storage_index].get_path_to(inventory_slots[inventory_index])
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
z_index = 82
|
||||||
|
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
var blocker := ColorRect.new()
|
||||||
|
blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
blocker.color = Color(0.02, 0.08, 0.10, 0.55)
|
||||||
|
blocker.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
|
add_child(blocker)
|
||||||
|
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.set_anchors_preset(Control.PRESET_CENTER)
|
||||||
|
panel.position = Vector2(-580.0, -310.0)
|
||||||
|
panel.size = Vector2(1160.0, 620.0)
|
||||||
|
panel.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_DEEP, 22),
|
||||||
|
)
|
||||||
|
add_child(panel)
|
||||||
|
var outer := MarginContainer.new()
|
||||||
|
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
|
||||||
|
outer.add_theme_constant_override(side, 20)
|
||||||
|
panel.add_child(outer)
|
||||||
|
var layout := VBoxContainer.new()
|
||||||
|
layout.add_theme_constant_override("separation", 12)
|
||||||
|
outer.add_child(layout)
|
||||||
|
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
layout.add_child(header)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = "private storage"
|
||||||
|
title.add_theme_font_size_override("font_size", 32)
|
||||||
|
title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(title)
|
||||||
|
var close := Button.new()
|
||||||
|
close.text = "×"
|
||||||
|
close.tooltip_text = "Close storage"
|
||||||
|
close.custom_minimum_size = Vector2(48.0, 48.0)
|
||||||
|
close.flat = true
|
||||||
|
close.add_theme_font_size_override("font_size", 30)
|
||||||
|
close.pressed.connect(close_storage)
|
||||||
|
header.add_child(close)
|
||||||
|
|
||||||
|
var columns := HBoxContainer.new()
|
||||||
|
columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
columns.add_theme_constant_override("separation", 18)
|
||||||
|
layout.add_child(columns)
|
||||||
|
var inventory_column := _build_column(columns, "inventory")
|
||||||
|
_inventory_count = inventory_column["count"]
|
||||||
|
_inventory_grid = GeneralInventoryGrid.new()
|
||||||
|
(inventory_column["scroll"] as ScrollContainer).add_child(_inventory_grid)
|
||||||
|
var storage_column := _build_column(columns, "storage")
|
||||||
|
_storage_count = storage_column["count"]
|
||||||
|
_storage_grid = GeneralInventoryGrid.new()
|
||||||
|
(storage_column["scroll"] as ScrollContainer).add_child(_storage_grid)
|
||||||
|
|
||||||
|
_feedback = Label.new()
|
||||||
|
_feedback.text = "select an item to move it · drag to choose a slot"
|
||||||
|
_feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_feedback.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
layout.add_child(_feedback)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.custom_minimum_size = Vector2(550.0, 0.0)
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_MID, 16),
|
||||||
|
)
|
||||||
|
parent.add_child(panel)
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
|
||||||
|
margin.add_theme_constant_override(side, 12)
|
||||||
|
panel.add_child(margin)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 8)
|
||||||
|
margin.add_child(column)
|
||||||
|
var header := HBoxContainer.new()
|
||||||
|
column.add_child(header)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = title_text
|
||||||
|
title.add_theme_font_size_override("font_size", 22)
|
||||||
|
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
header.add_child(title)
|
||||||
|
var count := Label.new()
|
||||||
|
count.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||||
|
count.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
header.add_child(count)
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
column.add_child(scroll)
|
||||||
|
return {"count": count, "scroll": scroll}
|
||||||
1
ui/player_storage.gd.uid
Normal file
1
ui/player_storage.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d3g0sjl7sepnx
|
||||||
14
ui/player_storage.tscn
Normal file
14
ui/player_storage.tscn
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[gd_scene load_steps=2 format=3]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://ui/player_storage.gd" id="1_script"]
|
||||||
|
|
||||||
|
[node name="PlayerStorage" type="Control"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
visible = false
|
||||||
|
layout_mode = 3
|
||||||
|
anchors_preset = 15
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
grow_horizontal = 2
|
||||||
|
grow_vertical = 2
|
||||||
|
script = ExtResource("1_script")
|
||||||
|
|
@ -191,7 +191,10 @@ func _build() -> void:
|
||||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
root.add_child(scroll)
|
root.add_child(scroll)
|
||||||
_list = VBoxContainer.new()
|
_list = VBoxContainer.new()
|
||||||
_list.custom_minimum_size = Vector2(1032, 0)
|
_list.custom_minimum_size = Vector2(
|
||||||
|
UtilityPageStyle.LAPTOP_CONTENT_SIZE.x,
|
||||||
|
0.0,
|
||||||
|
)
|
||||||
_list.add_theme_constant_override("separation", 7)
|
_list.add_theme_constant_override("separation", 7)
|
||||||
scroll.add_child(_list)
|
scroll.add_child(_list)
|
||||||
_status = Label.new()
|
_status = Label.new()
|
||||||
|
|
@ -207,18 +210,22 @@ func _build_host_settings(root: VBoxContainer) -> void:
|
||||||
"panel", UtilityPageStyle.row_style(false)
|
"panel", UtilityPageStyle.row_style(false)
|
||||||
)
|
)
|
||||||
root.add_child(_host_settings_panel)
|
root.add_child(_host_settings_panel)
|
||||||
var row := HBoxContainer.new()
|
var settings := VBoxContainer.new()
|
||||||
row.custom_minimum_size.y = 58.0
|
settings.custom_minimum_size.y = 104.0
|
||||||
row.add_theme_constant_override("separation", 10)
|
settings.add_theme_constant_override("separation", 8)
|
||||||
_host_settings_panel.add_child(row)
|
_host_settings_panel.add_child(settings)
|
||||||
|
var identity_row := HBoxContainer.new()
|
||||||
|
identity_row.add_theme_constant_override("separation", 10)
|
||||||
|
settings.add_child(identity_row)
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.text = "room name"
|
label.text = "room name"
|
||||||
label.add_theme_color_override(
|
label.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
row.add_child(label)
|
identity_row.add_child(label)
|
||||||
_room_name_edit = LineEdit.new()
|
_room_name_edit = LineEdit.new()
|
||||||
_room_name_edit.custom_minimum_size = Vector2(300.0, 42.0)
|
_room_name_edit.custom_minimum_size = Vector2(0.0, 42.0)
|
||||||
|
_room_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH
|
_room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH
|
||||||
_room_name_edit.placeholder_text = "Player Name's Server"
|
_room_name_edit.placeholder_text = "Player Name's Server"
|
||||||
UtilityPageStyle.apply_ocean_line_edit(_room_name_edit)
|
UtilityPageStyle.apply_ocean_line_edit(_room_name_edit)
|
||||||
|
|
@ -226,11 +233,14 @@ func _build_host_settings(root: VBoxContainer) -> void:
|
||||||
func(_value: String) -> void: _commit_room_name()
|
func(_value: String) -> void: _commit_room_name()
|
||||||
)
|
)
|
||||||
_room_name_edit.focus_exited.connect(_commit_room_name)
|
_room_name_edit.focus_exited.connect(_commit_room_name)
|
||||||
row.add_child(_room_name_edit)
|
identity_row.add_child(_room_name_edit)
|
||||||
_online_toggle = _build_host_toggle("online", row)
|
var access_row := HBoxContainer.new()
|
||||||
|
access_row.add_theme_constant_override("separation", 10)
|
||||||
|
settings.add_child(access_row)
|
||||||
|
_online_toggle = _build_host_toggle("online", access_row)
|
||||||
_online_state_label = _online_toggle.get_node("StateBadge/State") as Label
|
_online_state_label = _online_toggle.get_node("StateBadge/State") as Label
|
||||||
_online_toggle.pressed.connect(_on_online_pressed)
|
_online_toggle.pressed.connect(_on_online_pressed)
|
||||||
_discoverable_toggle = _build_host_toggle("discovery", row)
|
_discoverable_toggle = _build_host_toggle("discovery", access_row)
|
||||||
_discoverable_state_label = (
|
_discoverable_state_label = (
|
||||||
_discoverable_toggle.get_node("StateBadge/State") as Label
|
_discoverable_toggle.get_node("StateBadge/State") as Label
|
||||||
)
|
)
|
||||||
|
|
@ -244,12 +254,12 @@ func _build_host_settings(root: VBoxContainer) -> void:
|
||||||
_host_discovery_status.add_theme_color_override(
|
_host_discovery_status.add_theme_color_override(
|
||||||
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
)
|
)
|
||||||
row.add_child(_host_discovery_status)
|
access_row.add_child(_host_discovery_status)
|
||||||
|
|
||||||
|
|
||||||
func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button:
|
func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button:
|
||||||
var button := Button.new()
|
var button := Button.new()
|
||||||
button.custom_minimum_size = Vector2(150.0, 46.0)
|
button.custom_minimum_size = Vector2(140.0, 46.0)
|
||||||
button.text = label_text
|
button.text = label_text
|
||||||
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
button.clip_contents = false
|
button.clip_contents = false
|
||||||
|
|
@ -448,7 +458,8 @@ func _build_active_rows() -> void:
|
||||||
for entry: PlayerListEntry in entries:
|
for entry: PlayerListEntry in entries:
|
||||||
var row := _make_row()
|
var row := _make_row()
|
||||||
var identity := Label.new()
|
var identity := Label.new()
|
||||||
identity.custom_minimum_size.x = 430
|
identity.custom_minimum_size.x = 250
|
||||||
|
identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
var markers: Array[String] = []
|
var markers: Array[String] = []
|
||||||
if entry.is_host:
|
if entry.is_host:
|
||||||
markers.append("host")
|
markers.append("host")
|
||||||
|
|
@ -470,7 +481,7 @@ func _build_active_rows() -> void:
|
||||||
)
|
)
|
||||||
row.add_child(identity)
|
row.add_child(identity)
|
||||||
var ping := Label.new()
|
var ping := Label.new()
|
||||||
ping.custom_minimum_size.x = 80
|
ping.custom_minimum_size.x = 60
|
||||||
ping.text = (
|
ping.text = (
|
||||||
"Local" if entry.is_host
|
"Local" if entry.is_host
|
||||||
else "%d ms" % entry.ping_to_host_ms
|
else "%d ms" % entry.ping_to_host_ms
|
||||||
|
|
@ -484,31 +495,31 @@ func _build_active_rows() -> void:
|
||||||
mute.text = "unmute" if entry.muted else "mute"
|
mute.text = "unmute" if entry.muted else "mute"
|
||||||
mute.disabled = entry.is_local_player
|
mute.disabled = entry.is_local_player
|
||||||
mute.pressed.connect(_toggle_mute.bind(entry))
|
mute.pressed.connect(_toggle_mute.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(mute)
|
UtilityPageStyle.apply_compact_ocean_button(mute)
|
||||||
row.add_child(mute)
|
row.add_child(mute)
|
||||||
var block := Button.new()
|
var block := Button.new()
|
||||||
block.text = "block"
|
block.text = "block"
|
||||||
block.disabled = entry.is_local_player
|
block.disabled = entry.is_local_player
|
||||||
block.pressed.connect(_confirm_block.bind(entry))
|
block.pressed.connect(_confirm_block.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(block)
|
UtilityPageStyle.apply_compact_ocean_button(block)
|
||||||
row.add_child(block)
|
row.add_child(block)
|
||||||
if entry.can_manage_operator:
|
if entry.can_manage_operator:
|
||||||
var operator := Button.new()
|
var operator := Button.new()
|
||||||
operator.text = "deop" if entry.is_operator else "op"
|
operator.text = "deop" if entry.is_operator else "op"
|
||||||
operator.pressed.connect(_confirm_operator.bind(entry))
|
operator.pressed.connect(_confirm_operator.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(operator)
|
UtilityPageStyle.apply_compact_ocean_button(operator)
|
||||||
row.add_child(operator)
|
row.add_child(operator)
|
||||||
var kick := Button.new()
|
var kick := Button.new()
|
||||||
kick.text = "kick"
|
kick.text = "kick"
|
||||||
kick.disabled = not entry.can_kick
|
kick.disabled = not entry.can_kick
|
||||||
kick.pressed.connect(_confirm_kick.bind(entry))
|
kick.pressed.connect(_confirm_kick.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(kick)
|
UtilityPageStyle.apply_compact_ocean_button(kick)
|
||||||
row.add_child(kick)
|
row.add_child(kick)
|
||||||
var ban := Button.new()
|
var ban := Button.new()
|
||||||
ban.text = "ban"
|
ban.text = "ban"
|
||||||
ban.disabled = not entry.can_ban
|
ban.disabled = not entry.can_ban
|
||||||
ban.pressed.connect(_confirm_ban.bind(entry))
|
ban.pressed.connect(_confirm_ban.bind(entry))
|
||||||
UtilityPageStyle.apply_ocean_button(ban)
|
UtilityPageStyle.apply_compact_ocean_button(ban)
|
||||||
row.add_child(ban)
|
row.add_child(ban)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -516,7 +527,8 @@ func _build_session_artwork_controls() -> void:
|
||||||
var counts: Vector2i = _service.get_session_artwork_counts()
|
var counts: Vector2i = _service.get_session_artwork_counts()
|
||||||
var row := _make_row()
|
var row := _make_row()
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.custom_minimum_size.x = 830
|
label.custom_minimum_size.x = 600
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
label.text = "session artwork · %d layers · %d painted pixels" % [
|
label.text = "session artwork · %d layers · %d painted pixels" % [
|
||||||
counts.x, counts.y,
|
counts.x, counts.y,
|
||||||
]
|
]
|
||||||
|
|
@ -534,7 +546,7 @@ func _build_session_artwork_controls() -> void:
|
||||||
_service.reset_session_artwork,
|
_service.reset_session_artwork,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(reset)
|
UtilityPageStyle.apply_compact_ocean_button(reset)
|
||||||
row.add_child(reset)
|
row.add_child(reset)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -546,7 +558,8 @@ func _build_relationship_rows() -> void:
|
||||||
for record: Dictionary in records:
|
for record: Dictionary in records:
|
||||||
var row := _make_row()
|
var row := _make_row()
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.custom_minimum_size.x = 690
|
label.custom_minimum_size.x = 560
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
var fingerprint := str(record["fingerprint"])
|
var fingerprint := str(record["fingerprint"])
|
||||||
label.text = "%s · %s %s" % [
|
label.text = "%s · %s %s" % [
|
||||||
str(record.get("last_known_display_name", "Player")),
|
str(record.get("last_known_display_name", "Player")),
|
||||||
|
|
@ -564,7 +577,7 @@ func _build_relationship_rows() -> void:
|
||||||
unblock.pressed.connect(func() -> void:
|
unblock.pressed.connect(func() -> void:
|
||||||
_service.set_blocked(fingerprint, str(record["last_known_display_name"]), false)
|
_service.set_blocked(fingerprint, str(record["last_known_display_name"]), false)
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(unblock)
|
UtilityPageStyle.apply_compact_ocean_button(unblock)
|
||||||
row.add_child(unblock)
|
row.add_child(unblock)
|
||||||
var unmute := Button.new()
|
var unmute := Button.new()
|
||||||
unmute.text = "unmute"
|
unmute.text = "unmute"
|
||||||
|
|
@ -572,7 +585,7 @@ func _build_relationship_rows() -> void:
|
||||||
unmute.pressed.connect(func() -> void:
|
unmute.pressed.connect(func() -> void:
|
||||||
_service.set_muted(fingerprint, str(record["last_known_display_name"]), false)
|
_service.set_muted(fingerprint, str(record["last_known_display_name"]), false)
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_ocean_button(unmute)
|
UtilityPageStyle.apply_compact_ocean_button(unmute)
|
||||||
row.add_child(unmute)
|
row.add_child(unmute)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -585,7 +598,8 @@ func _build_ban_rows() -> void:
|
||||||
var row := _make_row()
|
var row := _make_row()
|
||||||
var fingerprint := str(record["target_fingerprint"])
|
var fingerprint := str(record["target_fingerprint"])
|
||||||
var label := Label.new()
|
var label := Label.new()
|
||||||
label.custom_minimum_size.x = 830
|
label.custom_minimum_size.x = 600
|
||||||
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
label.text = "%s · %s banned %s" % [
|
label.text = "%s · %s banned %s" % [
|
||||||
str(record.get("last_known_display_name", "Player")),
|
str(record.get("last_known_display_name", "Player")),
|
||||||
NetworkIdentityCrypto.compact_suffix(fingerprint),
|
NetworkIdentityCrypto.compact_suffix(fingerprint),
|
||||||
|
|
@ -599,7 +613,7 @@ func _build_ban_rows() -> void:
|
||||||
var unban := Button.new()
|
var unban := Button.new()
|
||||||
unban.text = "unban"
|
unban.text = "unban"
|
||||||
unban.pressed.connect(_confirm_unban.bind(fingerprint))
|
unban.pressed.connect(_confirm_unban.bind(fingerprint))
|
||||||
UtilityPageStyle.apply_ocean_button(unban)
|
UtilityPageStyle.apply_compact_ocean_button(unban)
|
||||||
row.add_child(unban)
|
row.add_child(unban)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,16 @@ extends Control
|
||||||
|
|
||||||
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
const CHECK_DEBOUNCE_SECONDS: float = 0.4
|
||||||
const APPEARANCE_PREVIEW_INTERVAL_SECONDS: float = 0.08
|
const APPEARANCE_PREVIEW_INTERVAL_SECONDS: float = 0.08
|
||||||
const OPTION_GRID_COLUMNS: int = 6
|
const OPTION_GRID_COLUMNS: int = 3
|
||||||
const FUR_PALETTE_GRID_COLUMNS: int = 10
|
const FUR_PALETTE_GRID_COLUMNS: int = 6
|
||||||
const FUR_PATTERN_GRID_COLUMNS: int = 4
|
const FUR_PATTERN_GRID_COLUMNS: int = 2
|
||||||
|
const FUR_CHANNEL_GRID_COLUMNS: int = 2
|
||||||
const FUR_PALETTE_SWATCH_SIZE: float = 38.0
|
const FUR_PALETTE_SWATCH_SIZE: float = 38.0
|
||||||
const FUR_CHANNEL_SWATCH_SIZE: float = 22.0
|
const FUR_CHANNEL_SWATCH_SIZE: float = 22.0
|
||||||
|
const FUR_PART_TAB_WIDTH: float = 58.0
|
||||||
const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560)
|
const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560)
|
||||||
const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300)
|
const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300)
|
||||||
|
const VOICE_OPTION_BUTTON_SIZE: Vector2 = Vector2(68.0, 32.0)
|
||||||
const FUR_SECTION_PATTERNS: String = "patterns"
|
const FUR_SECTION_PATTERNS: String = "patterns"
|
||||||
const FUR_SECTION_COLORS: String = "colors"
|
const FUR_SECTION_COLORS: String = "colors"
|
||||||
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
|
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
|
||||||
|
|
@ -722,7 +725,7 @@ func _build_ui() -> void:
|
||||||
_category_list.add_theme_constant_override("separation", 5)
|
_category_list.add_theme_constant_override("separation", 5)
|
||||||
category_scroll.add_child(_category_list)
|
category_scroll.add_child(_category_list)
|
||||||
_option_list = VBoxContainer.new()
|
_option_list = VBoxContainer.new()
|
||||||
_option_list.custom_minimum_size = Vector2(360, 0)
|
_option_list.custom_minimum_size = Vector2(326, 0)
|
||||||
_option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
_option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_option_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_option_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
_option_list.add_theme_constant_override("separation", 5)
|
_option_list.add_theme_constant_override("separation", 5)
|
||||||
|
|
@ -732,13 +735,13 @@ func _build_ui() -> void:
|
||||||
body_spacer.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
body_spacer.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
|
||||||
body.add_child(body_spacer)
|
body.add_child(body_spacer)
|
||||||
var preview_stack := VBoxContainer.new()
|
var preview_stack := VBoxContainer.new()
|
||||||
preview_stack.custom_minimum_size.x = 260.0
|
preview_stack.custom_minimum_size.x = 240.0
|
||||||
preview_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
preview_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||||
preview_stack.add_theme_constant_override("separation", 7)
|
preview_stack.add_theme_constant_override("separation", 7)
|
||||||
body.add_child(preview_stack)
|
body.add_child(preview_stack)
|
||||||
var preview_frame := PanelContainer.new()
|
var preview_frame := PanelContainer.new()
|
||||||
preview_frame.custom_minimum_size = Vector2(260.0, 0.0)
|
preview_frame.custom_minimum_size = Vector2(240.0, 0.0)
|
||||||
preview_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
preview_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
preview_frame.add_theme_stylebox_override(
|
preview_frame.add_theme_stylebox_override(
|
||||||
"panel", UtilityPageStyle.rounded_style(
|
"panel", UtilityPageStyle.rounded_style(
|
||||||
|
|
@ -752,7 +755,7 @@ func _build_ui() -> void:
|
||||||
preview_layer.mouse_filter = Control.MOUSE_FILTER_PASS
|
preview_layer.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||||
preview_frame.add_child(preview_layer)
|
preview_frame.add_child(preview_layer)
|
||||||
_preview = preload("res://ui/profile_preview.tscn").instantiate()
|
_preview = preload("res://ui/profile_preview.tscn").instantiate()
|
||||||
_preview.custom_minimum_size = Vector2(260.0, 0.0)
|
_preview.custom_minimum_size = Vector2(240.0, 0.0)
|
||||||
_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
preview_layer.add_child(_preview)
|
preview_layer.add_child(_preview)
|
||||||
|
|
@ -911,7 +914,7 @@ func _build_voice_options() -> void:
|
||||||
var settings_grid := GridContainer.new()
|
var settings_grid := GridContainer.new()
|
||||||
settings_grid.name = "VoiceSettingsGrid"
|
settings_grid.name = "VoiceSettingsGrid"
|
||||||
settings_grid.columns = 2
|
settings_grid.columns = 2
|
||||||
settings_grid.add_theme_constant_override("h_separation", 10)
|
settings_grid.add_theme_constant_override("h_separation", 8)
|
||||||
settings_grid.add_theme_constant_override("v_separation", 8)
|
settings_grid.add_theme_constant_override("v_separation", 8)
|
||||||
settings_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
settings_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
_option_list.add_child(settings_grid)
|
_option_list.add_child(settings_grid)
|
||||||
|
|
@ -943,7 +946,7 @@ func _build_voice_options() -> void:
|
||||||
sample_set_button.text = str(option.get("label", sample_set_id))
|
sample_set_button.text = str(option.get("label", sample_set_id))
|
||||||
sample_set_button.toggle_mode = true
|
sample_set_button.toggle_mode = true
|
||||||
sample_set_button.button_group = sample_set_group
|
sample_set_button.button_group = sample_set_group
|
||||||
sample_set_button.custom_minimum_size = Vector2(108.0, 32.0)
|
sample_set_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
|
||||||
sample_set_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
sample_set_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
sample_set_button.button_pressed = (
|
sample_set_button.button_pressed = (
|
||||||
_draft_sample_set_id == sample_set_id
|
_draft_sample_set_id == sample_set_id
|
||||||
|
|
@ -951,7 +954,7 @@ func _build_voice_options() -> void:
|
||||||
sample_set_button.pressed.connect(
|
sample_set_button.pressed.connect(
|
||||||
_select_sample_set_option.bind(sample_set_id)
|
_select_sample_set_option.bind(sample_set_id)
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_compact_ocean_button(sample_set_button)
|
_apply_voice_option_button(sample_set_button)
|
||||||
sample_set_grid.add_child(sample_set_button)
|
sample_set_grid.add_child(sample_set_button)
|
||||||
var pitch_title := Label.new()
|
var pitch_title := Label.new()
|
||||||
pitch_title.text = "pitch"
|
pitch_title.text = "pitch"
|
||||||
|
|
@ -976,11 +979,11 @@ func _build_voice_options() -> void:
|
||||||
button.text = str(option.get("label", option_id))
|
button.text = str(option.get("label", option_id))
|
||||||
button.toggle_mode = true
|
button.toggle_mode = true
|
||||||
button.button_group = pitch_group
|
button.button_group = pitch_group
|
||||||
button.custom_minimum_size = Vector2(108.0, 32.0)
|
button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
|
||||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
button.button_pressed = _draft_voice_id == option_id
|
button.button_pressed = _draft_voice_id == option_id
|
||||||
button.pressed.connect(_select_voice_option.bind(option_id))
|
button.pressed.connect(_select_voice_option.bind(option_id))
|
||||||
UtilityPageStyle.apply_compact_ocean_button(button)
|
_apply_voice_option_button(button)
|
||||||
grid.add_child(button)
|
grid.add_child(button)
|
||||||
var speed_title := Label.new()
|
var speed_title := Label.new()
|
||||||
speed_title.text = "playback speed"
|
speed_title.text = "playback speed"
|
||||||
|
|
@ -1008,13 +1011,13 @@ func _build_voice_options() -> void:
|
||||||
)
|
)
|
||||||
speed_button.toggle_mode = true
|
speed_button.toggle_mode = true
|
||||||
speed_button.button_group = speed_group
|
speed_button.button_group = speed_group
|
||||||
speed_button.custom_minimum_size = Vector2(108.0, 32.0)
|
speed_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
|
||||||
speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
speed_button.button_pressed = _draft_speech_speed_id == speed_id
|
speed_button.button_pressed = _draft_speech_speed_id == speed_id
|
||||||
speed_button.pressed.connect(
|
speed_button.pressed.connect(
|
||||||
_select_speech_speed_option.bind(speed_id)
|
_select_speech_speed_option.bind(speed_id)
|
||||||
)
|
)
|
||||||
UtilityPageStyle.apply_compact_ocean_button(speed_button)
|
_apply_voice_option_button(speed_button)
|
||||||
speed_grid.add_child(speed_button)
|
speed_grid.add_child(speed_button)
|
||||||
var call_title := Label.new()
|
var call_title := Label.new()
|
||||||
call_title.text = "call (G)"
|
call_title.text = "call (G)"
|
||||||
|
|
@ -1039,14 +1042,29 @@ func _build_voice_options() -> void:
|
||||||
call_button.text = str(option.get("label", call_id))
|
call_button.text = str(option.get("label", call_id))
|
||||||
call_button.toggle_mode = true
|
call_button.toggle_mode = true
|
||||||
call_button.button_group = call_group
|
call_button.button_group = call_group
|
||||||
call_button.custom_minimum_size = Vector2(108.0, 32.0)
|
call_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
|
||||||
call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
call_button.button_pressed = _draft_call_id == call_id
|
call_button.button_pressed = _draft_call_id == call_id
|
||||||
call_button.pressed.connect(_select_call_option.bind(call_id))
|
call_button.pressed.connect(_select_call_option.bind(call_id))
|
||||||
UtilityPageStyle.apply_compact_ocean_button(call_button)
|
_apply_voice_option_button(call_button)
|
||||||
call_grid.add_child(call_button)
|
call_grid.add_child(call_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_voice_option_button(button: Button) -> void:
|
||||||
|
UtilityPageStyle.apply_compact_ocean_button(button)
|
||||||
|
button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
|
||||||
|
button.add_theme_font_size_override("font_size", 12)
|
||||||
|
for state: StringName in [
|
||||||
|
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
||||||
|
]:
|
||||||
|
var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
|
||||||
|
if style == null:
|
||||||
|
continue
|
||||||
|
style.content_margin_left = 6.0
|
||||||
|
style.content_margin_right = 6.0
|
||||||
|
button.add_theme_stylebox_override(state, style)
|
||||||
|
|
||||||
|
|
||||||
func _select_sample_set_option(sample_set_id: String) -> void:
|
func _select_sample_set_option(sample_set_id: String) -> void:
|
||||||
if not VoiceProfilesType.is_valid_sample_set(sample_set_id):
|
if not VoiceProfilesType.is_valid_sample_set(sample_set_id):
|
||||||
return
|
return
|
||||||
|
|
@ -1473,7 +1491,7 @@ func _build_fur_pattern_options() -> void:
|
||||||
var part_tabs := HBoxContainer.new()
|
var part_tabs := HBoxContainer.new()
|
||||||
part_tabs.name = "FurPatternPartTabs"
|
part_tabs.name = "FurPatternPartTabs"
|
||||||
part_tabs.custom_minimum_size.y = 38.0
|
part_tabs.custom_minimum_size.y = 38.0
|
||||||
part_tabs.add_theme_constant_override("separation", 4)
|
part_tabs.add_theme_constant_override("separation", 2)
|
||||||
part_tab_margin.add_child(part_tabs)
|
part_tab_margin.add_child(part_tabs)
|
||||||
for style_index: int in range(
|
for style_index: int in range(
|
||||||
CharacterCustomizationCatalog.FUR_STYLE_IDS.size()
|
CharacterCustomizationCatalog.FUR_STYLE_IDS.size()
|
||||||
|
|
@ -1486,7 +1504,7 @@ func _build_fur_pattern_options() -> void:
|
||||||
style_field
|
style_field
|
||||||
)
|
)
|
||||||
part_tab.palette_index = mini(style_index, 2)
|
part_tab.palette_index = mini(style_index, 2)
|
||||||
part_tab.custom_minimum_size = Vector2(90.0, 38.0)
|
part_tab.custom_minimum_size = Vector2(FUR_PART_TAB_WIDTH, 38.0)
|
||||||
part_tab.focus_mode = Control.FOCUS_ALL
|
part_tab.focus_mode = Control.FOCUS_ALL
|
||||||
part_tab.mouse_filter = Control.MOUSE_FILTER_STOP
|
part_tab.mouse_filter = Control.MOUSE_FILTER_STOP
|
||||||
part_tab.pressed.connect(_select_fur_pattern_part.bind(style_field))
|
part_tab.pressed.connect(_select_fur_pattern_part.bind(style_field))
|
||||||
|
|
@ -1586,7 +1604,7 @@ func _build_fur_color_channels(options: Array) -> void:
|
||||||
|
|
||||||
var channel_grid := GridContainer.new()
|
var channel_grid := GridContainer.new()
|
||||||
channel_grid.name = "FurColorChannelGrid"
|
channel_grid.name = "FurColorChannelGrid"
|
||||||
channel_grid.columns = CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
|
channel_grid.columns = FUR_CHANNEL_GRID_COLUMNS
|
||||||
channel_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
channel_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
channel_grid.add_theme_constant_override("h_separation", 10)
|
channel_grid.add_theme_constant_override("h_separation", 10)
|
||||||
channel_grid.add_theme_constant_override("v_separation", 8)
|
channel_grid.add_theme_constant_override("v_separation", 8)
|
||||||
|
|
|
||||||
366
ui/shop_sell_inventory.gd
Normal file
366
ui/shop_sell_inventory.gd
Normal file
|
|
@ -0,0 +1,366 @@
|
||||||
|
class_name ShopSellInventory
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
|
||||||
|
|
||||||
|
var _layout: PlayerInventoryLayout
|
||||||
|
var _bag: PlayerBag
|
||||||
|
var _fish_inventory: FishInventory
|
||||||
|
var _item_catalog: ItemCatalog
|
||||||
|
var _buyer: FishBuyerProfile
|
||||||
|
var _reservations: PlayerAssetReservationService
|
||||||
|
var _network_sale: NetworkSaleService
|
||||||
|
var _inventory_grid: GeneralInventoryGrid
|
||||||
|
var _tray_grid: GridContainer
|
||||||
|
var _total_label: Label
|
||||||
|
var _feedback: Label
|
||||||
|
var _sell_button: Button
|
||||||
|
var _staged: Dictionary[String, Dictionary] = {}
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
UtilityPageStyle.apply_page(self)
|
||||||
|
_build_ui()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(
|
||||||
|
layout: PlayerInventoryLayout,
|
||||||
|
bag: PlayerBag,
|
||||||
|
fish_inventory: FishInventory,
|
||||||
|
hotbar: PlayerHotbar,
|
||||||
|
item_catalog: ItemCatalog,
|
||||||
|
buyer: FishBuyerProfile,
|
||||||
|
reservations: PlayerAssetReservationService,
|
||||||
|
network_sale: NetworkSaleService,
|
||||||
|
) -> void:
|
||||||
|
_layout = layout
|
||||||
|
_bag = bag
|
||||||
|
_fish_inventory = fish_inventory
|
||||||
|
_item_catalog = item_catalog
|
||||||
|
_buyer = buyer
|
||||||
|
_reservations = reservations
|
||||||
|
_network_sale = network_sale
|
||||||
|
_inventory_grid.setup(
|
||||||
|
layout,
|
||||||
|
bag,
|
||||||
|
fish_inventory,
|
||||||
|
hotbar,
|
||||||
|
item_catalog,
|
||||||
|
PlayerInventoryLayout.InventoryContainer.INVENTORY,
|
||||||
|
)
|
||||||
|
_inventory_grid.entry_selected.connect(_on_inventory_entry_selected)
|
||||||
|
if _layout != null:
|
||||||
|
_layout.layout_changed.connect(_refresh)
|
||||||
|
if _network_sale != null:
|
||||||
|
_network_sale.local_sale_pending.connect(_on_sale_pending)
|
||||||
|
_network_sale.local_sale_finished.connect(_on_sale_finished)
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func activate() -> void:
|
||||||
|
_feedback.text = "select or drag items into the sell tray"
|
||||||
|
_refresh()
|
||||||
|
var slot := _inventory_grid.get_first_occupied_slot()
|
||||||
|
if slot != null:
|
||||||
|
slot.grab_focus()
|
||||||
|
|
||||||
|
|
||||||
|
func clear_staged() -> void:
|
||||||
|
_staged.clear()
|
||||||
|
_refresh_tray()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_inventory_entry_selected(kind: int, identity: StringName) -> void:
|
||||||
|
_stage(kind, identity)
|
||||||
|
|
||||||
|
|
||||||
|
func _stage(kind: int, identity: StringName) -> void:
|
||||||
|
if identity.is_empty():
|
||||||
|
return
|
||||||
|
var key := (
|
||||||
|
PlayerInventoryLayout.catch_key(identity)
|
||||||
|
if kind == PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
else PlayerInventoryLayout.item_key(identity)
|
||||||
|
)
|
||||||
|
if _staged.has(key):
|
||||||
|
_staged.erase(key)
|
||||||
|
_refresh_tray()
|
||||||
|
return
|
||||||
|
if kind == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
var fish_catch := _fish_inventory.get_catch_by_id(identity)
|
||||||
|
if fish_catch == null:
|
||||||
|
_feedback.text = "that catch is no longer available"
|
||||||
|
return
|
||||||
|
if fish_catch.is_favorited:
|
||||||
|
_feedback.text = "favorite catches cannot be sold"
|
||||||
|
return
|
||||||
|
if _reservations != null and _reservations.is_fish_reserved(identity):
|
||||||
|
_feedback.text = "reserved in a letter"
|
||||||
|
return
|
||||||
|
_staged[key] = {"kind": kind, "identity": identity, "quantity": 1}
|
||||||
|
else:
|
||||||
|
var item := _item_catalog.get_item_by_id(identity)
|
||||||
|
if not ItemResalePolicy.is_sellable(item):
|
||||||
|
_feedback.text = "equipment cannot be sold"
|
||||||
|
return
|
||||||
|
var available := _bag.get_quantity(identity)
|
||||||
|
if _reservations != null:
|
||||||
|
available = _reservations.get_available_item_quantity(identity)
|
||||||
|
if available < 1:
|
||||||
|
_feedback.text = "that item is reserved"
|
||||||
|
return
|
||||||
|
_staged[key] = {
|
||||||
|
"kind": kind,
|
||||||
|
"identity": identity,
|
||||||
|
"quantity": available,
|
||||||
|
}
|
||||||
|
_feedback.text = ""
|
||||||
|
_refresh_tray()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_drop_payload(payload: Dictionary) -> void:
|
||||||
|
var payload_kind := str(payload.get("kind", ""))
|
||||||
|
_stage(
|
||||||
|
PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
if payload_kind == "cooler_fish"
|
||||||
|
else PlayerInventoryLayout.EntryKind.ITEM,
|
||||||
|
StringName(str(payload.get(
|
||||||
|
"catch_id" if payload_kind == "cooler_fish" else "item_id", ""
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_remove_requested(key: String) -> void:
|
||||||
|
_staged.erase(key)
|
||||||
|
_refresh_tray()
|
||||||
|
|
||||||
|
|
||||||
|
func _submit_sale() -> void:
|
||||||
|
if _staged.is_empty() or _network_sale == null:
|
||||||
|
return
|
||||||
|
var catch_ids: Array[StringName] = []
|
||||||
|
var items: Dictionary[StringName, int] = {}
|
||||||
|
for record: Dictionary in _staged.values():
|
||||||
|
var identity := StringName(str(record.get("identity", "")))
|
||||||
|
if int(record.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
catch_ids.append(identity)
|
||||||
|
else:
|
||||||
|
items[identity] = int(record.get("quantity", 0))
|
||||||
|
if _network_sale.request_local_mixed_sale(
|
||||||
|
catch_ids, items, MAIN_SHOP_BUYER_ID
|
||||||
|
).is_empty():
|
||||||
|
_feedback.text = "sale could not be started"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_sale_pending(_request_id: String) -> void:
|
||||||
|
_sell_button.disabled = true
|
||||||
|
_feedback.text = "selling…"
|
||||||
|
|
||||||
|
|
||||||
|
func _on_sale_finished(
|
||||||
|
_request_id: String,
|
||||||
|
accepted: bool,
|
||||||
|
message: String,
|
||||||
|
_catch_ids: Array[StringName],
|
||||||
|
_payout: int,
|
||||||
|
) -> void:
|
||||||
|
if accepted:
|
||||||
|
_staged.clear()
|
||||||
|
_feedback.text = message.to_lower()
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh() -> void:
|
||||||
|
_validate_staged()
|
||||||
|
_inventory_grid.refresh()
|
||||||
|
_refresh_tray()
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_staged() -> void:
|
||||||
|
for key: String in _staged.keys():
|
||||||
|
var record: Dictionary = _staged[key]
|
||||||
|
var identity := StringName(str(record.get("identity", "")))
|
||||||
|
if int(record.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
if (
|
||||||
|
_fish_inventory.get_catch_by_id(identity) == null
|
||||||
|
or not _layout.is_catch_in_inventory(identity)
|
||||||
|
):
|
||||||
|
_staged.erase(key)
|
||||||
|
elif (
|
||||||
|
_bag.get_quantity(identity) < int(record.get("quantity", 0))
|
||||||
|
or not _layout.is_item_in_inventory(identity)
|
||||||
|
):
|
||||||
|
_staged.erase(key)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_tray() -> void:
|
||||||
|
_refresh_inventory_staged_highlights()
|
||||||
|
for child: Node in _tray_grid.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
var keys: Array[String] = []
|
||||||
|
keys.assign(_staged.keys())
|
||||||
|
keys.sort()
|
||||||
|
for key: String in keys:
|
||||||
|
var record: Dictionary = _staged[key]
|
||||||
|
var identity := StringName(str(record["identity"]))
|
||||||
|
var icon: Texture2D
|
||||||
|
var label: String
|
||||||
|
if int(record["kind"]) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
var fish_catch := _fish_inventory.get_catch_by_id(identity)
|
||||||
|
if fish_catch == null:
|
||||||
|
continue
|
||||||
|
icon = fish_catch.fish.display_texture
|
||||||
|
label = fish_catch.fish.display_name
|
||||||
|
else:
|
||||||
|
var item := _item_catalog.get_item_by_id(identity)
|
||||||
|
if item == null:
|
||||||
|
continue
|
||||||
|
icon = item.icon
|
||||||
|
label = item.display_name
|
||||||
|
var slot := ShopSaleTraySlot.new()
|
||||||
|
_tray_grid.add_child(slot)
|
||||||
|
slot.configure(key, icon, label, int(record.get("quantity", 1)))
|
||||||
|
slot.remove_requested.connect(_on_remove_requested)
|
||||||
|
slot.drop_requested.connect(_on_drop_payload)
|
||||||
|
var drop_slot := ShopSaleTraySlot.new()
|
||||||
|
_tray_grid.add_child(drop_slot)
|
||||||
|
drop_slot.configure("", null, "drop an item here")
|
||||||
|
drop_slot.disabled = false
|
||||||
|
drop_slot.drop_requested.connect(_on_drop_payload)
|
||||||
|
var total := _calculate_total()
|
||||||
|
_total_label.text = str(total)
|
||||||
|
_sell_button.disabled = (
|
||||||
|
_staged.is_empty()
|
||||||
|
or total < 0
|
||||||
|
or _network_sale == null
|
||||||
|
or _network_sale.is_local_sale_pending()
|
||||||
|
)
|
||||||
|
call_deferred("_configure_focus")
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_inventory_staged_highlights() -> void:
|
||||||
|
if _inventory_grid == null:
|
||||||
|
return
|
||||||
|
for slot: GeneralInventorySlot in _inventory_grid.get_slots():
|
||||||
|
var key: String = ""
|
||||||
|
if not slot.entry_identity.is_empty():
|
||||||
|
key = (
|
||||||
|
PlayerInventoryLayout.catch_key(slot.entry_identity)
|
||||||
|
if slot.entry_kind == PlayerInventoryLayout.EntryKind.CATCH
|
||||||
|
else PlayerInventoryLayout.item_key(slot.entry_identity)
|
||||||
|
)
|
||||||
|
slot.set_staged(not key.is_empty() and _staged.has(key))
|
||||||
|
|
||||||
|
|
||||||
|
func _configure_focus() -> void:
|
||||||
|
var controls: Array[Control] = []
|
||||||
|
controls.assign(_inventory_grid.get_slots())
|
||||||
|
for child: Node in _tray_grid.get_children():
|
||||||
|
var control := child as Control
|
||||||
|
if control != null and control.focus_mode != Control.FOCUS_NONE:
|
||||||
|
controls.append(control)
|
||||||
|
controls.append(_sell_button)
|
||||||
|
ControllerFocusNavigation.configure_spatial_neighbors(controls)
|
||||||
|
|
||||||
|
|
||||||
|
func _calculate_total() -> int:
|
||||||
|
var total: int = 0
|
||||||
|
for record: Dictionary in _staged.values():
|
||||||
|
var identity := StringName(str(record["identity"]))
|
||||||
|
if int(record["kind"]) == PlayerInventoryLayout.EntryKind.CATCH:
|
||||||
|
var fish_catch := _fish_inventory.get_catch_by_id(identity)
|
||||||
|
if fish_catch == null:
|
||||||
|
return -1
|
||||||
|
total += _buyer.get_quality_offer(
|
||||||
|
fish_catch.fish.get_sale_value_for_weight(fish_catch.weight_lb),
|
||||||
|
fish_catch.quality,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
var item := _item_catalog.get_item_by_id(identity)
|
||||||
|
var unit_value := ItemResalePolicy.get_unit_value(item)
|
||||||
|
if unit_value < 0:
|
||||||
|
return -1
|
||||||
|
total += unit_value * int(record.get("quantity", 0))
|
||||||
|
return total
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ui() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
var outer := MarginContainer.new()
|
||||||
|
outer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
outer.add_theme_constant_override("margin_left", 68)
|
||||||
|
outer.add_theme_constant_override("margin_top", 92)
|
||||||
|
outer.add_theme_constant_override("margin_right", 68)
|
||||||
|
outer.add_theme_constant_override("margin_bottom", 44)
|
||||||
|
add_child(outer)
|
||||||
|
var columns := HBoxContainer.new()
|
||||||
|
columns.add_theme_constant_override("separation", 20)
|
||||||
|
outer.add_child(columns)
|
||||||
|
|
||||||
|
var source := _build_panel(columns, "inventory")
|
||||||
|
_inventory_grid = GeneralInventoryGrid.new()
|
||||||
|
(source as VBoxContainer).add_child(_inventory_grid)
|
||||||
|
var tray := _build_panel(columns, "sell tray")
|
||||||
|
var tray_scroll := ScrollContainer.new()
|
||||||
|
tray_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||||
|
tray_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
(tray as VBoxContainer).add_child(tray_scroll)
|
||||||
|
_tray_grid = GridContainer.new()
|
||||||
|
_tray_grid.columns = PlayerInventoryLayout.INVENTORY_COLUMNS
|
||||||
|
_tray_grid.add_theme_constant_override(
|
||||||
|
"h_separation", GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
|
||||||
|
)
|
||||||
|
_tray_grid.add_theme_constant_override(
|
||||||
|
"v_separation", GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
|
||||||
|
)
|
||||||
|
tray_scroll.add_child(_tray_grid)
|
||||||
|
_feedback = Label.new()
|
||||||
|
_feedback.text = "select or drag items into the sell tray"
|
||||||
|
_feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||||
|
_feedback.add_theme_color_override(
|
||||||
|
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
|
||||||
|
)
|
||||||
|
(tray as VBoxContainer).add_child(_feedback)
|
||||||
|
var total_row := HBoxContainer.new()
|
||||||
|
(tray as VBoxContainer).add_child(total_row)
|
||||||
|
var total_title := Label.new()
|
||||||
|
total_title.text = "total"
|
||||||
|
total_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
total_row.add_child(total_title)
|
||||||
|
var coin := TextureRect.new()
|
||||||
|
coin.custom_minimum_size = Vector2(24.0, 24.0)
|
||||||
|
coin.texture = preload("res://items/icons/shop/32_currency.png")
|
||||||
|
coin.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
|
coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||||
|
coin.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
|
||||||
|
total_row.add_child(coin)
|
||||||
|
_total_label = Label.new()
|
||||||
|
_total_label.text = "0"
|
||||||
|
total_row.add_child(_total_label)
|
||||||
|
_sell_button = Button.new()
|
||||||
|
_sell_button.text = "sell"
|
||||||
|
UtilityPageStyle.apply_ocean_button(_sell_button)
|
||||||
|
_sell_button.pressed.connect(_submit_sale)
|
||||||
|
(tray as VBoxContainer).add_child(_sell_button)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_panel(parent: HBoxContainer, title_text: String) -> VBoxContainer:
|
||||||
|
var panel := PanelContainer.new()
|
||||||
|
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||||
|
panel.add_theme_stylebox_override(
|
||||||
|
"panel",
|
||||||
|
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_MID, 16),
|
||||||
|
)
|
||||||
|
parent.add_child(panel)
|
||||||
|
var margin := MarginContainer.new()
|
||||||
|
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
|
||||||
|
margin.add_theme_constant_override(side, 12)
|
||||||
|
panel.add_child(margin)
|
||||||
|
var column := VBoxContainer.new()
|
||||||
|
column.add_theme_constant_override("separation", 8)
|
||||||
|
margin.add_child(column)
|
||||||
|
var title := Label.new()
|
||||||
|
title.text = title_text
|
||||||
|
title.add_theme_font_size_override("font_size", 22)
|
||||||
|
column.add_child(title)
|
||||||
|
return column
|
||||||
1
ui/shop_sell_inventory.gd.uid
Normal file
1
ui/shop_sell_inventory.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://cct5tvsc5igda
|
||||||
|
|
@ -26,7 +26,8 @@ const GREEN: Color = Color("31594d")
|
||||||
const LIGHT_TEXT: Color = Color("f5eed9")
|
const LIGHT_TEXT: Color = Color("f5eed9")
|
||||||
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
|
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
|
||||||
const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween"
|
const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween"
|
||||||
const LAPTOP_RECT: Rect2 = Rect2(66.0, 132.0, 1148.0, 520.0)
|
const LAPTOP_RECT: Rect2 = Rect2(199.0, 132.0, 882.0, 520.0)
|
||||||
|
const LAPTOP_CONTENT_SIZE: Vector2 = Vector2(794.0, 442.0)
|
||||||
const SUPPLY_BADGE_MIN_WIDTH: float = 46.0
|
const SUPPLY_BADGE_MIN_WIDTH: float = 46.0
|
||||||
const SUPPLY_BADGE_HEIGHT: float = 24.0
|
const SUPPLY_BADGE_HEIGHT: float = 24.0
|
||||||
const SUPPLY_BADGE_EDGE_MARGIN: float = 3.0
|
const SUPPLY_BADGE_EDGE_MARGIN: float = 3.0
|
||||||
|
|
|
||||||
114
world/digging/diggable_area_3d.gd
Normal file
114
world/digging/diggable_area_3d.gd
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
@tool
|
||||||
|
class_name DiggableArea3D
|
||||||
|
extends Node3D
|
||||||
|
|
||||||
|
@export var area_id: StringName
|
||||||
|
@export_node_path("Node3D") var terrain_source: NodePath
|
||||||
|
@export var surface_materials: Array[StringName] = []
|
||||||
|
@export var generation_bounds := Rect2(-50.0, -50.0, 100.0, 100.0)
|
||||||
|
@export_range(-100.0, 100.0, 0.01) var minimum_global_y: float = -100.0
|
||||||
|
@export_range(-100.0, 100.0, 0.01) var maximum_global_y: float = 100.0
|
||||||
|
@export_range(0.0, 1.0, 0.01) var minimum_up_dot: float = 0.6
|
||||||
|
|
||||||
|
|
||||||
|
func get_surface_triangles() -> Array[PackedVector3Array]:
|
||||||
|
var triangles: Array[PackedVector3Array] = []
|
||||||
|
if area_id.is_empty() or surface_materials.is_empty():
|
||||||
|
return triangles
|
||||||
|
var terrain_root: Node = get_node_or_null(terrain_source)
|
||||||
|
if terrain_root == null:
|
||||||
|
return triangles
|
||||||
|
for mesh_instance: MeshInstance3D in _collect_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 surface_materials.has(StringName(material.resource_name))
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
_append_surface_triangles(
|
||||||
|
triangles,
|
||||||
|
mesh_instance,
|
||||||
|
mesh.surface_get_arrays(surface_index),
|
||||||
|
)
|
||||||
|
return triangles
|
||||||
|
|
||||||
|
|
||||||
|
func _append_surface_triangles(
|
||||||
|
result: Array[PackedVector3Array],
|
||||||
|
mesh_instance: MeshInstance3D,
|
||||||
|
arrays: Array,
|
||||||
|
) -> void:
|
||||||
|
if arrays.size() <= Mesh.ARRAY_INDEX:
|
||||||
|
return
|
||||||
|
var vertices := arrays[Mesh.ARRAY_VERTEX] as PackedVector3Array
|
||||||
|
var indices := arrays[Mesh.ARRAY_INDEX] as PackedInt32Array
|
||||||
|
if vertices.is_empty():
|
||||||
|
return
|
||||||
|
if indices.is_empty():
|
||||||
|
for vertex_index: int in range(0, vertices.size() - 2, 3):
|
||||||
|
_append_triangle(
|
||||||
|
result,
|
||||||
|
mesh_instance.to_global(vertices[vertex_index]),
|
||||||
|
mesh_instance.to_global(vertices[vertex_index + 1]),
|
||||||
|
mesh_instance.to_global(vertices[vertex_index + 2]),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
for index_offset: int in range(0, indices.size() - 2, 3):
|
||||||
|
_append_triangle(
|
||||||
|
result,
|
||||||
|
mesh_instance.to_global(vertices[indices[index_offset]]),
|
||||||
|
mesh_instance.to_global(vertices[indices[index_offset + 1]]),
|
||||||
|
mesh_instance.to_global(vertices[indices[index_offset + 2]]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _append_triangle(
|
||||||
|
result: Array[PackedVector3Array],
|
||||||
|
a: Vector3,
|
||||||
|
b: Vector3,
|
||||||
|
c: Vector3,
|
||||||
|
) -> void:
|
||||||
|
if (
|
||||||
|
a.y < minimum_global_y
|
||||||
|
or b.y < minimum_global_y
|
||||||
|
or c.y < minimum_global_y
|
||||||
|
or a.y > maximum_global_y
|
||||||
|
or b.y > maximum_global_y
|
||||||
|
or c.y > maximum_global_y
|
||||||
|
):
|
||||||
|
return
|
||||||
|
var center := (a + b + c) / 3.0
|
||||||
|
if not generation_bounds.has_point(Vector2(center.x, center.z)):
|
||||||
|
return
|
||||||
|
var cross := (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 _collect_mesh_instances(root: Node) -> Array[MeshInstance3D]:
|
||||||
|
var meshes: Array[MeshInstance3D] = []
|
||||||
|
if root is MeshInstance3D:
|
||||||
|
meshes.append(root as MeshInstance3D)
|
||||||
|
for child: Node in root.get_children():
|
||||||
|
meshes.append_array(_collect_mesh_instances(child))
|
||||||
|
return meshes
|
||||||
|
|
||||||
|
|
||||||
|
func _get_configuration_warnings() -> PackedStringArray:
|
||||||
|
var warnings := PackedStringArray()
|
||||||
|
if area_id.is_empty():
|
||||||
|
warnings.append("Diggable area ID is required.")
|
||||||
|
if get_node_or_null(terrain_source) == null:
|
||||||
|
warnings.append("Diggable area terrain source is unavailable.")
|
||||||
|
if surface_materials.is_empty():
|
||||||
|
warnings.append("At least one terrain material is required.")
|
||||||
|
if maximum_global_y < minimum_global_y:
|
||||||
|
warnings.append("Maximum height must not be below minimum height.")
|
||||||
|
return warnings
|
||||||
1
world/digging/diggable_area_3d.gd.uid
Normal file
1
world/digging/diggable_area_3d.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://d33f4164b78ww
|
||||||
|
|
@ -26,6 +26,65 @@ uniform float moon_edge_softness : hint_range(0.001, 0.03) = 0.003;
|
||||||
uniform float moon_halo_radius : hint_range(0.02, 0.35) = 0.11;
|
uniform float moon_halo_radius : hint_range(0.02, 0.35) = 0.11;
|
||||||
uniform float moon_halo_strength : hint_range(0.0, 1.0) = 0.12;
|
uniform float moon_halo_strength : hint_range(0.0, 1.0) = 0.12;
|
||||||
|
|
||||||
|
uniform vec4 star_color : source_color = vec4(0.72, 0.82, 0.96, 1.0);
|
||||||
|
uniform float star_visibility : hint_range(0.0, 1.0) = 0.0;
|
||||||
|
uniform float star_strength : hint_range(0.0, 1.0) = 0.72;
|
||||||
|
|
||||||
|
|
||||||
|
float star_hash(vec2 point) {
|
||||||
|
vec3 value = fract(vec3(point.xyx) * vec3(0.1031, 0.1030, 0.0973));
|
||||||
|
value += dot(value, value.yzx + 33.33);
|
||||||
|
return fract((value.x + value.y) * value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
vec2 star_hash2(vec2 point) {
|
||||||
|
return vec2(
|
||||||
|
star_hash(point),
|
||||||
|
star_hash(point + vec2(19.19, 7.73))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
float procedural_star_field(vec3 view_direction) {
|
||||||
|
// Map the upper hemisphere to one stable world-direction diamond. This
|
||||||
|
// avoids a longitude seam and keeps the stars fixed as the camera moves.
|
||||||
|
float direction_sum = max(
|
||||||
|
abs(view_direction.x)
|
||||||
|
+ abs(view_direction.y)
|
||||||
|
+ abs(view_direction.z),
|
||||||
|
0.0001
|
||||||
|
);
|
||||||
|
vec2 star_uv = view_direction.xz / direction_sum * 0.5 + 0.5;
|
||||||
|
vec2 star_point = star_uv * 190.0;
|
||||||
|
vec2 cell = floor(star_point);
|
||||||
|
vec2 local_point = fract(star_point);
|
||||||
|
float spawn_roll = star_hash(cell + vec2(3.17, 11.83));
|
||||||
|
vec2 point_center = mix(
|
||||||
|
vec2(0.18),
|
||||||
|
vec2(0.82),
|
||||||
|
star_hash2(cell + vec2(41.0, 73.0))
|
||||||
|
);
|
||||||
|
float point_radius = mix(
|
||||||
|
0.065,
|
||||||
|
0.13,
|
||||||
|
star_hash(cell + vec2(89.0, 17.0))
|
||||||
|
);
|
||||||
|
float point_shape = 1.0 - smoothstep(
|
||||||
|
point_radius,
|
||||||
|
point_radius + 0.045,
|
||||||
|
distance(local_point, point_center)
|
||||||
|
);
|
||||||
|
float point_exists = step(0.965, spawn_roll);
|
||||||
|
float point_brightness = mix(
|
||||||
|
0.42,
|
||||||
|
1.0,
|
||||||
|
smoothstep(0.965, 1.0, spawn_roll)
|
||||||
|
);
|
||||||
|
float upper_sky_fade = smoothstep(0.055, 0.22, view_direction.y);
|
||||||
|
return point_shape * point_exists * point_brightness * upper_sky_fade;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void sky() {
|
void sky() {
|
||||||
vec3 view_direction = normalize(EYEDIR);
|
vec3 view_direction = normalize(EYEDIR);
|
||||||
|
|
@ -50,6 +109,12 @@ void sky() {
|
||||||
view_direction.y
|
view_direction.y
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
float stars = procedural_star_field(view_direction);
|
||||||
|
base_color = mix(
|
||||||
|
base_color,
|
||||||
|
star_color.rgb,
|
||||||
|
stars * star_visibility * star_strength
|
||||||
|
);
|
||||||
|
|
||||||
float alignment = dot(view_direction, normalize(sun_direction));
|
float alignment = dot(view_direction, normalize(sun_direction));
|
||||||
float disc_outer = cos(sun_radius + sun_edge_softness);
|
float disc_outer = cos(sun_radius + sun_edge_softness);
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue