786 lines
21 KiB
GDScript
786 lines
21 KiB
GDScript
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)]
|