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
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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue