Add starter RV progression and fishing feedback
This commit is contained in:
parent
eed361681a
commit
d8bf59bca0
47 changed files with 2268 additions and 467 deletions
|
|
@ -684,6 +684,10 @@ func _is_reel_input_pressed() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func is_reel_input_active() -> bool:
|
||||
return state == FishingState.FIGHTING and _is_reel_input_pressed()
|
||||
|
||||
|
||||
func _begin_aiming(player: PlayerType) -> void:
|
||||
if state != FishingState.READY or _active_player != null:
|
||||
return
|
||||
|
|
|
|||
Binary file not shown.
BIN
homes/assets/starter_rv_interior.glb
Normal file
BIN
homes/assets/starter_rv_interior.glb
Normal file
Binary file not shown.
45
homes/assets/starter_rv_interior.glb.import
Normal file
45
homes/assets/starter_rv_interior.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://b815y7svs3ern"
|
||||
path="res://.godot/imported/starter_rv_interior.glb-9ad8a345e9e831e82874b4b276341224.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://homes/assets/starter_rv_interior.glb"
|
||||
dest_files=["res://.godot/imported/starter_rv_interior.glb-9ad8a345e9e831e82874b4b276341224.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
BIN
homes/assets/tent_interior.glb
Normal file
BIN
homes/assets/tent_interior.glb
Normal file
Binary file not shown.
45
homes/assets/tent_interior.glb.import
Normal file
45
homes/assets/tent_interior.glb.import
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
[remap]
|
||||
|
||||
importer="scene"
|
||||
importer_version=1
|
||||
type="PackedScene"
|
||||
uid="uid://dkfqs051shmxe"
|
||||
path="res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.scn"
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://homes/assets/tent_interior.glb"
|
||||
dest_files=["res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.scn"]
|
||||
|
||||
[params]
|
||||
|
||||
nodes/root_type=""
|
||||
nodes/root_name=""
|
||||
nodes/root_script=null
|
||||
mesh_library/use_node_names_as_mesh_names=false
|
||||
array_mesh/deduplicate_surfaces=true
|
||||
nodes/apply_root_scale=true
|
||||
nodes/root_scale=1.0
|
||||
nodes/import_as_skeleton_bones=false
|
||||
nodes/use_name_suffixes=true
|
||||
nodes/use_node_type_suffixes=true
|
||||
meshes/ensure_tangents=true
|
||||
meshes/generate_lods=true
|
||||
meshes/create_shadow_meshes=true
|
||||
meshes/light_baking=1
|
||||
meshes/lightmap_texel_size=0.2
|
||||
meshes/force_disable_compression=false
|
||||
skins/use_named_skins=true
|
||||
animation/import=true
|
||||
animation/fps=30
|
||||
animation/trimming=false
|
||||
animation/remove_immutable_tracks=true
|
||||
animation/import_rest_as_RESET=false
|
||||
import_script/path=""
|
||||
materials/extract=0
|
||||
materials/extract_format=0
|
||||
materials/extract_path=""
|
||||
_subresources={}
|
||||
gltf/naming_version=2
|
||||
gltf/embedded_image_handling=1
|
||||
gltf/texture_map_mode=1
|
||||
|
|
@ -130,6 +130,8 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _toolbar != null and _toolbar.visible:
|
||||
_toolbar.set_interactive(_can_decorate())
|
||||
if _drag_instance_id.is_empty():
|
||||
return
|
||||
# Menus and other gameplay input handoffs must cancel an active drag before
|
||||
|
|
@ -472,9 +474,20 @@ func _on_home_changed() -> void:
|
|||
func _refresh_toolbar() -> void:
|
||||
if _toolbar == null:
|
||||
return
|
||||
_toolbar.set_active(_can_decorate())
|
||||
var inside_home: bool = _is_inside_home_space()
|
||||
_toolbar.set_active(inside_home)
|
||||
_toolbar.set_interactive(_can_decorate())
|
||||
if not _toolbar.visible:
|
||||
_clear_selection()
|
||||
elif not _can_decorate():
|
||||
_toolbar.report_status("decor tools pause while another menu is open")
|
||||
|
||||
|
||||
func _is_inside_home_space() -> bool:
|
||||
return (
|
||||
_network_home != null
|
||||
and String(_network_home.get_local_space()).begins_with("rv:")
|
||||
)
|
||||
|
||||
|
||||
func _can_decorate() -> bool:
|
||||
|
|
|
|||
|
|
@ -9,23 +9,42 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|||
const STARTER_MOTORCOACH_SCENE: PackedScene = preload(
|
||||
"res://homes/assets/starter_motorcoach.glb"
|
||||
)
|
||||
const TENT_INTERIOR_SCENE: PackedScene = preload(
|
||||
"res://homes/assets/tent_interior.glb"
|
||||
)
|
||||
const STARTER_RV_INTERIOR_SCENE: PackedScene = preload(
|
||||
"res://homes/assets/starter_rv_interior.glb"
|
||||
)
|
||||
const HOME_PROMPT_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf")
|
||||
|
||||
const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35)
|
||||
const EXTERIOR_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48)
|
||||
const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 0.25, 2.45)
|
||||
const RV_EXTERIOR_BODY_SIZE := Vector3(11.2, 5.0, 4.7)
|
||||
const RV_EXTERIOR_BODY_CENTER_Y: float = 2.7
|
||||
const RV_EXTERIOR_DOOR_LOCAL := Vector3(-0.4, 1.7, 2.96)
|
||||
const RV_EXTERIOR_SPAWN_LOCAL := Vector3(-0.4, 0.25, 3.95)
|
||||
const TENT_EXTERIOR_BODY_SIZE := Vector3(4.6, 3.0, 4.6)
|
||||
const TENT_EXTERIOR_BODY_CENTER_Y: float = 1.5
|
||||
const TENT_EXTERIOR_DOOR_LOCAL := Vector3(0.0, 1.0, 2.3)
|
||||
const TENT_EXTERIOR_SPAWN_LOCAL := Vector3(0.0, 0.25, 3.3)
|
||||
const RV_EXTERIOR_PROMPT_LOCAL := Vector3(-0.4, 3.72, 3.02)
|
||||
const TENT_EXTERIOR_PROMPT_LOCAL := Vector3(0.0, 2.48, 2.36)
|
||||
const HOME_PROMPT_SIZE := Vector2(0.95, 0.28)
|
||||
const HOME_PROMPT_CORNER_RADIUS: float = 0.08
|
||||
const HOME_PROMPT_TEXT_PIXEL_SIZE: float = 0.0045
|
||||
const HOME_PROMPT_TEXT_COLOR := Color("c3dfe6")
|
||||
const HOME_PROMPT_BACKGROUND_COLOR := Color("0a2d3a")
|
||||
const EXTERIOR_INTERACTION_DISTANCE: float = 2.7
|
||||
const INTERIOR_EXIT_DISTANCE: float = 2.1
|
||||
const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0)
|
||||
const INTERIOR_SPACING: float = 14.0
|
||||
const INTERIOR_MAX_SPAWN_SLOTS: int = 64
|
||||
const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5)
|
||||
const INTERIOR_WALL_HEIGHT: float = 2.7
|
||||
const TENT_INTERIOR_SIZE := Vector2(4.0, 4.0)
|
||||
const STARTER_RV_INTERIOR_SIZE := Vector2(4.0, 6.0)
|
||||
const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0
|
||||
const MIN_EXTERIOR_SEPARATION: float = 9.0
|
||||
const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(3.85, 2.55)
|
||||
const MIN_EXTERIOR_SEPARATION: float = 14.6
|
||||
const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(6.65, 3.73)
|
||||
const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0
|
||||
const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24
|
||||
const EXTERIOR_CLEARANCE_HEIGHT: float = 2.7
|
||||
const EXTERIOR_CLEARANCE_HEIGHT: float = 5.5
|
||||
const WORLD_GEOMETRY_COLLISION_LAYER: int = 1
|
||||
const DECOR_SELECTION_COLLISION_LAYER: int = 1 << 8
|
||||
# Includes the player's 0.45 m capsule radius plus enough movement margin for
|
||||
|
|
@ -113,7 +132,9 @@ func apply_manifest(data: Dictionary) -> bool:
|
|||
var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
||||
data["home"]
|
||||
)
|
||||
var exterior := _build_exterior(fingerprint, exterior_transform)
|
||||
var exterior := _build_exterior(
|
||||
fingerprint, exterior_transform, home_data
|
||||
)
|
||||
var interior := _build_interior(fingerprint, interior_index, home_data)
|
||||
var presentation: Dictionary = {
|
||||
"owner_peer_id": int(data["owner_peer_id"]),
|
||||
|
|
@ -191,19 +212,27 @@ func choose_exterior_transform(
|
|||
)
|
||||
if dry_result != Transform3D.IDENTITY:
|
||||
push_warning(
|
||||
"RV exterior used a dry fallback with obstructed clearance."
|
||||
"Home exterior used a dry fallback with obstructed clearance."
|
||||
)
|
||||
return dry_result
|
||||
push_error("No dry, level RV exterior footprint was available.")
|
||||
var fallback: Transform3D = spawn_transform
|
||||
var column: int = posmod(owner_peer_id - 1, 4)
|
||||
var row: int = maxi(floori(float(owner_peer_id - 1) / 4.0), 0)
|
||||
fallback.origin += Vector3(
|
||||
-12.0 + float(column) * 8.0,
|
||||
0.0,
|
||||
8.0 + float(row) * 7.0,
|
||||
)
|
||||
return fallback
|
||||
var emergency_result: Transform3D = _find_exterior_transform(
|
||||
owner_peer_id,
|
||||
triangles,
|
||||
surface_index,
|
||||
spawn_transform,
|
||||
[],
|
||||
false,
|
||||
false,
|
||||
)
|
||||
if emergency_result != Transform3D.IDENTITY:
|
||||
push_warning(
|
||||
"Home exterior used an accessible emergency placement."
|
||||
)
|
||||
return emergency_result
|
||||
push_error("No dry, level home exterior footprint was available.")
|
||||
# The authored spawn is the only universally reachable point. Never strand
|
||||
# a home on an unchecked elevation when malformed terrain offers no site.
|
||||
return spawn_transform
|
||||
|
||||
|
||||
func _find_exterior_transform(
|
||||
|
|
@ -229,7 +258,7 @@ func _find_exterior_transform(
|
|||
) / 3.0
|
||||
if (
|
||||
require_spawn_clearance
|
||||
and position.distance_to(spawn_transform.origin) < 6.0
|
||||
and position.distance_to(spawn_transform.origin) < 9.0
|
||||
):
|
||||
continue
|
||||
if _too_close_to_reserved(position, reserved_transforms):
|
||||
|
|
@ -240,6 +269,13 @@ func _find_exterior_transform(
|
|||
)
|
||||
if not grounded_position.is_finite():
|
||||
continue
|
||||
if (
|
||||
_world != null
|
||||
and not _world.is_home_exterior_position_accessible(
|
||||
grounded_position
|
||||
)
|
||||
):
|
||||
continue
|
||||
if (
|
||||
require_obstacle_clearance
|
||||
and not _exterior_volume_is_clear(grounded_position, yaw)
|
||||
|
|
@ -444,16 +480,48 @@ func get_exterior_transform(owner_fingerprint: String) -> Transform3D:
|
|||
|
||||
|
||||
func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3:
|
||||
return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL
|
||||
return get_exterior_transform(owner_fingerprint) * (
|
||||
_exterior_prompt_local(owner_fingerprint)
|
||||
)
|
||||
|
||||
|
||||
func set_exterior_prompt_visible(
|
||||
owner_fingerprint: String,
|
||||
requested_visible: bool,
|
||||
action_text: String = "enter home",
|
||||
) -> void:
|
||||
for fingerprint: String in _presentations_by_fingerprint:
|
||||
var presentation: Dictionary = _presentations_by_fingerprint[fingerprint]
|
||||
var exterior: Node3D = presentation.get("exterior")
|
||||
if not is_instance_valid(exterior):
|
||||
continue
|
||||
var prompt := exterior.get_node_or_null("DoorPrompt") as Node3D
|
||||
if prompt == null:
|
||||
continue
|
||||
prompt.visible = (
|
||||
requested_visible
|
||||
and fingerprint == owner_fingerprint
|
||||
and not _dedicated_runtime
|
||||
)
|
||||
if prompt.visible:
|
||||
var label := prompt.get_node_or_null("Message") as Label3D
|
||||
if label != null:
|
||||
label.text = action_text
|
||||
|
||||
|
||||
func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D:
|
||||
var exterior_transform: Transform3D = get_exterior_transform(
|
||||
owner_fingerprint
|
||||
)
|
||||
# Exterior local +Z leads away from the doorway. Player visuals face local
|
||||
# -Z, so turn the authoritative spawn basis around to start the day facing
|
||||
# outward instead of looking back into the home.
|
||||
var outward_spawn_basis: Basis = (
|
||||
exterior_transform.basis * Basis(Vector3.UP, PI)
|
||||
)
|
||||
return Transform3D(
|
||||
exterior_transform.basis,
|
||||
exterior_transform * EXTERIOR_SPAWN_LOCAL,
|
||||
outward_spawn_basis,
|
||||
exterior_transform * _exterior_spawn_local(owner_fingerprint),
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -461,17 +529,26 @@ func get_interior_spawn_transform(
|
|||
owner_fingerprint: String,
|
||||
visitor_offset: int = 0,
|
||||
) -> Transform3D:
|
||||
var anchor: Vector3 = get_interior_anchor(owner_fingerprint)
|
||||
# `visitor_offset` is allocated by the host as a compact room-local slot.
|
||||
# Keep it bounded as a last line of defense if a future caller accidentally
|
||||
# supplies an engine peer ID instead of a slot.
|
||||
var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS)
|
||||
# Build the origin as a value rather than mutating Transform3D.origin.
|
||||
var spawn_origin: Vector3 = anchor + Vector3(
|
||||
-1.0 + float(posmod(visitor_slot, 4)) * 0.65,
|
||||
0.25,
|
||||
0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55,
|
||||
var exit_direction: Vector3 = get_interior_exit_direction(owner_fingerprint)
|
||||
var lane_offset: float = (
|
||||
float(posmod(visitor_slot, 4)) - 1.5
|
||||
) * 0.28
|
||||
var row_offset: float = (
|
||||
float(floori(float(visitor_slot) / 4.0)) * 0.28
|
||||
)
|
||||
# Spawn just through the authored doorway. Local presentation then walks the
|
||||
# avatar farther into the room before returning movement control.
|
||||
var spawn_origin: Vector3 = (
|
||||
get_interior_exit_position(owner_fingerprint)
|
||||
+ exit_direction * 0.42
|
||||
- exit_direction * row_offset
|
||||
+ Vector3(0.0, 0.0, lane_offset)
|
||||
)
|
||||
spawn_origin.y = get_interior_anchor(owner_fingerprint).y
|
||||
return Transform3D(Basis.IDENTITY, spawn_origin)
|
||||
|
||||
|
||||
|
|
@ -487,7 +564,9 @@ func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool:
|
|||
if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint):
|
||||
return false
|
||||
var transform: Transform3D = get_exterior_transform(owner_fingerprint)
|
||||
var door_position: Vector3 = transform * EXTERIOR_DOOR_LOCAL
|
||||
var door_position: Vector3 = (
|
||||
transform * _exterior_door_local(owner_fingerprint)
|
||||
)
|
||||
return avatar.global_position.distance_to(door_position) <= (
|
||||
EXTERIOR_INTERACTION_DISTANCE
|
||||
)
|
||||
|
|
@ -504,11 +583,46 @@ func is_avatar_near_interior_exit(
|
|||
) <= INTERIOR_EXIT_DISTANCE
|
||||
|
||||
|
||||
func has_avatar_crossed_interior_exit_threshold(
|
||||
avatar: Player,
|
||||
owner_fingerprint: String,
|
||||
) -> bool:
|
||||
if avatar == null:
|
||||
return false
|
||||
var local_position: Vector3 = interior_global_to_local(
|
||||
owner_fingerprint,
|
||||
avatar.global_position,
|
||||
)
|
||||
var threshold_position: Vector3 = interior_global_to_local(
|
||||
owner_fingerprint,
|
||||
get_interior_exit_position(owner_fingerprint),
|
||||
)
|
||||
var exit_direction: Vector3 = get_interior_exit_direction(
|
||||
owner_fingerprint
|
||||
)
|
||||
return (
|
||||
(local_position - threshold_position).dot(exit_direction) >= 0.0
|
||||
and absf(local_position.z - threshold_position.z) <= 0.85
|
||||
and local_position.y >= -0.25
|
||||
and local_position.y <= 2.25
|
||||
)
|
||||
|
||||
|
||||
func get_interior_exit_direction(owner_fingerprint: String) -> Vector3:
|
||||
return (
|
||||
Vector3.LEFT
|
||||
if _home_tier(owner_fingerprint)
|
||||
>= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else Vector3.RIGHT
|
||||
)
|
||||
|
||||
|
||||
func get_interior_exit_position(owner_fingerprint: String) -> Vector3:
|
||||
return get_interior_anchor(owner_fingerprint) + Vector3(
|
||||
-INTERIOR_FLOOR_SIZE.x * 0.5 + 0.6,
|
||||
0.35,
|
||||
0.0,
|
||||
var tier: int = _home_tier(owner_fingerprint)
|
||||
return get_interior_anchor(owner_fingerprint) + (
|
||||
Vector3(-STARTER_RV_INTERIOR_SIZE.x * 0.5 + 0.6, 0.35, 0.0)
|
||||
if tier >= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else Vector3(TENT_INTERIOR_SIZE.x * 0.5 - 0.55, 0.35, 0.0)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -705,7 +819,7 @@ func find_nearby_exterior_owner(avatar: Player) -> String:
|
|||
for fingerprint: String in _presentations_by_fingerprint:
|
||||
var transform: Transform3D = get_exterior_transform(fingerprint)
|
||||
var distance: float = avatar.global_position.distance_to(
|
||||
transform * EXTERIOR_DOOR_LOCAL
|
||||
transform * _exterior_door_local(fingerprint)
|
||||
)
|
||||
if distance <= closest_distance:
|
||||
closest = fingerprint
|
||||
|
|
@ -713,6 +827,41 @@ func find_nearby_exterior_owner(avatar: Player) -> String:
|
|||
return closest
|
||||
|
||||
|
||||
func _home_tier(owner_fingerprint: String) -> int:
|
||||
var presentation: Dictionary = _presentations_by_fingerprint.get(
|
||||
owner_fingerprint, {}
|
||||
)
|
||||
var home_data: Dictionary = presentation.get("home", {})
|
||||
return int(home_data.get("tier", PlayerHomeStateType.STARTER_TIER))
|
||||
|
||||
|
||||
func _exterior_door_local(owner_fingerprint: String) -> Vector3:
|
||||
return (
|
||||
RV_EXTERIOR_DOOR_LOCAL
|
||||
if _home_tier(owner_fingerprint)
|
||||
>= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_EXTERIOR_DOOR_LOCAL
|
||||
)
|
||||
|
||||
|
||||
func _exterior_spawn_local(owner_fingerprint: String) -> Vector3:
|
||||
return (
|
||||
RV_EXTERIOR_SPAWN_LOCAL
|
||||
if _home_tier(owner_fingerprint)
|
||||
>= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_EXTERIOR_SPAWN_LOCAL
|
||||
)
|
||||
|
||||
|
||||
func _exterior_prompt_local(owner_fingerprint: String) -> Vector3:
|
||||
return (
|
||||
RV_EXTERIOR_PROMPT_LOCAL
|
||||
if _home_tier(owner_fingerprint)
|
||||
>= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_EXTERIOR_PROMPT_LOCAL
|
||||
)
|
||||
|
||||
|
||||
func set_local_space(space_id: StringName) -> void:
|
||||
_local_space = space_id
|
||||
_refresh_local_visibility()
|
||||
|
|
@ -822,24 +971,44 @@ func _ensure_roots() -> void:
|
|||
func _build_exterior(
|
||||
fingerprint: String,
|
||||
world_transform: Transform3D,
|
||||
home_data: Dictionary,
|
||||
) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "RV_%s" % fingerprint.left(8)
|
||||
root.name = "Home_%s" % fingerprint.left(8)
|
||||
_exteriors_root.add_child(root)
|
||||
root.global_transform = world_transform
|
||||
var tier: int = int(home_data.get(
|
||||
"tier", PlayerHomeStateType.STARTER_TIER
|
||||
))
|
||||
if tier >= PlayerHomeStateType.STARTER_RV_TIER:
|
||||
if not _dedicated_runtime:
|
||||
var motorcoach: Node3D = (
|
||||
STARTER_MOTORCOACH_SCENE.instantiate() as Node3D
|
||||
)
|
||||
if motorcoach != null:
|
||||
motorcoach.name = "StarterMotorcoach"
|
||||
root.add_child(motorcoach)
|
||||
else:
|
||||
push_error(
|
||||
"The starter motorcoach exterior could not be instantiated."
|
||||
)
|
||||
_add_box_collision(
|
||||
root,
|
||||
"Body",
|
||||
RV_EXTERIOR_BODY_SIZE,
|
||||
Vector3(0.0, RV_EXTERIOR_BODY_CENTER_Y, 0.0),
|
||||
)
|
||||
else:
|
||||
if not _dedicated_runtime:
|
||||
_add_tent_exterior(root)
|
||||
_add_box_collision(
|
||||
root,
|
||||
"Body",
|
||||
TENT_EXTERIOR_BODY_SIZE,
|
||||
Vector3(0.0, TENT_EXTERIOR_BODY_CENTER_Y, 0.0),
|
||||
)
|
||||
if not _dedicated_runtime:
|
||||
var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D
|
||||
if motorcoach != null:
|
||||
motorcoach.name = "StarterMotorcoach"
|
||||
root.add_child(motorcoach)
|
||||
else:
|
||||
push_error("The starter motorcoach exterior could not be instantiated.")
|
||||
_add_box_collision(
|
||||
root,
|
||||
"Body",
|
||||
EXTERIOR_BODY_SIZE,
|
||||
Vector3(0.0, 1.35, 0.0),
|
||||
)
|
||||
_add_exterior_prompt(root, tier)
|
||||
root.visible = not _dedicated_runtime
|
||||
return root
|
||||
|
||||
|
|
@ -850,59 +1019,26 @@ func _build_interior(
|
|||
home_data: Dictionary,
|
||||
) -> Node3D:
|
||||
var root := Node3D.new()
|
||||
root.name = "RVInterior_%s" % fingerprint.left(8)
|
||||
root.name = "HomeInterior_%s" % fingerprint.left(8)
|
||||
root.position = INTERIOR_ORIGIN + Vector3(
|
||||
float(interior_index) * INTERIOR_SPACING, 0.0, 0.0
|
||||
)
|
||||
var half_width: float = INTERIOR_FLOOR_SIZE.x * 0.5
|
||||
var half_depth: float = INTERIOR_FLOOR_SIZE.z * 0.5
|
||||
var wall_center_y: float = INTERIOR_WALL_HEIGHT * 0.5
|
||||
_interiors_root.add_child(root)
|
||||
_add_box(
|
||||
root, "Floor", INTERIOR_FLOOR_SIZE, Vector3.ZERO,
|
||||
Color("a6754e"), true,
|
||||
)
|
||||
_add_wall(
|
||||
root, "WallNorth",
|
||||
Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
|
||||
Vector3(0.0, wall_center_y, -half_depth),
|
||||
)
|
||||
_add_wall(
|
||||
root, "WallSouth",
|
||||
Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18),
|
||||
Vector3(0.0, wall_center_y, half_depth),
|
||||
)
|
||||
_add_wall(
|
||||
root, "WallWest",
|
||||
Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
|
||||
Vector3(-half_width, wall_center_y, 0.0),
|
||||
)
|
||||
_add_wall(
|
||||
root, "WallEast",
|
||||
Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z),
|
||||
Vector3(half_width, wall_center_y, 0.0),
|
||||
)
|
||||
for x_position: float in [-1.75, 1.75]:
|
||||
_add_box(
|
||||
root, "WallNorthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
|
||||
Vector3(1.65, 0.95, 0.04),
|
||||
Vector3(x_position, 1.55, -half_depth + 0.11),
|
||||
Color("153640"), false,
|
||||
)
|
||||
_add_box(
|
||||
root, "WallSouthWindow%s" % ("Left" if x_position < 0.0 else "Right"),
|
||||
Vector3(1.65, 0.95, 0.04),
|
||||
Vector3(x_position, 1.55, half_depth - 0.11),
|
||||
Color("153640"), false,
|
||||
)
|
||||
_add_box(
|
||||
root, "WallEastWindow", Vector3(0.04, 0.95, 1.55),
|
||||
Vector3(half_width - 0.11, 1.55, 0.0), Color("153640"), false,
|
||||
)
|
||||
_add_box(
|
||||
root, "WallWestDoor", Vector3(0.04, 1.9, 1.25),
|
||||
Vector3(-half_width + 0.11, 1.0, 0.0), Color("2897a8"), false,
|
||||
var tier: int = int(home_data.get(
|
||||
"tier", PlayerHomeStateType.STARTER_TIER
|
||||
))
|
||||
var scene: PackedScene = (
|
||||
STARTER_RV_INTERIOR_SCENE
|
||||
if tier >= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_INTERIOR_SCENE
|
||||
)
|
||||
var shell: Node3D = scene.instantiate() as Node3D
|
||||
if shell == null:
|
||||
push_error("The home interior shell could not be instantiated.")
|
||||
else:
|
||||
shell.name = "InteriorShell"
|
||||
root.add_child(shell)
|
||||
_prepare_imported_interior(shell)
|
||||
var decor := Node3D.new()
|
||||
decor.name = "Decor"
|
||||
root.add_child(decor)
|
||||
|
|
@ -913,6 +1049,153 @@ func _build_interior(
|
|||
return root
|
||||
|
||||
|
||||
func _add_tent_exterior(parent: Node3D) -> void:
|
||||
var tent := MeshInstance3D.new()
|
||||
tent.name = "StarterTent"
|
||||
var mesh := CylinderMesh.new()
|
||||
mesh.top_radius = 0.0
|
||||
mesh.bottom_radius = 3.05
|
||||
mesh.height = 3.0
|
||||
mesh.radial_segments = 4
|
||||
mesh.rings = 1
|
||||
mesh.cap_top = false
|
||||
mesh.material = _material(Color("9a7049"))
|
||||
tent.mesh = mesh
|
||||
tent.position.y = 1.5
|
||||
tent.rotation.y = PI * 0.25
|
||||
parent.add_child(tent)
|
||||
_add_box(
|
||||
parent,
|
||||
"TentDoor",
|
||||
Vector3(1.25, 1.85, 0.04),
|
||||
Vector3(0.0, 0.95, 2.18),
|
||||
Color("173b32"),
|
||||
false,
|
||||
)
|
||||
|
||||
|
||||
func _add_exterior_prompt(parent: Node3D, tier: int) -> void:
|
||||
var prompt := Node3D.new()
|
||||
prompt.name = "DoorPrompt"
|
||||
prompt.position = (
|
||||
RV_EXTERIOR_PROMPT_LOCAL
|
||||
if tier >= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_EXTERIOR_PROMPT_LOCAL
|
||||
)
|
||||
prompt.visible = false
|
||||
parent.add_child(prompt)
|
||||
|
||||
var background := MeshInstance3D.new()
|
||||
background.name = "Background"
|
||||
background.mesh = _rounded_rectangle_mesh(
|
||||
HOME_PROMPT_SIZE,
|
||||
HOME_PROMPT_CORNER_RADIUS,
|
||||
)
|
||||
background.material_override = _prompt_material(
|
||||
HOME_PROMPT_BACKGROUND_COLOR
|
||||
)
|
||||
prompt.add_child(background)
|
||||
|
||||
var label := Label3D.new()
|
||||
label.name = "Message"
|
||||
label.text = "enter home"
|
||||
label.position.z = 0.012
|
||||
label.font = HOME_PROMPT_FONT
|
||||
label.font_size = 28
|
||||
label.pixel_size = HOME_PROMPT_TEXT_PIXEL_SIZE
|
||||
label.modulate = HOME_PROMPT_TEXT_COLOR
|
||||
label.outline_size = 0
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
label.width = HOME_PROMPT_SIZE.x / HOME_PROMPT_TEXT_PIXEL_SIZE
|
||||
label.double_sided = true
|
||||
prompt.add_child(label)
|
||||
|
||||
|
||||
func _rounded_rectangle_mesh(size: Vector2, radius: float) -> ArrayMesh:
|
||||
var half_size: Vector2 = size * 0.5
|
||||
var clamped_radius: float = minf(
|
||||
radius,
|
||||
minf(half_size.x, half_size.y),
|
||||
)
|
||||
var corners: Array[Vector2] = [
|
||||
Vector2(half_size.x - clamped_radius, half_size.y - clamped_radius),
|
||||
Vector2(-half_size.x + clamped_radius, half_size.y - clamped_radius),
|
||||
Vector2(-half_size.x + clamped_radius, -half_size.y + clamped_radius),
|
||||
Vector2(half_size.x - clamped_radius, -half_size.y + clamped_radius),
|
||||
]
|
||||
var start_angles: Array[float] = [0.0, PI * 0.5, PI, PI * 1.5]
|
||||
var perimeter: Array[Vector2] = []
|
||||
const CORNER_SEGMENTS: int = 4
|
||||
for corner_index: int in corners.size():
|
||||
for segment_index: int in range(CORNER_SEGMENTS + 1):
|
||||
var angle: float = start_angles[corner_index] + (
|
||||
PI * 0.5 * float(segment_index) / float(CORNER_SEGMENTS)
|
||||
)
|
||||
perimeter.append(
|
||||
corners[corner_index]
|
||||
+ Vector2(cos(angle), sin(angle)) * clamped_radius
|
||||
)
|
||||
var surface := SurfaceTool.new()
|
||||
surface.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
for index: int in perimeter.size():
|
||||
var first: Vector2 = perimeter[index]
|
||||
var second: Vector2 = perimeter[(index + 1) % perimeter.size()]
|
||||
for point: Vector2 in [Vector2.ZERO, first, second]:
|
||||
surface.set_normal(Vector3.FORWARD)
|
||||
surface.add_vertex(Vector3(point.x, point.y, 0.0))
|
||||
return surface.commit()
|
||||
|
||||
|
||||
func _prompt_material(color: Color) -> StandardMaterial3D:
|
||||
var material := StandardMaterial3D.new()
|
||||
material.albedo_color = color
|
||||
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
material.roughness = 1.0
|
||||
material.metallic = 0.0
|
||||
material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
return material
|
||||
|
||||
|
||||
func _prepare_imported_interior(shell: Node3D) -> void:
|
||||
for value: Node in shell.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := value as MeshInstance3D
|
||||
if mesh_instance == null or mesh_instance.mesh == null:
|
||||
continue
|
||||
for surface_index: int in mesh_instance.mesh.get_surface_count():
|
||||
var source_material: Material = mesh_instance.get_active_material(
|
||||
surface_index
|
||||
)
|
||||
var material := source_material as StandardMaterial3D
|
||||
if material == null:
|
||||
continue
|
||||
var flat_material := material.duplicate() as StandardMaterial3D
|
||||
flat_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
flat_material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST
|
||||
flat_material.roughness = 1.0
|
||||
flat_material.metallic = 0.0
|
||||
mesh_instance.set_surface_override_material(
|
||||
surface_index, flat_material
|
||||
)
|
||||
_add_imported_mesh_collision(mesh_instance)
|
||||
|
||||
|
||||
func _add_imported_mesh_collision(mesh_instance: MeshInstance3D) -> void:
|
||||
var parent: Node = mesh_instance.get_parent()
|
||||
if parent == null or mesh_instance.mesh == null:
|
||||
return
|
||||
var shape: ConcavePolygonShape3D = mesh_instance.mesh.create_trimesh_shape()
|
||||
if shape == null:
|
||||
return
|
||||
var body := StaticBody3D.new()
|
||||
body.name = "%sCollision" % mesh_instance.name
|
||||
body.transform = mesh_instance.transform
|
||||
var collision := CollisionShape3D.new()
|
||||
collision.shape = shape
|
||||
body.add_child(collision)
|
||||
parent.add_child(body)
|
||||
|
||||
|
||||
func _rebuild_decor(
|
||||
parent: Node3D,
|
||||
home_data: Dictionary,
|
||||
|
|
@ -1115,9 +1398,9 @@ func _refresh_local_visibility() -> void:
|
|||
for wall_name: String in [
|
||||
"WallNorth", "WallSouth", "WallWest", "WallEast",
|
||||
]:
|
||||
var body: StaticBody3D = active_interior.get_node_or_null(
|
||||
"%sCollision" % wall_name
|
||||
)
|
||||
var body := active_interior.find_child(
|
||||
"%sCollision" % wall_name, true, false
|
||||
) as StaticBody3D
|
||||
if body != null:
|
||||
excluded_wall_rids.append(body.get_rid())
|
||||
local_player.set_rv_interior_camera_mode(
|
||||
|
|
@ -1145,12 +1428,20 @@ func _refresh_nearest_wall() -> void:
|
|||
).normalized()
|
||||
var horizontal_view_weight: float = absf(local_view_direction.x)
|
||||
var depth_view_weight: float = absf(local_view_direction.z)
|
||||
var tier: int = int((presentation.get("home", {}) as Dictionary).get(
|
||||
"tier", PlayerHomeStateType.STARTER_TIER
|
||||
))
|
||||
var interior_size: Vector2 = (
|
||||
STARTER_RV_INTERIOR_SIZE
|
||||
if tier >= PlayerHomeStateType.STARTER_RV_TIER
|
||||
else TENT_INTERIOR_SIZE
|
||||
)
|
||||
var hidden_walls: Dictionary[String, bool] = {}
|
||||
var outside_horizontal_wall: bool = (
|
||||
absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05
|
||||
absf(local_camera.x) >= interior_size.x * 0.5 - 0.05
|
||||
)
|
||||
var outside_depth_wall: bool = (
|
||||
absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 0.5 - 0.05
|
||||
absf(local_camera.z) >= interior_size.y * 0.5 - 0.05
|
||||
)
|
||||
# At a diagonal, both near walls occupy the view. Use the camera's viewing
|
||||
# direction to hide both instead of choosing only the dominant position
|
||||
|
|
@ -1181,10 +1472,14 @@ func _refresh_nearest_wall() -> void:
|
|||
for wall_name: String in [
|
||||
"WallNorth", "WallSouth", "WallWest", "WallEast",
|
||||
]:
|
||||
var wall: MeshInstance3D = interior.get_node_or_null(wall_name)
|
||||
var wall := interior.find_child(
|
||||
wall_name, true, false
|
||||
) as MeshInstance3D
|
||||
if wall != null:
|
||||
wall.visible = not hidden_walls.has(wall_name)
|
||||
for child: Node in interior.get_children():
|
||||
for child: Node in interior.find_children(
|
||||
"%s*" % wall_name, "GeometryInstance3D", true, false
|
||||
):
|
||||
if (
|
||||
child is GeometryInstance3D
|
||||
and String(child.name).begins_with(wall_name)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,12 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|||
|
||||
const HOME_SCHEMA_VERSION: int = 1
|
||||
const STARTER_TIER: int = 0
|
||||
const STARTER_EXTERIOR_ID: StringName = &"starter_motorcoach"
|
||||
const MAX_TIER: int = 8
|
||||
const STARTER_EXTERIOR_ID: StringName = &"starter_tent"
|
||||
const STARTER_RV_TIER: int = 1
|
||||
const STARTER_RV_EXTERIOR_ID: StringName = &"starter_motorcoach"
|
||||
const MAX_TIER: int = STARTER_RV_TIER
|
||||
const HOME_UPGRADE_COSTS: Array[int] = [50]
|
||||
const STORAGE_CAPACITIES: Array[int] = [9, 18]
|
||||
const MAX_PLACEMENTS: int = 128
|
||||
const MAX_INSTANCE_ID_LENGTH: int = 96
|
||||
const MAX_REVISION: int = 2147483647
|
||||
|
|
@ -16,11 +20,8 @@ const GRID_SIZE: float = 0.5
|
|||
const FREE_PLACEMENT_SEARCH_STEP: float = 0.1
|
||||
const FREE_PLACEMENT_SEARCH_ANGLES: int = 24
|
||||
const NEAREST_PLACEMENT_SEARCH_DISTANCE: float = 2.0
|
||||
# The starter RV is intentionally cramped. Later RV tiers can replace these
|
||||
# bounds with larger layouts when tiered interiors are introduced.
|
||||
# The starter interior is 8.0 m by 4.5 m with 0.18 m walls. These bounds
|
||||
# follow the walls' inner faces, with a small visual gap to avoid z-fighting.
|
||||
const INTERIOR_HALF_EXTENTS := Vector2(3.91, 2.16)
|
||||
const TENT_INTERIOR_HALF_EXTENTS := Vector2(1.91, 1.91)
|
||||
const STARTER_RV_INTERIOR_HALF_EXTENTS := Vector2(1.91, 2.91)
|
||||
const PLACEMENT_EDGE_PADDING: float = 0.02
|
||||
|
||||
enum Privacy {
|
||||
|
|
@ -50,6 +51,51 @@ func get_exterior_id() -> StringName:
|
|||
return _exterior_id
|
||||
|
||||
|
||||
func get_storage_capacity() -> int:
|
||||
return storage_capacity_for_tier(_tier)
|
||||
|
||||
|
||||
func get_next_upgrade_cost() -> int:
|
||||
if _tier >= MAX_TIER:
|
||||
return -1
|
||||
return HOME_UPGRADE_COSTS[_tier]
|
||||
|
||||
|
||||
func get_next_storage_capacity() -> int:
|
||||
if _tier >= MAX_TIER:
|
||||
return -1
|
||||
return storage_capacity_for_tier(_tier + 1)
|
||||
|
||||
|
||||
func upgrade_to_next_tier() -> bool:
|
||||
if _tier >= MAX_TIER:
|
||||
return false
|
||||
_tier += 1
|
||||
_exterior_id = exterior_id_for_tier(_tier)
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
static func exterior_id_for_tier(tier: int) -> StringName:
|
||||
return (
|
||||
STARTER_RV_EXTERIOR_ID
|
||||
if tier >= STARTER_RV_TIER
|
||||
else STARTER_EXTERIOR_ID
|
||||
)
|
||||
|
||||
|
||||
static func storage_capacity_for_tier(tier: int) -> int:
|
||||
return STORAGE_CAPACITIES[clampi(tier, STARTER_TIER, MAX_TIER)]
|
||||
|
||||
|
||||
static func interior_half_extents_for_tier(tier: int) -> Vector2:
|
||||
return (
|
||||
STARTER_RV_INTERIOR_HALF_EXTENTS
|
||||
if tier >= STARTER_RV_TIER
|
||||
else TENT_INTERIOR_HALF_EXTENTS
|
||||
)
|
||||
|
||||
|
||||
func get_privacy() -> Privacy:
|
||||
return _privacy
|
||||
|
||||
|
|
@ -248,7 +294,7 @@ func get_nearest_valid_placement_transform(
|
|||
candidate_validator: Callable = Callable(),
|
||||
) -> Dictionary:
|
||||
var normalized: Dictionary = normalized_placement_transform(
|
||||
decor_id, position, yaw_radians, mode
|
||||
decor_id, position, yaw_radians, mode, _tier
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return {}
|
||||
|
|
@ -322,7 +368,7 @@ func restore_from_save_data(data: Dictionary) -> bool:
|
|||
if sanitized.is_empty():
|
||||
return false
|
||||
_tier = int(sanitized["tier"])
|
||||
_exterior_id = StringName(str(sanitized["exterior_id"]))
|
||||
_exterior_id = exterior_id_for_tier(_tier)
|
||||
_privacy = int(sanitized["privacy"])
|
||||
_owned_decor.clear()
|
||||
for key: Variant in (sanitized["owned_decor"] as Dictionary):
|
||||
|
|
@ -384,6 +430,7 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary:
|
|||
or next_sequence > MAX_REVISION
|
||||
):
|
||||
return {}
|
||||
exterior_id = String(exterior_id_for_tier(tier))
|
||||
var sanitized_owned: Dictionary = {}
|
||||
var owned: Dictionary = data["owned_decor"]
|
||||
for key: Variant in owned:
|
||||
|
|
@ -408,7 +455,7 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary:
|
|||
for value: Variant in values:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return {}
|
||||
var sanitized: Dictionary = sanitize_placement(value)
|
||||
var sanitized: Dictionary = sanitize_placement(value, tier)
|
||||
if sanitized.is_empty():
|
||||
return {}
|
||||
var instance_id: String = str(sanitized["instance_id"])
|
||||
|
|
@ -440,7 +487,10 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary:
|
|||
}
|
||||
|
||||
|
||||
static func sanitize_placement(value: Dictionary) -> Dictionary:
|
||||
static func sanitize_placement(
|
||||
value: Dictionary,
|
||||
home_tier: int = STARTER_TIER,
|
||||
) -> Dictionary:
|
||||
for key: String in [
|
||||
"instance_id", "decor_id", "mode", "position", "yaw",
|
||||
]:
|
||||
|
|
@ -483,6 +533,7 @@ static func sanitize_placement(value: Dictionary) -> Dictionary:
|
|||
position,
|
||||
yaw,
|
||||
mode as PlacementMode,
|
||||
home_tier,
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return {}
|
||||
|
|
@ -509,6 +560,7 @@ static func normalized_placement_transform(
|
|||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
home_tier: int = STARTER_TIER,
|
||||
) -> Dictionary:
|
||||
if (
|
||||
not DecorCatalogType.has_product(decor_id)
|
||||
|
|
@ -530,11 +582,12 @@ static func normalized_placement_transform(
|
|||
(footprint.x * cosine + footprint.y * sine) * 0.5,
|
||||
(footprint.x * sine + footprint.y * cosine) * 0.5,
|
||||
)
|
||||
var interior_half_extents := interior_half_extents_for_tier(home_tier)
|
||||
var maximum_x: float = (
|
||||
INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING
|
||||
interior_half_extents.x - half_size.x - PLACEMENT_EDGE_PADDING
|
||||
)
|
||||
var maximum_z: float = (
|
||||
INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING
|
||||
interior_half_extents.y - half_size.y - PLACEMENT_EDGE_PADDING
|
||||
)
|
||||
if maximum_x < 0.0 or maximum_z < 0.0:
|
||||
return {}
|
||||
|
|
@ -580,6 +633,7 @@ func _find_nearest_grid_transform(
|
|||
),
|
||||
yaw,
|
||||
PlacementMode.GRID,
|
||||
_tier,
|
||||
)
|
||||
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||
candidate, seen
|
||||
|
|
@ -630,6 +684,7 @@ func _find_nearest_free_transform(
|
|||
),
|
||||
yaw,
|
||||
PlacementMode.FREE,
|
||||
_tier,
|
||||
)
|
||||
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||
candidate, seen
|
||||
|
|
|
|||
411
main/main.gd
411
main/main.gd
|
|
@ -40,6 +40,9 @@ const FishingShopInteractionType = preload(
|
|||
const DecorShopInteractionType = preload(
|
||||
"res://world/decor_shop_interaction.gd"
|
||||
)
|
||||
const RVUpgradeInteractionType = preload(
|
||||
"res://world/rv_upgrade_interaction.gd"
|
||||
)
|
||||
const PlayerStorageInteractionType = preload(
|
||||
"res://world/player_storage_interaction.gd"
|
||||
)
|
||||
|
|
@ -148,6 +151,13 @@ const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0)
|
|||
const SHOP_PATTERN_SCALE: float = 1.75
|
||||
const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10
|
||||
const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30
|
||||
const HOME_ENTRY_INPUT_OWNER: StringName = &"home_entry_transition"
|
||||
const HOME_EXIT_INPUT_OWNER: StringName = &"home_exit_transition"
|
||||
const HOME_EXIT_FADE_GENERATION: int = -104729
|
||||
const HOME_ENTRY_WALK_DISTANCE: float = 1.05
|
||||
const HOME_ENTRY_WALK_DURATION: float = 0.55
|
||||
const HOME_EXIT_WALK_DISTANCE: float = 1.25
|
||||
const HOME_EXIT_WALK_DURATION: float = 0.62
|
||||
|
||||
@export var fish_catalog: FishPoolType
|
||||
@export var pelican_buyer_profile: FishBuyerProfileType
|
||||
|
|
@ -260,6 +270,7 @@ var _gameplay_started: bool = false
|
|||
var _save_slots := PlayerSaveSlotCatalogType.new()
|
||||
var _shop_interaction: FishingShopInteractionType
|
||||
var _decor_shop_interaction: DecorShopInteractionType
|
||||
var _rv_upgrade_shop_interaction: RVUpgradeInteractionType
|
||||
var _storage_interaction: PlayerStorageInteractionType
|
||||
var _title_music_tween: Tween
|
||||
var _title_music_transition_generation: int = 0
|
||||
|
|
@ -290,6 +301,12 @@ var _pending_existing_root_path := ""
|
|||
var _local_recovery_attempt_id: String = ""
|
||||
var _dedicated_runtime: bool = false
|
||||
var _dedicated_metrics_accumulator: float = 0.0
|
||||
var _home_exit_transition_active: bool = false
|
||||
var _home_exit_waiting_for_network: bool = false
|
||||
var _home_exit_start_position: Vector3 = Vector3.ZERO
|
||||
var _home_exit_walk_tween: Tween
|
||||
var _home_entry_transition_active: bool = false
|
||||
var _home_entry_walk_tween: Tween
|
||||
|
||||
@onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience
|
||||
var _rain_ambience: RainAmbienceType
|
||||
|
|
@ -743,6 +760,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_player.wallet,
|
||||
_asset_reservations
|
||||
)
|
||||
_player.cooler_capacity.bind_home_state(_home_state)
|
||||
_player.bag.setup(item_catalog)
|
||||
_player.inventory_layout.setup(
|
||||
_player.bag,
|
||||
|
|
@ -760,19 +778,24 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
)
|
||||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||
_rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop()
|
||||
_storage_interaction = _test_world.get_player_storage()
|
||||
if not dedicated:
|
||||
if not _home_state.changed.is_connected(_on_home_state_changed):
|
||||
_home_state.changed.connect(_on_home_state_changed)
|
||||
_shop_interaction.setup_local_player(_player)
|
||||
_shop_interaction.local_player_range_changed.connect(
|
||||
_on_shop_range_changed
|
||||
)
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
_refresh_shop_npc_speech_state()
|
||||
_decor_shop_interaction.setup_local_player(_player)
|
||||
_decor_shop_interaction.local_player_range_changed.connect(
|
||||
_on_decor_shop_range_changed
|
||||
)
|
||||
_rv_upgrade_shop_interaction.setup_local_player(_player)
|
||||
_rv_upgrade_shop_interaction.local_player_range_changed.connect(
|
||||
_on_rv_upgrade_shop_range_changed
|
||||
)
|
||||
_storage_interaction.setup_local_player(_player)
|
||||
_storage_interaction.local_player_range_changed.connect(
|
||||
_on_storage_range_changed
|
||||
|
|
@ -947,6 +970,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_home_state,
|
||||
_network_home,
|
||||
_decor_shop_interaction,
|
||||
_rv_upgrade_shop_interaction,
|
||||
)
|
||||
_fishing_spot.setup(
|
||||
_player,
|
||||
|
|
@ -987,6 +1011,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_shop_interaction,
|
||||
_storage_interaction,
|
||||
_decor_shop_interaction,
|
||||
_rv_upgrade_shop_interaction,
|
||||
_player.item_effects,
|
||||
_player.cooler_capacity,
|
||||
_network_session,
|
||||
|
|
@ -1024,6 +1049,12 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_home_decor_controller.begin_decorating
|
||||
)
|
||||
_network_home.local_space_changed.connect(_on_local_home_space_changed)
|
||||
_network_home.local_transition_finished.connect(
|
||||
_on_local_home_transition_finished
|
||||
)
|
||||
_game_ui.get_screen_fade().transition_completed.connect(
|
||||
_on_home_exit_fade_transition_completed
|
||||
)
|
||||
_game_ui.rv_storage_requested.connect(_on_rv_storage_requested)
|
||||
_game_ui.setup_data_and_identity(
|
||||
_data_root,
|
||||
|
|
@ -1539,6 +1570,9 @@ func _input(event: InputEvent) -> void:
|
|||
if _game_ui.get_decor_shop().consume_escape():
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if _game_ui.get_rv_upgrade_shop().consume_escape():
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if fishing_shop.consume_escape():
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -1653,17 +1687,12 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
return
|
||||
var home_owner: String = _network_home.get_local_home_owner_fingerprint()
|
||||
if not home_owner.is_empty():
|
||||
if _home_world.is_avatar_near_interior_exit(_player, home_owner):
|
||||
if _network_home.request_exit():
|
||||
_game_ui.set_storage_prompt_visible(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_network_home.can_local_decorate()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _game_ui.get_player_storage().open_storage_from_home(true)
|
||||
):
|
||||
_game_ui.set_storage_prompt_visible(false)
|
||||
_refresh_home_door_prompt({})
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var nearby_home_owner: String = _home_world.find_nearby_exterior_owner(
|
||||
|
|
@ -1671,9 +1700,18 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
)
|
||||
if not nearby_home_owner.is_empty():
|
||||
if _network_home.request_enter(nearby_home_owner):
|
||||
_game_ui.set_storage_prompt_visible(false)
|
||||
_refresh_home_door_prompt({})
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _game_ui.get_rv_upgrade_shop().open_shop()
|
||||
):
|
||||
_game_ui.set_shop_prompt_visible(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
|
|
@ -1703,14 +1741,24 @@ func _process(delta: float) -> void:
|
|||
)
|
||||
_print_dedicated_metrics()
|
||||
return
|
||||
_maybe_begin_home_exit_transition()
|
||||
var show_rv_upgrade_prompt := _can_show_rv_upgrade_shop_prompt()
|
||||
var show_decor_shop_prompt := _can_show_decor_shop_prompt()
|
||||
var show_shop_prompt := show_decor_shop_prompt or _can_show_shop_prompt()
|
||||
var show_shop_prompt := (
|
||||
show_rv_upgrade_prompt
|
||||
or show_decor_shop_prompt
|
||||
or _can_show_shop_prompt()
|
||||
)
|
||||
var shop_prompt_anchor := (
|
||||
_decor_shop_interaction.get_prompt_anchor_position()
|
||||
if show_decor_shop_prompt
|
||||
_rv_upgrade_shop_interaction.get_prompt_anchor_position()
|
||||
if show_rv_upgrade_prompt
|
||||
else (
|
||||
_shop_interaction.get_prompt_anchor_position()
|
||||
if _shop_interaction != null else Vector3.ZERO
|
||||
_decor_shop_interaction.get_prompt_anchor_position()
|
||||
if show_decor_shop_prompt
|
||||
else (
|
||||
_shop_interaction.get_prompt_anchor_position()
|
||||
if _shop_interaction != null else Vector3.ZERO
|
||||
)
|
||||
)
|
||||
)
|
||||
_game_ui.set_shop_prompt_visible(
|
||||
|
|
@ -1718,11 +1766,7 @@ func _process(delta: float) -> void:
|
|||
shop_prompt_anchor,
|
||||
)
|
||||
var home_prompt := _get_home_prompt()
|
||||
_game_ui.set_storage_prompt_visible(
|
||||
not home_prompt.is_empty(),
|
||||
home_prompt.get("anchor", Vector3.ZERO),
|
||||
str(home_prompt.get("label", "enter RV")),
|
||||
)
|
||||
_refresh_home_door_prompt(home_prompt)
|
||||
_game_ui.set_rv_storage_button_visible(
|
||||
_can_show_rv_storage_button()
|
||||
)
|
||||
|
|
@ -2577,12 +2621,23 @@ func _refresh_active_world_bindings() -> void:
|
|||
_decor_shop_interaction.local_player_range_changed.disconnect(
|
||||
_on_decor_shop_range_changed
|
||||
)
|
||||
if is_instance_valid(_rv_upgrade_shop_interaction):
|
||||
if _rv_upgrade_shop_interaction.local_player_range_changed.is_connected(
|
||||
_on_rv_upgrade_shop_range_changed
|
||||
):
|
||||
_rv_upgrade_shop_interaction.local_player_range_changed.disconnect(
|
||||
_on_rv_upgrade_shop_range_changed
|
||||
)
|
||||
_shop_interaction = _test_world.get_fishing_shop()
|
||||
_decor_shop_interaction = _test_world.get_decor_shop()
|
||||
_rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop()
|
||||
_storage_interaction = _test_world.get_player_storage()
|
||||
_network_sale.set_shop_interaction(_shop_interaction)
|
||||
_network_shop.set_shop_interaction(_shop_interaction)
|
||||
_network_shop.set_decor_shop_interaction(_decor_shop_interaction)
|
||||
_network_shop.set_rv_upgrade_shop_interaction(
|
||||
_rv_upgrade_shop_interaction
|
||||
)
|
||||
if _dedicated_runtime:
|
||||
return
|
||||
if _shop_interaction != null:
|
||||
|
|
@ -2600,10 +2655,16 @@ func _refresh_active_world_bindings() -> void:
|
|||
_decor_shop_interaction.local_player_range_changed.connect(
|
||||
_on_decor_shop_range_changed
|
||||
)
|
||||
if _rv_upgrade_shop_interaction != null:
|
||||
_rv_upgrade_shop_interaction.setup_local_player(_player)
|
||||
_rv_upgrade_shop_interaction.local_player_range_changed.connect(
|
||||
_on_rv_upgrade_shop_range_changed
|
||||
)
|
||||
_game_ui.set_world_interactions(
|
||||
_shop_interaction,
|
||||
_storage_interaction,
|
||||
_decor_shop_interaction,
|
||||
_rv_upgrade_shop_interaction,
|
||||
)
|
||||
_on_shop_range_changed(
|
||||
_shop_interaction != null
|
||||
|
|
@ -2617,6 +2678,10 @@ func _refresh_active_world_bindings() -> void:
|
|||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
_on_rv_upgrade_shop_range_changed(
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
|
||||
|
||||
func _on_water_recovery_starting() -> void:
|
||||
|
|
@ -2631,6 +2696,7 @@ func _on_water_recovery_starting() -> void:
|
|||
_game_ui.get_pause_menu().close_for_water_recovery()
|
||||
_game_ui.get_fishing_shop().close_for_water_recovery()
|
||||
_game_ui.get_decor_shop().close_for_water_recovery()
|
||||
_game_ui.get_rv_upgrade_shop().close_for_water_recovery()
|
||||
_game_ui.get_player_storage().close_for_water_recovery()
|
||||
|
||||
|
||||
|
|
@ -2679,6 +2745,7 @@ func _on_active_hotbar_item_changed(
|
|||
and item != null
|
||||
and item.is_available()
|
||||
and _player.bag.owns_item(item_id)
|
||||
and not String(_network_home.get_local_space()).begins_with("rv:")
|
||||
)
|
||||
var active_is_catching_net: bool = (
|
||||
item_id == FishingShopStockType.CRAB_NET_ID
|
||||
|
|
@ -2828,6 +2895,8 @@ func _finish_quit() -> void:
|
|||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_cancel_home_entry_transition()
|
||||
_cancel_home_exit_transition(true)
|
||||
if _dedicated_runtime and _network_session != null:
|
||||
_network_session.disconnect_session("Dedicated server stopping.")
|
||||
if _title_music_tween != null:
|
||||
|
|
@ -2845,52 +2914,46 @@ func _exit_tree() -> void:
|
|||
|
||||
|
||||
func _on_shop_range_changed(in_range: bool) -> void:
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
in_range
|
||||
or (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
)
|
||||
_refresh_shop_npc_speech_state()
|
||||
if not in_range:
|
||||
_game_ui.get_fishing_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_shop_prompt())
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
|
||||
|
||||
func _on_storage_range_changed(in_range: bool) -> void:
|
||||
if not in_range:
|
||||
_game_ui.get_player_storage().close_for_range_exit()
|
||||
var prompt := _get_home_prompt()
|
||||
_game_ui.set_storage_prompt_visible(
|
||||
not prompt.is_empty(),
|
||||
prompt.get("anchor", Vector3.ZERO),
|
||||
str(prompt.get("label", "enter RV")),
|
||||
)
|
||||
_refresh_home_door_prompt(prompt)
|
||||
_game_ui.set_rv_storage_button_visible(
|
||||
_can_show_rv_storage_button()
|
||||
)
|
||||
|
||||
|
||||
func _on_decor_shop_range_changed(in_range: bool) -> void:
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
in_range
|
||||
or (
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
)
|
||||
_refresh_shop_npc_speech_state()
|
||||
if not in_range:
|
||||
_game_ui.get_decor_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(
|
||||
_can_show_shop_prompt() or _can_show_decor_shop_prompt()
|
||||
)
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
|
||||
|
||||
func _on_rv_upgrade_shop_range_changed(in_range: bool) -> void:
|
||||
_refresh_shop_npc_speech_state()
|
||||
if not in_range:
|
||||
_game_ui.get_rv_upgrade_shop().close_for_range_exit()
|
||||
_game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt())
|
||||
|
||||
|
||||
func _on_home_state_changed() -> void:
|
||||
_refresh_shop_npc_speech_state()
|
||||
|
||||
|
||||
func _on_local_home_space_changed(
|
||||
space_id: StringName,
|
||||
_owner_fingerprint: String,
|
||||
owner_fingerprint: String,
|
||||
) -> void:
|
||||
var inside_rv: bool = String(space_id).begins_with("rv:")
|
||||
_refresh_active_hotbar_item()
|
||||
_fishing_spot.set_fishing_environment_enabled(not inside_rv)
|
||||
_game_ui.set_rv_interior_camera_mode(
|
||||
inside_rv
|
||||
|
|
@ -2898,6 +2961,171 @@ func _on_local_home_space_changed(
|
|||
_game_ui.set_rv_storage_button_visible(
|
||||
_can_show_rv_storage_button()
|
||||
)
|
||||
if inside_rv and not _home_exit_transition_active:
|
||||
_begin_home_entry_transition(owner_fingerprint)
|
||||
elif not inside_rv and _home_entry_transition_active:
|
||||
_cancel_home_entry_transition()
|
||||
|
||||
|
||||
func _begin_home_entry_transition(owner_fingerprint: String) -> void:
|
||||
if owner_fingerprint.is_empty() or _home_entry_transition_active:
|
||||
return
|
||||
_home_entry_transition_active = true
|
||||
_refresh_home_door_prompt({})
|
||||
_player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, true)
|
||||
_player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, true)
|
||||
var inward_direction: Vector3 = (
|
||||
-_home_world.get_interior_exit_direction(owner_fingerprint)
|
||||
)
|
||||
_player.face_world_direction(inward_direction)
|
||||
_player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION)
|
||||
_home_entry_walk_tween = create_tween()
|
||||
_home_entry_walk_tween.set_trans(Tween.TRANS_SINE)
|
||||
_home_entry_walk_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_home_entry_walk_tween.tween_property(
|
||||
_player,
|
||||
"global_position",
|
||||
_player.global_position + inward_direction * HOME_ENTRY_WALK_DISTANCE,
|
||||
HOME_ENTRY_WALK_DURATION,
|
||||
)
|
||||
_home_entry_walk_tween.finished.connect(
|
||||
_cancel_home_entry_transition,
|
||||
CONNECT_ONE_SHOT,
|
||||
)
|
||||
|
||||
|
||||
func _cancel_home_entry_transition() -> void:
|
||||
if _home_entry_walk_tween != null and _home_entry_walk_tween.is_valid():
|
||||
_home_entry_walk_tween.kill()
|
||||
_home_entry_walk_tween = null
|
||||
if is_instance_valid(_player):
|
||||
_player.end_animation_action()
|
||||
_player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, false)
|
||||
_player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, false)
|
||||
_home_entry_transition_active = false
|
||||
|
||||
|
||||
func _maybe_begin_home_exit_transition() -> void:
|
||||
if (
|
||||
_home_exit_transition_active
|
||||
or _home_entry_transition_active
|
||||
or not _gameplay_started
|
||||
or _water_recovery.is_recovery_active()
|
||||
or not _fishing_spot.can_open_fishing_shop()
|
||||
or _game_ui.get_player_storage().visible
|
||||
or _game_ui.get_fishing_shop().visible
|
||||
or _game_ui.get_decor_shop().visible
|
||||
or _game_ui.get_rv_upgrade_shop().visible
|
||||
):
|
||||
return
|
||||
var owner_fingerprint: String = (
|
||||
_network_home.get_local_home_owner_fingerprint()
|
||||
)
|
||||
if (
|
||||
owner_fingerprint.is_empty()
|
||||
or not _home_world.has_avatar_crossed_interior_exit_threshold(
|
||||
_player,
|
||||
owner_fingerprint,
|
||||
)
|
||||
):
|
||||
return
|
||||
_begin_home_exit_transition(owner_fingerprint)
|
||||
|
||||
|
||||
func _begin_home_exit_transition(owner_fingerprint: String) -> void:
|
||||
_home_exit_transition_active = true
|
||||
_home_exit_waiting_for_network = false
|
||||
_home_exit_start_position = _player.global_position
|
||||
_refresh_home_door_prompt({})
|
||||
_player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
|
||||
_player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, true)
|
||||
var exit_direction: Vector3 = _home_world.get_interior_exit_direction(
|
||||
owner_fingerprint
|
||||
)
|
||||
_player.face_world_direction(exit_direction)
|
||||
_player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION)
|
||||
_home_exit_walk_tween = create_tween()
|
||||
_home_exit_walk_tween.set_trans(Tween.TRANS_SINE)
|
||||
_home_exit_walk_tween.set_ease(Tween.EASE_IN_OUT)
|
||||
_home_exit_walk_tween.tween_property(
|
||||
_player,
|
||||
"global_position",
|
||||
_player.global_position + exit_direction * HOME_EXIT_WALK_DISTANCE,
|
||||
HOME_EXIT_WALK_DURATION,
|
||||
)
|
||||
_home_exit_walk_tween.finished.connect(
|
||||
_on_home_exit_walk_finished,
|
||||
CONNECT_ONE_SHOT,
|
||||
)
|
||||
|
||||
|
||||
func _on_home_exit_walk_finished() -> void:
|
||||
_home_exit_walk_tween = null
|
||||
if not _home_exit_transition_active:
|
||||
return
|
||||
# End the walking action on the same frame translation ends. The fade and
|
||||
# authoritative relocation may take longer, but a stationary avatar must
|
||||
# never keep cycling its walk animation during that wait.
|
||||
_player.end_animation_action()
|
||||
_game_ui.get_screen_fade().fade_to_black(HOME_EXIT_FADE_GENERATION)
|
||||
|
||||
|
||||
func _on_home_exit_fade_transition_completed(
|
||||
generation: int,
|
||||
faded_to_black: bool,
|
||||
) -> void:
|
||||
if (
|
||||
generation != HOME_EXIT_FADE_GENERATION
|
||||
or not _home_exit_transition_active
|
||||
):
|
||||
return
|
||||
if faded_to_black:
|
||||
# A local host can apply the authoritative transition synchronously from
|
||||
# request_exit(). Mark this first so its completion signal is not lost.
|
||||
_home_exit_waiting_for_network = true
|
||||
var request_started: bool = _network_home.request_exit()
|
||||
if not request_started:
|
||||
_home_exit_waiting_for_network = false
|
||||
_player.global_position = _home_exit_start_position
|
||||
_game_ui.get_screen_fade().fade_from_black(
|
||||
HOME_EXIT_FADE_GENERATION
|
||||
)
|
||||
return
|
||||
_cancel_home_exit_transition(false)
|
||||
|
||||
|
||||
func _on_local_home_transition_finished(
|
||||
accepted: bool,
|
||||
_message: String,
|
||||
) -> void:
|
||||
if (
|
||||
not _home_exit_transition_active
|
||||
or not _home_exit_waiting_for_network
|
||||
):
|
||||
return
|
||||
_home_exit_waiting_for_network = false
|
||||
if not accepted:
|
||||
_player.global_position = _home_exit_start_position
|
||||
_game_ui.get_screen_fade().fade_from_black(HOME_EXIT_FADE_GENERATION)
|
||||
|
||||
|
||||
func _cancel_home_exit_transition(reset_fade: bool) -> void:
|
||||
if _home_exit_walk_tween != null and _home_exit_walk_tween.is_valid():
|
||||
_home_exit_walk_tween.kill()
|
||||
_home_exit_walk_tween = null
|
||||
if is_instance_valid(_player):
|
||||
_player.end_animation_action()
|
||||
_player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, false)
|
||||
_player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, false)
|
||||
if (
|
||||
reset_fade
|
||||
and is_instance_valid(_game_ui)
|
||||
and is_instance_valid(_game_ui.get_screen_fade())
|
||||
):
|
||||
_game_ui.get_screen_fade().reset_immediately()
|
||||
_home_exit_transition_active = false
|
||||
_home_exit_waiting_for_network = false
|
||||
_home_exit_start_position = Vector3.ZERO
|
||||
|
||||
|
||||
func _on_rv_storage_requested() -> void:
|
||||
|
|
@ -2913,6 +3141,7 @@ func _can_show_shop_prompt() -> bool:
|
|||
and _shop_interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
)
|
||||
|
|
@ -2925,15 +3154,96 @@ func _can_show_decor_shop_prompt() -> bool:
|
|||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
)
|
||||
|
||||
|
||||
func _can_show_rv_upgrade_shop_prompt() -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
and _rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
and not _game_ui.get_rv_upgrade_shop().visible
|
||||
and not _game_ui.get_fishing_shop().visible
|
||||
and not _game_ui.get_decor_shop().visible
|
||||
and not _water_recovery.is_recovery_active()
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
)
|
||||
|
||||
|
||||
func _can_show_any_shop_prompt() -> bool:
|
||||
return (
|
||||
_can_show_shop_prompt()
|
||||
or _can_show_decor_shop_prompt()
|
||||
or _can_show_rv_upgrade_shop_prompt()
|
||||
)
|
||||
|
||||
|
||||
func _is_any_shop_npc_in_range() -> bool:
|
||||
return (
|
||||
(
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
or (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
or (
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _refresh_shop_npc_speech_state() -> void:
|
||||
var interaction := _get_active_shop_npc_interaction()
|
||||
var dialogue_text: String = ""
|
||||
if interaction != null:
|
||||
dialogue_text = interaction.get_dialogue_text(_home_state.get_tier())
|
||||
_game_ui.set_shop_npc_player_in_range(
|
||||
interaction != null,
|
||||
dialogue_text,
|
||||
)
|
||||
|
||||
|
||||
func _get_active_shop_npc_interaction() -> FishingShopInteractionType:
|
||||
# Match the interaction and prompt priority used above: RV, decor, fishing.
|
||||
if (
|
||||
_rv_upgrade_shop_interaction != null
|
||||
and _rv_upgrade_shop_interaction.is_local_player_in_range()
|
||||
):
|
||||
return _rv_upgrade_shop_interaction
|
||||
if (
|
||||
_decor_shop_interaction != null
|
||||
and _decor_shop_interaction.is_local_player_in_range()
|
||||
):
|
||||
return _decor_shop_interaction
|
||||
if (
|
||||
_shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
):
|
||||
return _shop_interaction
|
||||
return null
|
||||
|
||||
|
||||
func _can_show_storage_prompt() -> bool:
|
||||
return not _get_home_prompt().is_empty()
|
||||
|
||||
|
||||
func _refresh_home_door_prompt(prompt: Dictionary) -> void:
|
||||
# Home entry belongs to the doorway in world space. Keep the retired HUD
|
||||
# prompt hidden so it cannot overlap the compact door-mounted sign.
|
||||
_game_ui.set_storage_prompt_visible(false)
|
||||
_home_world.set_exterior_prompt_visible(
|
||||
str(prompt.get("owner_fingerprint", "")),
|
||||
not prompt.is_empty(),
|
||||
str(prompt.get("label", "enter home")),
|
||||
)
|
||||
|
||||
|
||||
func _can_show_rv_storage_button() -> bool:
|
||||
return (
|
||||
_gameplay_started
|
||||
|
|
@ -2950,6 +3260,7 @@ func _get_home_prompt() -> Dictionary:
|
|||
or _game_ui.get_player_storage().visible
|
||||
or _game_ui.get_fishing_shop().visible
|
||||
or _game_ui.get_decor_shop().visible
|
||||
or _game_ui.get_rv_upgrade_shop().visible
|
||||
or _water_recovery.is_recovery_active()
|
||||
or not _fishing_spot.can_open_fishing_shop()
|
||||
):
|
||||
|
|
@ -2958,20 +3269,12 @@ func _get_home_prompt() -> Dictionary:
|
|||
_network_home.get_local_home_owner_fingerprint()
|
||||
)
|
||||
if not owner_fingerprint.is_empty():
|
||||
if _home_world.is_avatar_near_interior_exit(
|
||||
_player, owner_fingerprint
|
||||
):
|
||||
return {
|
||||
"label": "leave RV",
|
||||
"anchor": _home_world.get_interior_prompt_position(
|
||||
owner_fingerprint
|
||||
),
|
||||
}
|
||||
return {}
|
||||
var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player)
|
||||
if nearby_owner.is_empty():
|
||||
return {}
|
||||
return {
|
||||
"label": "enter RV",
|
||||
"label": "enter home",
|
||||
"anchor": _home_world.get_exterior_prompt_position(nearby_owner),
|
||||
"owner_fingerprint": nearby_owner,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ var _manifests_by_fingerprint: Dictionary[String, Dictionary] = {}
|
|||
var _fingerprint_by_peer: Dictionary[int, String] = {}
|
||||
var _authorized_owned_counts: Dictionary[int, Dictionary] = {}
|
||||
var _authorization_result_owner: Dictionary[String, int] = {}
|
||||
var _authorized_home_tiers: Dictionary[int, int] = {}
|
||||
var _home_upgrade_result_owner: Dictionary[String, int] = {}
|
||||
var _interior_slots_by_space: Dictionary[StringName, Dictionary] = {}
|
||||
var _next_interior_index: int = 0
|
||||
var _suppress_local_submission: bool = false
|
||||
|
|
@ -103,6 +105,17 @@ func get_authoritative_owned_count(
|
|||
return int(owned.get(String(decor_id), 0))
|
||||
|
||||
|
||||
func get_authoritative_home_tier(peer_id: int) -> int:
|
||||
if _authorized_home_tiers.has(peer_id):
|
||||
return int(_authorized_home_tiers[peer_id])
|
||||
var fingerprint: String = _fingerprint_for_peer(peer_id)
|
||||
var manifest: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
|
||||
if manifest.is_empty():
|
||||
return -1
|
||||
var home: Dictionary = manifest.get("home", {})
|
||||
return int(home.get("tier", -1))
|
||||
|
||||
|
||||
func place_local_decor(
|
||||
decor_id: StringName,
|
||||
position: Vector3,
|
||||
|
|
@ -337,6 +350,35 @@ func finalize_decor_purchase(
|
|||
_authorized_owned_counts.erase(peer_id)
|
||||
|
||||
|
||||
func authorize_home_upgrade(
|
||||
peer_id: int,
|
||||
resulting_tier: int,
|
||||
result_id: String,
|
||||
) -> void:
|
||||
if (
|
||||
not _session.is_host()
|
||||
or peer_id <= 0
|
||||
or result_id.is_empty()
|
||||
or resulting_tier < PlayerHomeStateType.STARTER_TIER
|
||||
or resulting_tier > PlayerHomeStateType.MAX_TIER
|
||||
):
|
||||
return
|
||||
_authorized_home_tiers[peer_id] = resulting_tier
|
||||
_home_upgrade_result_owner[result_id] = peer_id
|
||||
|
||||
|
||||
func finalize_home_upgrade(
|
||||
peer_id: int,
|
||||
result_id: String,
|
||||
applied: bool,
|
||||
) -> void:
|
||||
if _home_upgrade_result_owner.get(result_id, 0) != peer_id:
|
||||
return
|
||||
_home_upgrade_result_owner.erase(result_id)
|
||||
if not applied:
|
||||
_authorized_home_tiers.erase(peer_id)
|
||||
|
||||
|
||||
func refresh_world_presentations() -> void:
|
||||
if _world_service == null:
|
||||
return
|
||||
|
|
@ -442,7 +484,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
|
|||
not NetworkHomeProtocolType.validate_manifest_request(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
):
|
||||
_reject_manifest(peer_id, "The RV update was invalid.")
|
||||
_reject_manifest(peer_id, "The home update was invalid.")
|
||||
return
|
||||
var record := _session.get_peer_record(peer_id)
|
||||
var fingerprint: String = str(data["owner_fingerprint"])
|
||||
|
|
@ -456,7 +498,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
|
|||
data["sender_signature"],
|
||||
)
|
||||
):
|
||||
_reject_manifest(peer_id, "The RV owner could not be verified.")
|
||||
_reject_manifest(peer_id, "The home owner could not be verified.")
|
||||
return
|
||||
var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data(
|
||||
data["home"]
|
||||
|
|
@ -464,7 +506,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
|
|||
var prior: Dictionary = _manifests_by_fingerprint.get(fingerprint, {})
|
||||
var is_first_manifest: bool = prior.is_empty()
|
||||
if not _valid_manifest_revision(peer_id, prior, home_data):
|
||||
_reject_manifest(peer_id, "The RV update was rejected by the host.")
|
||||
_reject_manifest(peer_id, "The home update was rejected by the host.")
|
||||
return
|
||||
if not _moved_decor_clear_of_players(fingerprint, prior, home_data):
|
||||
_reject_manifest(peer_id, "A player is in the way of that decor.")
|
||||
|
|
@ -472,7 +514,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void:
|
|||
var manifest: Dictionary
|
||||
if prior.is_empty():
|
||||
if _manifests_by_fingerprint.size() >= MAX_MANIFESTS:
|
||||
_reject_manifest(peer_id, "This room cannot place another RV.")
|
||||
_reject_manifest(peer_id, "This room cannot place another home.")
|
||||
return
|
||||
var reserved: Array[Transform3D] = []
|
||||
for existing: Dictionary in _manifests_by_fingerprint.values():
|
||||
|
|
@ -523,10 +565,17 @@ func _valid_manifest_revision(
|
|||
# no prior room ledger with which to compare its first manifest.
|
||||
return true
|
||||
var prior_home: Dictionary = prior_manifest["home"]
|
||||
var prior_tier: int = int(prior_home["tier"])
|
||||
var next_tier: int = int(home_data["tier"])
|
||||
var tier_is_authorized: bool = (
|
||||
next_tier != prior_tier
|
||||
and int(_authorized_home_tiers.get(peer_id, -1)) == next_tier
|
||||
)
|
||||
if (
|
||||
int(home_data["revision"]) <= int(prior_home["revision"])
|
||||
or int(home_data["tier"]) != int(prior_home["tier"])
|
||||
or str(home_data["exterior_id"]) != str(prior_home["exterior_id"])
|
||||
or (next_tier != prior_tier and not tier_is_authorized)
|
||||
or str(home_data["exterior_id"])
|
||||
!= str(PlayerHomeStateType.exterior_id_for_tier(next_tier))
|
||||
):
|
||||
return false
|
||||
var prior_owned: Dictionary = prior_home["owned_decor"]
|
||||
|
|
@ -619,6 +668,11 @@ static func _placement_position(placement: Dictionary) -> Vector3:
|
|||
|
||||
|
||||
func _consume_owned_authorizations(peer_id: int, home_data: Dictionary) -> void:
|
||||
if (
|
||||
_authorized_home_tiers.has(peer_id)
|
||||
and int(home_data["tier"]) == int(_authorized_home_tiers[peer_id])
|
||||
):
|
||||
_authorized_home_tiers.erase(peer_id)
|
||||
var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {})
|
||||
if authorized.is_empty():
|
||||
return
|
||||
|
|
@ -1027,7 +1081,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void:
|
|||
data["sender_signature"],
|
||||
)
|
||||
):
|
||||
_send_transition_rejection(peer_id, "The RV transition was invalid.")
|
||||
_send_transition_rejection(peer_id, "The home transition was invalid.")
|
||||
return
|
||||
var owner_fingerprint: String = str(data["owner_fingerprint"])
|
||||
var action: int = int(data["action"])
|
||||
|
|
@ -1036,7 +1090,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void:
|
|||
owner_fingerprint, {}
|
||||
)
|
||||
if avatar == null or manifest.is_empty():
|
||||
_send_transition_rejection(peer_id, "That RV is unavailable.")
|
||||
_send_transition_rejection(peer_id, "That home is unavailable.")
|
||||
return
|
||||
if action == NetworkHomeProtocolType.TransitionAction.ENTER:
|
||||
var record := _session.get_peer_record(peer_id)
|
||||
|
|
@ -1052,7 +1106,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void:
|
|||
avatar, owner_fingerprint
|
||||
)
|
||||
):
|
||||
_send_transition_rejection(peer_id, "You cannot enter that RV.")
|
||||
_send_transition_rejection(peer_id, "You cannot enter that home.")
|
||||
return
|
||||
var interior_space: StringName = (
|
||||
NetworkHomeProtocolType.make_space_id(owner_fingerprint)
|
||||
|
|
@ -1076,7 +1130,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void:
|
|||
avatar, owner_fingerprint
|
||||
)
|
||||
):
|
||||
_send_transition_rejection(peer_id, "Move closer to the RV door.")
|
||||
_send_transition_rejection(peer_id, "Move closer to the home door.")
|
||||
return
|
||||
_apply_host_transition(
|
||||
peer_id,
|
||||
|
|
@ -1282,6 +1336,10 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
var fingerprint: String = _fingerprint_by_peer.get(peer_id, "")
|
||||
_fingerprint_by_peer.erase(peer_id)
|
||||
_authorized_owned_counts.erase(peer_id)
|
||||
_authorized_home_tiers.erase(peer_id)
|
||||
for result_id: String in _home_upgrade_result_owner.keys():
|
||||
if int(_home_upgrade_result_owner[result_id]) == peer_id:
|
||||
_home_upgrade_result_owner.erase(result_id)
|
||||
if fingerprint.is_empty():
|
||||
return
|
||||
if _session.is_host():
|
||||
|
|
@ -1336,6 +1394,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_fingerprint_by_peer.clear()
|
||||
_authorized_owned_counts.clear()
|
||||
_authorization_result_owner.clear()
|
||||
_authorized_home_tiers.clear()
|
||||
_home_upgrade_result_owner.clear()
|
||||
_publish_queued = false
|
||||
_last_submitted_local_home_revision = -1
|
||||
_pending_local_decor_transactions.clear()
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ const FRIENDS_CAPABILITY: String = "friends_v1"
|
|||
const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2"
|
||||
const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2"
|
||||
const HOME_CAPABILITY: String = "portable_home_v1"
|
||||
const HOME_UPGRADE_CAPABILITY: String = "home_upgrade_v1"
|
||||
const DEFAULT_WORLD_SEED: int = 13001
|
||||
const MAX_WORLD_SEED: int = 2147483646
|
||||
|
||||
|
|
@ -205,6 +206,7 @@ static func make_client_hello(
|
|||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
FISHING_REPLICATION_CAPABILITY,
|
||||
HOME_CAPABILITY,
|
||||
HOME_UPGRADE_CAPABILITY,
|
||||
]),
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
"identity_fingerprint": identity_fingerprint,
|
||||
|
|
@ -346,6 +348,7 @@ static func make_server_hello(
|
|||
MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
FISHING_REPLICATION_CAPABILITY,
|
||||
HOME_CAPABILITY,
|
||||
HOME_UPGRADE_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
|
|||
|
|
@ -340,6 +340,7 @@ func _register_player_host() -> void:
|
|||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
NetworkProtocol.HOME_UPGRADE_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
|
|
@ -668,6 +669,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
NetworkProtocol.HOME_UPGRADE_CAPABILITY,
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
|
|
@ -1638,6 +1640,7 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY,
|
||||
NetworkProtocol.FISHING_REPLICATION_CAPABILITY,
|
||||
NetworkProtocol.HOME_CAPABILITY,
|
||||
NetworkProtocol.HOME_UPGRADE_CAPABILITY,
|
||||
]),
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const CAPABILITY: StringName = &"shop_v1"
|
|||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||
const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1"
|
||||
const DECOR_CAPABILITY: StringName = &"portable_home_v1"
|
||||
const HOME_UPGRADE_CAPABILITY: StringName = &"home_upgrade_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
|
|
@ -15,7 +16,7 @@ enum ProductCategory {
|
|||
ROD,
|
||||
REEL_SPEED_UPGRADE,
|
||||
BARRIER_POWER_UPGRADE,
|
||||
COOLER_CAPACITY_UPGRADE,
|
||||
HOME_UPGRADE,
|
||||
ART_KIT,
|
||||
ART_UPGRADE,
|
||||
BACKPACK_CAPACITY_UPGRADE,
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
|||
|
||||
const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
|
||||
const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
|
||||
const RV_UPGRADE_SHOP_ID: StringName = &"rv_upgrade_shop"
|
||||
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
||||
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
|
||||
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
|
||||
const HOME_UPGRADE_PRODUCT_ID: StringName = &"starter_rv_upgrade"
|
||||
const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade"
|
||||
const MAX_LEDGER_ENTRIES_PER_PEER: int = 64
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ var _spawn_service: PlayerSpawnService
|
|||
var _network_fishing: NetworkFishingService
|
||||
var _interaction: FishingShopInteraction
|
||||
var _decor_interaction: DecorShopInteraction
|
||||
var _rv_upgrade_interaction: FishingShopInteraction
|
||||
var _wallet: PlayerWallet
|
||||
var _bag: PlayerBag
|
||||
var _item_catalog: ItemCatalog
|
||||
|
|
@ -71,6 +73,7 @@ func setup(
|
|||
home_state: PlayerHomeState = null,
|
||||
network_home: NetworkHomeService = null,
|
||||
decor_interaction: DecorShopInteraction = null,
|
||||
rv_upgrade_interaction: FishingShopInteraction = null,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
|
|
@ -88,6 +91,7 @@ func setup(
|
|||
_home_state = home_state
|
||||
_network_home = network_home
|
||||
_decor_interaction = decor_interaction
|
||||
_rv_upgrade_interaction = rv_upgrade_interaction
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
if not _session.state_changed.is_connected(_on_session_state_changed):
|
||||
|
|
@ -104,6 +108,12 @@ func set_decor_shop_interaction(
|
|||
_decor_interaction = interaction
|
||||
|
||||
|
||||
func set_rv_upgrade_shop_interaction(
|
||||
interaction: FishingShopInteraction,
|
||||
) -> void:
|
||||
_rv_upgrade_interaction = interaction
|
||||
|
||||
|
||||
func can_request_purchase() -> bool:
|
||||
return (
|
||||
_session != null
|
||||
|
|
@ -154,6 +164,19 @@ func can_request_decor_purchase() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func can_request_home_upgrade() -> bool:
|
||||
return (
|
||||
can_request_purchase()
|
||||
and _home_state != null
|
||||
and (
|
||||
_session.is_host()
|
||||
or _session.supports_server_capability(
|
||||
NetworkShopProtocol.HOME_UPGRADE_CAPABILITY
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_local_purchase_pending() -> bool:
|
||||
return not _pending_local_request.is_empty()
|
||||
|
||||
|
|
@ -204,12 +227,12 @@ func request_barrier_power_upgrade() -> String:
|
|||
)
|
||||
|
||||
|
||||
func request_cooler_capacity_upgrade() -> String:
|
||||
func request_home_upgrade() -> String:
|
||||
return _request_purchase(
|
||||
COOLER_PRODUCT_ID,
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE,
|
||||
HOME_UPGRADE_PRODUCT_ID,
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE,
|
||||
1,
|
||||
_cooler_capacity.get_level() if _cooler_capacity != null else 0
|
||||
_home_state.get_tier() if _home_state != null else 0
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -308,6 +331,15 @@ func _request_purchase(
|
|||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
if (
|
||||
category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE
|
||||
and not can_request_home_upgrade()
|
||||
):
|
||||
local_purchase_finished.emit(
|
||||
"", false, "RV upgrades require a newer server.",
|
||||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
if (
|
||||
category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
and not can_request_decor_purchase()
|
||||
|
|
@ -318,11 +350,7 @@ func _request_purchase(
|
|||
)
|
||||
return ""
|
||||
var request_id: String = _new_id("shop")
|
||||
var shop_id: StringName = (
|
||||
DECOR_SHOP_ID
|
||||
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
else FISHING_SHOP_ID
|
||||
)
|
||||
var shop_id: StringName = _shop_id_for_category(category)
|
||||
var request: Dictionary = {
|
||||
"request_id": request_id,
|
||||
"session_id": _session.get_session_id(),
|
||||
|
|
@ -402,9 +430,7 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
|
|||
if not _is_shop_available_for_peer(peer_id, requested_shop_id):
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
var requested_interaction: FishingShopInteraction = (
|
||||
_decor_interaction
|
||||
if requested_shop_id == DECOR_SHOP_ID
|
||||
else _interaction
|
||||
_interaction_for_shop(requested_shop_id)
|
||||
)
|
||||
var message: String = (
|
||||
"The shop is unavailable."
|
||||
|
|
@ -416,16 +442,22 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
|
|||
var result: Dictionary = _build_authoritative_result(peer_id, data)
|
||||
if (
|
||||
bool(result.get("accepted", false))
|
||||
and int(result.get("category", -1))
|
||||
== NetworkShopProtocol.ProductCategory.DECOR
|
||||
and _network_home != null
|
||||
):
|
||||
_network_home.authorize_decor_purchase(
|
||||
peer_id,
|
||||
StringName(str(result["product_id"])),
|
||||
int(result["resulting_state"]),
|
||||
str(result["result_id"]),
|
||||
)
|
||||
match int(result.get("category", -1)):
|
||||
NetworkShopProtocol.ProductCategory.DECOR:
|
||||
_network_home.authorize_decor_purchase(
|
||||
peer_id,
|
||||
StringName(str(result["product_id"])),
|
||||
int(result["resulting_state"]),
|
||||
str(result["result_id"]),
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE:
|
||||
_network_home.authorize_home_upgrade(
|
||||
peer_id,
|
||||
int(result["resulting_state"]),
|
||||
str(result["result_id"]),
|
||||
)
|
||||
_pending_by_peer[peer_id] = request_id
|
||||
_record_and_send(peer_id, result)
|
||||
|
||||
|
|
@ -448,14 +480,19 @@ func _build_authoritative_result(
|
|||
)
|
||||
if authoritative_count >= 0:
|
||||
current_state = authoritative_count
|
||||
elif (
|
||||
category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE
|
||||
and _network_home != null
|
||||
):
|
||||
var authoritative_tier: int = (
|
||||
_network_home.get_authoritative_home_tier(peer_id)
|
||||
)
|
||||
if authoritative_tier >= 0:
|
||||
current_state = authoritative_tier
|
||||
var cost: int = -1
|
||||
var resulting_state: int = current_state
|
||||
var rejection: String = ""
|
||||
var expected_shop_id: StringName = (
|
||||
DECOR_SHOP_ID
|
||||
if category == NetworkShopProtocol.ProductCategory.DECOR
|
||||
else FISHING_SHOP_ID
|
||||
)
|
||||
var expected_shop_id: StringName = _shop_id_for_category(category)
|
||||
if StringName(str(request["shop_id"])) != expected_shop_id:
|
||||
rejection = "The shop is unavailable."
|
||||
elif quantity < 1:
|
||||
|
|
@ -569,18 +606,14 @@ func _build_authoritative_result(
|
|||
]
|
||||
)
|
||||
resulting_state = current_state + 1
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE:
|
||||
if (
|
||||
product_id != COOLER_PRODUCT_ID
|
||||
or current_state >= PlayerCoolerCapacity.MAX_LEVEL
|
||||
product_id != HOME_UPGRADE_PRODUCT_ID
|
||||
or current_state >= PlayerHomeState.MAX_TIER
|
||||
):
|
||||
rejection = "Upgrade is already at maximum."
|
||||
else:
|
||||
cost = (
|
||||
PlayerCoolerCapacity.EXPANSION_COSTS[
|
||||
current_state
|
||||
]
|
||||
)
|
||||
cost = PlayerHomeState.HOME_UPGRADE_COSTS[current_state]
|
||||
resulting_state = current_state + 1
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
if (
|
||||
|
|
@ -874,10 +907,12 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
return "Purchase could not be completed."
|
||||
if _upgrades.get_next_barrier_power_cost() != cost:
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||
if _cooler_capacity.get_level() != expected_state:
|
||||
return "Purchase could not be completed."
|
||||
if _cooler_capacity.get_next_cost() != cost:
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE:
|
||||
if (
|
||||
product_id != HOME_UPGRADE_PRODUCT_ID
|
||||
or _home_state.get_tier() != expected_state
|
||||
or _home_state.get_next_upgrade_cost() != cost
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
if (
|
||||
|
|
@ -947,8 +982,11 @@ func _apply_local_product(data: Dictionary) -> bool:
|
|||
return _upgrades.purchase_reel_speed(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE:
|
||||
return _upgrades.purchase_barrier_power(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||
return _cooler_capacity.purchase(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE:
|
||||
return (
|
||||
_wallet.debit(int(data["total_cost"]))
|
||||
and _home_state.upgrade_to_next_tier()
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE:
|
||||
return (
|
||||
_inventory_layout != null
|
||||
|
|
@ -1061,6 +1099,9 @@ func _handle_acknowledgement(
|
|||
_network_home.finalize_decor_purchase(
|
||||
peer_id, result_id, applied
|
||||
)
|
||||
_network_home.finalize_home_upgrade(
|
||||
peer_id, result_id, applied
|
||||
)
|
||||
_acknowledged_results[result_id] = true
|
||||
_bound_dictionary(_acknowledged_results)
|
||||
if _pending_by_peer.get(peer_id, "") == request_id:
|
||||
|
|
@ -1071,11 +1112,7 @@ func _is_shop_available_for_peer(
|
|||
peer_id: int,
|
||||
shop_id: StringName,
|
||||
) -> bool:
|
||||
var interaction: FishingShopInteraction = (
|
||||
_decor_interaction
|
||||
if shop_id == DECOR_SHOP_ID
|
||||
else _interaction
|
||||
)
|
||||
var interaction: FishingShopInteraction = _interaction_for_shop(shop_id)
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_host()
|
||||
|
|
@ -1097,6 +1134,24 @@ func _is_shop_available_for_peer(
|
|||
)
|
||||
|
||||
|
||||
func _shop_id_for_category(category: int) -> StringName:
|
||||
match category:
|
||||
NetworkShopProtocol.ProductCategory.DECOR:
|
||||
return DECOR_SHOP_ID
|
||||
NetworkShopProtocol.ProductCategory.HOME_UPGRADE:
|
||||
return RV_UPGRADE_SHOP_ID
|
||||
return FISHING_SHOP_ID
|
||||
|
||||
|
||||
func _interaction_for_shop(shop_id: StringName) -> FishingShopInteraction:
|
||||
match shop_id:
|
||||
DECOR_SHOP_ID:
|
||||
return _decor_interaction
|
||||
RV_UPGRADE_SHOP_ID:
|
||||
return _rv_upgrade_interaction
|
||||
return _interaction
|
||||
|
||||
|
||||
func _on_peer_removed(peer_id: int) -> void:
|
||||
_request_ledgers.erase(peer_id)
|
||||
_pending_by_peer.erase(peer_id)
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@ const CATCH_SHOWCASE_INPUT_OWNER: StringName = &"catch_showcase"
|
|||
# safe IDs so newer clients can extend it, but Player only presents actions
|
||||
# explicitly approved by this catalog.
|
||||
const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [
|
||||
CHARACTER_WALKING_ANIMATION,
|
||||
CHARACTER_CASTING_ANIMATION,
|
||||
CHARACTER_NET_DRAW_ANIMATION,
|
||||
CHARACTER_NET_STRIKE_ANIMATION,
|
||||
|
|
@ -436,6 +437,11 @@ var _rv_previous_camera_environment: Environment
|
|||
@export var maximum_zoom: float = 8.0
|
||||
@export var zoom_step: float = 0.75
|
||||
@export var zoom_smoothing: float = 12.0
|
||||
@export_range(0.0, 1.0, 0.01) var fishing_reel_zoom_amount: float = 0.22
|
||||
@export_range(0.0, 1.5, 0.01) var fishing_barrier_break_zoom: float = 0.38
|
||||
@export_range(0.05, 1.0, 0.01) var fishing_break_zoom_decay: float = 0.34
|
||||
@export_range(0.0, 0.2, 0.005) var fishing_barrier_hit_shake: float = 0.018
|
||||
@export_range(0.0, 0.3, 0.005) var fishing_barrier_break_shake: float = 0.045
|
||||
@export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0
|
||||
@export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55
|
||||
@export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0
|
||||
|
|
@ -497,6 +503,12 @@ var _remote_recovery_presentation_active: bool = false
|
|||
var _remote_recovery_visual_origin: Vector3
|
||||
var _remote_recovery_elapsed: float = 0.0
|
||||
var _target_zoom: float = 5.0
|
||||
var _fishing_reel_camera_active: bool = false
|
||||
var _fishing_break_zoom_offset: float = 0.0
|
||||
var _fishing_camera_shake_remaining: float = 0.0
|
||||
var _fishing_camera_shake_duration: float = 0.0
|
||||
var _fishing_camera_shake_strength: float = 0.0
|
||||
var _fishing_camera_shake_phase: float = 0.0
|
||||
var _showcase_rod_visibility: bool = true
|
||||
var _showcase_net_visibility: bool = false
|
||||
var _showcase_shovel_visibility: bool = false
|
||||
|
|
@ -1162,6 +1174,7 @@ func _process(delta: float) -> void:
|
|||
)
|
||||
if not local_control_enabled:
|
||||
return
|
||||
_update_fishing_camera_effects(delta)
|
||||
if _free_camera_active:
|
||||
if _is_camera_input_enabled():
|
||||
_rotate_free_camera(
|
||||
|
|
@ -1190,13 +1203,68 @@ func _process(delta: float) -> void:
|
|||
)
|
||||
|
||||
var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta)
|
||||
var fishing_zoom_offset: float = (
|
||||
fishing_reel_zoom_amount
|
||||
if _fishing_reel_camera_active
|
||||
else 0.0
|
||||
) + _fishing_break_zoom_offset
|
||||
var effective_target_zoom: float = clampf(
|
||||
_target_zoom - fishing_zoom_offset,
|
||||
1.25 if _rv_interior_camera_mode else minimum_zoom,
|
||||
16.0 if _rv_interior_camera_mode else maximum_zoom,
|
||||
)
|
||||
_spring_arm.spring_length = lerpf(
|
||||
_spring_arm.spring_length,
|
||||
_target_zoom,
|
||||
effective_target_zoom,
|
||||
zoom_weight
|
||||
)
|
||||
|
||||
|
||||
func set_fishing_reel_camera_active(active: bool) -> void:
|
||||
_fishing_reel_camera_active = active
|
||||
|
||||
|
||||
func play_fishing_barrier_camera_feedback(barrier_broken: bool) -> void:
|
||||
if not local_control_enabled or _free_camera_active:
|
||||
return
|
||||
_fishing_camera_shake_strength = (
|
||||
fishing_barrier_break_shake
|
||||
if barrier_broken
|
||||
else fishing_barrier_hit_shake
|
||||
)
|
||||
_fishing_camera_shake_duration = 0.20 if barrier_broken else 0.10
|
||||
_fishing_camera_shake_remaining = _fishing_camera_shake_duration
|
||||
if barrier_broken:
|
||||
_fishing_break_zoom_offset = fishing_barrier_break_zoom
|
||||
|
||||
|
||||
func _update_fishing_camera_effects(delta: float) -> void:
|
||||
_fishing_break_zoom_offset = move_toward(
|
||||
_fishing_break_zoom_offset,
|
||||
0.0,
|
||||
(
|
||||
fishing_barrier_break_zoom
|
||||
/ maxf(fishing_break_zoom_decay, 0.05)
|
||||
) * delta,
|
||||
)
|
||||
if _fishing_camera_shake_remaining <= 0.0:
|
||||
_camera.h_offset = 0.0
|
||||
_camera.v_offset = 0.0
|
||||
return
|
||||
_fishing_camera_shake_remaining = maxf(
|
||||
_fishing_camera_shake_remaining - delta,
|
||||
0.0,
|
||||
)
|
||||
_fishing_camera_shake_phase += delta * 92.0
|
||||
var decay: float = (
|
||||
_fishing_camera_shake_remaining
|
||||
/ maxf(_fishing_camera_shake_duration, 0.001)
|
||||
)
|
||||
var strength: float = _fishing_camera_shake_strength * decay
|
||||
_camera.h_offset = sin(_fishing_camera_shake_phase) * strength
|
||||
_camera.v_offset = cos(_fishing_camera_shake_phase * 1.31) * strength * 0.7
|
||||
|
||||
|
||||
func _update_blink(delta: float) -> void:
|
||||
if _fighting_visual_active:
|
||||
return
|
||||
|
|
@ -3052,6 +3120,19 @@ func set_camera_active(active: bool) -> void:
|
|||
_camera.current = active
|
||||
|
||||
|
||||
func face_world_direction(direction: Vector3) -> void:
|
||||
var horizontal_direction := Vector3(direction.x, 0.0, direction.z)
|
||||
if horizontal_direction.is_zero_approx() or _visuals == null:
|
||||
return
|
||||
horizontal_direction = horizontal_direction.normalized()
|
||||
var target_world_yaw: float = atan2(
|
||||
-horizontal_direction.x,
|
||||
-horizontal_direction.z,
|
||||
)
|
||||
_visuals.rotation.y = target_world_yaw - global_rotation.y
|
||||
_network_target_visual_yaw = _visuals.rotation.y
|
||||
|
||||
|
||||
func apply_camera_settings(
|
||||
new_mouse_sensitivity: float,
|
||||
new_controller_sensitivity: float,
|
||||
|
|
|
|||
|
|
@ -2,62 +2,77 @@ class_name PlayerCoolerCapacity
|
|||
extends Node
|
||||
|
||||
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||
const PlayerHomeStateType = preload("res://homes/player_home_state.gd")
|
||||
|
||||
signal capacity_changed(level: int, capacity: int)
|
||||
|
||||
## Kept under the historical class name so existing saves and network product
|
||||
## identifiers migrate without losing their purchased level. This progression
|
||||
## now controls the player's private storage box rather than carried catches.
|
||||
## Compatibility adapter for saves and systems that still refer to the old
|
||||
## cooler-capacity object. Storage is now derived exclusively from the player's
|
||||
## home tier; this object is not independently purchasable.
|
||||
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 _home_state: PlayerHomeStateType
|
||||
var _legacy_capacity_level: int = 0
|
||||
|
||||
|
||||
func bind_home_state(home_state: PlayerHomeStateType) -> void:
|
||||
if _home_state == home_state:
|
||||
return
|
||||
if (
|
||||
_home_state != null
|
||||
and _home_state.changed.is_connected(_on_home_changed)
|
||||
):
|
||||
_home_state.changed.disconnect(_on_home_changed)
|
||||
_home_state = home_state
|
||||
if (
|
||||
_home_state != null
|
||||
and not _home_state.changed.is_connected(_on_home_changed)
|
||||
):
|
||||
_home_state.changed.connect(_on_home_changed)
|
||||
_on_home_changed()
|
||||
|
||||
|
||||
func get_level() -> int:
|
||||
return _capacity_level
|
||||
return _legacy_capacity_level
|
||||
|
||||
|
||||
func get_capacity() -> int:
|
||||
return CAPACITIES[_capacity_level]
|
||||
if _home_state != null:
|
||||
# A new home upgrade grants its authored capacity. Older saves retain any
|
||||
# separately purchased storage level so reducing the first RV tier cannot
|
||||
# hide or discard items that were already placed in later slots.
|
||||
return maxi(
|
||||
_home_state.get_storage_capacity(),
|
||||
CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)],
|
||||
)
|
||||
return CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)]
|
||||
|
||||
|
||||
func get_next_capacity() -> int:
|
||||
if _capacity_level >= MAX_LEVEL:
|
||||
return -1
|
||||
return CAPACITIES[_capacity_level + 1]
|
||||
return -1
|
||||
|
||||
|
||||
func get_next_cost() -> int:
|
||||
if _capacity_level >= MAX_LEVEL:
|
||||
return -1
|
||||
return EXPANSION_COSTS[_capacity_level]
|
||||
return -1
|
||||
|
||||
|
||||
func can_purchase(wallet: PlayerWalletType) -> bool:
|
||||
var cost: int = get_next_cost()
|
||||
return wallet != null and cost >= 0 and wallet.can_afford(cost)
|
||||
func can_purchase(_wallet: PlayerWalletType) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func purchase(wallet: PlayerWalletType) -> bool:
|
||||
if not can_purchase(wallet):
|
||||
return false
|
||||
var cost: int = get_next_cost()
|
||||
if not wallet.debit(cost):
|
||||
return false
|
||||
_capacity_level += 1
|
||||
capacity_changed.emit(_capacity_level, get_capacity())
|
||||
return true
|
||||
func purchase(_wallet: PlayerWalletType) -> bool:
|
||||
return false
|
||||
|
||||
|
||||
func restore_level(level: int) -> bool:
|
||||
var validated: int = clampi(level, 0, MAX_LEVEL)
|
||||
var changed: bool = _capacity_level != validated
|
||||
_capacity_level = validated
|
||||
var changed: bool = _legacy_capacity_level != validated
|
||||
_legacy_capacity_level = validated
|
||||
if changed:
|
||||
capacity_changed.emit(_capacity_level, get_capacity())
|
||||
capacity_changed.emit(get_level(), get_capacity())
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -66,4 +81,8 @@ func reset_to_defaults() -> void:
|
|||
|
||||
|
||||
func to_save_data() -> Dictionary:
|
||||
return {"capacity_level": _capacity_level}
|
||||
return {"capacity_level": get_level()}
|
||||
|
||||
|
||||
func _on_home_changed() -> void:
|
||||
capacity_changed.emit(get_level(), get_capacity())
|
||||
|
|
|
|||
|
|
@ -338,6 +338,11 @@ func load_player_data() -> bool:
|
|||
snapshot.reel_speed_level,
|
||||
snapshot.barrier_power_level
|
||||
)
|
||||
# Home tier is now the authority for private-storage capacity, so restore it
|
||||
# before the compatibility capacity adapter and inventory layout.
|
||||
var home_restored: bool = _home_state.restore_from_save_data(
|
||||
snapshot.home_data
|
||||
)
|
||||
var cooler_restored: bool = _cooler_capacity.restore_level(
|
||||
snapshot.cooler_capacity_level
|
||||
)
|
||||
|
|
@ -368,9 +373,6 @@ func load_player_data() -> bool:
|
|||
snapshot.world_weather_seconds_remaining,
|
||||
)
|
||||
var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data)
|
||||
var home_restored: bool = _home_state.restore_from_save_data(
|
||||
snapshot.home_data
|
||||
)
|
||||
_is_restoring = false
|
||||
if (
|
||||
not inventory_restored
|
||||
|
|
@ -1324,6 +1326,26 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
PlayerCoolerCapacityType.MAX_LEVEL,
|
||||
"cooler.capacity_level"
|
||||
)
|
||||
# Before homes had upgrade tiers, storage was purchased separately. Preserve
|
||||
# those saves by granting the starter RV to anybody who owned at least one
|
||||
# legacy expansion. PlayerCoolerCapacity also retains the purchased legacy
|
||||
# capacity so entries beyond the new RV's first 18 slots remain accessible.
|
||||
if (
|
||||
snapshot.cooler_capacity_level > 0
|
||||
and int(snapshot.home_data.get("tier", 0))
|
||||
== PlayerHomeStateType.STARTER_TIER
|
||||
):
|
||||
var migrated_home: Dictionary = home_data.duplicate(true)
|
||||
migrated_home["tier"] = PlayerHomeStateType.STARTER_RV_TIER
|
||||
migrated_home["exterior_id"] = String(
|
||||
PlayerHomeStateType.STARTER_RV_EXTERIOR_ID
|
||||
)
|
||||
var sanitized_migrated_home := (
|
||||
PlayerHomeStateType.sanitize_save_data(migrated_home)
|
||||
)
|
||||
if sanitized_migrated_home.is_empty():
|
||||
return null
|
||||
snapshot.home_data = sanitized_migrated_home
|
||||
snapshot.art_unlock_mask = _read_integer(
|
||||
art_data.get("unlock_mask"),
|
||||
0,
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ func _run() -> void:
|
|||
)
|
||||
assert(player != null)
|
||||
assert(catalog != null and catalog.candidates.size() == 316)
|
||||
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 313)
|
||||
assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 314)
|
||||
assert(sale_service != null)
|
||||
assert(shop_service != null)
|
||||
assert(session != null and session.is_host())
|
||||
|
|
@ -82,7 +82,7 @@ func _run() -> void:
|
|||
main, player, catalog, sale_service, reservations
|
||||
)
|
||||
_test_host_backpack_purchase(player, shop_service)
|
||||
_test_host_storage_purchase(player, shop_service)
|
||||
await _test_host_home_upgrade(main, player, shop_service)
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
assert(not shop_service.is_local_purchase_pending())
|
||||
|
||||
|
|
@ -175,7 +175,6 @@ func _run_multiplayer_client() -> void:
|
|||
assert(player.inventory_layout.restore_backpack_level(
|
||||
PlayerInventoryLayout.MAX_BACKPACK_LEVEL
|
||||
))
|
||||
assert(player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL))
|
||||
player.inventory.set_inventory_layout(null)
|
||||
var catch_ids: Array[StringName] = []
|
||||
for fish: FishData in catalog.candidates:
|
||||
|
|
@ -202,7 +201,6 @@ func _run_multiplayer_client() -> void:
|
|||
assert(not player.inventory.contains_catch_id(catch_id))
|
||||
assert(not sale_service.is_local_sale_pending())
|
||||
assert(player.inventory_layout.restore_backpack_level(0))
|
||||
assert(player.cooler_capacity.restore_level(0))
|
||||
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
player.global_position = interaction.global_position
|
||||
|
|
@ -567,27 +565,49 @@ func _test_host_backpack_purchase(
|
|||
assert(layout.get_next_backpack_cost() == -1)
|
||||
|
||||
|
||||
func _test_host_storage_purchase(
|
||||
func _test_host_home_upgrade(
|
||||
main: Node,
|
||||
player: Player,
|
||||
shop_service: NetworkShopService,
|
||||
) -> void:
|
||||
var interaction := main.get(
|
||||
"_rv_upgrade_shop_interaction"
|
||||
) as RVUpgradeInteraction
|
||||
var home_state := main.get("_home_state") as PlayerHomeState
|
||||
var capacity := player.cooler_capacity
|
||||
assert(interaction != null and home_state != null)
|
||||
player.global_position = interaction.global_position
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
assert(interaction.is_avatar_in_range(player))
|
||||
assert(home_state.get_tier() == PlayerHomeState.STARTER_TIER)
|
||||
assert(
|
||||
interaction.get_dialogue_text(home_state.get_tier())
|
||||
== RVUpgradeInteraction.TENT_DIALOGUE_TEXT
|
||||
)
|
||||
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 upgrade_cost: int = home_state.get_next_upgrade_cost()
|
||||
assert(upgrade_cost == PlayerHomeState.HOME_UPGRADE_COSTS.front())
|
||||
assert(upgrade_cost == 50)
|
||||
if player.wallet.get_balance() < upgrade_cost:
|
||||
assert(player.wallet.credit(
|
||||
upgrade_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]),
|
||||
"Cooler purchase failed: %s" % [_shop_result],
|
||||
)
|
||||
assert(capacity.get_capacity() == expected_capacity)
|
||||
assert(player.wallet.get_balance() == balance_before - total_cost)
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_home_upgrade().is_empty())
|
||||
assert(
|
||||
not _shop_result.is_empty() and bool(_shop_result[1]),
|
||||
"Home purchase failed: %s" % [_shop_result],
|
||||
)
|
||||
assert(home_state.get_tier() == PlayerHomeState.STARTER_RV_TIER)
|
||||
assert(
|
||||
interaction.get_dialogue_text(home_state.get_tier())
|
||||
== RVUpgradeInteraction.RV_DIALOGUE_TEXT
|
||||
)
|
||||
assert(capacity.get_capacity() == 18)
|
||||
assert(player.wallet.get_balance() == balance_before - upgrade_cost)
|
||||
assert(home_state.get_next_upgrade_cost() == -1)
|
||||
assert(capacity.get_next_cost() == -1)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ func _run_host() -> void:
|
|||
host_fingerprint
|
||||
)
|
||||
assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
|
||||
assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint))
|
||||
assert(network_home.request_enter(host_fingerprint))
|
||||
await physics_frame
|
||||
host_avatar.global_position = home_world.get_interior_exit_position(
|
||||
|
|
@ -57,6 +58,7 @@ func _run_host() -> void:
|
|||
assert(network_home.request_exit())
|
||||
await physics_frame
|
||||
assert(_same_door_position(host_avatar.global_position, host_spawn.origin))
|
||||
assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint))
|
||||
|
||||
var remote_peer_id: int = await _wait_for_remote_peer(session)
|
||||
assert(remote_peer_id > 1)
|
||||
|
|
@ -70,6 +72,9 @@ func _run_host() -> void:
|
|||
assert(_same_door_position(
|
||||
remote_avatar.global_position, remote_spawn.origin
|
||||
))
|
||||
assert(_faces_away_from_home(
|
||||
remote_avatar, home_world, remote_fingerprint
|
||||
))
|
||||
|
||||
var disconnect_deadline: int = Time.get_ticks_msec() + 12000
|
||||
while (
|
||||
|
|
@ -118,6 +123,7 @@ func _run_client() -> void:
|
|||
assert(_same_door_position(
|
||||
player.global_position, exterior_spawn.origin
|
||||
))
|
||||
assert(_faces_away_from_home(player, home_world, fingerprint))
|
||||
print("Home spawn multiplayer client validation: PASS")
|
||||
await _cleanup(main, session)
|
||||
|
||||
|
|
@ -151,6 +157,23 @@ func _wait_for_home(
|
|||
return false
|
||||
|
||||
|
||||
func _faces_away_from_home(
|
||||
avatar: Player,
|
||||
home_world: HomeWorldService,
|
||||
fingerprint: String,
|
||||
) -> bool:
|
||||
var exterior_basis: Basis = home_world.get_exterior_transform(
|
||||
fingerprint
|
||||
).basis
|
||||
var outward_direction := exterior_basis.z
|
||||
outward_direction.y = 0.0
|
||||
if outward_direction.is_zero_approx():
|
||||
return false
|
||||
return avatar.get_facing_direction().dot(
|
||||
outward_direction.normalized()
|
||||
) >= 0.99
|
||||
|
||||
|
||||
func _wait_for_remote_peer(session: NetworkSession) -> int:
|
||||
var deadline: int = Time.get_ticks_msec() + 20000
|
||||
while Time.get_ticks_msec() < deadline:
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ func _validate_save_round_trip(
|
|||
assert(catalog != null)
|
||||
assert(catalog.candidates.size() == 316)
|
||||
var active_species := LogbookCatalog.ordered_species(catalog.candidates)
|
||||
assert(active_species.size() == 313)
|
||||
assert(active_species.size() == 314)
|
||||
# Keep this runtime save fixture within maximum player capacity while the
|
||||
# focused catalog validation exercises every active species individually.
|
||||
var round_trip_species: Array[FishData] = []
|
||||
|
|
@ -164,12 +164,14 @@ func _validate_save_round_trip(
|
|||
player.inventory_layout.restore_backpack_level(
|
||||
PlayerInventoryLayout.MAX_BACKPACK_LEVEL
|
||||
)
|
||||
player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL)
|
||||
var home_state := main.get("_home_state") as PlayerHomeState
|
||||
assert(home_state != null)
|
||||
assert(home_state.upgrade_to_next_tier())
|
||||
assert(
|
||||
player.inventory_layout.get_backpack_level()
|
||||
== PlayerInventoryLayout.MAX_BACKPACK_LEVEL
|
||||
)
|
||||
assert(player.cooler_capacity.get_level() == PlayerCoolerCapacity.MAX_LEVEL)
|
||||
assert(player.cooler_capacity.get_capacity() == 18)
|
||||
player.inventory.set_inventory_layout(null)
|
||||
for index: int in 4:
|
||||
_add_test_catch(player, round_trip_species[index])
|
||||
|
|
|
|||
10
ui/fishing_danger_vignette.gdshader
Normal file
10
ui/fishing_danger_vignette.gdshader
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
shader_type canvas_item;
|
||||
|
||||
uniform float intensity : hint_range(0.0, 1.0) = 0.0;
|
||||
|
||||
void fragment() {
|
||||
vec2 centered = UV * 2.0 - 1.0;
|
||||
centered.x *= 0.56;
|
||||
float edge = smoothstep(0.42, 1.02, length(centered));
|
||||
COLOR = vec4(0.16, 0.018, 0.025, edge * intensity * 0.72);
|
||||
}
|
||||
1
ui/fishing_danger_vignette.gdshader.uid
Normal file
1
ui/fishing_danger_vignette.gdshader.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c1qcj3m4b10x4
|
||||
|
|
@ -15,9 +15,6 @@ const PlayerFishingUpgradesType = preload(
|
|||
"res://progression/player_fishing_upgrades.gd"
|
||||
)
|
||||
const PlayerType = preload("res://player/player.gd")
|
||||
const PlayerCoolerCapacityType = preload(
|
||||
"res://progression/player_cooler_capacity.gd"
|
||||
)
|
||||
const PlayerArtUnlocksType = preload(
|
||||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
|
|
@ -154,9 +151,6 @@ const ROD_PRICE_ICON_SIZE: float = 22.0
|
|||
@onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble
|
||||
@onready var _barrier_purchase: Button = %BarrierPurchase
|
||||
@onready var _supplies_list: VBoxContainer = %SuppliesList
|
||||
@onready var _cooler_cost: CurrencyAmount = %CoolerCost
|
||||
@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
|
||||
@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
|
||||
|
|
@ -172,7 +166,6 @@ var _inventory: FishInventory
|
|||
var _hotbar: PlayerHotbar
|
||||
var _inventory_layout: PlayerInventoryLayout
|
||||
var _item_catalog: ItemCatalogType
|
||||
var _cooler_capacity: PlayerCoolerCapacityType
|
||||
var _art_unlocks: PlayerArtUnlocksType
|
||||
var _network_shop: NetworkShopService
|
||||
var _network_sale: NetworkSaleService
|
||||
|
|
@ -202,7 +195,6 @@ func _ready() -> void:
|
|||
%CloseButton.pressed.connect(close_shop)
|
||||
_reel_purchase.pressed.connect(_purchase_reel_speed)
|
||||
_barrier_purchase.pressed.connect(_purchase_barrier_power)
|
||||
_cooler_purchase.pressed.connect(_purchase_cooler_capacity)
|
||||
_backpack_purchase.pressed.connect(_purchase_backpack_capacity)
|
||||
|
||||
|
||||
|
|
@ -353,7 +345,6 @@ func _focus_shop_section() -> void:
|
|||
for upgrade_button: Button in [
|
||||
_reel_purchase,
|
||||
_barrier_purchase,
|
||||
_cooler_purchase,
|
||||
_backpack_purchase,
|
||||
]:
|
||||
if not upgrade_button.disabled:
|
||||
|
|
@ -509,7 +500,6 @@ func _apply_shop_styles() -> void:
|
|||
for panel: PanelContainer in [
|
||||
_reel_price_bubble,
|
||||
_barrier_price_bubble,
|
||||
_cooler_price_bubble,
|
||||
_backpack_price_bubble,
|
||||
]:
|
||||
var price_style := UtilityPageStyleType.rounded_style(
|
||||
|
|
@ -526,7 +516,6 @@ func _apply_shop_styles() -> void:
|
|||
for price: CurrencyAmount in [
|
||||
_reel_cost,
|
||||
_barrier_cost,
|
||||
_cooler_cost,
|
||||
_backpack_cost,
|
||||
]:
|
||||
var amount_label := price.get_amount_label()
|
||||
|
|
@ -538,7 +527,6 @@ func _apply_shop_styles() -> void:
|
|||
%CloseButton,
|
||||
_reel_purchase,
|
||||
_barrier_purchase,
|
||||
_cooler_purchase,
|
||||
_backpack_purchase,
|
||||
]:
|
||||
UtilityPageStyleType.apply_ocean_button(button)
|
||||
|
|
@ -559,7 +547,6 @@ func setup(
|
|||
hotbar: PlayerHotbar,
|
||||
inventory_layout: PlayerInventoryLayout,
|
||||
item_catalog: ItemCatalogType,
|
||||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
art_unlocks: PlayerArtUnlocksType,
|
||||
network_shop: NetworkShopService,
|
||||
network_sale: NetworkSaleService,
|
||||
|
|
@ -576,7 +563,6 @@ func setup(
|
|||
_hotbar = hotbar
|
||||
_inventory_layout = inventory_layout
|
||||
_item_catalog = item_catalog
|
||||
_cooler_capacity = cooler_capacity
|
||||
_art_unlocks = art_unlocks
|
||||
_network_shop = network_shop
|
||||
_network_sale = network_sale
|
||||
|
|
@ -612,10 +598,6 @@ func setup(
|
|||
_upgrades.upgrades_changed.connect(_on_upgrades_changed)
|
||||
if not _bag.contents_changed.is_connected(_on_bag_changed):
|
||||
_bag.contents_changed.connect(_on_bag_changed)
|
||||
if not _cooler_capacity.capacity_changed.is_connected(
|
||||
_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
|
||||
):
|
||||
|
|
@ -836,7 +818,6 @@ func _refresh_all() -> void:
|
|||
_refresh_wallet()
|
||||
_refresh_upgrades()
|
||||
_refresh_supplies()
|
||||
_refresh_cooler_capacity()
|
||||
_refresh_backpack_capacity()
|
||||
|
||||
|
||||
|
|
@ -1587,41 +1568,6 @@ func _add_supply_icon_tile(
|
|||
return tile_host
|
||||
|
||||
|
||||
func _refresh_cooler_capacity() -> void:
|
||||
if _cooler_capacity == null:
|
||||
return
|
||||
var level: int = _cooler_capacity.get_level()
|
||||
var cost: int = _cooler_capacity.get_next_cost()
|
||||
var next_capacity: int = _cooler_capacity.get_next_capacity()
|
||||
_cooler_purchase.disabled = (
|
||||
cost < 0
|
||||
or _transaction_in_progress
|
||||
or _closing
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_purchase()
|
||||
or not _cooler_capacity.can_purchase(_wallet)
|
||||
)
|
||||
var cooler_effect: String
|
||||
if cost < 0:
|
||||
cooler_effect = "%d slots · maximum level" % (
|
||||
_cooler_capacity.get_capacity()
|
||||
)
|
||||
_cooler_price_bubble.hide()
|
||||
else:
|
||||
_cooler_price_bubble.show()
|
||||
cooler_effect = "%d → %d slots" % [
|
||||
_cooler_capacity.get_capacity(),
|
||||
next_capacity,
|
||||
]
|
||||
_cooler_cost.set_amount(cost)
|
||||
_cooler_purchase.tooltip_text = _upgrade_tooltip(
|
||||
"storage capacity",
|
||||
level,
|
||||
cooler_effect,
|
||||
)
|
||||
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
|
||||
|
||||
|
||||
func _refresh_backpack_capacity() -> void:
|
||||
if _inventory_layout == null:
|
||||
return
|
||||
|
|
@ -1723,23 +1669,6 @@ func _purchase_art_upgrade(product_id: StringName) -> void:
|
|||
_network_shop.request_art_upgrade(product_id)
|
||||
|
||||
|
||||
func _purchase_cooler_capacity() -> void:
|
||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||
_set_feedback("unable to complete purchase.")
|
||||
return
|
||||
var cost: int = _cooler_capacity.get_next_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_cooler_capacity_upgrade()
|
||||
|
||||
|
||||
func _purchase_backpack_capacity() -> void:
|
||||
if _transaction_in_progress or not _is_transaction_context_valid():
|
||||
_set_feedback("unable to complete purchase.")
|
||||
|
|
@ -1860,7 +1789,6 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void:
|
|||
_refresh_wallet()
|
||||
_refresh_upgrades()
|
||||
_refresh_supplies()
|
||||
_refresh_cooler_capacity()
|
||||
_refresh_backpack_capacity()
|
||||
|
||||
|
||||
|
|
@ -1875,10 +1803,6 @@ func _on_bag_changed() -> void:
|
|||
_refresh_supplies()
|
||||
|
||||
|
||||
func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
|
||||
_refresh_cooler_capacity()
|
||||
|
||||
|
||||
func _on_backpack_capacity_changed(_level: int, _capacity: int) -> void:
|
||||
_refresh_backpack_capacity()
|
||||
|
||||
|
|
|
|||
|
|
@ -257,35 +257,6 @@ layout_mode = 2
|
|||
amount = 100
|
||||
icon_size = 24.0
|
||||
|
||||
[node name="CoolerTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
|
||||
custom_minimum_size = Vector2(144, 168)
|
||||
layout_mode = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
|
||||
unique_name_in_owner = true
|
||||
offset_right = 144.0
|
||||
offset_bottom = 144.0
|
||||
tooltip_text = "storage capacity"
|
||||
icon = ExtResource("7_close")
|
||||
expand_icon = true
|
||||
icon_max_width = 92
|
||||
|
||||
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
|
||||
unique_name_in_owner = true
|
||||
z_index = 2
|
||||
offset_left = 22.0
|
||||
offset_top = 128.0
|
||||
offset_right = 122.0
|
||||
offset_bottom = 160.0
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="CoolerCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile/CoolerPriceBubble" instance=ExtResource("8_currency")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
amount = 75
|
||||
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
|
||||
|
|
|
|||
153
ui/game_ui.gd
153
ui/game_ui.gd
|
|
@ -21,6 +21,7 @@ const PauseMenuType = preload("res://ui/pause_menu.gd")
|
|||
const NetworkSessionType = preload("res://network/network_session.gd")
|
||||
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
||||
const DecorShopType = preload("res://ui/decor_shop.gd")
|
||||
const RVUpgradeShopType = preload("res://ui/rv_upgrade_shop.gd")
|
||||
const SettingsPanelType = preload("res://ui/settings_panel.gd")
|
||||
const PlayerFishingUpgradesType = preload(
|
||||
"res://progression/player_fishing_upgrades.gd"
|
||||
|
|
@ -31,6 +32,9 @@ const ShopInteractionType = preload(
|
|||
const DecorShopInteractionType = preload(
|
||||
"res://world/decor_shop_interaction.gd"
|
||||
)
|
||||
const RVUpgradeInteractionType = preload(
|
||||
"res://world/rv_upgrade_interaction.gd"
|
||||
)
|
||||
const PlayerStorageType = preload("res://ui/player_storage.gd")
|
||||
const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd")
|
||||
const PlayerStorageInteractionType = preload(
|
||||
|
|
@ -129,6 +133,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0
|
|||
@onready var _barrier_health: Label = %BarrierHealth
|
||||
@onready var _showcase_details: Label = %ShowcaseDetails
|
||||
@onready var _fishing_panel: PanelContainer = %FishingPanel
|
||||
@onready var _fishing_danger_vignette: ColorRect = %FishingDangerVignette
|
||||
@onready var _experience_panel: PanelContainer = %ExperienceProgressPanel
|
||||
@onready var _experience_level_label: Label = %ExperienceLevelLabel
|
||||
@onready var _experience_award_label: Label = %ExperienceAwardLabel
|
||||
|
|
@ -144,6 +149,8 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural"
|
|||
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
|
||||
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
|
||||
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
||||
const FISHING_DANGER_START_GAP: float = 0.15
|
||||
const FISHING_DANGER_FULL_GAP: float = 0.025
|
||||
|
||||
|
||||
@onready var _canonical_stage: Control = %CanonicalStage
|
||||
|
|
@ -155,6 +162,7 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
|
|||
@onready var _hotbar_ui: HotbarUIType = %Hotbar
|
||||
@onready var _fishing_shop: FishingShopType = %FishingShop
|
||||
@onready var _decor_shop: DecorShopType = %DecorShop
|
||||
@onready var _rv_upgrade_shop: RVUpgradeShopType = %RVUpgradeShop
|
||||
@onready var _player_storage: PlayerStorageType = %PlayerStorage
|
||||
@onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar
|
||||
@onready var _rv_storage_button: Button = %RVStorageButton
|
||||
|
|
@ -208,6 +216,7 @@ var _player_menu_hotbar_visible: bool = false
|
|||
var _main_shop_buyer: FishBuyerProfileType
|
||||
var _shop_interaction: ShopInteractionType
|
||||
var _decor_shop_interaction: DecorShopInteractionType
|
||||
var _rv_upgrade_shop_interaction: RVUpgradeInteractionType
|
||||
var _storage_interaction: PlayerStorageInteractionType
|
||||
var _surface_drawing: NetworkSurfaceDrawingService
|
||||
var _surface_drawing_hotbar_selected: bool = false
|
||||
|
|
@ -235,6 +244,12 @@ var _controller_text_entry_is_open: Callable
|
|||
var _controller_text_entry_close: Callable
|
||||
var _restore_chat_keyboard_after_fishing: bool = false
|
||||
var _rv_storage_button_requested_visible: bool = false
|
||||
var _previous_barrier_health := PackedInt32Array()
|
||||
var _barrier_feedback_initialized: bool = false
|
||||
var _fishing_danger_target: float = 0.0
|
||||
var _fishing_danger_intensity: float = 0.0
|
||||
var _fishing_danger_pulse_time: float = 0.0
|
||||
var _catch_track_shake_tween: Tween
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -363,6 +378,7 @@ func setup(
|
|||
shop_interaction: ShopInteractionType,
|
||||
storage_interaction: PlayerStorageInteractionType,
|
||||
decor_shop_interaction: DecorShopInteractionType,
|
||||
rv_upgrade_shop_interaction: RVUpgradeInteractionType,
|
||||
item_effects: PlayerItemEffectsType,
|
||||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
network_session: NetworkSessionType,
|
||||
|
|
@ -506,7 +522,6 @@ func setup(
|
|||
hotbar,
|
||||
inventory_layout,
|
||||
item_catalog,
|
||||
cooler_capacity,
|
||||
art_unlocks,
|
||||
network_shop_service,
|
||||
network_sale_service,
|
||||
|
|
@ -521,6 +536,14 @@ func setup(
|
|||
home_state,
|
||||
network_shop_service,
|
||||
)
|
||||
_rv_upgrade_shop.setup(
|
||||
player,
|
||||
wallet,
|
||||
fishing_spot,
|
||||
rv_upgrade_shop_interaction,
|
||||
home_state,
|
||||
network_shop_service,
|
||||
)
|
||||
_player_storage.setup(
|
||||
player,
|
||||
fishing_spot,
|
||||
|
|
@ -538,6 +561,8 @@ func setup(
|
|||
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||
_decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||
_decor_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||
_rv_upgrade_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||
_rv_upgrade_shop.menu_exit_started.connect(_on_shop_exit_started)
|
||||
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
|
||||
_fishing_shop.shop_cooler_return_requested.connect(
|
||||
_on_shop_cooler_return_requested
|
||||
|
|
@ -548,6 +573,7 @@ func setup(
|
|||
_main_shop_buyer = main_shop_buyer
|
||||
_shop_interaction = shop_interaction
|
||||
_decor_shop_interaction = decor_shop_interaction
|
||||
_rv_upgrade_shop_interaction = rv_upgrade_shop_interaction
|
||||
_storage_interaction = storage_interaction
|
||||
_surface_drawing = surface_drawing
|
||||
if (
|
||||
|
|
@ -571,13 +597,16 @@ func set_world_interactions(
|
|||
shop_interaction: ShopInteractionType,
|
||||
storage_interaction: PlayerStorageInteractionType,
|
||||
decor_shop_interaction: DecorShopInteractionType,
|
||||
rv_upgrade_shop_interaction: RVUpgradeInteractionType,
|
||||
) -> void:
|
||||
_shop_interaction = shop_interaction
|
||||
_storage_interaction = storage_interaction
|
||||
_decor_shop_interaction = decor_shop_interaction
|
||||
_rv_upgrade_shop_interaction = rv_upgrade_shop_interaction
|
||||
_fishing_shop.set_world_interaction(shop_interaction)
|
||||
_player_storage.set_world_interaction(storage_interaction)
|
||||
_decor_shop.set_world_interaction(decor_shop_interaction)
|
||||
_rv_upgrade_shop.set_world_interaction(rv_upgrade_shop_interaction)
|
||||
|
||||
|
||||
func _prioritize_surface_drawing_pointer_input() -> void:
|
||||
|
|
@ -1539,6 +1568,13 @@ func _process(delta: float) -> void:
|
|||
_update_virtual_mouse(delta)
|
||||
_update_controller_menu_scroll(delta)
|
||||
_update_experience_bubble_position()
|
||||
_update_fishing_danger_vignette(delta)
|
||||
_try_start_shop_npc_speech()
|
||||
if _player != null:
|
||||
_player.set_fishing_reel_camera_active(
|
||||
_fishing_spot != null
|
||||
and _fishing_spot.is_reel_input_active()
|
||||
)
|
||||
|
||||
|
||||
func _update_controller_menu_scroll(delta: float) -> void:
|
||||
|
|
@ -1793,6 +1829,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
|
|||
close_player_menu_for_session_end()
|
||||
_fishing_shop.close_for_session_end()
|
||||
_decor_shop.close_for_session_end()
|
||||
_rv_upgrade_shop.close_for_session_end()
|
||||
_player_storage.close_for_session_end()
|
||||
_fishing_panel.visible = false
|
||||
_shop_prompt.hide()
|
||||
|
|
@ -1933,6 +1970,10 @@ func get_decor_shop() -> DecorShopType:
|
|||
return _decor_shop
|
||||
|
||||
|
||||
func get_rv_upgrade_shop() -> RVUpgradeShopType:
|
||||
return _rv_upgrade_shop
|
||||
|
||||
|
||||
func get_player_storage() -> PlayerStorageType:
|
||||
return _player_storage
|
||||
|
||||
|
|
@ -2027,8 +2068,19 @@ func set_shop_prompt_visible(
|
|||
_position_shop_prompt(world_anchor)
|
||||
|
||||
|
||||
func set_shop_npc_player_in_range(in_range: bool) -> void:
|
||||
if _shop_npc_player_in_range == in_range:
|
||||
func set_shop_npc_player_in_range(
|
||||
in_range: bool,
|
||||
dialogue_text: String = "",
|
||||
) -> void:
|
||||
var dialogue_changed: bool = (
|
||||
not dialogue_text.is_empty()
|
||||
and _shop_prompt_message.text != dialogue_text
|
||||
)
|
||||
if dialogue_changed:
|
||||
_shop_prompt_message.text = dialogue_text
|
||||
_shop_prompt_message.visible_characters = -1
|
||||
_shop_npc_spoken_for_current_visit = false
|
||||
if _shop_npc_player_in_range == in_range and not dialogue_changed:
|
||||
return
|
||||
_shop_npc_player_in_range = in_range
|
||||
if not in_range:
|
||||
|
|
@ -2044,10 +2096,10 @@ func _try_start_shop_npc_speech() -> void:
|
|||
or not _gameplay_ui_enabled
|
||||
):
|
||||
return
|
||||
_shop_npc_spoken_for_current_visit = true
|
||||
var now_msec: int = Time.get_ticks_msec()
|
||||
if now_msec < _shop_npc_next_speech_msec:
|
||||
return
|
||||
_shop_npc_spoken_for_current_visit = true
|
||||
_shop_npc_next_speech_msec = (
|
||||
now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS
|
||||
)
|
||||
|
|
@ -2310,6 +2362,12 @@ func _on_catch_display_changed(
|
|||
and _fishing_spot != null
|
||||
and _fishing_spot.is_fighting()
|
||||
)
|
||||
_update_fishing_feedback(
|
||||
progress,
|
||||
chase_progress,
|
||||
barrier_health,
|
||||
encounter_visible,
|
||||
)
|
||||
_green_catch_progress.value = progress * 100.0
|
||||
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
|
||||
_catch_track.visible = encounter_visible
|
||||
|
|
@ -2346,6 +2404,93 @@ func _on_catch_display_changed(
|
|||
_refresh_fishing_panel_visibility()
|
||||
|
||||
|
||||
func _update_fishing_feedback(
|
||||
progress: float,
|
||||
chase_progress: float,
|
||||
barrier_health: PackedInt32Array,
|
||||
encounter_visible: bool,
|
||||
) -> void:
|
||||
if not encounter_visible:
|
||||
_previous_barrier_health.clear()
|
||||
_barrier_feedback_initialized = false
|
||||
_fishing_danger_target = 0.0
|
||||
return
|
||||
if _barrier_feedback_initialized:
|
||||
var comparison_count: int = mini(
|
||||
_previous_barrier_health.size(),
|
||||
barrier_health.size(),
|
||||
)
|
||||
var barrier_hit: bool = false
|
||||
var barrier_broken: bool = false
|
||||
for barrier_index: int in range(comparison_count):
|
||||
if (
|
||||
barrier_health[barrier_index]
|
||||
>= _previous_barrier_health[barrier_index]
|
||||
):
|
||||
continue
|
||||
barrier_hit = true
|
||||
barrier_broken = (
|
||||
barrier_broken
|
||||
or barrier_health[barrier_index] <= 0
|
||||
)
|
||||
if barrier_hit:
|
||||
_play_barrier_feedback(barrier_broken)
|
||||
_previous_barrier_health = barrier_health.duplicate()
|
||||
_barrier_feedback_initialized = true
|
||||
var chase_gap: float = progress - maxf(chase_progress, 0.0)
|
||||
_fishing_danger_target = clampf(
|
||||
inverse_lerp(
|
||||
FISHING_DANGER_START_GAP,
|
||||
FISHING_DANGER_FULL_GAP,
|
||||
chase_gap,
|
||||
),
|
||||
0.0,
|
||||
1.0,
|
||||
)
|
||||
|
||||
|
||||
func _play_barrier_feedback(barrier_broken: bool) -> void:
|
||||
if _player != null:
|
||||
_player.play_fishing_barrier_camera_feedback(barrier_broken)
|
||||
if _catch_track_shake_tween != null:
|
||||
_catch_track_shake_tween.kill()
|
||||
_catch_track.rotation = 0.0
|
||||
var tilt: float = 0.018 if barrier_broken else 0.008
|
||||
var beat: float = 0.038 if barrier_broken else 0.028
|
||||
_catch_track_shake_tween = create_tween()
|
||||
_catch_track_shake_tween.tween_property(
|
||||
_catch_track, "rotation", -tilt, beat
|
||||
)
|
||||
_catch_track_shake_tween.tween_property(
|
||||
_catch_track, "rotation", tilt, beat
|
||||
)
|
||||
_catch_track_shake_tween.tween_property(
|
||||
_catch_track, "rotation", -tilt * 0.45, beat
|
||||
)
|
||||
_catch_track_shake_tween.tween_property(
|
||||
_catch_track, "rotation", 0.0, beat
|
||||
)
|
||||
|
||||
|
||||
func _update_fishing_danger_vignette(delta: float) -> void:
|
||||
_fishing_danger_pulse_time += delta
|
||||
_fishing_danger_intensity = lerpf(
|
||||
_fishing_danger_intensity,
|
||||
_fishing_danger_target,
|
||||
1.0 - exp(-8.0 * delta),
|
||||
)
|
||||
var pulse: float = (
|
||||
0.88 + sin(_fishing_danger_pulse_time * 5.5) * 0.12
|
||||
)
|
||||
var shown_intensity: float = _fishing_danger_intensity * pulse
|
||||
_fishing_danger_vignette.visible = shown_intensity > 0.01
|
||||
var vignette_material := (
|
||||
_fishing_danger_vignette.material as ShaderMaterial
|
||||
)
|
||||
if vignette_material != null:
|
||||
vignette_material.set_shader_parameter("intensity", shown_intensity)
|
||||
|
||||
|
||||
func _update_barrier_markers(
|
||||
positions: PackedFloat32Array,
|
||||
health: PackedInt32Array,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=24 format=3]
|
||||
[gd_scene load_steps=27 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
|
||||
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
|
||||
|
|
@ -17,6 +17,11 @@
|
|||
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"]
|
||||
[ext_resource type="PackedScene" path="res://ui/decor_shop.tscn" id="16_decor_shop"]
|
||||
[ext_resource type="PackedScene" path="res://ui/home_decor_toolbar.tscn" id="17_decor_toolbar"]
|
||||
[ext_resource type="PackedScene" path="res://ui/rv_upgrade_shop.tscn" id="18_rv_upgrade_shop"]
|
||||
[ext_resource type="Shader" path="res://ui/fishing_danger_vignette.gdshader" id="19_fishing_vignette"]
|
||||
|
||||
[sub_resource type="ShaderMaterial" id="ShaderMaterial_fishing_vignette"]
|
||||
shader = ExtResource("19_fishing_vignette")
|
||||
|
||||
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
|
||||
bg_color = Color(0.032, 0.118, 0.15, 1)
|
||||
|
|
@ -86,6 +91,20 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
z_index = 50
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
mouse_filter = 2
|
||||
material = SubResource("ShaderMaterial_fishing_vignette")
|
||||
color = Color(1, 1, 1, 1)
|
||||
|
||||
[node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
|
||||
unique_name_in_owner = true
|
||||
z_index = 54
|
||||
|
|
@ -312,6 +331,7 @@ offset_right = 420.0
|
|||
offset_bottom = -124.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 0
|
||||
pivot_offset = Vector2(420, 18)
|
||||
mouse_filter = 2
|
||||
|
||||
[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"]
|
||||
|
|
@ -440,7 +460,7 @@ theme_override_constants/margin_bottom = 8
|
|||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 18
|
||||
text = "Hey, if you've got any fish I've got cash! OwO"
|
||||
text = "Listen, you may suck at fishing but I've got some things that might help."
|
||||
horizontal_alignment = 1
|
||||
vertical_alignment = 1
|
||||
autowrap_mode = 2
|
||||
|
|
@ -477,6 +497,9 @@ unique_name_in_owner = true
|
|||
[node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="RVUpgradeShop" parent="UIRoot/CanonicalStage" instance=ExtResource("18_rv_upgrade_shop")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,10 @@ var _mode_button: Button
|
|||
var _selection_label: Label
|
||||
var _rotate_button: Button
|
||||
var _store_button: Button
|
||||
var _clear_button: Button
|
||||
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
|
||||
var _interactive: bool = true
|
||||
var _selection_active: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -22,31 +25,33 @@ func set_active(active: bool) -> void:
|
|||
if visible == active:
|
||||
return
|
||||
visible = active
|
||||
if active and _mode_button != null:
|
||||
_mode_button.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func set_interactive(interactive: bool) -> void:
|
||||
_interactive = interactive
|
||||
_refresh_button_interactivity()
|
||||
|
||||
|
||||
func set_mode(mode: PlayerHomeState.PlacementMode) -> void:
|
||||
_mode = mode
|
||||
if _mode_button != null:
|
||||
_mode_button.text = (
|
||||
"placement: grid"
|
||||
"grid"
|
||||
if mode == PlayerHomeState.PlacementMode.GRID
|
||||
else "placement: free"
|
||||
else "free"
|
||||
)
|
||||
_mode_button.tooltip_text = "placement mode: %s" % _mode_button.text
|
||||
|
||||
|
||||
func set_selection(instance_id: String, display_name: String = "") -> void:
|
||||
var selected: bool = not instance_id.is_empty()
|
||||
_selection_active = selected
|
||||
if _selection_label != null:
|
||||
_selection_label.text = (
|
||||
"selected: %s" % display_name
|
||||
if selected else "click decor to select; drag to move"
|
||||
if selected else "use held decor to place; click or drag to edit"
|
||||
)
|
||||
if _rotate_button != null:
|
||||
_rotate_button.disabled = not selected
|
||||
if _store_button != null:
|
||||
_store_button.disabled = not selected
|
||||
_refresh_button_interactivity()
|
||||
|
||||
|
||||
func report_status(message: String) -> void:
|
||||
|
|
@ -55,65 +60,92 @@ func report_status(message: String) -> void:
|
|||
|
||||
|
||||
func _build_ui() -> void:
|
||||
custom_minimum_size = Vector2(330.0, 0.0)
|
||||
position = Vector2(925.0, 540.0)
|
||||
z_index = 58
|
||||
set_anchors_preset(Control.PRESET_TOP_RIGHT)
|
||||
offset_left = -500.0
|
||||
offset_top = 0.0
|
||||
offset_right = 0.0
|
||||
offset_bottom = 106.0
|
||||
custom_minimum_size = Vector2(500.0, 0.0)
|
||||
z_index = 85
|
||||
mouse_filter = Control.MOUSE_FILTER_STOP
|
||||
add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyle.rounded_style(
|
||||
Color(UtilityPageStyle.OCEAN_PANEL_DEEP, 0.96), 16
|
||||
),
|
||||
)
|
||||
var panel_style: StyleBoxFlat = UtilityPageStyle.panel_style()
|
||||
panel_style.set_corner_radius_all(10)
|
||||
panel_style.content_margin_left = 6.0
|
||||
panel_style.content_margin_top = 6.0
|
||||
panel_style.content_margin_right = 6.0
|
||||
panel_style.content_margin_bottom = 6.0
|
||||
add_theme_stylebox_override("panel", panel_style)
|
||||
var margin := MarginContainer.new()
|
||||
for side: StringName in [
|
||||
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
|
||||
]:
|
||||
margin.add_theme_constant_override(side, 12)
|
||||
margin.add_theme_constant_override(side, 6)
|
||||
add_child(margin)
|
||||
var layout := VBoxContainer.new()
|
||||
layout.add_theme_constant_override("separation", 8)
|
||||
layout.add_theme_constant_override("separation", 5)
|
||||
margin.add_child(layout)
|
||||
var title := Label.new()
|
||||
title.text = "RV decor"
|
||||
title.add_theme_font_size_override("font_size", 24)
|
||||
layout.add_child(title)
|
||||
_selection_label = Label.new()
|
||||
_selection_label.text = "click decor to select; drag to move"
|
||||
_selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_selection_label.add_theme_font_size_override("font_size", 17)
|
||||
layout.add_child(_selection_label)
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
row.add_theme_constant_override("separation", 5)
|
||||
layout.add_child(row)
|
||||
_mode_button = Button.new()
|
||||
_mode_button.text = "placement: grid"
|
||||
_mode_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
UtilityPageStyle.apply_ocean_button(_mode_button)
|
||||
var title := Label.new()
|
||||
title.text = "decor"
|
||||
title.custom_minimum_size = Vector2(70.0, 46.0)
|
||||
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
title.add_theme_font_size_override("font_size", 19)
|
||||
row.add_child(title)
|
||||
_mode_button = _add_tool_button(row, "grid", "switch placement mode", 82.0)
|
||||
_mode_button.pressed.connect(_toggle_mode)
|
||||
row.add_child(_mode_button)
|
||||
_rotate_button = Button.new()
|
||||
_rotate_button.text = "rotate"
|
||||
_rotate_button.disabled = true
|
||||
UtilityPageStyle.apply_ocean_button(_rotate_button)
|
||||
_rotate_button = _add_tool_button(row, "rotate", "rotate selected decor", 92.0)
|
||||
_rotate_button.pressed.connect(rotate_requested.emit)
|
||||
row.add_child(_rotate_button)
|
||||
var action_row := HBoxContainer.new()
|
||||
action_row.add_theme_constant_override("separation", 8)
|
||||
layout.add_child(action_row)
|
||||
_store_button = Button.new()
|
||||
_store_button.text = "pack up"
|
||||
_store_button.disabled = true
|
||||
_store_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
UtilityPageStyle.apply_ocean_button(_store_button)
|
||||
_store_button = _add_tool_button(row, "pack", "pack selected decor", 82.0)
|
||||
_store_button.pressed.connect(store_requested.emit)
|
||||
action_row.add_child(_store_button)
|
||||
var cancel_button := Button.new()
|
||||
cancel_button.text = "clear selection"
|
||||
cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
UtilityPageStyle.apply_ocean_button(cancel_button)
|
||||
cancel_button.pressed.connect(cancel_requested.emit)
|
||||
action_row.add_child(cancel_button)
|
||||
_clear_button = _add_tool_button(row, "clear", "clear selection", 82.0)
|
||||
_clear_button.pressed.connect(cancel_requested.emit)
|
||||
_selection_label = Label.new()
|
||||
_selection_label.text = "use held decor to place; click or drag to edit"
|
||||
_selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
_selection_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_selection_label.add_theme_font_size_override("font_size", 15)
|
||||
layout.add_child(_selection_label)
|
||||
_refresh_button_interactivity()
|
||||
|
||||
|
||||
func _add_tool_button(
|
||||
parent: Container,
|
||||
button_text: String,
|
||||
button_tooltip: String,
|
||||
minimum_width: float,
|
||||
) -> Button:
|
||||
var button := Button.new()
|
||||
button.text = button_text
|
||||
button.tooltip_text = button_tooltip
|
||||
button.accessibility_name = button_tooltip
|
||||
button.custom_minimum_size = Vector2(minimum_width, 46.0)
|
||||
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
UtilityPageStyle.apply_ocean_button(button)
|
||||
for style_name: StringName in [
|
||||
&"normal", &"hover", &"pressed", &"focus", &"disabled",
|
||||
]:
|
||||
var existing: StyleBox = button.get_theme_stylebox(style_name)
|
||||
var rounded := existing.duplicate() as StyleBoxFlat
|
||||
if rounded == null:
|
||||
continue
|
||||
rounded.set_corner_radius_all(20)
|
||||
button.add_theme_stylebox_override(style_name, rounded)
|
||||
parent.add_child(button)
|
||||
return button
|
||||
|
||||
|
||||
func _refresh_button_interactivity() -> void:
|
||||
if _mode_button != null:
|
||||
_mode_button.disabled = not _interactive
|
||||
if _rotate_button != null:
|
||||
_rotate_button.disabled = not _interactive or not _selection_active
|
||||
if _store_button != null:
|
||||
_store_button.disabled = not _interactive or not _selection_active
|
||||
if _clear_button != null:
|
||||
_clear_button.disabled = not _interactive or not _selection_active
|
||||
|
||||
|
||||
func _toggle_mode() -> void:
|
||||
|
|
|
|||
|
|
@ -2575,7 +2575,10 @@ func _on_tackle_button_gui_input(
|
|||
call_deferred("_release_pointer_focus", button)
|
||||
if (
|
||||
mouse_event == null
|
||||
or mouse_event.button_index != MOUSE_BUTTON_RIGHT
|
||||
or mouse_event.button_index not in [
|
||||
MOUSE_BUTTON_LEFT,
|
||||
MOUSE_BUTTON_RIGHT,
|
||||
]
|
||||
or not mouse_event.pressed
|
||||
):
|
||||
return
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ func _open_storage(
|
|||
):
|
||||
return false
|
||||
_home_access = home_access
|
||||
_title.text = "RV storage" if home_access else "private storage"
|
||||
_title.text = "home storage" if home_access else "private storage"
|
||||
_decorate_button.visible = home_access and allow_decorating
|
||||
_prior_movement_enabled = _player.is_movement_enabled()
|
||||
_prior_camera_enabled = _player.is_camera_input_enabled()
|
||||
|
|
@ -290,8 +290,8 @@ func _build_ui() -> void:
|
|||
_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header.add_child(_title)
|
||||
_decorate_button = Button.new()
|
||||
_decorate_button.text = "decorate RV"
|
||||
_decorate_button.tooltip_text = "Place decor inside your RV"
|
||||
_decorate_button.text = "decorate home"
|
||||
_decorate_button.tooltip_text = "Place decor inside your home"
|
||||
_decorate_button.custom_minimum_size = Vector2(160.0, 48.0)
|
||||
UtilityPageStyle.apply_ocean_button(_decorate_button)
|
||||
_decorate_button.hide()
|
||||
|
|
|
|||
330
ui/rv_upgrade_shop.gd
Normal file
330
ui/rv_upgrade_shop.gd
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
class_name RVUpgradeShop
|
||||
extends Control
|
||||
|
||||
const INPUT_OWNER: StringName = &"rv_upgrade_shop"
|
||||
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
|
||||
const CurrencyPresentationType = preload("res://ui/currency_presentation.gd")
|
||||
|
||||
signal menu_visibility_changed(is_open: bool)
|
||||
signal menu_exit_started
|
||||
|
||||
var _player: Player
|
||||
var _wallet: PlayerWallet
|
||||
var _fishing_spot: FishingSpot
|
||||
var _interaction: RVUpgradeInteraction
|
||||
var _home_state: PlayerHomeState
|
||||
var _network_shop: NetworkShopService
|
||||
var _wallet_amount: CurrencyAmount
|
||||
var _price_amount: CurrencyAmount
|
||||
var _home_label: Label
|
||||
var _storage_label: Label
|
||||
var _feedback: Label
|
||||
var _upgrade_button: Button
|
||||
var _close_button: Button
|
||||
var _transaction_pending: bool = false
|
||||
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:
|
||||
UtilityPageStyleType.apply_page(self)
|
||||
_build_ui()
|
||||
hide()
|
||||
|
||||
|
||||
func setup(
|
||||
player: Player,
|
||||
wallet: PlayerWallet,
|
||||
fishing_spot: FishingSpot,
|
||||
interaction: RVUpgradeInteraction,
|
||||
home_state: PlayerHomeState,
|
||||
network_shop: NetworkShopService,
|
||||
) -> void:
|
||||
_player = player
|
||||
_wallet = wallet
|
||||
_fishing_spot = fishing_spot
|
||||
_interaction = interaction
|
||||
_home_state = home_state
|
||||
_network_shop = network_shop
|
||||
if not _wallet.balance_changed.is_connected(_on_wallet_changed):
|
||||
_wallet.balance_changed.connect(_on_wallet_changed)
|
||||
if not _home_state.changed.is_connected(_refresh):
|
||||
_home_state.changed.connect(_refresh)
|
||||
if (
|
||||
_network_shop != null
|
||||
and not _network_shop.local_purchase_pending.is_connected(
|
||||
_on_purchase_pending
|
||||
)
|
||||
):
|
||||
_network_shop.local_purchase_pending.connect(_on_purchase_pending)
|
||||
_network_shop.local_purchase_finished.connect(_on_purchase_finished)
|
||||
_refresh()
|
||||
|
||||
|
||||
func set_world_interaction(interaction: RVUpgradeInteraction) -> void:
|
||||
_interaction = interaction
|
||||
|
||||
|
||||
func open_shop() -> bool:
|
||||
if (
|
||||
visible
|
||||
or _player == null
|
||||
or _wallet == null
|
||||
or _fishing_spot == null
|
||||
or _interaction == null
|
||||
or not _interaction.is_local_player_in_range()
|
||||
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()
|
||||
_player.set_movement_enabled(false)
|
||||
_player.set_camera_input_enabled(false)
|
||||
_prior_mouse_mode = _restorable_mouse_mode(Input.mouse_mode)
|
||||
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_transaction_pending = (
|
||||
_network_shop != null
|
||||
and _network_shop.is_local_purchase_pending()
|
||||
)
|
||||
_feedback.text = ""
|
||||
_refresh()
|
||||
show()
|
||||
_upgrade_button.call_deferred("grab_focus")
|
||||
menu_visibility_changed.emit(true)
|
||||
return true
|
||||
|
||||
|
||||
func close_shop(restore_controls: bool = true) -> void:
|
||||
if not visible:
|
||||
return
|
||||
menu_exit_started.emit()
|
||||
get_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 consume_escape() -> bool:
|
||||
if not visible:
|
||||
return false
|
||||
close_shop()
|
||||
return true
|
||||
|
||||
|
||||
func close_for_range_exit() -> void:
|
||||
close_shop()
|
||||
|
||||
|
||||
func close_for_water_recovery() -> void:
|
||||
close_shop(false)
|
||||
|
||||
|
||||
func close_for_session_end() -> void:
|
||||
close_shop(false)
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible or not event.is_action_pressed("ui_cancel"):
|
||||
return
|
||||
close_shop()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _purchase_upgrade() -> void:
|
||||
if _network_shop == null or _transaction_pending:
|
||||
return
|
||||
var request_id: String = _network_shop.request_home_upgrade()
|
||||
if request_id.is_empty():
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_purchase_pending(_request_id: String) -> void:
|
||||
_transaction_pending = true
|
||||
_feedback.text = "upgrading…"
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_purchase_finished(
|
||||
_request_id: String,
|
||||
accepted: bool,
|
||||
message: String,
|
||||
product_id: StringName,
|
||||
category: int,
|
||||
_quantity: int,
|
||||
_total_cost: int,
|
||||
) -> void:
|
||||
if (
|
||||
category != NetworkShopProtocol.ProductCategory.HOME_UPGRADE
|
||||
or product_id != NetworkShopService.HOME_UPGRADE_PRODUCT_ID
|
||||
):
|
||||
return
|
||||
_transaction_pending = false
|
||||
_feedback.text = "starter RV unlocked" if accepted else message.to_lower()
|
||||
_refresh()
|
||||
|
||||
|
||||
func _on_wallet_changed(_balance: int, _delta: int) -> void:
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh() -> void:
|
||||
if _wallet_amount == null:
|
||||
return
|
||||
var balance: int = _wallet.get_balance() if _wallet != null else 0
|
||||
var tier: int = _home_state.get_tier() if _home_state != null else 0
|
||||
var current_capacity: int = (
|
||||
_home_state.get_storage_capacity() if _home_state != null else 9
|
||||
)
|
||||
var next_capacity: int = (
|
||||
_home_state.get_next_storage_capacity() if _home_state != null else -1
|
||||
)
|
||||
var cost: int = (
|
||||
_home_state.get_next_upgrade_cost() if _home_state != null else -1
|
||||
)
|
||||
_wallet_amount.set_amount(balance)
|
||||
_price_amount.set_amount(maxi(cost, 0))
|
||||
if tier >= PlayerHomeState.MAX_TIER:
|
||||
_home_label.text = "starter RV"
|
||||
_storage_label.text = "storage · %d slots" % current_capacity
|
||||
_upgrade_button.text = "unlocked"
|
||||
_upgrade_button.disabled = true
|
||||
_price_amount.visible = false
|
||||
else:
|
||||
_home_label.text = "tent → starter RV"
|
||||
_storage_label.text = "storage · %d → %d slots" % [
|
||||
current_capacity, next_capacity,
|
||||
]
|
||||
_upgrade_button.text = "upgrade"
|
||||
_upgrade_button.disabled = (
|
||||
_transaction_pending
|
||||
or _wallet == null
|
||||
or not _wallet.can_afford(cost)
|
||||
or _network_shop == null
|
||||
or not _network_shop.can_request_home_upgrade()
|
||||
)
|
||||
_price_amount.visible = true
|
||||
|
||||
|
||||
func _restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
|
||||
return (
|
||||
Input.MOUSE_MODE_VISIBLE
|
||||
if mouse_mode == Input.MOUSE_MODE_CAPTURED
|
||||
else mouse_mode
|
||||
)
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
z_index = 83
|
||||
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(-390.0, -225.0)
|
||||
panel.size = Vector2(780.0, 450.0)
|
||||
panel.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyleType.rounded_style(
|
||||
UtilityPageStyleType.OCEAN_PANEL_DEEP, 24
|
||||
),
|
||||
)
|
||||
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, 28)
|
||||
panel.add_child(margin)
|
||||
var layout := VBoxContainer.new()
|
||||
layout.add_theme_constant_override("separation", 18)
|
||||
margin.add_child(layout)
|
||||
|
||||
var header := HBoxContainer.new()
|
||||
header.add_theme_constant_override("separation", 12)
|
||||
layout.add_child(header)
|
||||
var title := Label.new()
|
||||
title.text = "home upgrades"
|
||||
title.add_theme_font_size_override("font_size", 38)
|
||||
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header.add_child(title)
|
||||
_wallet_amount = CurrencyPresentationType.instantiate_amount(0, 24.0)
|
||||
header.add_child(_wallet_amount)
|
||||
|
||||
var offer := PanelContainer.new()
|
||||
offer.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
offer.add_theme_stylebox_override(
|
||||
"panel",
|
||||
UtilityPageStyleType.rounded_style(
|
||||
UtilityPageStyleType.OCEAN_FIELD, 18
|
||||
),
|
||||
)
|
||||
layout.add_child(offer)
|
||||
var offer_margin := MarginContainer.new()
|
||||
for side: StringName in [
|
||||
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
|
||||
]:
|
||||
offer_margin.add_theme_constant_override(side, 28)
|
||||
offer.add_child(offer_margin)
|
||||
var details := VBoxContainer.new()
|
||||
details.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
details.add_theme_constant_override("separation", 14)
|
||||
offer_margin.add_child(details)
|
||||
_home_label = Label.new()
|
||||
_home_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_home_label.add_theme_font_size_override("font_size", 34)
|
||||
details.add_child(_home_label)
|
||||
var description := Label.new()
|
||||
description.text = (
|
||||
"A weatherproof home with a larger interior and built-in storage."
|
||||
)
|
||||
description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
description.add_theme_font_size_override("font_size", 20)
|
||||
details.add_child(description)
|
||||
_storage_label = Label.new()
|
||||
_storage_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
_storage_label.add_theme_font_size_override("font_size", 22)
|
||||
details.add_child(_storage_label)
|
||||
var purchase_row := HBoxContainer.new()
|
||||
purchase_row.alignment = BoxContainer.ALIGNMENT_CENTER
|
||||
purchase_row.add_theme_constant_override("separation", 10)
|
||||
details.add_child(purchase_row)
|
||||
_upgrade_button = Button.new()
|
||||
_upgrade_button.custom_minimum_size = Vector2(220.0, 60.0)
|
||||
UtilityPageStyleType.apply_ocean_button(_upgrade_button)
|
||||
_upgrade_button.pressed.connect(_purchase_upgrade)
|
||||
purchase_row.add_child(_upgrade_button)
|
||||
_price_amount = CurrencyPresentationType.instantiate_amount(0, 24.0)
|
||||
purchase_row.add_child(_price_amount)
|
||||
|
||||
var footer := HBoxContainer.new()
|
||||
footer.add_theme_constant_override("separation", 12)
|
||||
layout.add_child(footer)
|
||||
_feedback = Label.new()
|
||||
_feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_feedback.add_theme_font_size_override("font_size", 18)
|
||||
footer.add_child(_feedback)
|
||||
_close_button = Button.new()
|
||||
_close_button.text = "close"
|
||||
_close_button.custom_minimum_size = Vector2(150.0, 50.0)
|
||||
UtilityPageStyleType.apply_ocean_button(_close_button)
|
||||
_close_button.pressed.connect(close_shop)
|
||||
footer.add_child(_close_button)
|
||||
_upgrade_button.focus_neighbor_bottom = _upgrade_button.get_path_to(
|
||||
_close_button
|
||||
)
|
||||
_close_button.focus_neighbor_top = _close_button.get_path_to(
|
||||
_upgrade_button
|
||||
)
|
||||
1
ui/rv_upgrade_shop.gd.uid
Normal file
1
ui/rv_upgrade_shop.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b5pnsuph7fxrd
|
||||
14
ui/rv_upgrade_shop.tscn
Normal file
14
ui/rv_upgrade_shop.tscn
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/rv_upgrade_shop.gd" id="1_script"]
|
||||
|
||||
[node name="RVUpgradeShop" 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")
|
||||
|
|
@ -1,2 +1,10 @@
|
|||
class_name DecorShopInteraction
|
||||
extends FishingShopInteraction
|
||||
|
||||
const DECOR_DIALOGUE_TEXT: String = (
|
||||
"You gonna decorate that hot box or what? I've got some new decor in, take a look."
|
||||
)
|
||||
|
||||
|
||||
func get_dialogue_text(_home_tier: int = -1) -> String:
|
||||
return DECOR_DIALOGUE_TEXT
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ extends Area3D
|
|||
|
||||
signal local_player_range_changed(in_range: bool)
|
||||
|
||||
const DIALOGUE_TEXT: String = (
|
||||
"Listen, you may suck at fishing but I've got some things that might help."
|
||||
)
|
||||
|
||||
@export_node_path("Node3D") var look_character_path: NodePath = ^"../Shopkeeper"
|
||||
|
||||
var _local_player: Player
|
||||
|
|
@ -37,6 +41,10 @@ func is_avatar_in_range(avatar: Player) -> bool:
|
|||
)
|
||||
|
||||
|
||||
func get_dialogue_text(_home_tier: int = -1) -> String:
|
||||
return DIALOGUE_TEXT
|
||||
|
||||
|
||||
func get_prompt_anchor_position() -> Vector3:
|
||||
if _look_character != null:
|
||||
return _look_character.get_head_anchor_position()
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ const PlayerStorageInteractionType = preload(
|
|||
const DecorShopInteractionType = preload(
|
||||
"res://world/decor_shop_interaction.gd"
|
||||
)
|
||||
const RVUpgradeInteractionType = preload(
|
||||
"res://world/rv_upgrade_interaction.gd"
|
||||
)
|
||||
const PrecipitationOcclusionType = preload(
|
||||
"res://world/environment/precipitation_occlusion.gd"
|
||||
)
|
||||
|
|
@ -52,6 +55,8 @@ const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6
|
|||
const WATER_RECOVERY_INSET_STEP := 0.25
|
||||
const WATER_RECOVERY_MAXIMUM_INSET := 1.5
|
||||
const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0]
|
||||
const HOME_GROUND_HEIGHT_TOLERANCE := 0.5
|
||||
const HOME_ELEVATION_HEIGHT_TOLERANCE := 0.6
|
||||
const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [
|
||||
Vector2(1.0, 0.0),
|
||||
Vector2(0.70710678, 0.70710678),
|
||||
|
|
@ -86,9 +91,13 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [
|
|||
@onready var _decor_shop: DecorShopInteractionType = (
|
||||
$Interactables/DecorShopWorld/InteractionArea
|
||||
)
|
||||
@onready var _rv_upgrade_shop: RVUpgradeInteractionType = (
|
||||
$Interactables/RVUpgradeShopWorld/InteractionArea
|
||||
)
|
||||
@onready var _shop_root: Node3D = %FishingShopWorld
|
||||
@onready var _storage_root: Node3D = %PlayerStorageBox
|
||||
@onready var _decor_shop_root: Node3D = %DecorShopWorld
|
||||
@onready var _rv_upgrade_shop_root: Node3D = %RVUpgradeShopWorld
|
||||
@onready var _decorations: Node3D = %Decorations
|
||||
@onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks
|
||||
@onready var _diggable_beach: DiggableArea3D = %DiggableBeach
|
||||
|
|
@ -104,6 +113,7 @@ var _placed_prop_groups: Array[StringName] = []
|
|||
var _placed_group_coordinates: Dictionary[StringName, Array] = {}
|
||||
var _biome_assignments: Dictionary[Vector2i, StringName] = {}
|
||||
var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {}
|
||||
var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -159,6 +169,10 @@ func get_decor_shop() -> DecorShopInteractionType:
|
|||
return _decor_shop
|
||||
|
||||
|
||||
func get_rv_upgrade_shop() -> RVUpgradeInteractionType:
|
||||
return _rv_upgrade_shop
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
return _shoreline_reference
|
||||
|
||||
|
|
@ -184,6 +198,7 @@ func get_spawn_surface_triangles(
|
|||
material_names: Array[StringName],
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float = 0.6,
|
||||
maximum_global_y: float = 100.0,
|
||||
) -> Array[PackedVector3Array]:
|
||||
var triangles: Array[PackedVector3Array] = []
|
||||
if material_names.is_empty():
|
||||
|
|
@ -204,10 +219,27 @@ func get_spawn_surface_triangles(
|
|||
mesh.surface_get_arrays(surface_index),
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
return triangles
|
||||
|
||||
|
||||
func is_home_exterior_position_accessible(position: Vector3) -> bool:
|
||||
var local_position := _generator.to_local(position)
|
||||
if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE:
|
||||
return true
|
||||
var coordinate := _generator.coordinate_for_local_position(local_position)
|
||||
for value: Variant in _home_accessible_elevations_by_coordinate.get(
|
||||
coordinate,
|
||||
[],
|
||||
):
|
||||
if absf(local_position.y - float(value)) <= (
|
||||
HOME_ELEVATION_HEIGHT_TOLERANCE
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func get_water_recovery_position(
|
||||
entry_position: Vector3,
|
||||
fallback_position: Vector3,
|
||||
|
|
@ -242,6 +274,7 @@ func get_water_recovery_position(
|
|||
func _on_generation_completed(summary: Dictionary) -> void:
|
||||
_diggable_beach.invalidate_surface_cache()
|
||||
var records: Array[Dictionary] = _generator.placement_records()
|
||||
_cache_home_accessible_elevations(records)
|
||||
_assign_biomes(records, summary)
|
||||
var spawn_position := Vector3.ZERO
|
||||
_clear_children(_decorations)
|
||||
|
|
@ -388,6 +421,97 @@ func _on_generation_completed(summary: Dictionary) -> void:
|
|||
world_generated.emit(_current_seed, summary)
|
||||
|
||||
|
||||
func _cache_home_accessible_elevations(
|
||||
records: Array[Dictionary],
|
||||
) -> void:
|
||||
_home_accessible_elevations_by_coordinate.clear()
|
||||
var records_by_coordinate: Dictionary[Vector2i, Dictionary] = {}
|
||||
var elevation_tiers: Dictionary[int, bool] = {}
|
||||
for record: Dictionary in records:
|
||||
var coordinate: Vector2i = record.get(
|
||||
"coordinate",
|
||||
Vector2i(-1, -1),
|
||||
)
|
||||
records_by_coordinate[coordinate] = record
|
||||
for tier: int in _elevation_tiers_for_tags(
|
||||
record.get("tags", PackedStringArray())
|
||||
):
|
||||
elevation_tiers[tier] = true
|
||||
var visited: Dictionary[String, bool] = {}
|
||||
for tier: int in elevation_tiers:
|
||||
var elevation_tag := "elevation_%d" % tier
|
||||
for coordinate: Vector2i in records_by_coordinate:
|
||||
var visit_key := _home_elevation_visit_key(coordinate, tier)
|
||||
if visited.has(visit_key):
|
||||
continue
|
||||
var record: Dictionary = records_by_coordinate[coordinate]
|
||||
var tags: PackedStringArray = record.get(
|
||||
"tags",
|
||||
PackedStringArray(),
|
||||
)
|
||||
if elevation_tag not in tags:
|
||||
continue
|
||||
var component: Array[Vector2i] = []
|
||||
var pending: Array[Vector2i] = [coordinate]
|
||||
var has_ramp := false
|
||||
while not pending.is_empty():
|
||||
var current: Vector2i = pending.pop_back()
|
||||
var current_key := _home_elevation_visit_key(current, tier)
|
||||
if visited.has(current_key):
|
||||
continue
|
||||
var current_record: Dictionary = records_by_coordinate.get(
|
||||
current,
|
||||
{},
|
||||
)
|
||||
var current_tags: PackedStringArray = current_record.get(
|
||||
"tags",
|
||||
PackedStringArray(),
|
||||
)
|
||||
if elevation_tag not in current_tags:
|
||||
continue
|
||||
visited[current_key] = true
|
||||
component.append(current)
|
||||
has_ramp = has_ramp or "ramp" in current_tags
|
||||
for offset: Vector2i in [
|
||||
Vector2i.LEFT,
|
||||
Vector2i.RIGHT,
|
||||
Vector2i.UP,
|
||||
Vector2i.DOWN,
|
||||
]:
|
||||
pending.append(current + offset)
|
||||
if not has_ramp:
|
||||
continue
|
||||
var surface_height := (
|
||||
float(tier - 1) * _generator.elevated_cliff_level_height
|
||||
)
|
||||
for component_coordinate: Vector2i in component:
|
||||
var heights: Array = (
|
||||
_home_accessible_elevations_by_coordinate.get(
|
||||
component_coordinate,
|
||||
[],
|
||||
)
|
||||
)
|
||||
heights.append(surface_height)
|
||||
_home_accessible_elevations_by_coordinate[
|
||||
component_coordinate
|
||||
] = heights
|
||||
|
||||
|
||||
func _elevation_tiers_for_tags(tags: PackedStringArray) -> Array[int]:
|
||||
var tiers: Array[int] = []
|
||||
for tag_value: String in tags:
|
||||
if not tag_value.begins_with("elevation_"):
|
||||
continue
|
||||
var suffix := tag_value.trim_prefix("elevation_")
|
||||
if suffix.is_valid_int():
|
||||
tiers.append(suffix.to_int())
|
||||
return tiers
|
||||
|
||||
|
||||
func _home_elevation_visit_key(coordinate: Vector2i, tier: int) -> String:
|
||||
return "%d:%d:%d" % [tier, coordinate.x, coordinate.y]
|
||||
|
||||
|
||||
func _mark_precipitation_occluders() -> void:
|
||||
for prop: Node in _decorations.get_children():
|
||||
var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName
|
||||
|
|
@ -441,6 +565,8 @@ func _place_spawn_amenities(center: Vector3) -> void:
|
|||
_storage_root.rotation.y = PI * 0.5
|
||||
_decor_shop_root.position = center + Vector3(-2.4, 0.0, 1.2)
|
||||
_decor_shop_root.rotation.y = PI * 0.5
|
||||
_rv_upgrade_shop_root.position = center + Vector3(-2.35, 0.0, -1.35)
|
||||
_rv_upgrade_shop_root.rotation.y = PI
|
||||
|
||||
|
||||
func _configure_static_water() -> void:
|
||||
|
|
@ -1304,6 +1430,7 @@ func _terrain_surface_triangles_by_coordinate(
|
|||
mesh_instance.mesh.surface_get_arrays(surface_index),
|
||||
-INF,
|
||||
0.35,
|
||||
100.0,
|
||||
)
|
||||
if triangles.is_empty():
|
||||
continue
|
||||
|
|
@ -1651,6 +1778,7 @@ func _append_surface_triangles(
|
|||
arrays: Array,
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float,
|
||||
maximum_global_y: float,
|
||||
) -> void:
|
||||
if arrays.size() <= Mesh.ARRAY_INDEX:
|
||||
return
|
||||
|
|
@ -1660,10 +1788,28 @@ func _append_surface_triangles(
|
|||
return
|
||||
if indices.is_empty():
|
||||
for index: int in range(0, vertices.size() - 2, 3):
|
||||
_append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot)
|
||||
_append_triangle(
|
||||
result,
|
||||
mesh_instance,
|
||||
vertices[index],
|
||||
vertices[index + 1],
|
||||
vertices[index + 2],
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
return
|
||||
for index: int in range(0, indices.size() - 2, 3):
|
||||
_append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot)
|
||||
_append_triangle(
|
||||
result,
|
||||
mesh_instance,
|
||||
vertices[indices[index]],
|
||||
vertices[indices[index + 1]],
|
||||
vertices[indices[index + 2]],
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
|
||||
|
||||
func _append_triangle(
|
||||
|
|
@ -1674,11 +1820,15 @@ func _append_triangle(
|
|||
local_c: Vector3,
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float,
|
||||
maximum_global_y: float,
|
||||
) -> void:
|
||||
var a := mesh_instance.to_global(local_a)
|
||||
var b := mesh_instance.to_global(local_b)
|
||||
var c := mesh_instance.to_global(local_c)
|
||||
if minf(a.y, minf(b.y, c.y)) <= minimum_global_y:
|
||||
if (
|
||||
maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y
|
||||
or minf(a.y, minf(b.y, c.y)) >= maximum_global_y
|
||||
):
|
||||
return
|
||||
var cross := (b - a).cross(c - a)
|
||||
if cross.length_squared() <= 0.0000001:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=15 format=3]
|
||||
[gd_scene load_steps=16 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"]
|
||||
[ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"]
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
[ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"]
|
||||
[ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"]
|
||||
[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="14_decor_shop"]
|
||||
[ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="15_rv_upgrade"]
|
||||
|
||||
[node name="GeneratedWorldRegion" type="Node3D"]
|
||||
script = ExtResource("1_region")
|
||||
|
|
@ -102,6 +103,9 @@ unique_name_in_owner = true
|
|||
[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("14_decor_shop")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="RVUpgradeShopWorld" parent="Interactables" instance=ExtResource("15_rv_upgrade")]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="Decorations" type="Node3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
||||
|
|
|
|||
|
|
@ -4414,6 +4414,19 @@ func chunk_position(coordinate: Vector2i) -> Vector3:
|
|||
)
|
||||
|
||||
|
||||
func coordinate_for_local_position(position: Vector3) -> Vector2i:
|
||||
if catalog == null or catalog.chunk_size <= 0.0:
|
||||
return Vector2i(-1, -1)
|
||||
var half_grid := Vector2(
|
||||
float(grid_size.x - 1) * 0.5,
|
||||
float(grid_size.y - 1) * 0.5,
|
||||
)
|
||||
return Vector2i(
|
||||
roundi(position.x / catalog.chunk_size + half_grid.x),
|
||||
roundi(position.z / catalog.chunk_size + half_grid.y),
|
||||
)
|
||||
|
||||
|
||||
func get_generated_chunks_root() -> Node3D:
|
||||
return _generated_chunks
|
||||
|
||||
|
|
|
|||
|
|
@ -6,19 +6,19 @@
|
|||
|
||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||
resource_local_to_scene = true
|
||||
radius = 3.0
|
||||
radius = 2.1
|
||||
|
||||
[node name="DecorShopWorld" type="Node3D"]
|
||||
|
||||
[node name="Shopkeeper" parent="." instance=ExtResource("2_character")]
|
||||
script = ExtResource("3_display")
|
||||
species_id = "round"
|
||||
fur_color_id = "orange"
|
||||
ears_id = "pointy_short"
|
||||
tail_id = "cat"
|
||||
eyes_id = "simple_shine"
|
||||
nose_id = "dog_round"
|
||||
mouth_id = "three"
|
||||
fur_color_id = "brown"
|
||||
ears_id = "bear"
|
||||
tail_id = "bear"
|
||||
eyes_id = "tired"
|
||||
nose_id = "triangle_pink"
|
||||
mouth_id = "simple_smile"
|
||||
head_look_enabled = true
|
||||
|
||||
[node name="AnimationPlayer" parent="Shopkeeper"]
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||
resource_local_to_scene = true
|
||||
radius = 3.0
|
||||
radius = 2.1
|
||||
|
||||
[node name="FishingShopWorld" type="Node3D"]
|
||||
|
||||
|
|
|
|||
36
world/interactables/rv_upgrade_shop_world.tscn
Normal file
36
world/interactables/rv_upgrade_shop_world.tscn
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://world/rv_upgrade_interaction.gd" id="1_shop"]
|
||||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"]
|
||||
[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="InteractionShape"]
|
||||
resource_local_to_scene = true
|
||||
radius = 2.1
|
||||
|
||||
[node name="RVUpgradeShopWorld" type="Node3D"]
|
||||
|
||||
[node name="Shopkeeper" parent="." instance=ExtResource("2_character")]
|
||||
script = ExtResource("3_display")
|
||||
species_id = "pointy"
|
||||
fur_color_id = "teal"
|
||||
ears_id = "pointy_long"
|
||||
tail_id = "fox"
|
||||
eyes_id = "interested"
|
||||
nose_id = "triangle_small"
|
||||
mouth_id = "cheeky_smile"
|
||||
head_look_enabled = true
|
||||
|
||||
[node name="AnimationPlayer" parent="Shopkeeper"]
|
||||
autoplay = "idle"
|
||||
|
||||
[node name="InteractionArea" type="Area3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 1, -0.3)
|
||||
collision_layer = 0
|
||||
collision_mask = 2
|
||||
script = ExtResource("1_shop")
|
||||
|
||||
[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"]
|
||||
shape = SubResource("InteractionShape")
|
||||
vertical_icon_alignment = 1
|
||||
|
|
@ -11,6 +11,9 @@ const PlayerStorageInteractionType = preload(
|
|||
const DecorShopInteractionType = preload(
|
||||
"res://world/decor_shop_interaction.gd"
|
||||
)
|
||||
const RVUpgradeInteractionType = preload(
|
||||
"res://world/rv_upgrade_interaction.gd"
|
||||
)
|
||||
const FOLIAGE_WIND_SHADER: Shader = preload(
|
||||
"res://world/materials/foliage_wind.gdshader"
|
||||
)
|
||||
|
|
@ -56,6 +59,10 @@ var player_storage_path: NodePath = (
|
|||
var decor_shop_path: NodePath = (
|
||||
^"Interactables/DecorShopWorld/InteractionArea"
|
||||
)
|
||||
@export_node_path("Area3D")
|
||||
var rv_upgrade_shop_path: NodePath = (
|
||||
^"Interactables/RVUpgradeShopWorld/InteractionArea"
|
||||
)
|
||||
@export_group("Terrain Collision")
|
||||
# The imported GLB is the collision authority. Rebuild once per region load so
|
||||
# newly authored terrain and props cannot retain a stale saved fallback shape.
|
||||
|
|
@ -95,6 +102,10 @@ func get_decor_shop() -> DecorShopInteractionType:
|
|||
return get_node_or_null(decor_shop_path) as DecorShopInteractionType
|
||||
|
||||
|
||||
func get_rv_upgrade_shop() -> RVUpgradeInteractionType:
|
||||
return get_node_or_null(rv_upgrade_shop_path) as RVUpgradeInteractionType
|
||||
|
||||
|
||||
func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
||||
return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D
|
||||
|
||||
|
|
@ -150,6 +161,7 @@ func get_spawn_surface_triangles(
|
|||
material_names: Array[StringName],
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float = 0.6,
|
||||
maximum_global_y: float = 100.0,
|
||||
) -> Array[PackedVector3Array]:
|
||||
var triangles: Array[PackedVector3Array] = []
|
||||
if material_names.is_empty():
|
||||
|
|
@ -188,6 +200,7 @@ func get_spawn_surface_triangles(
|
|||
vertices[vertex_index + 2],
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
else:
|
||||
for index_offset: int in range(0, indices.size() - 2, 3):
|
||||
|
|
@ -199,6 +212,7 @@ func get_spawn_surface_triangles(
|
|||
vertices[indices[index_offset + 2]],
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
return triangles
|
||||
|
||||
|
|
@ -211,14 +225,14 @@ func _append_spawn_triangle(
|
|||
local_c: Vector3,
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float,
|
||||
maximum_global_y: float,
|
||||
) -> void:
|
||||
var a: Vector3 = mesh_instance.to_global(local_a)
|
||||
var b: Vector3 = mesh_instance.to_global(local_b)
|
||||
var c: Vector3 = mesh_instance.to_global(local_c)
|
||||
if (
|
||||
a.y <= minimum_global_y
|
||||
or b.y <= minimum_global_y
|
||||
or c.y <= minimum_global_y
|
||||
maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y
|
||||
or minf(a.y, minf(b.y, c.y)) >= maximum_global_y
|
||||
):
|
||||
return
|
||||
var cross: Vector3 = (b - a).cross(c - a)
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -22,6 +22,10 @@ func get_decor_shop() -> DecorShopInteraction:
|
|||
return null
|
||||
|
||||
|
||||
func get_rv_upgrade_shop() -> RVUpgradeInteraction:
|
||||
return null
|
||||
|
||||
|
||||
func get_player_storage() -> PlayerStorageInteraction:
|
||||
return null
|
||||
|
||||
|
|
@ -42,10 +46,24 @@ func get_spawn_surface_triangles(
|
|||
_material_names: Array[StringName],
|
||||
_minimum_global_y: float,
|
||||
_minimum_up_dot: float = 0.6,
|
||||
_maximum_global_y: float = 100.0,
|
||||
) -> Array[PackedVector3Array]:
|
||||
return []
|
||||
|
||||
|
||||
func get_saltwater_surface_height() -> float:
|
||||
for water_region: FishableWaterRegion in get_fishable_water_regions():
|
||||
if water_region.water_type == WaterType.Type.SALT_WATER:
|
||||
return water_region.get_surface_height()
|
||||
return NAN
|
||||
|
||||
|
||||
## Authored regions are assumed to expose only deliberately reachable home
|
||||
## sites. Procedural regions override this to reject isolated elevations.
|
||||
func is_home_exterior_position_accessible(_position: Vector3) -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func get_fishable_water_regions() -> Array[FishableWaterRegion]:
|
||||
var regions: Array[FishableWaterRegion] = []
|
||||
var root: Node = get_node_or_null(fishable_water_root)
|
||||
|
|
|
|||
15
world/rv_upgrade_interaction.gd
Normal file
15
world/rv_upgrade_interaction.gd
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
class_name RVUpgradeInteraction
|
||||
extends FishingShopInteraction
|
||||
|
||||
const TENT_DIALOGUE_TEXT: String = (
|
||||
"That tent is looking awful cramped. Can I talk you into an upgrade?"
|
||||
)
|
||||
const RV_DIALOGUE_TEXT: String = "How's the new RV treating you?"
|
||||
|
||||
|
||||
func get_dialogue_text(home_tier: int = -1) -> String:
|
||||
return (
|
||||
RV_DIALOGUE_TEXT
|
||||
if home_tier >= PlayerHomeState.STARTER_RV_TIER
|
||||
else TENT_DIALOGUE_TEXT
|
||||
)
|
||||
1
world/rv_upgrade_interaction.gd.uid
Normal file
1
world/rv_upgrade_interaction.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c0h08goorv2of
|
||||
|
|
@ -10,6 +10,9 @@ const PlayerStorageInteractionType = preload(
|
|||
const DecorShopInteractionType = preload(
|
||||
"res://world/decor_shop_interaction.gd"
|
||||
)
|
||||
const RVUpgradeInteractionType = preload(
|
||||
"res://world/rv_upgrade_interaction.gd"
|
||||
)
|
||||
const WorldLayoutType = preload("res://world/world_layout.gd")
|
||||
const GENERATED_REGION_SCENE: PackedScene = preload(
|
||||
"res://world/generation/generated_world_region.tscn"
|
||||
|
|
@ -75,6 +78,10 @@ func get_decor_shop() -> DecorShopInteractionType:
|
|||
return _active_region.get_decor_shop()
|
||||
|
||||
|
||||
func get_rv_upgrade_shop() -> RVUpgradeInteractionType:
|
||||
return _active_region.get_rv_upgrade_shop()
|
||||
|
||||
|
||||
func get_player_spawn_transform() -> Transform3D:
|
||||
return _active_region.get_player_spawn_transform()
|
||||
|
||||
|
|
@ -170,10 +177,29 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D:
|
|||
func get_spawn_surface_triangles(
|
||||
material_names: Array[StringName],
|
||||
minimum_global_y: float,
|
||||
minimum_up_dot: float = 0.6,
|
||||
maximum_global_y: float = 100.0,
|
||||
) -> Array[PackedVector3Array]:
|
||||
return _active_region.get_spawn_surface_triangles(
|
||||
material_names,
|
||||
minimum_global_y,
|
||||
minimum_up_dot,
|
||||
maximum_global_y,
|
||||
)
|
||||
|
||||
|
||||
func get_saltwater_surface_height() -> float:
|
||||
return (
|
||||
_active_region.get_saltwater_surface_height()
|
||||
if _active_region != null
|
||||
else NAN
|
||||
)
|
||||
|
||||
|
||||
func is_home_exterior_position_accessible(position: Vector3) -> bool:
|
||||
return (
|
||||
_active_region != null
|
||||
and _active_region.is_home_exterior_position_accessible(position)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue