Add starter RV progression and fishing feedback

This commit is contained in:
Alexander Sellite 2026-08-31 16:03:55 -04:00
parent eed361681a
commit d8bf59bca0
47 changed files with 2268 additions and 467 deletions

Binary file not shown.

Binary file not shown.

View 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

Binary file not shown.

View 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

View file

@ -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:

View file

@ -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)

View file

@ -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