Add portable RV homes and decor prototype
This commit is contained in:
parent
81decf2b71
commit
fe099f47dd
56 changed files with 5459 additions and 112 deletions
59
homes/decor_catalog.gd
Normal file
59
homes/decor_catalog.gd
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
class_name DecorCatalog
|
||||
extends RefCounted
|
||||
|
||||
const BASIC_CUBE_ID: StringName = &"basic_cube"
|
||||
const MAX_OWNED_PER_PRODUCT: int = 32
|
||||
|
||||
const _PRODUCTS: Dictionary = {
|
||||
BASIC_CUBE_ID: {
|
||||
"display_name": "basic cube",
|
||||
"description": "a simple piece of decor for testing your RV layout.",
|
||||
"price": 1,
|
||||
"footprint": Vector2(1.0, 0.8),
|
||||
"height": 0.8,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
static func get_product_ids() -> Array[StringName]:
|
||||
var ids: Array[StringName] = []
|
||||
for value: Variant in _PRODUCTS.keys():
|
||||
ids.append(StringName(str(value)))
|
||||
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
|
||||
return String(first) < String(second)
|
||||
)
|
||||
return ids
|
||||
|
||||
|
||||
static func has_product(product_id: StringName) -> bool:
|
||||
return _PRODUCTS.has(product_id)
|
||||
|
||||
|
||||
static func get_product(product_id: StringName) -> Dictionary:
|
||||
var value: Variant = _PRODUCTS.get(product_id)
|
||||
return (
|
||||
(value as Dictionary).duplicate(true)
|
||||
if typeof(value) == TYPE_DICTIONARY
|
||||
else {}
|
||||
)
|
||||
|
||||
|
||||
static func get_display_name(product_id: StringName) -> String:
|
||||
return str(get_product(product_id).get("display_name", product_id))
|
||||
|
||||
|
||||
static func get_description(product_id: StringName) -> String:
|
||||
return str(get_product(product_id).get("description", ""))
|
||||
|
||||
|
||||
static func get_price(product_id: StringName) -> int:
|
||||
return int(get_product(product_id).get("price", -1))
|
||||
|
||||
|
||||
static func get_footprint(product_id: StringName) -> Vector2:
|
||||
var value: Variant = get_product(product_id).get("footprint", Vector2.ZERO)
|
||||
return value as Vector2 if value is Vector2 else Vector2.ZERO
|
||||
|
||||
|
||||
static func get_height(product_id: StringName) -> float:
|
||||
return float(get_product(product_id).get("height", 0.0))
|
||||
1
homes/decor_catalog.gd.uid
Normal file
1
homes/decor_catalog.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bvgwmexas2gps
|
||||
457
homes/home_decor_controller.gd
Normal file
457
homes/home_decor_controller.gd
Normal file
|
|
@ -0,0 +1,457 @@
|
|||
class_name HomeDecorController
|
||||
extends Node
|
||||
|
||||
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
|
||||
const DRAG_RESPONSE: float = 22.0
|
||||
const DRAG_POSITION_EPSILON: float = 0.005
|
||||
|
||||
var _player: Player
|
||||
var _hotbar: PlayerHotbar
|
||||
var _bag: PlayerBag
|
||||
var _home_state: PlayerHomeState
|
||||
var _network_home: NetworkHomeService
|
||||
var _home_world: HomeWorldService
|
||||
var _fishing_spot: FishingSpot
|
||||
var _toolbar: HomeDecorToolbar
|
||||
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
|
||||
var _selected_instance_id: String = ""
|
||||
var _drag_instance_id: String = ""
|
||||
var _drag_owner_fingerprint: String = ""
|
||||
var _drag_pointer_offset: Vector3 = Vector3.ZERO
|
||||
var _drag_original_position: Vector3 = Vector3.ZERO
|
||||
var _drag_original_yaw: float = 0.0
|
||||
var _drag_target_position: Vector3 = Vector3.ZERO
|
||||
var _drag_target_yaw: float = 0.0
|
||||
var _drag_target_valid: bool = false
|
||||
|
||||
|
||||
func setup(
|
||||
player: Player,
|
||||
hotbar: PlayerHotbar,
|
||||
bag: PlayerBag,
|
||||
home_state: PlayerHomeState,
|
||||
network_home: NetworkHomeService,
|
||||
home_world: HomeWorldService,
|
||||
fishing_spot: FishingSpot,
|
||||
toolbar: HomeDecorToolbar,
|
||||
) -> void:
|
||||
_player = player
|
||||
_hotbar = hotbar
|
||||
_bag = bag
|
||||
_home_state = home_state
|
||||
_network_home = network_home
|
||||
_home_world = home_world
|
||||
_fishing_spot = fishing_spot
|
||||
_toolbar = toolbar
|
||||
if not _network_home.local_space_changed.is_connected(
|
||||
_on_local_space_changed
|
||||
):
|
||||
_network_home.local_space_changed.connect(_on_local_space_changed)
|
||||
if not _home_state.changed.is_connected(_on_home_changed):
|
||||
_home_state.changed.connect(_on_home_changed)
|
||||
if not _toolbar.placement_mode_requested.is_connected(
|
||||
_on_placement_mode_requested
|
||||
):
|
||||
_toolbar.placement_mode_requested.connect(
|
||||
_on_placement_mode_requested
|
||||
)
|
||||
_toolbar.rotate_requested.connect(_rotate_selected)
|
||||
_toolbar.store_requested.connect(_store_selected)
|
||||
_toolbar.cancel_requested.connect(_clear_selection)
|
||||
_toolbar.set_mode(_mode)
|
||||
_refresh_toolbar()
|
||||
|
||||
|
||||
func begin_decorating() -> void:
|
||||
_refresh_toolbar()
|
||||
if _toolbar.visible:
|
||||
_toolbar.report_status(
|
||||
"use held decor to place it; click or drag placed decor"
|
||||
)
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if _drag_instance_id.is_empty():
|
||||
return
|
||||
var pointer_motion := event as InputEventMouseMotion
|
||||
if pointer_motion != null:
|
||||
_update_drag(pointer_motion.position)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var pointer_button := event as InputEventMouseButton
|
||||
if (
|
||||
pointer_button != null
|
||||
and pointer_button.button_index == MOUSE_BUTTON_LEFT
|
||||
and not pointer_button.pressed
|
||||
):
|
||||
_finish_drag()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
var pointer_button := event as InputEventMouseButton
|
||||
if (
|
||||
pointer_button != null
|
||||
and pointer_button.button_index == MOUSE_BUTTON_LEFT
|
||||
):
|
||||
if not pointer_button.pressed or not _can_decorate():
|
||||
return
|
||||
if _begin_drag(pointer_button.position):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var pointer_decor_id: StringName = _get_selected_decor_id()
|
||||
if (
|
||||
not pointer_decor_id.is_empty()
|
||||
and _bag.get_quantity(pointer_decor_id) > 0
|
||||
):
|
||||
_place_carried_decor(pointer_decor_id)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if not _can_decorate() or not event.is_action_pressed("fish_primary"):
|
||||
return
|
||||
if event is InputEventKey and event.echo:
|
||||
return
|
||||
var decor_id: StringName = _get_selected_decor_id()
|
||||
if not decor_id.is_empty() and _bag.get_quantity(decor_id) > 0:
|
||||
_place_carried_decor(decor_id)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if _drag_instance_id.is_empty():
|
||||
return
|
||||
if not Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT):
|
||||
_finish_drag()
|
||||
return
|
||||
if not _drag_target_valid:
|
||||
return
|
||||
if not _can_decorate():
|
||||
_cancel_drag()
|
||||
return
|
||||
var current: Dictionary = _home_world.get_decor_preview_transform(
|
||||
_drag_owner_fingerprint, _drag_instance_id
|
||||
)
|
||||
if current.is_empty():
|
||||
_cancel_drag()
|
||||
return
|
||||
var current_position: Vector3 = current["position"]
|
||||
var current_yaw: float = float(current["yaw"])
|
||||
var blend: float = 1.0 - exp(-DRAG_RESPONSE * delta)
|
||||
_home_world.set_decor_preview_transform(
|
||||
_drag_owner_fingerprint,
|
||||
_drag_instance_id,
|
||||
current_position.lerp(_drag_target_position, blend),
|
||||
lerp_angle(current_yaw, _drag_target_yaw, blend),
|
||||
)
|
||||
|
||||
|
||||
func _begin_drag(screen_position: Vector2) -> bool:
|
||||
var owner_fingerprint: String = (
|
||||
_network_home.get_local_home_owner_fingerprint()
|
||||
)
|
||||
var camera: Camera3D = _player.get_active_gameplay_camera()
|
||||
var clicked_instance: String = _home_world.find_decor_instance_at_screen(
|
||||
owner_fingerprint, camera, screen_position
|
||||
)
|
||||
if clicked_instance.is_empty():
|
||||
return false
|
||||
var placement: Dictionary = _home_state.get_placement(clicked_instance)
|
||||
if placement.is_empty():
|
||||
return false
|
||||
var original_position: Vector3 = _placement_position(placement)
|
||||
if not original_position.is_finite():
|
||||
return false
|
||||
if not _home_world.set_decor_manipulation_active(
|
||||
owner_fingerprint, clicked_instance, true
|
||||
):
|
||||
return false
|
||||
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
|
||||
owner_fingerprint, camera, screen_position
|
||||
)
|
||||
_drag_instance_id = clicked_instance
|
||||
_drag_owner_fingerprint = owner_fingerprint
|
||||
_drag_original_position = original_position
|
||||
_drag_original_yaw = float(placement.get("yaw", 0.0))
|
||||
_drag_target_position = original_position
|
||||
_drag_target_yaw = _drag_original_yaw
|
||||
_drag_target_valid = true
|
||||
_drag_pointer_offset = (
|
||||
original_position - pointer_position
|
||||
if pointer_position.is_finite()
|
||||
else Vector3.ZERO
|
||||
)
|
||||
_drag_pointer_offset.y = 0.0
|
||||
_select_instance(clicked_instance)
|
||||
_toolbar.report_status("drag decor, then release to place it")
|
||||
return true
|
||||
|
||||
|
||||
func _update_drag(screen_position: Vector2) -> void:
|
||||
if not _can_decorate():
|
||||
_cancel_drag()
|
||||
return
|
||||
var camera: Camera3D = _player.get_active_gameplay_camera()
|
||||
var pointer_position: Vector3 = _home_world.get_floor_position_from_screen(
|
||||
_drag_owner_fingerprint, camera, screen_position
|
||||
)
|
||||
if not pointer_position.is_finite():
|
||||
# Keep the most recent valid target when the pointer briefly leaves the
|
||||
# RV floor. Releasing there should place at that target, not throw the
|
||||
# decor all the way back to where the drag began.
|
||||
_toolbar.report_status("keep the pointer over the RV floor")
|
||||
return
|
||||
var placement: Dictionary = _home_state.get_placement(_drag_instance_id)
|
||||
if placement.is_empty():
|
||||
_clear_selection()
|
||||
return
|
||||
var candidate: Vector3 = pointer_position + _drag_pointer_offset
|
||||
candidate.y = 0.0
|
||||
var normalized: Dictionary = _network_home.get_valid_local_decor_transform(
|
||||
_drag_instance_id,
|
||||
candidate,
|
||||
float(placement.get("yaw", _drag_original_yaw)),
|
||||
_mode,
|
||||
)
|
||||
if normalized.is_empty():
|
||||
# A densely furnished area may have no nearby solution. Preserve the
|
||||
# last valid preview so the interaction remains stable while the player
|
||||
# moves the pointer toward open space.
|
||||
_toolbar.report_status("holding at the nearest available position")
|
||||
return
|
||||
_drag_target_position = normalized["position"]
|
||||
_drag_target_yaw = float(normalized["yaw"])
|
||||
_drag_target_valid = true
|
||||
_toolbar.report_status("release to place decor")
|
||||
|
||||
|
||||
func _finish_drag() -> void:
|
||||
var instance_id: String = _drag_instance_id
|
||||
var owner_fingerprint: String = _drag_owner_fingerprint
|
||||
var original_position: Vector3 = _drag_original_position
|
||||
var original_yaw: float = _drag_original_yaw
|
||||
var target_position: Vector3 = _drag_target_position
|
||||
var target_yaw: float = _drag_target_yaw
|
||||
var target_valid: bool = _drag_target_valid
|
||||
var placement: Dictionary = _home_state.get_placement(instance_id)
|
||||
var decor_id := StringName(str(placement.get("decor_id", "")))
|
||||
if (
|
||||
not target_valid
|
||||
or decor_id.is_empty()
|
||||
or not _home_world.is_decor_placement_clear_of_players(
|
||||
owner_fingerprint, decor_id, target_position, target_yaw
|
||||
)
|
||||
):
|
||||
_home_world.set_decor_preview_transform(
|
||||
owner_fingerprint,
|
||||
instance_id,
|
||||
original_position,
|
||||
original_yaw,
|
||||
)
|
||||
_home_world.set_decor_manipulation_active(
|
||||
owner_fingerprint, instance_id, false
|
||||
)
|
||||
_reset_drag_state()
|
||||
_toolbar.report_status(
|
||||
"a player is in the way"
|
||||
if target_valid
|
||||
else "that space is not available"
|
||||
)
|
||||
return
|
||||
_home_world.set_decor_preview_transform(
|
||||
owner_fingerprint, instance_id, target_position, target_yaw
|
||||
)
|
||||
if (
|
||||
original_position.distance_to(target_position)
|
||||
<= DRAG_POSITION_EPSILON
|
||||
and is_zero_approx(angle_difference(original_yaw, target_yaw))
|
||||
):
|
||||
_home_world.set_decor_manipulation_active(
|
||||
owner_fingerprint, instance_id, false
|
||||
)
|
||||
_reset_drag_state()
|
||||
_select_instance(instance_id)
|
||||
return
|
||||
if _network_home.update_local_decor(
|
||||
instance_id, target_position, target_yaw, _mode
|
||||
):
|
||||
_home_world.set_decor_manipulation_active(
|
||||
owner_fingerprint, instance_id, false
|
||||
)
|
||||
_reset_drag_state()
|
||||
_toolbar.report_status("decor moved")
|
||||
return
|
||||
_home_world.set_decor_preview_transform(
|
||||
owner_fingerprint,
|
||||
instance_id,
|
||||
original_position,
|
||||
original_yaw,
|
||||
)
|
||||
_home_world.set_decor_manipulation_active(
|
||||
owner_fingerprint, instance_id, false
|
||||
)
|
||||
_reset_drag_state()
|
||||
_toolbar.report_status("that space is not available")
|
||||
|
||||
|
||||
func _cancel_drag() -> void:
|
||||
if _drag_instance_id.is_empty():
|
||||
return
|
||||
_home_world.set_decor_preview_transform(
|
||||
_drag_owner_fingerprint,
|
||||
_drag_instance_id,
|
||||
_drag_original_position,
|
||||
_drag_original_yaw,
|
||||
)
|
||||
_home_world.set_decor_manipulation_active(
|
||||
_drag_owner_fingerprint, _drag_instance_id, false
|
||||
)
|
||||
_reset_drag_state()
|
||||
|
||||
|
||||
func _reset_drag_state() -> void:
|
||||
_drag_instance_id = ""
|
||||
_drag_owner_fingerprint = ""
|
||||
_drag_pointer_offset = Vector3.ZERO
|
||||
_drag_original_position = Vector3.ZERO
|
||||
_drag_original_yaw = 0.0
|
||||
_drag_target_position = Vector3.ZERO
|
||||
_drag_target_yaw = 0.0
|
||||
_drag_target_valid = false
|
||||
|
||||
|
||||
static func _placement_position(placement: Dictionary) -> Vector3:
|
||||
if placement.is_empty():
|
||||
return Vector3.INF
|
||||
var values: Array = placement.get("position", [])
|
||||
if values.size() != 3:
|
||||
return Vector3.INF
|
||||
return Vector3(
|
||||
float(values[0]), float(values[1]), float(values[2])
|
||||
)
|
||||
|
||||
|
||||
func _place_carried_decor(decor_id: StringName) -> void:
|
||||
var owner_fingerprint: String = (
|
||||
_network_home.get_local_home_owner_fingerprint()
|
||||
)
|
||||
var forward: Vector3 = -_player.global_basis.z
|
||||
forward.y = 0.0
|
||||
forward = forward.normalized()
|
||||
var global_position: Vector3 = _player.global_position + forward * 1.45
|
||||
var local_position: Vector3 = _home_world.interior_global_to_local(
|
||||
owner_fingerprint, global_position
|
||||
)
|
||||
var instance_id: String = _network_home.place_local_decor(
|
||||
decor_id, local_position, _player.global_rotation.y, _mode
|
||||
)
|
||||
if instance_id.is_empty():
|
||||
_toolbar.report_status("make room in front of you to place this decor")
|
||||
return
|
||||
_select_instance(instance_id)
|
||||
_toolbar.report_status("decor placed")
|
||||
|
||||
|
||||
func _select_instance(instance_id: String) -> void:
|
||||
var placement: Dictionary = _home_state.get_placement(instance_id)
|
||||
if placement.is_empty():
|
||||
_clear_selection()
|
||||
return
|
||||
_selected_instance_id = instance_id
|
||||
var decor_id := StringName(str(placement.get("decor_id", "")))
|
||||
_toolbar.set_selection(
|
||||
instance_id, DecorCatalogType.get_display_name(decor_id)
|
||||
)
|
||||
|
||||
|
||||
func _rotate_selected() -> void:
|
||||
if not _can_decorate() or _selected_instance_id.is_empty():
|
||||
return
|
||||
var placement: Dictionary = _home_state.get_placement(
|
||||
_selected_instance_id
|
||||
)
|
||||
if placement.is_empty():
|
||||
_clear_selection()
|
||||
return
|
||||
var values: Array = placement.get("position", [])
|
||||
if values.size() != 3:
|
||||
return
|
||||
var position := Vector3(
|
||||
float(values[0]), float(values[1]), float(values[2])
|
||||
)
|
||||
if _network_home.update_local_decor(
|
||||
_selected_instance_id,
|
||||
position,
|
||||
float(placement.get("yaw", 0.0)) + PI * 0.5,
|
||||
_mode,
|
||||
):
|
||||
_toolbar.report_status("decor rotated")
|
||||
else:
|
||||
_toolbar.report_status("not enough room to rotate here")
|
||||
|
||||
|
||||
func _store_selected() -> void:
|
||||
if _selected_instance_id.is_empty():
|
||||
return
|
||||
if _network_home.remove_local_decor(_selected_instance_id):
|
||||
_clear_selection()
|
||||
_toolbar.report_status("decor packed into storage")
|
||||
else:
|
||||
_toolbar.report_status("inventory and storage are full")
|
||||
|
||||
|
||||
func _clear_selection() -> void:
|
||||
_cancel_drag()
|
||||
_selected_instance_id = ""
|
||||
if _toolbar != null:
|
||||
_toolbar.set_selection("")
|
||||
|
||||
|
||||
func _on_placement_mode_requested(mode: int) -> void:
|
||||
_mode = mode as PlayerHomeState.PlacementMode
|
||||
|
||||
|
||||
func _on_local_space_changed(
|
||||
_space_id: StringName,
|
||||
_owner_fingerprint: String,
|
||||
) -> void:
|
||||
_clear_selection()
|
||||
_refresh_toolbar()
|
||||
|
||||
|
||||
func _on_home_changed() -> void:
|
||||
if (
|
||||
not _selected_instance_id.is_empty()
|
||||
and _home_state.get_placement(_selected_instance_id).is_empty()
|
||||
):
|
||||
_clear_selection()
|
||||
_refresh_toolbar()
|
||||
|
||||
|
||||
func _refresh_toolbar() -> void:
|
||||
if _toolbar == null:
|
||||
return
|
||||
_toolbar.set_active(_can_decorate())
|
||||
if not _toolbar.visible:
|
||||
_clear_selection()
|
||||
|
||||
|
||||
func _can_decorate() -> bool:
|
||||
return (
|
||||
_player != null
|
||||
and _network_home != null
|
||||
and _network_home.can_local_decorate()
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and _player.is_movement_enabled()
|
||||
)
|
||||
|
||||
|
||||
func _get_selected_decor_id() -> StringName:
|
||||
if (
|
||||
_hotbar == null
|
||||
or _hotbar.get_selected_assignment_kind()
|
||||
!= PlayerHotbar.AssignmentKind.ITEM
|
||||
):
|
||||
return StringName()
|
||||
var item_id: StringName = _hotbar.get_selected_item_id()
|
||||
return item_id if DecorCatalogType.has_product(item_id) else StringName()
|
||||
1
homes/home_decor_controller.gd.uid
Normal file
1
homes/home_decor_controller.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b5j0mqcvhmi54
|
||||
1183
homes/home_world_service.gd
Normal file
1183
homes/home_world_service.gd
Normal file
File diff suppressed because it is too large
Load diff
1
homes/home_world_service.gd.uid
Normal file
1
homes/home_world_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cy2kjoxhwxujd
|
||||
786
homes/player_home_state.gd
Normal file
786
homes/player_home_state.gd
Normal file
|
|
@ -0,0 +1,786 @@
|
|||
class_name PlayerHomeState
|
||||
extends Node
|
||||
|
||||
signal changed
|
||||
|
||||
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 MAX_PLACEMENTS: int = 128
|
||||
const MAX_INSTANCE_ID_LENGTH: int = 96
|
||||
const MAX_REVISION: int = 2147483647
|
||||
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 PLACEMENT_EDGE_PADDING: float = 0.02
|
||||
|
||||
enum Privacy {
|
||||
OPEN,
|
||||
CLOSED,
|
||||
}
|
||||
|
||||
enum PlacementMode {
|
||||
GRID,
|
||||
FREE,
|
||||
}
|
||||
|
||||
var _tier: int = STARTER_TIER
|
||||
var _exterior_id: StringName = STARTER_EXTERIOR_ID
|
||||
var _privacy: Privacy = Privacy.OPEN
|
||||
var _owned_decor: Dictionary[StringName, int] = {}
|
||||
var _placements: Array[Dictionary] = []
|
||||
var _revision: int = 0
|
||||
var _next_instance_sequence: int = 1
|
||||
|
||||
|
||||
func get_tier() -> int:
|
||||
return _tier
|
||||
|
||||
|
||||
func get_exterior_id() -> StringName:
|
||||
return _exterior_id
|
||||
|
||||
|
||||
func get_privacy() -> Privacy:
|
||||
return _privacy
|
||||
|
||||
|
||||
func get_revision() -> int:
|
||||
return _revision
|
||||
|
||||
|
||||
func get_owned_count(decor_id: StringName) -> int:
|
||||
return int(_owned_decor.get(decor_id, 0))
|
||||
|
||||
|
||||
func get_placed_count(decor_id: StringName) -> int:
|
||||
var count: int = 0
|
||||
for placement: Dictionary in _placements:
|
||||
if StringName(str(placement.get("decor_id", ""))) == decor_id:
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
func get_available_count(decor_id: StringName) -> int:
|
||||
return maxi(
|
||||
get_owned_count(decor_id) - get_placed_count(decor_id),
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
func get_owned_decor() -> Dictionary[StringName, int]:
|
||||
return _owned_decor.duplicate()
|
||||
|
||||
|
||||
func get_placements() -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for placement: Dictionary in _placements:
|
||||
result.append(placement.duplicate(true))
|
||||
return result
|
||||
|
||||
|
||||
func get_placement(instance_id: String) -> Dictionary:
|
||||
var index: int = _placement_index(instance_id)
|
||||
return _placements[index].duplicate(true) if index >= 0 else {}
|
||||
|
||||
|
||||
func can_place_decor(
|
||||
decor_id: StringName,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
) -> bool:
|
||||
if (
|
||||
get_available_count(decor_id) <= 0
|
||||
or _placements.size() >= MAX_PLACEMENTS
|
||||
or not _valid_mode(mode)
|
||||
):
|
||||
return false
|
||||
return not get_nearest_valid_placement_transform(
|
||||
decor_id,
|
||||
position,
|
||||
yaw_radians,
|
||||
mode,
|
||||
"",
|
||||
).is_empty()
|
||||
|
||||
|
||||
func set_privacy(value: Privacy) -> bool:
|
||||
if value < Privacy.OPEN or value > Privacy.CLOSED:
|
||||
return false
|
||||
if _privacy == value:
|
||||
return true
|
||||
_privacy = value
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
func add_owned_decor(
|
||||
decor_id: StringName,
|
||||
quantity: int = 1,
|
||||
) -> bool:
|
||||
if (
|
||||
not DecorCatalogType.has_product(decor_id)
|
||||
or quantity <= 0
|
||||
):
|
||||
return false
|
||||
var current: int = get_owned_count(decor_id)
|
||||
if current + quantity > DecorCatalogType.MAX_OWNED_PER_PRODUCT:
|
||||
return false
|
||||
_owned_decor[decor_id] = current + quantity
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
func remove_owned_decor(
|
||||
decor_id: StringName,
|
||||
quantity: int = 1,
|
||||
) -> bool:
|
||||
if quantity <= 0:
|
||||
return false
|
||||
var current: int = get_owned_count(decor_id)
|
||||
if current - quantity < get_placed_count(decor_id):
|
||||
return false
|
||||
var remaining: int = current - quantity
|
||||
if remaining < 0:
|
||||
return false
|
||||
if remaining == 0:
|
||||
_owned_decor.erase(decor_id)
|
||||
else:
|
||||
_owned_decor[decor_id] = remaining
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
func place_decor(
|
||||
decor_id: StringName,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
) -> String:
|
||||
if (
|
||||
get_available_count(decor_id) <= 0
|
||||
or _placements.size() >= MAX_PLACEMENTS
|
||||
or not _valid_mode(mode)
|
||||
):
|
||||
return ""
|
||||
var normalized := get_nearest_valid_placement_transform(
|
||||
decor_id,
|
||||
position,
|
||||
yaw_radians,
|
||||
mode,
|
||||
"",
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return ""
|
||||
var instance_id := _make_instance_id()
|
||||
var placement: Dictionary = {
|
||||
"instance_id": instance_id,
|
||||
"decor_id": String(decor_id),
|
||||
"mode": int(mode),
|
||||
"position": _vector3_to_array(normalized["position"]),
|
||||
"yaw": float(normalized["yaw"]),
|
||||
}
|
||||
_placements.append(placement)
|
||||
_touch()
|
||||
return instance_id
|
||||
|
||||
|
||||
func update_placement(
|
||||
instance_id: String,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
) -> bool:
|
||||
var index: int = _placement_index(instance_id)
|
||||
var normalized: Dictionary = get_valid_update_transform(
|
||||
instance_id, position, yaw_radians, mode
|
||||
)
|
||||
if index < 0 or normalized.is_empty():
|
||||
return false
|
||||
var placement: Dictionary = _placements[index].duplicate(true)
|
||||
placement["mode"] = int(mode)
|
||||
placement["position"] = _vector3_to_array(normalized["position"])
|
||||
placement["yaw"] = float(normalized["yaw"])
|
||||
_placements[index] = placement
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
func get_valid_update_transform(
|
||||
instance_id: String,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
candidate_validator: Callable = Callable(),
|
||||
) -> Dictionary:
|
||||
var index: int = _placement_index(instance_id)
|
||||
if index < 0 or not _valid_mode(mode):
|
||||
return {}
|
||||
var decor_id := StringName(
|
||||
str(_placements[index].get("decor_id", ""))
|
||||
)
|
||||
return get_nearest_valid_placement_transform(
|
||||
decor_id,
|
||||
position,
|
||||
yaw_radians,
|
||||
mode,
|
||||
instance_id,
|
||||
candidate_validator,
|
||||
)
|
||||
|
||||
|
||||
func get_nearest_valid_placement_transform(
|
||||
decor_id: StringName,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
ignored_instance_id: String = "",
|
||||
candidate_validator: Callable = Callable(),
|
||||
) -> Dictionary:
|
||||
var normalized: Dictionary = normalized_placement_transform(
|
||||
decor_id, position, yaw_radians, mode
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return {}
|
||||
if (
|
||||
not _overlaps_existing(
|
||||
decor_id, normalized, ignored_instance_id
|
||||
)
|
||||
and _candidate_is_accepted(normalized, candidate_validator)
|
||||
):
|
||||
return normalized
|
||||
return (
|
||||
_find_nearest_grid_transform(
|
||||
decor_id,
|
||||
normalized,
|
||||
ignored_instance_id,
|
||||
candidate_validator,
|
||||
)
|
||||
if mode == PlacementMode.GRID
|
||||
else _find_nearest_free_transform(
|
||||
decor_id,
|
||||
normalized,
|
||||
ignored_instance_id,
|
||||
candidate_validator,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func remove_placement(instance_id: String) -> bool:
|
||||
var index: int = _placement_index(instance_id)
|
||||
if index < 0:
|
||||
return false
|
||||
_placements.remove_at(index)
|
||||
_touch()
|
||||
return true
|
||||
|
||||
|
||||
func reset_to_defaults() -> void:
|
||||
_tier = STARTER_TIER
|
||||
_exterior_id = STARTER_EXTERIOR_ID
|
||||
_privacy = Privacy.OPEN
|
||||
_owned_decor.clear()
|
||||
_placements.clear()
|
||||
_revision = 0
|
||||
_next_instance_sequence = 1
|
||||
changed.emit()
|
||||
|
||||
|
||||
func to_save_data() -> Dictionary:
|
||||
var owned: Dictionary = {}
|
||||
var ids: Array[StringName] = []
|
||||
ids.assign(_owned_decor.keys())
|
||||
ids.sort_custom(func(first: StringName, second: StringName) -> bool:
|
||||
return String(first) < String(second)
|
||||
)
|
||||
for decor_id: StringName in ids:
|
||||
owned[String(decor_id)] = _owned_decor[decor_id]
|
||||
return {
|
||||
"schema_version": HOME_SCHEMA_VERSION,
|
||||
"tier": _tier,
|
||||
"exterior_id": String(_exterior_id),
|
||||
"privacy": int(_privacy),
|
||||
"owned_decor": owned,
|
||||
"placements": get_placements(),
|
||||
"revision": _revision,
|
||||
"next_instance_sequence": _next_instance_sequence,
|
||||
}
|
||||
|
||||
|
||||
func restore_from_save_data(data: Dictionary) -> bool:
|
||||
var sanitized: Dictionary = sanitize_save_data(data)
|
||||
if sanitized.is_empty():
|
||||
return false
|
||||
_tier = int(sanitized["tier"])
|
||||
_exterior_id = StringName(str(sanitized["exterior_id"]))
|
||||
_privacy = int(sanitized["privacy"])
|
||||
_owned_decor.clear()
|
||||
for key: Variant in (sanitized["owned_decor"] as Dictionary):
|
||||
_owned_decor[StringName(str(key))] = int(
|
||||
(sanitized["owned_decor"] as Dictionary)[key]
|
||||
)
|
||||
_placements.clear()
|
||||
for value: Variant in sanitized["placements"] as Array:
|
||||
_placements.append((value as Dictionary).duplicate(true))
|
||||
_revision = int(sanitized["revision"])
|
||||
_next_instance_sequence = int(sanitized["next_instance_sequence"])
|
||||
changed.emit()
|
||||
return true
|
||||
|
||||
|
||||
static func default_save_data() -> Dictionary:
|
||||
return {
|
||||
"schema_version": HOME_SCHEMA_VERSION,
|
||||
"tier": STARTER_TIER,
|
||||
"exterior_id": String(STARTER_EXTERIOR_ID),
|
||||
"privacy": int(Privacy.OPEN),
|
||||
"owned_decor": {},
|
||||
"placements": [],
|
||||
"revision": 0,
|
||||
"next_instance_sequence": 1,
|
||||
}
|
||||
|
||||
|
||||
static func sanitize_save_data(data: Dictionary) -> Dictionary:
|
||||
if (
|
||||
not _is_integer_value(data.get("schema_version"))
|
||||
or int(data.get("schema_version", -1)) != HOME_SCHEMA_VERSION
|
||||
or not _is_integer_value(data.get("tier"))
|
||||
or typeof(data.get("exterior_id")) not in [
|
||||
TYPE_STRING, TYPE_STRING_NAME,
|
||||
]
|
||||
or not _is_integer_value(data.get("privacy"))
|
||||
or typeof(data.get("owned_decor")) != TYPE_DICTIONARY
|
||||
or typeof(data.get("placements")) != TYPE_ARRAY
|
||||
or not _is_integer_value(data.get("revision"))
|
||||
or not _is_integer_value(data.get("next_instance_sequence"))
|
||||
):
|
||||
return {}
|
||||
var tier: int = int(data["tier"])
|
||||
var exterior_id: String = str(data["exterior_id"])
|
||||
var privacy: int = int(data["privacy"])
|
||||
var revision: int = int(data["revision"])
|
||||
var next_sequence: int = int(data["next_instance_sequence"])
|
||||
if (
|
||||
tier < STARTER_TIER
|
||||
or tier > MAX_TIER
|
||||
or exterior_id.is_empty()
|
||||
or exterior_id.length() > 96
|
||||
or privacy < Privacy.OPEN
|
||||
or privacy > Privacy.CLOSED
|
||||
or revision < 0
|
||||
or revision > MAX_REVISION
|
||||
or next_sequence < 1
|
||||
or next_sequence > MAX_REVISION
|
||||
):
|
||||
return {}
|
||||
var sanitized_owned: Dictionary = {}
|
||||
var owned: Dictionary = data["owned_decor"]
|
||||
for key: Variant in owned:
|
||||
if typeof(key) not in [TYPE_STRING, TYPE_STRING_NAME]:
|
||||
return {}
|
||||
var decor_id := StringName(str(key))
|
||||
var quantity_value: Variant = owned[key]
|
||||
if (
|
||||
not DecorCatalogType.has_product(decor_id)
|
||||
or not _is_integer_value(quantity_value)
|
||||
or int(quantity_value) <= 0
|
||||
or int(quantity_value) > DecorCatalogType.MAX_OWNED_PER_PRODUCT
|
||||
):
|
||||
return {}
|
||||
sanitized_owned[String(decor_id)] = int(quantity_value)
|
||||
var values: Array = data["placements"]
|
||||
if values.size() > MAX_PLACEMENTS:
|
||||
return {}
|
||||
var sanitized_placements: Array[Dictionary] = []
|
||||
var seen_instance_ids: Dictionary[String, bool] = {}
|
||||
var placed_counts: Dictionary[StringName, int] = {}
|
||||
for value: Variant in values:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
return {}
|
||||
var sanitized: Dictionary = sanitize_placement(value)
|
||||
if sanitized.is_empty():
|
||||
return {}
|
||||
var instance_id: String = str(sanitized["instance_id"])
|
||||
var decor_id := StringName(str(sanitized["decor_id"]))
|
||||
if seen_instance_ids.has(instance_id):
|
||||
return {}
|
||||
seen_instance_ids[instance_id] = true
|
||||
placed_counts[decor_id] = int(
|
||||
placed_counts.get(decor_id, 0)
|
||||
) + 1
|
||||
if (
|
||||
int(placed_counts[decor_id])
|
||||
> int(sanitized_owned.get(String(decor_id), 0))
|
||||
):
|
||||
return {}
|
||||
for previous: Dictionary in sanitized_placements:
|
||||
if placements_overlap(previous, sanitized):
|
||||
return {}
|
||||
sanitized_placements.append(sanitized)
|
||||
return {
|
||||
"schema_version": HOME_SCHEMA_VERSION,
|
||||
"tier": tier,
|
||||
"exterior_id": exterior_id,
|
||||
"privacy": privacy,
|
||||
"owned_decor": sanitized_owned,
|
||||
"placements": sanitized_placements,
|
||||
"revision": revision,
|
||||
"next_instance_sequence": next_sequence,
|
||||
}
|
||||
|
||||
|
||||
static func sanitize_placement(value: Dictionary) -> Dictionary:
|
||||
for key: String in [
|
||||
"instance_id", "decor_id", "mode", "position", "yaw",
|
||||
]:
|
||||
if not value.has(key):
|
||||
return {}
|
||||
if (
|
||||
typeof(value["instance_id"]) != TYPE_STRING
|
||||
or typeof(value["decor_id"]) not in [
|
||||
TYPE_STRING, TYPE_STRING_NAME,
|
||||
]
|
||||
or not _is_integer_value(value["mode"])
|
||||
or typeof(value["position"]) != TYPE_ARRAY
|
||||
or typeof(value["yaw"]) not in [TYPE_FLOAT, TYPE_INT]
|
||||
):
|
||||
return {}
|
||||
var instance_id: String = value["instance_id"]
|
||||
var decor_id := StringName(str(value["decor_id"]))
|
||||
var mode: int = int(value["mode"])
|
||||
var position_values: Array = value["position"]
|
||||
var yaw: float = float(value["yaw"])
|
||||
if (
|
||||
instance_id.is_empty()
|
||||
or instance_id.length() > MAX_INSTANCE_ID_LENGTH
|
||||
or not DecorCatalogType.has_product(decor_id)
|
||||
or not _valid_mode(mode)
|
||||
or position_values.size() != 3
|
||||
or not is_finite(yaw)
|
||||
):
|
||||
return {}
|
||||
for component: Variant in position_values:
|
||||
if typeof(component) not in [TYPE_FLOAT, TYPE_INT]:
|
||||
return {}
|
||||
var position := Vector3(
|
||||
float(position_values[0]),
|
||||
float(position_values[1]),
|
||||
float(position_values[2]),
|
||||
)
|
||||
var normalized := normalized_placement_transform(
|
||||
decor_id,
|
||||
position,
|
||||
yaw,
|
||||
mode as PlacementMode,
|
||||
)
|
||||
if normalized.is_empty():
|
||||
return {}
|
||||
return {
|
||||
"instance_id": instance_id,
|
||||
"decor_id": String(decor_id),
|
||||
"mode": mode,
|
||||
"position": _vector3_to_array(normalized["position"]),
|
||||
"yaw": float(normalized["yaw"]),
|
||||
}
|
||||
|
||||
|
||||
static func _is_integer_value(value: Variant) -> bool:
|
||||
if typeof(value) == TYPE_INT:
|
||||
return true
|
||||
if typeof(value) != TYPE_FLOAT:
|
||||
return false
|
||||
var number: float = float(value)
|
||||
return is_finite(number) and is_equal_approx(number, roundf(number))
|
||||
|
||||
|
||||
static func normalized_placement_transform(
|
||||
decor_id: StringName,
|
||||
position: Vector3,
|
||||
yaw_radians: float,
|
||||
mode: PlacementMode,
|
||||
) -> Dictionary:
|
||||
if (
|
||||
not DecorCatalogType.has_product(decor_id)
|
||||
or not position.is_finite()
|
||||
or not is_finite(yaw_radians)
|
||||
or not _valid_mode(mode)
|
||||
):
|
||||
return {}
|
||||
var normalized_position := Vector3(position.x, 0.0, position.z)
|
||||
var normalized_yaw: float = wrapf(yaw_radians, -PI, PI)
|
||||
if mode == PlacementMode.GRID:
|
||||
normalized_position.x = snappedf(normalized_position.x, GRID_SIZE)
|
||||
normalized_position.z = snappedf(normalized_position.z, GRID_SIZE)
|
||||
normalized_yaw = snappedf(normalized_yaw, PI * 0.5)
|
||||
var footprint: Vector2 = DecorCatalogType.get_footprint(decor_id)
|
||||
var cosine: float = absf(cos(normalized_yaw))
|
||||
var sine: float = absf(sin(normalized_yaw))
|
||||
var half_size := Vector2(
|
||||
(footprint.x * cosine + footprint.y * sine) * 0.5,
|
||||
(footprint.x * sine + footprint.y * cosine) * 0.5,
|
||||
)
|
||||
var maximum_x: float = (
|
||||
INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING
|
||||
)
|
||||
var maximum_z: float = (
|
||||
INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING
|
||||
)
|
||||
if maximum_x < 0.0 or maximum_z < 0.0:
|
||||
return {}
|
||||
# Clamp after grid snapping so a grid-placed item can still sit flush at a
|
||||
# wall even when the wall itself is not on an exact grid interval.
|
||||
normalized_position.x = clampf(
|
||||
normalized_position.x, -maximum_x, maximum_x
|
||||
)
|
||||
normalized_position.z = clampf(
|
||||
normalized_position.z, -maximum_z, maximum_z
|
||||
)
|
||||
return {
|
||||
"position": normalized_position,
|
||||
"yaw": normalized_yaw,
|
||||
}
|
||||
|
||||
|
||||
func _find_nearest_grid_transform(
|
||||
decor_id: StringName,
|
||||
origin: Dictionary,
|
||||
ignored_instance_id: String,
|
||||
candidate_validator: Callable,
|
||||
) -> Dictionary:
|
||||
var origin_position: Vector3 = origin["position"]
|
||||
var yaw: float = float(origin["yaw"])
|
||||
var maximum_ring: int = ceili(
|
||||
NEAREST_PLACEMENT_SEARCH_DISTANCE / GRID_SIZE
|
||||
)
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for ring: int in range(1, maximum_ring + 1):
|
||||
var nearest: Dictionary = {}
|
||||
var nearest_distance_squared: float = INF
|
||||
for x_offset: int in range(-ring, ring + 1):
|
||||
for z_offset: int in range(-ring, ring + 1):
|
||||
if maxi(absi(x_offset), absi(z_offset)) != ring:
|
||||
continue
|
||||
var candidate: Dictionary = normalized_placement_transform(
|
||||
decor_id,
|
||||
origin_position + Vector3(
|
||||
float(x_offset) * GRID_SIZE,
|
||||
0.0,
|
||||
float(z_offset) * GRID_SIZE,
|
||||
),
|
||||
yaw,
|
||||
PlacementMode.GRID,
|
||||
)
|
||||
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||
candidate, seen
|
||||
):
|
||||
continue
|
||||
if _overlaps_existing(
|
||||
decor_id, candidate, ignored_instance_id
|
||||
) or not _candidate_is_accepted(
|
||||
candidate, candidate_validator
|
||||
):
|
||||
continue
|
||||
var candidate_position: Vector3 = candidate["position"]
|
||||
var distance_squared: float = origin_position.distance_squared_to(
|
||||
candidate_position
|
||||
)
|
||||
if distance_squared < nearest_distance_squared:
|
||||
nearest = candidate
|
||||
nearest_distance_squared = distance_squared
|
||||
if not nearest.is_empty():
|
||||
return nearest
|
||||
return {}
|
||||
|
||||
|
||||
func _find_nearest_free_transform(
|
||||
decor_id: StringName,
|
||||
origin: Dictionary,
|
||||
ignored_instance_id: String,
|
||||
candidate_validator: Callable,
|
||||
) -> Dictionary:
|
||||
var origin_position: Vector3 = origin["position"]
|
||||
var yaw: float = float(origin["yaw"])
|
||||
var maximum_ring: int = ceili(
|
||||
NEAREST_PLACEMENT_SEARCH_DISTANCE / FREE_PLACEMENT_SEARCH_STEP
|
||||
)
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for ring: int in range(1, maximum_ring + 1):
|
||||
var radius: float = float(ring) * FREE_PLACEMENT_SEARCH_STEP
|
||||
var nearest: Dictionary = {}
|
||||
var nearest_distance_squared: float = INF
|
||||
for angle_index: int in FREE_PLACEMENT_SEARCH_ANGLES:
|
||||
var angle: float = (
|
||||
TAU * float(angle_index) / float(FREE_PLACEMENT_SEARCH_ANGLES)
|
||||
)
|
||||
var candidate: Dictionary = normalized_placement_transform(
|
||||
decor_id,
|
||||
origin_position + Vector3(
|
||||
cos(angle) * radius, 0.0, sin(angle) * radius
|
||||
),
|
||||
yaw,
|
||||
PlacementMode.FREE,
|
||||
)
|
||||
if candidate.is_empty() or not _mark_candidate_unseen(
|
||||
candidate, seen
|
||||
):
|
||||
continue
|
||||
if _overlaps_existing(
|
||||
decor_id, candidate, ignored_instance_id
|
||||
) or not _candidate_is_accepted(
|
||||
candidate, candidate_validator
|
||||
):
|
||||
continue
|
||||
var candidate_position: Vector3 = candidate["position"]
|
||||
var distance_squared: float = origin_position.distance_squared_to(
|
||||
candidate_position
|
||||
)
|
||||
if distance_squared < nearest_distance_squared:
|
||||
nearest = candidate
|
||||
nearest_distance_squared = distance_squared
|
||||
if not nearest.is_empty():
|
||||
return nearest
|
||||
return {}
|
||||
|
||||
|
||||
static func _mark_candidate_unseen(
|
||||
candidate: Dictionary,
|
||||
seen: Dictionary[String, bool],
|
||||
) -> bool:
|
||||
var position: Vector3 = candidate["position"]
|
||||
var key := "%d:%d" % [
|
||||
roundi(position.x * 10000.0),
|
||||
roundi(position.z * 10000.0),
|
||||
]
|
||||
if seen.has(key):
|
||||
return false
|
||||
seen[key] = true
|
||||
return true
|
||||
|
||||
|
||||
static func _candidate_is_accepted(
|
||||
candidate: Dictionary,
|
||||
validator: Callable,
|
||||
) -> bool:
|
||||
return not validator.is_valid() or bool(validator.call(candidate))
|
||||
|
||||
|
||||
static func placements_overlap(first: Dictionary, second: Dictionary) -> bool:
|
||||
var first_id := StringName(str(first.get("decor_id", "")))
|
||||
var second_id := StringName(str(second.get("decor_id", "")))
|
||||
if (
|
||||
not DecorCatalogType.has_product(first_id)
|
||||
or not DecorCatalogType.has_product(second_id)
|
||||
):
|
||||
return true
|
||||
var first_position: Vector3 = _array_to_vector3(first.get("position", []))
|
||||
var second_position: Vector3 = _array_to_vector3(second.get("position", []))
|
||||
var first_yaw: float = float(first.get("yaw", 0.0))
|
||||
var second_yaw: float = float(second.get("yaw", 0.0))
|
||||
var first_half: Vector2 = (
|
||||
DecorCatalogType.get_footprint(first_id) * 0.5
|
||||
)
|
||||
var second_half: Vector2 = (
|
||||
DecorCatalogType.get_footprint(second_id) * 0.5
|
||||
)
|
||||
var first_axes: Array[Vector2] = _placement_axes(first_yaw)
|
||||
var second_axes: Array[Vector2] = _placement_axes(second_yaw)
|
||||
var delta := Vector2(
|
||||
second_position.x - first_position.x,
|
||||
second_position.z - first_position.z,
|
||||
)
|
||||
for axis: Vector2 in [
|
||||
first_axes[0], first_axes[1], second_axes[0], second_axes[1],
|
||||
]:
|
||||
var first_radius: float = (
|
||||
absf(axis.dot(first_axes[0])) * first_half.x
|
||||
+ absf(axis.dot(first_axes[1])) * first_half.y
|
||||
)
|
||||
var second_radius: float = (
|
||||
absf(axis.dot(second_axes[0])) * second_half.x
|
||||
+ absf(axis.dot(second_axes[1])) * second_half.y
|
||||
)
|
||||
if absf(delta.dot(axis)) >= (
|
||||
first_radius + second_radius - 0.001
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _make_instance_id() -> String:
|
||||
var result := "decor-%d-%d" % [
|
||||
Time.get_ticks_usec(),
|
||||
_next_instance_sequence,
|
||||
]
|
||||
_next_instance_sequence = mini(
|
||||
_next_instance_sequence + 1,
|
||||
MAX_REVISION,
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
func _placement_index(instance_id: String) -> int:
|
||||
if instance_id.is_empty():
|
||||
return -1
|
||||
for index: int in _placements.size():
|
||||
if str(_placements[index].get("instance_id", "")) == instance_id:
|
||||
return index
|
||||
return -1
|
||||
|
||||
|
||||
func _overlaps_existing(
|
||||
decor_id: StringName,
|
||||
normalized: Dictionary,
|
||||
ignored_instance_id: String,
|
||||
) -> bool:
|
||||
var candidate: Dictionary = {
|
||||
"decor_id": String(decor_id),
|
||||
"position": _vector3_to_array(normalized["position"]),
|
||||
"yaw": float(normalized["yaw"]),
|
||||
}
|
||||
for placement: Dictionary in _placements:
|
||||
if str(placement.get("instance_id", "")) == ignored_instance_id:
|
||||
continue
|
||||
if placements_overlap(candidate, placement):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _touch() -> void:
|
||||
_revision = mini(_revision + 1, MAX_REVISION)
|
||||
changed.emit()
|
||||
|
||||
|
||||
static func _valid_mode(value: int) -> bool:
|
||||
return value >= PlacementMode.GRID and value <= PlacementMode.FREE
|
||||
|
||||
|
||||
static func _vector3_to_array(value: Vector3) -> Array[float]:
|
||||
return [value.x, value.y, value.z]
|
||||
|
||||
|
||||
static func _array_to_vector3(value: Variant) -> Vector3:
|
||||
if typeof(value) != TYPE_ARRAY or (value as Array).size() != 3:
|
||||
return Vector3.INF
|
||||
var components: Array = value
|
||||
return Vector3(
|
||||
float(components[0]),
|
||||
float(components[1]),
|
||||
float(components[2]),
|
||||
)
|
||||
|
||||
|
||||
static func _placement_axes(yaw: float) -> Array[Vector2]:
|
||||
var cosine: float = cos(yaw)
|
||||
var sine: float = sin(yaw)
|
||||
return [Vector2(cosine, sine), Vector2(-sine, cosine)]
|
||||
1
homes/player_home_state.gd.uid
Normal file
1
homes/player_home_state.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ggfdoak2ixxx
|
||||
Loading…
Add table
Add a link
Reference in a new issue