From fe099f47dd4e5be4f18387c592c8a0cb5715ee1a Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 21:37:26 -0400 Subject: [PATCH] Add portable RV homes and decor prototype --- fishing/fishing_spot.gd | 88 +- homes/decor_catalog.gd | 59 + homes/decor_catalog.gd.uid | 1 + homes/home_decor_controller.gd | 457 +++++++ homes/home_decor_controller.gd.uid | 1 + homes/home_world_service.gd | 1183 +++++++++++++++++ homes/home_world_service.gd.uid | 1 + homes/player_home_state.gd | 786 +++++++++++ homes/player_home_state.gd.uid | 1 + inventory/player_bag.gd | 22 + inventory/player_hotbar.gd | 19 + inventory/player_inventory_layout.gd | 84 +- items/catalog/basic_cube.tres | 17 + items/catalog/item_catalog.tres | 5 +- main/main.gd | 267 +++- main/main.tscn | 20 + network/network_home_protocol.gd | 176 +++ network/network_home_protocol.gd.uid | 1 + network/network_home_service.gd | 956 +++++++++++++ network/network_home_service.gd.uid | 1 + network/network_item_use_service.gd | 5 + network/network_protocol.gd | 14 +- network/network_session.gd | 38 +- network/network_shop_protocol.gd | 2 + network/network_shop_service.gd | 156 ++- player/player.gd | 135 +- save/player_save_manager.gd | 36 +- tests/art_tools_validation.gd | 13 +- tests/fish_hotbar_showcase_validation.gd | 5 +- tests/fish_quality_validation.gd | 5 +- tests/home_spawn_multiplayer_validation.gd | 178 +++ .../home_spawn_multiplayer_validation.gd.uid | 1 + tests/job_system_validation.gd | 5 +- tests/player_experience_validation.gd | 5 +- tests/progression_archive_validation.gd | 26 +- tests/unified_inventory_validation.gd | 35 + ui/decor_shop.gd | 323 +++++ ui/decor_shop.gd.uid | 1 + ui/decor_shop.tscn | 14 + ui/game_ui.gd | 97 ++ ui/game_ui.tscn | 32 +- ui/home_decor_toolbar.gd | 126 ++ ui/home_decor_toolbar.gd.uid | 1 + ui/home_decor_toolbar.tscn | 8 + ui/hotbar.gd | 11 +- ui/player_storage.gd | 57 +- world/decor_shop_interaction.gd | 2 + world/decor_shop_interaction.gd.uid | 1 + world/generation/generated_world_region.gd | 13 + world/generation/generated_world_region.tscn | 6 +- world/interactables/decor_shop_world.tscn | 35 + world/interactables/player_storage_box.tscn | 4 + world/regions/starter_island_region.gd | 11 + world/regions/starter_island_region.tscn | 5 + world/regions/world_region.gd | 4 + world/test_world.gd | 16 + 56 files changed, 5459 insertions(+), 112 deletions(-) create mode 100644 homes/decor_catalog.gd create mode 100644 homes/decor_catalog.gd.uid create mode 100644 homes/home_decor_controller.gd create mode 100644 homes/home_decor_controller.gd.uid create mode 100644 homes/home_world_service.gd create mode 100644 homes/home_world_service.gd.uid create mode 100644 homes/player_home_state.gd create mode 100644 homes/player_home_state.gd.uid create mode 100644 items/catalog/basic_cube.tres create mode 100644 network/network_home_protocol.gd create mode 100644 network/network_home_protocol.gd.uid create mode 100644 network/network_home_service.gd create mode 100644 network/network_home_service.gd.uid create mode 100644 tests/home_spawn_multiplayer_validation.gd create mode 100644 tests/home_spawn_multiplayer_validation.gd.uid create mode 100644 ui/decor_shop.gd create mode 100644 ui/decor_shop.gd.uid create mode 100644 ui/decor_shop.tscn create mode 100644 ui/home_decor_toolbar.gd create mode 100644 ui/home_decor_toolbar.gd.uid create mode 100644 ui/home_decor_toolbar.tscn create mode 100644 world/decor_shop_interaction.gd create mode 100644 world/decor_shop_interaction.gd.uid create mode 100644 world/interactables/decor_shop_world.tscn diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 8e3e169..8160eec 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -41,8 +41,6 @@ const FishingSurfaceSampleType = preload( const FishingSurfaceResolverType = preload( "res://fishing/fishing_surface_resolver.gd" ) -const ArtShopStockType = preload("res://economy/art_shop_stock.gd") -const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") const WorldTimeServiceType = preload("res://world/world_time_service.gd") const WorldWeatherServiceType = preload( "res://world/world_weather_service.gd" @@ -134,6 +132,7 @@ const ALTERNATE_REEL_ACTION: StringName = &"reel_alternate" var state: FishingState = FishingState.READY var _external_input_blocked: bool = false var _gameplay_input_enabled: bool = false +var _fishing_environment_enabled: bool = true var _local_menu_input_owners: Dictionary[StringName, bool] = {} var _local_player: PlayerType var _local_inventory: FishInventoryType @@ -399,6 +398,22 @@ func set_gameplay_input_enabled(enabled: bool) -> void: reset_transient_state() +func set_fishing_environment_enabled(enabled: bool) -> void: + if _fishing_environment_enabled == enabled: + return + _fishing_environment_enabled = enabled + if enabled: + _new_cast_press_armed = not Input.is_action_pressed("fish_primary") + return + if _network_fishing != null and _network_fishing.has_local_attempt(): + _network_fishing.cancel_local_attempt("") + reset_transient_state() + + +func is_fishing_environment_enabled() -> bool: + return _fishing_environment_enabled + + func reset_transient_state(restore_movement: bool = true) -> void: var local_state_was_active: bool = ( state != FishingState.READY or _active_player != null @@ -533,6 +548,7 @@ func _process(delta: float) -> void: func _unhandled_input(event: InputEvent) -> void: if ( not _gameplay_input_enabled + or not _fishing_environment_enabled or _external_input_blocked or not _local_menu_input_owners.is_empty() ): @@ -545,51 +561,23 @@ func _unhandled_input(event: InputEvent) -> void: alternate_reel and state == FishingState.FIGHTING ): return + # Fishing owns the primary action only after a rod starts a cast, or while + # completing an already-active fishing sequence. An empty hand, decor, + # gathering tools, and other items must remain available to their own world + # interaction controllers. + if state == FishingState.READY and not has_active_fishing_rod(): + return # A held keyboard key emits echo events. Treat the alternate reel exactly # like a mouse button: one barrier hit per physical press, while the normal # held-input path continues driving free reeling between barriers. if event is InputEventKey and event.echo: get_viewport().set_input_as_handled() return - var selected_item: ItemDataType = _get_active_item() - if ( - state == FishingState.READY - and selected_item != null - and FishingShopStockType.is_gathering_tool(selected_item.item_id) - ): - return if event.is_pressed(): match state: FishingState.READY: if not _new_cast_press_armed: return - var active_item: ItemDataType = _get_active_item() - if ( - active_item != null - and ( - active_item.category == ItemDataType.Category.CONSUMABLE - or ( - active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID - ) - ) - ): - if _network_item_use != null: - _network_item_use.request_use(active_item.item_id) - elif _item_effects.use_item(active_item, _local_bag): - status_changed.emit(_item_effects.get_feedback( - active_item.item_id - )) - elif ( - active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID - ): - local_speech_requested.emit( - PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE - ) - get_viewport().set_input_as_handled() - return - if not has_active_fishing_rod(): - get_viewport().set_input_as_handled() - return if ( _is_inventory_full() ): @@ -661,6 +649,34 @@ func _unhandled_input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func try_use_active_non_fishing_item() -> bool: + if ( + not _gameplay_input_enabled + or _external_input_blocked + or not _local_menu_input_owners.is_empty() + or state != FishingState.READY + ): + return false + var active_item: ItemDataType = _get_active_item() + if ( + active_item == null + or ( + active_item.category != ItemDataType.Category.CONSUMABLE + and active_item.item_id != PlayerItemEffectsType.FISH_FINDER_ID + ) + ): + return false + if _network_item_use != null: + _network_item_use.request_use(active_item.item_id) + elif _item_effects.use_item(active_item, _local_bag): + status_changed.emit(_item_effects.get_feedback(active_item.item_id)) + elif active_item.item_id == PlayerItemEffectsType.FISH_FINDER_ID: + local_speech_requested.emit( + PlayerItemEffectsType.FISH_FINDER_DEAD_MESSAGE + ) + return true + + func _is_reel_input_pressed() -> bool: return ( Input.is_action_pressed("fish_primary") diff --git a/homes/decor_catalog.gd b/homes/decor_catalog.gd new file mode 100644 index 0000000..14e5e29 --- /dev/null +++ b/homes/decor_catalog.gd @@ -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)) diff --git a/homes/decor_catalog.gd.uid b/homes/decor_catalog.gd.uid new file mode 100644 index 0000000..10d2530 --- /dev/null +++ b/homes/decor_catalog.gd.uid @@ -0,0 +1 @@ +uid://bvgwmexas2gps diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd new file mode 100644 index 0000000..1a97a5d --- /dev/null +++ b/homes/home_decor_controller.gd @@ -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() diff --git a/homes/home_decor_controller.gd.uid b/homes/home_decor_controller.gd.uid new file mode 100644 index 0000000..1778125 --- /dev/null +++ b/homes/home_decor_controller.gd.uid @@ -0,0 +1 @@ +uid://b5j0mqcvhmi54 diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd new file mode 100644 index 0000000..4e1b422 --- /dev/null +++ b/homes/home_world_service.gd @@ -0,0 +1,1183 @@ +class_name HomeWorldService +extends Node3D + +const NetworkHomeProtocolType = preload( + "res://network/network_home_protocol.gd" +) +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") +const DecorCatalogType = preload("res://homes/decor_catalog.gd") + +const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35) +const EXTERIOR_DOOR_LOCAL := Vector3(-1.45, 0.85, 1.48) +const EXTERIOR_SPAWN_LOCAL := Vector3(-1.45, 0.25, 2.45) +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_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5) +const INTERIOR_WALL_HEIGHT: float = 2.7 +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 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 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 +# a full low-tick-rate frame before a newly solid placement can be reached. +const PLAYER_DECOR_CLEARANCE_RADIUS: float = 0.8 +const BASIC_CUBE_SIZE := Vector3(1.0, 0.8, 0.8) + +var _world: TestWorld +var _spawn_service: PlayerSpawnService +var _session: NetworkSession +var _dedicated_runtime: bool = false +var _exteriors_root: Node3D +var _interiors_root: Node3D +var _presentations_by_fingerprint: Dictionary[String, Dictionary] = {} +var _local_space: StringName = NetworkHomeProtocolType.WORLD_SPACE +var _decor_under_manipulation: Dictionary[String, bool] = {} +var _pending_solid_decor: Dictionary[String, Dictionary] = {} + + +func setup( + world: TestWorld, + spawn_service: PlayerSpawnService, + session: NetworkSession, + dedicated_runtime: bool, +) -> void: + _world = world + _spawn_service = spawn_service + _session = session + _dedicated_runtime = dedicated_runtime + _ensure_roots() + set_process(true) + + +func is_world_ready() -> bool: + return _world != null and _world.is_world_ready() + + +func clear_presentations() -> void: + _presentations_by_fingerprint.clear() + _decor_under_manipulation.clear() + _pending_solid_decor.clear() + if is_instance_valid(_exteriors_root): + for child: Node in _exteriors_root.get_children(): + child.queue_free() + if is_instance_valid(_interiors_root): + for child: Node in _interiors_root.get_children(): + child.queue_free() + + +func remove_presentation(owner_fingerprint: String) -> void: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + if presentation.is_empty(): + return + _clear_decor_collision_tracking(owner_fingerprint) + _free_presentation_nodes(presentation) + _presentations_by_fingerprint.erase(owner_fingerprint) + + +func apply_manifest(data: Dictionary) -> bool: + if ( + not is_world_ready() + or not NetworkHomeProtocolType.validate_manifest_broadcast(data) + ): + return false + _ensure_roots() + var fingerprint: String = str(data["owner_fingerprint"]) + var prior: Dictionary = _presentations_by_fingerprint.get(fingerprint, {}) + var prior_instance_ids: Dictionary[String, bool] = {} + if not prior.is_empty(): + var prior_home: Dictionary = prior.get("home", {}) + for value: Variant in prior_home.get("placements", []): + if typeof(value) == TYPE_DICTIONARY: + prior_instance_ids[str( + (value as Dictionary).get("instance_id", "") + )] = true + if not prior.is_empty(): + _clear_decor_collision_tracking(fingerprint) + _free_presentation_nodes(prior) + var exterior_transform := NetworkHomeProtocolType.array_to_transform( + data["exterior_transform"] + ) + var interior_index: int = int(data["interior_index"]) + var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data( + data["home"] + ) + var exterior := _build_exterior(fingerprint, exterior_transform) + var interior := _build_interior(fingerprint, interior_index, home_data) + var presentation: Dictionary = { + "owner_peer_id": int(data["owner_peer_id"]), + "fingerprint": fingerprint, + "home": home_data, + "exterior_transform": exterior_transform, + "interior_index": interior_index, + "exterior": exterior, + "interior": interior, + } + _presentations_by_fingerprint[fingerprint] = presentation + _refresh_local_visibility() + if not prior.is_empty(): + for value: Variant in home_data.get("placements", []): + if typeof(value) != TYPE_DICTIONARY: + continue + var instance_id: String = str( + (value as Dictionary).get("instance_id", "") + ) + if not instance_id.is_empty() and not prior_instance_ids.has( + instance_id + ): + call_deferred( + "_spawn_decor_poof", fingerprint, instance_id + ) + return true + + +func choose_exterior_transform( + owner_peer_id: int, + reserved_transforms: Array[Transform3D], +) -> Transform3D: + var spawn_transform: Transform3D = ( + _world.get_player_spawn_transform() + if _world != null else Transform3D.IDENTITY + ) + var triangles: Array[PackedVector3Array] = ( + _world.get_spawn_surface_triangles( + [&"grass_lite", &"grass", &"sand"], -0.5 + ) + if _world != null else [] + ) + if not triangles.is_empty(): + var surface_index: Dictionary = _build_surface_index(triangles) + var strict_result: Transform3D = _find_exterior_transform( + owner_peer_id, + triangles, + surface_index, + spawn_transform, + reserved_transforms, + true, + true, + ) + if strict_result != Transform3D.IDENTITY: + return strict_result + var relaxed_result: Transform3D = _find_exterior_transform( + owner_peer_id, + triangles, + surface_index, + spawn_transform, + reserved_transforms, + false, + true, + ) + if relaxed_result != Transform3D.IDENTITY: + return relaxed_result + var dry_result: Transform3D = _find_exterior_transform( + owner_peer_id, + triangles, + surface_index, + spawn_transform, + reserved_transforms, + false, + false, + ) + if dry_result != Transform3D.IDENTITY: + push_warning( + "RV 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 + + +func _find_exterior_transform( + owner_peer_id: int, + triangles: Array[PackedVector3Array], + surface_index: Dictionary, + spawn_transform: Transform3D, + reserved_transforms: Array[Transform3D], + require_spawn_clearance: bool, + require_obstacle_clearance: bool, +) -> Transform3D: + var triangle_count: int = triangles.size() + var start: int = posmod(owner_peer_id * 37, triangle_count) + var step: int = _coprime_step(triangle_count, 29) + for offset: int in triangle_count: + var triangle: PackedVector3Array = triangles[ + (start + offset * step) % triangle_count + ] + if not _is_exterior_candidate_triangle(triangle): + continue + var position: Vector3 = ( + triangle[0] + triangle[1] + triangle[2] + ) / 3.0 + if ( + require_spawn_clearance + and position.distance_to(spawn_transform.origin) < 6.0 + ): + continue + if _too_close_to_reserved(position, reserved_transforms): + continue + var yaw: float = float(posmod(owner_peer_id + offset, 4)) * PI * 0.5 + var grounded_position: Vector3 = _ground_exterior_footprint( + position, yaw, triangles, surface_index + ) + if not grounded_position.is_finite(): + continue + if ( + require_obstacle_clearance + and not _exterior_volume_is_clear(grounded_position, yaw) + ): + continue + return Transform3D(Basis(Vector3.UP, yaw), grounded_position) + return Transform3D.IDENTITY + + +func _is_exterior_candidate_triangle(triangle: PackedVector3Array) -> bool: + if triangle.size() != 3: + return false + var normal: Vector3 = (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ) + var area: float = normal.length() * 0.5 + return ( + area >= MIN_EXTERIOR_TRIANGLE_AREA + and absf(normal.normalized().y) >= 0.94 + ) + + +func _ground_exterior_footprint( + position: Vector3, + yaw: float, + triangles: Array[PackedVector3Array], + surface_index: Dictionary, +) -> Vector3: + var heights: Array[float] = [] + for local_sample: Vector2 in _exterior_footprint_samples(): + var rotated_sample: Vector3 = Vector3( + local_sample.x, 0.0, local_sample.y + ).rotated(Vector3.UP, yaw) + var sample := Vector2( + position.x + rotated_sample.x, + position.z + rotated_sample.z, + ) + var height: float = _surface_height_at( + sample, position.y, triangles, surface_index + ) + if not is_finite(height): + return Vector3.INF + heights.append(height) + var minimum_height: float = heights.min() + var maximum_height: float = heights.max() + if maximum_height - minimum_height > EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: + return Vector3.INF + return Vector3(position.x, maximum_height, position.z) + + +func _exterior_footprint_samples() -> Array[Vector2]: + var samples: Array[Vector2] = [] + for x_factor: float in [-1.0, -0.5, 0.0, 0.5, 1.0]: + for z_factor: float in [-1.0, 0.0, 1.0]: + samples.append(Vector2( + EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * x_factor, + EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * z_factor, + )) + return samples + + +func _build_surface_index( + triangles: Array[PackedVector3Array], +) -> Dictionary: + var result: Dictionary = {} + for triangle_index: int in triangles.size(): + var triangle: PackedVector3Array = triangles[triangle_index] + if triangle.size() != 3: + continue + var minimum_x: float = minf( + triangle[0].x, minf(triangle[1].x, triangle[2].x) + ) + var maximum_x: float = maxf( + triangle[0].x, maxf(triangle[1].x, triangle[2].x) + ) + var minimum_z: float = minf( + triangle[0].z, minf(triangle[1].z, triangle[2].z) + ) + var maximum_z: float = maxf( + triangle[0].z, maxf(triangle[1].z, triangle[2].z) + ) + var minimum_cell := Vector2i( + floori(minimum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + floori(minimum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + ) + var maximum_cell := Vector2i( + floori(maximum_x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + floori(maximum_z / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + ) + for cell_x: int in range(minimum_cell.x, maximum_cell.x + 1): + for cell_y: int in range(minimum_cell.y, maximum_cell.y + 1): + var cell := Vector2i(cell_x, cell_y) + var bucket: Array = result.get(cell, []) + bucket.append(triangle_index) + result[cell] = bucket + return result + + +func _surface_height_at( + point: Vector2, + preferred_height: float, + triangles: Array[PackedVector3Array], + surface_index: Dictionary, +) -> float: + var cell := Vector2i( + floori(point.x / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + floori(point.y / EXTERIOR_SURFACE_INDEX_CELL_SIZE), + ) + var best_height: float = INF + var best_delta: float = INF + for value: Variant in surface_index.get(cell, []): + var triangle: PackedVector3Array = triangles[int(value)] + var height: float = _triangle_height_at(point, triangle) + if not is_finite(height): + continue + var delta: float = absf(height - preferred_height) + if delta < best_delta: + best_delta = delta + best_height = height + return best_height + + +func _triangle_height_at( + point: Vector2, + triangle: PackedVector3Array, +) -> float: + var a := Vector2(triangle[0].x, triangle[0].z) + var b := Vector2(triangle[1].x, triangle[1].z) + var c := Vector2(triangle[2].x, triangle[2].z) + var denominator: float = ( + (b.y - c.y) * (a.x - c.x) + + (c.x - b.x) * (a.y - c.y) + ) + if absf(denominator) <= 0.000001: + return INF + var weight_a: float = ( + (b.y - c.y) * (point.x - c.x) + + (c.x - b.x) * (point.y - c.y) + ) / denominator + var weight_b: float = ( + (c.y - a.y) * (point.x - c.x) + + (a.x - c.x) * (point.y - c.y) + ) / denominator + var weight_c: float = 1.0 - weight_a - weight_b + const EDGE_EPSILON: float = 0.001 + if ( + weight_a < -EDGE_EPSILON + or weight_b < -EDGE_EPSILON + or weight_c < -EDGE_EPSILON + ): + return INF + return ( + weight_a * triangle[0].y + + weight_b * triangle[1].y + + weight_c * triangle[2].y + ) + + +func _exterior_volume_is_clear(position: Vector3, yaw: float) -> bool: + if not is_inside_tree() or get_world_3d() == null: + return true + var shape := BoxShape3D.new() + shape.size = Vector3( + EXTERIOR_FOOTPRINT_HALF_EXTENTS.x * 2.0, + EXTERIOR_CLEARANCE_HEIGHT, + EXTERIOR_FOOTPRINT_HALF_EXTENTS.y * 2.0, + ) + var query := PhysicsShapeQueryParameters3D.new() + query.shape = shape + query.transform = Transform3D( + Basis(Vector3.UP, yaw), + position + Vector3.UP * (EXTERIOR_CLEARANCE_HEIGHT * 0.5 + 0.2), + ) + query.collision_mask = 1 + query.collide_with_areas = false + query.collide_with_bodies = true + return get_world_3d().direct_space_state.intersect_shape(query, 1).is_empty() + + +func _coprime_step(value: int, preferred_step: int) -> int: + var step: int = mini(preferred_step, maxi(value - 1, 1)) + while step > 1 and _greatest_common_divisor(step, value) != 1: + step -= 1 + return step + + +func _greatest_common_divisor(first: int, second: int) -> int: + var a: int = absi(first) + var b: int = absi(second) + while b != 0: + var remainder: int = a % b + a = b + b = remainder + return a + + +func get_exterior_transform(owner_fingerprint: String) -> Transform3D: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + return presentation.get("exterior_transform", Transform3D.IDENTITY) + + +func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3: + return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL + + +func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D: + var exterior_transform: Transform3D = get_exterior_transform( + owner_fingerprint + ) + return Transform3D( + exterior_transform.basis, + exterior_transform * EXTERIOR_SPAWN_LOCAL, + ) + + +func get_interior_spawn_transform( + owner_fingerprint: String, + visitor_offset: int = 0, +) -> Transform3D: + var anchor: Vector3 = get_interior_anchor(owner_fingerprint) + var result := Transform3D(Basis.IDENTITY, anchor) + result.origin += Vector3( + -1.0 + float(posmod(visitor_offset, 4)) * 0.65, + 0.25, + 0.4 + float(floori(float(visitor_offset) / 4.0)) * 0.55, + ) + return result + + +func get_interior_anchor(owner_fingerprint: String) -> Vector3: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + var index: int = int(presentation.get("interior_index", 0)) + return INTERIOR_ORIGIN + Vector3(float(index) * INTERIOR_SPACING, 0.0, 0.0) + + +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 + return avatar.global_position.distance_to(door_position) <= ( + EXTERIOR_INTERACTION_DISTANCE + ) + + +func is_avatar_near_interior_exit( + avatar: Player, + owner_fingerprint: String, +) -> bool: + if avatar == null: + return false + return avatar.global_position.distance_to( + get_interior_exit_position(owner_fingerprint) + ) <= INTERIOR_EXIT_DISTANCE + + +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, + ) + + +func get_interior_prompt_position(owner_fingerprint: String) -> Vector3: + return get_interior_exit_position(owner_fingerprint) + Vector3.UP * 1.2 + + +func interior_local_to_global( + owner_fingerprint: String, + local_position: Vector3, +) -> Vector3: + return get_interior_anchor(owner_fingerprint) + local_position + + +func interior_global_to_local( + owner_fingerprint: String, + global_position: Vector3, +) -> Vector3: + return global_position - get_interior_anchor(owner_fingerprint) + + +func get_floor_position_from_screen( + owner_fingerprint: String, + camera: Camera3D, + screen_position: Vector2, +) -> Vector3: + if camera == null or owner_fingerprint.is_empty(): + return Vector3.INF + var ray_origin: Vector3 = camera.project_ray_origin(screen_position) + var ray_direction: Vector3 = camera.project_ray_normal(screen_position) + if absf(ray_direction.y) <= 0.0001: + return Vector3.INF + var floor_y: float = get_interior_anchor(owner_fingerprint).y + 0.1 + var distance: float = (floor_y - ray_origin.y) / ray_direction.y + if distance <= 0.0: + return Vector3.INF + return interior_global_to_local( + owner_fingerprint, + ray_origin + ray_direction * distance, + ) + + +func find_decor_instance_at_screen( + owner_fingerprint: String, + camera: Camera3D, + screen_position: Vector2, +) -> String: + if camera == null or owner_fingerprint.is_empty(): + return "" + var ray_origin: Vector3 = camera.project_ray_origin(screen_position) + var ray_end: Vector3 = ray_origin + camera.project_ray_normal( + screen_position + ) * 50.0 + var query := PhysicsRayQueryParameters3D.create(ray_origin, ray_end) + query.collision_mask = DECOR_SELECTION_COLLISION_LAYER + query.collide_with_areas = false + var result: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray( + query + ) + var collider: Object = result.get("collider") + if collider is Node and (collider as Node).has_meta(&"decor_instance_id"): + if str((collider as Node).get_meta(&"decor_owner_fingerprint")) == ( + owner_fingerprint + ): + return str((collider as Node).get_meta(&"decor_instance_id")) + return "" + + +func get_decor_preview_transform( + owner_fingerprint: String, + instance_id: String, +) -> Dictionary: + var decor: Node3D = _get_decor_presentation( + owner_fingerprint, instance_id + ) + if decor == null: + return {} + return { + "position": Vector3(decor.position.x, 0.0, decor.position.z), + "yaw": decor.rotation.y, + } + + +func set_decor_preview_transform( + owner_fingerprint: String, + instance_id: String, + position: Vector3, + yaw_radians: float, +) -> bool: + var decor: Node3D = _get_decor_presentation( + owner_fingerprint, instance_id + ) + if decor == null or not position.is_finite() or not is_finite(yaw_radians): + return false + decor.position = Vector3(position.x, 0.18, position.z) + decor.rotation.y = yaw_radians + return true + + +func set_decor_manipulation_active( + owner_fingerprint: String, + instance_id: String, + active: bool, +) -> bool: + var decor: Node3D = _get_decor_presentation( + owner_fingerprint, instance_id + ) + if decor == null: + return false + var body: StaticBody3D = decor.get_node_or_null( + "BasicCubeCollision" + ) as StaticBody3D + if body == null: + return false + var key: String = _decor_collision_key( + owner_fingerprint, instance_id + ) + if active: + _decor_under_manipulation[key] = true + _pending_solid_decor.erase(key) + body.collision_layer = DECOR_SELECTION_COLLISION_LAYER + return true + _decor_under_manipulation.erase(key) + _queue_decor_world_collision( + owner_fingerprint, + instance_id, + StringName(str(decor.get_meta(&"decor_id", ""))), + body, + ) + return true + + +func is_decor_placement_clear_of_players( + owner_fingerprint: String, + decor_id: StringName, + local_position: Vector3, + yaw_radians: float, +) -> bool: + if ( + owner_fingerprint.is_empty() + or not DecorCatalogType.has_product(decor_id) + or not local_position.is_finite() + or not is_finite(yaw_radians) + ): + return false + if _spawn_service == null: + return true + var global_center: Vector3 = interior_local_to_global( + owner_fingerprint, local_position + ) + var half_size: Vector2 = DecorCatalogType.get_footprint(decor_id) * 0.5 + for peer_id: int in _spawn_service.get_peer_ids(): + var avatar: Player = _spawn_service.get_avatar(peer_id) + if avatar == null or not is_instance_valid(avatar): + continue + var offset := Vector2( + avatar.global_position.x - global_center.x, + avatar.global_position.z - global_center.z, + ).rotated(-yaw_radians) + var nearest := Vector2( + clampf(offset.x, -half_size.x, half_size.x), + clampf(offset.y, -half_size.y, half_size.y), + ) + if offset.distance_squared_to(nearest) < ( + PLAYER_DECOR_CLEARANCE_RADIUS + * PLAYER_DECOR_CLEARANCE_RADIUS + ): + return false + return true + + +func _get_decor_presentation( + owner_fingerprint: String, + instance_id: String, +) -> Node3D: + if owner_fingerprint.is_empty() or instance_id.is_empty(): + return null + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + var interior: Node3D = presentation.get("interior") + if not is_instance_valid(interior): + return null + return interior.get_node_or_null( + "Decor/%s" % instance_id + ) as Node3D + + +func find_nearby_exterior_owner(avatar: Player) -> String: + if avatar == null: + return "" + var closest: String = "" + var closest_distance: float = EXTERIOR_INTERACTION_DISTANCE + 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 + ) + if distance <= closest_distance: + closest = fingerprint + closest_distance = distance + return closest + + +func set_local_space(space_id: StringName) -> void: + _local_space = space_id + _refresh_local_visibility() + + +func get_manifest(owner_fingerprint: String) -> Dictionary: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + return (presentation.get("home", {}) as Dictionary).duplicate(true) + + +func _process(_delta: float) -> void: + _refresh_pending_decor_collisions() + if not _dedicated_runtime: + _refresh_nearest_wall() + + +func _queue_decor_world_collision( + owner_fingerprint: String, + instance_id: String, + decor_id: StringName, + body: StaticBody3D, +) -> void: + if ( + owner_fingerprint.is_empty() + or instance_id.is_empty() + or not DecorCatalogType.has_product(decor_id) + or body == null + ): + return + var key: String = _decor_collision_key( + owner_fingerprint, instance_id + ) + body.collision_layer = DECOR_SELECTION_COLLISION_LAYER + _pending_solid_decor[key] = { + "owner_fingerprint": owner_fingerprint, + "instance_id": instance_id, + "decor_id": decor_id, + "body": body, + } + + +func _refresh_pending_decor_collisions() -> void: + if _pending_solid_decor.is_empty(): + return + var keys: Array[String] = [] + keys.assign(_pending_solid_decor.keys()) + for key: String in keys: + if _decor_under_manipulation.has(key): + continue + var pending: Dictionary = _pending_solid_decor.get(key, {}) + var body: StaticBody3D = pending.get("body") as StaticBody3D + var owner_fingerprint: String = str( + pending.get("owner_fingerprint", "") + ) + var instance_id: String = str(pending.get("instance_id", "")) + var decor_id := StringName(str(pending.get("decor_id", ""))) + var decor: Node3D = _get_decor_presentation( + owner_fingerprint, instance_id + ) + if body == null or decor == null or not is_instance_valid(body): + _pending_solid_decor.erase(key) + continue + if not is_decor_placement_clear_of_players( + owner_fingerprint, + decor_id, + Vector3(decor.position.x, 0.0, decor.position.z), + decor.rotation.y, + ): + continue + body.collision_layer = ( + WORLD_GEOMETRY_COLLISION_LAYER + | DECOR_SELECTION_COLLISION_LAYER + ) + _pending_solid_decor.erase(key) + + +func _clear_decor_collision_tracking(owner_fingerprint: String) -> void: + var prefix: String = "%s/" % owner_fingerprint + for key: String in _decor_under_manipulation.keys(): + if key.begins_with(prefix): + _decor_under_manipulation.erase(key) + for key: String in _pending_solid_decor.keys(): + if key.begins_with(prefix): + _pending_solid_decor.erase(key) + + +static func _decor_collision_key( + owner_fingerprint: String, + instance_id: String, +) -> String: + return "%s/%s" % [owner_fingerprint, instance_id] + + +func _ensure_roots() -> void: + if not is_instance_valid(_exteriors_root): + _exteriors_root = Node3D.new() + _exteriors_root.name = "RVExteriors" + add_child(_exteriors_root) + if not is_instance_valid(_interiors_root): + _interiors_root = Node3D.new() + _interiors_root.name = "RVInteriors" + add_child(_interiors_root) + + +func _build_exterior( + fingerprint: String, + world_transform: Transform3D, +) -> Node3D: + var root := Node3D.new() + root.name = "RV_%s" % fingerprint.left(8) + _exteriors_root.add_child(root) + root.global_transform = world_transform + _add_box( + root, "Body", EXTERIOR_BODY_SIZE, Vector3(0.0, 1.35, 0.0), + Color("d8d2bd"), true, + ) + root.visible = not _dedicated_runtime + return root + + +func _build_interior( + fingerprint: String, + interior_index: int, + home_data: Dictionary, +) -> Node3D: + var root := Node3D.new() + root.name = "RVInterior_%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 decor := Node3D.new() + decor.name = "Decor" + root.add_child(decor) + _rebuild_decor(decor, home_data, fingerprint) + root.visible = false + if _dedicated_runtime: + _set_geometry_visible(root, false) + return root + + +func _rebuild_decor( + parent: Node3D, + home_data: Dictionary, + owner_fingerprint: String, +) -> void: + for value: Variant in home_data.get("placements", []): + if typeof(value) != TYPE_DICTIONARY: + continue + var placement: Dictionary = value + var decor_id := StringName(str(placement["decor_id"])) + if decor_id != DecorCatalogType.BASIC_CUBE_ID: + continue + var position_values: Array = placement["position"] + var placement_root := Node3D.new() + placement_root.name = str(placement["instance_id"]) + placement_root.position = Vector3( + float(position_values[0]), 0.18, float(position_values[2]) + ) + placement_root.rotation.y = float(placement["yaw"]) + placement_root.set_meta( + &"decor_instance_id", str(placement["instance_id"]) + ) + placement_root.set_meta( + &"decor_owner_fingerprint", owner_fingerprint + ) + placement_root.set_meta(&"decor_id", String(decor_id)) + parent.add_child(placement_root) + var body := _add_box( + placement_root, "BasicCube", BASIC_CUBE_SIZE, + Vector3(0.0, BASIC_CUBE_SIZE.y * 0.5, 0.0), + Color("b77a55"), true, + ) + if body != null: + body.collision_layer = DECOR_SELECTION_COLLISION_LAYER + body.set_meta( + &"decor_instance_id", str(placement["instance_id"]) + ) + body.set_meta( + &"decor_owner_fingerprint", owner_fingerprint + ) + body.set_meta(&"decor_id", String(decor_id)) + _queue_decor_world_collision( + owner_fingerprint, + str(placement["instance_id"]), + decor_id, + body, + ) + _add_box( + placement_root, + "BasicCubeFront", + Vector3(BASIC_CUBE_SIZE.x * 0.82, BASIC_CUBE_SIZE.y * 0.68, 0.025), + Vector3( + 0.0, + BASIC_CUBE_SIZE.y * 0.5, + -BASIC_CUBE_SIZE.z * 0.5 - 0.013, + ), + Color("2897a8"), + false, + ) + + +func _add_wall( + parent: Node3D, + node_name: String, + size: Vector3, + position: Vector3, +) -> void: + _add_box(parent, node_name, size, position, Color("d6caaa"), true) + + +func _add_box( + parent: Node3D, + node_name: String, + size: Vector3, + position: Vector3, + color: Color, + with_collision: bool, + invert_faces: bool = false, +) -> StaticBody3D: + var mesh_instance := MeshInstance3D.new() + mesh_instance.name = node_name + var mesh := BoxMesh.new() + mesh.size = size + mesh.material = _material(color) + if invert_faces: + mesh.flip_faces = true + mesh_instance.mesh = mesh + mesh_instance.position = position + parent.add_child(mesh_instance) + if with_collision: + var body := StaticBody3D.new() + body.name = "%sCollision" % node_name + body.position = position + var collision := CollisionShape3D.new() + var shape := BoxShape3D.new() + shape.size = size + collision.shape = shape + body.add_child(collision) + parent.add_child(body) + return body + return null + + +func _spawn_decor_poof( + owner_fingerprint: String, + instance_id: String, +) -> void: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + var interior: Node3D = presentation.get("interior") + if not is_instance_valid(interior): + return + var placement_root: Node3D = interior.get_node_or_null( + "Decor/%s" % instance_id + ) + if placement_root == null: + return + var poof := CPUParticles3D.new() + poof.name = "DecorPoof" + poof.amount = 12 + poof.lifetime = 0.45 + poof.one_shot = true + poof.explosiveness = 0.9 + poof.direction = Vector3.UP + poof.spread = 80.0 + poof.initial_velocity_min = 0.7 + poof.initial_velocity_max = 1.5 + poof.scale_amount_min = 0.08 + poof.scale_amount_max = 0.16 + var particle_mesh := SphereMesh.new() + particle_mesh.radius = 0.06 + particle_mesh.height = 0.12 + particle_mesh.material = _material(Color("d9c5a3")) + poof.mesh = particle_mesh + placement_root.add_child(poof) + poof.emitting = true + get_tree().create_timer(poof.lifetime + 0.1).timeout.connect( + poof.queue_free + ) + + +func _material(color: Color) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.roughness = 1.0 + material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX + return material + + +func _free_presentation_nodes(presentation: Dictionary) -> void: + for key: String in ["exterior", "interior"]: + var node: Node = presentation.get(key) + if is_instance_valid(node): + node.queue_free() + + +func _refresh_local_visibility() -> void: + var active_fingerprint: String = "" + if String(_local_space).begins_with("rv:"): + active_fingerprint = String(_local_space).trim_prefix("rv:") + if _world != null: + _world.set_local_home_interior_presentation_active( + not active_fingerprint.is_empty() + ) + for fingerprint: String in _presentations_by_fingerprint: + var presentation: Dictionary = _presentations_by_fingerprint[fingerprint] + var interior: Node3D = presentation.get("interior") + if is_instance_valid(interior): + interior.visible = ( + not _dedicated_runtime and fingerprint == active_fingerprint + ) + var exterior: Node3D = presentation.get("exterior") + if is_instance_valid(exterior): + exterior.visible = ( + not _dedicated_runtime and active_fingerprint.is_empty() + ) + var local_player: Player = ( + _spawn_service.get_local_player() + if _spawn_service != null else null + ) + if local_player == null: + return + var excluded_wall_rids: Array[RID] = [] + if not active_fingerprint.is_empty(): + var active: Dictionary = _presentations_by_fingerprint.get( + active_fingerprint, {} + ) + var active_interior: Node3D = active.get("interior") + if is_instance_valid(active_interior): + for wall_name: String in [ + "WallNorth", "WallSouth", "WallWest", "WallEast", + ]: + var body: StaticBody3D = active_interior.get_node_or_null( + "%sCollision" % wall_name + ) + if body != null: + excluded_wall_rids.append(body.get_rid()) + local_player.set_rv_interior_camera_mode( + not active_fingerprint.is_empty(), excluded_wall_rids + ) + + +func _refresh_nearest_wall() -> void: + if not String(_local_space).begins_with("rv:") or _spawn_service == null: + return + var fingerprint: String = String(_local_space).trim_prefix("rv:") + var presentation: Dictionary = _presentations_by_fingerprint.get( + fingerprint, {} + ) + var interior: Node3D = presentation.get("interior") + var player: Player = _spawn_service.get_local_player() + if not is_instance_valid(interior) or player == null: + return + var camera: Camera3D = player.get_active_gameplay_camera() + if camera == null: + return + var local_camera: Vector3 = interior.to_local(camera.global_position) + var local_view_direction: Vector3 = ( + interior.global_basis.inverse() * -camera.global_basis.z + ).normalized() + var horizontal_view_weight: float = absf(local_view_direction.x) + var depth_view_weight: float = absf(local_view_direction.z) + var hidden_walls: Dictionary[String, bool] = {} + var outside_horizontal_wall: bool = ( + absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05 + ) + var outside_depth_wall: bool = ( + absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 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 + # axis and leaving the adjacent wall across the screen. + if ( + outside_horizontal_wall + and horizontal_view_weight >= depth_view_weight * 0.35 + ): + hidden_walls[ + "WallEast" if local_camera.x >= 0.0 else "WallWest" + ] = true + if ( + outside_depth_wall + and depth_view_weight >= horizontal_view_weight * 0.35 + ): + hidden_walls[ + "WallSouth" if local_camera.z >= 0.0 else "WallNorth" + ] = true + if hidden_walls.is_empty(): + var fallback_wall: String = ( + "WallEast" if local_camera.x >= 0.0 else "WallWest" + ) + if depth_view_weight > horizontal_view_weight: + fallback_wall = ( + "WallSouth" if local_camera.z >= 0.0 else "WallNorth" + ) + hidden_walls[fallback_wall] = true + for wall_name: String in [ + "WallNorth", "WallSouth", "WallWest", "WallEast", + ]: + var wall: MeshInstance3D = interior.get_node_or_null(wall_name) + if wall != null: + wall.visible = not hidden_walls.has(wall_name) + for child: Node in interior.get_children(): + if ( + child is GeometryInstance3D + and String(child.name).begins_with(wall_name) + ): + (child as GeometryInstance3D).visible = ( + not hidden_walls.has(wall_name) + ) + + +func _too_close_to_reserved( + position: Vector3, + reserved_transforms: Array[Transform3D], +) -> bool: + for transform: Transform3D in reserved_transforms: + if position.distance_to(transform.origin) < MIN_EXTERIOR_SEPARATION: + return true + return false + + +func _set_geometry_visible(root: Node, visible: bool) -> void: + for node: Node in root.find_children("*", "GeometryInstance3D", true, false): + (node as GeometryInstance3D).visible = visible diff --git a/homes/home_world_service.gd.uid b/homes/home_world_service.gd.uid new file mode 100644 index 0000000..5505d06 --- /dev/null +++ b/homes/home_world_service.gd.uid @@ -0,0 +1 @@ +uid://cy2kjoxhwxujd diff --git a/homes/player_home_state.gd b/homes/player_home_state.gd new file mode 100644 index 0000000..decd34a --- /dev/null +++ b/homes/player_home_state.gd @@ -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)] diff --git a/homes/player_home_state.gd.uid b/homes/player_home_state.gd.uid new file mode 100644 index 0000000..37836c6 --- /dev/null +++ b/homes/player_home_state.gd.uid @@ -0,0 +1 @@ +uid://ggfdoak2ixxx diff --git a/inventory/player_bag.gd b/inventory/player_bag.gd index e33f1e4..d94845d 100644 --- a/inventory/player_bag.gd +++ b/inventory/player_bag.gd @@ -60,6 +60,28 @@ func add_item(item_id: StringName, quantity: int = 1) -> bool: return true +func add_item_to_storage(item_id: StringName, quantity: int = 1) -> bool: + var existing: OwnedItemType = get_owned_item(item_id) + if existing != null: + return add_item(item_id, quantity) + var item: ItemDataType = _resolve_available_item(item_id) + if ( + item == null + or quantity <= 0 + or quantity > (item.max_stack if item.stackable else 1) + or _inventory_layout == null + or not _inventory_layout.can_accept_item_in_storage(item_id) + ): + return false + var owned := OwnedItemType.new() + owned.item_id = item_id + owned.quantity = quantity + owned.storage_slot = _first_available_storage_slot(item, _items) + _items.append(owned) + contents_changed.emit() + return true + + func can_add_item(item_id: StringName, quantity: int = 1) -> bool: var item: ItemDataType = _resolve_available_item(item_id) if item == null or quantity <= 0: diff --git a/inventory/player_hotbar.gd b/inventory/player_hotbar.gd index b7d5197..36068ea 100644 --- a/inventory/player_hotbar.gd +++ b/inventory/player_hotbar.gd @@ -314,6 +314,25 @@ func get_fish_slots() -> Array[StringName]: return _fish_slots.duplicate() +func find_item_slot(item_id: StringName) -> int: + return _slots.find(item_id) + + +func find_first_empty_slot() -> int: + for slot_index: int in SLOT_COUNT: + if get_assignment_kind(slot_index) != AssignmentKind.EMPTY: + continue + if ( + _inventory_layout == null + or _inventory_layout.get_key_at( + PlayerInventoryLayout.InventoryContainer.HOTBAR, + slot_index, + ).is_empty() + ): + return slot_index + return -1 + + func replace_state( slots: Array[StringName], selected_slot: int, diff --git a/inventory/player_inventory_layout.gd b/inventory/player_inventory_layout.gd index 244e10b..bce726e 100644 --- a/inventory/player_inventory_layout.gd +++ b/inventory/player_inventory_layout.gd @@ -37,6 +37,7 @@ var _fish_inventory: FishInventoryType var _item_catalog: ItemCatalogType var _storage_capacity: PlayerCoolerCapacityType var _placements: Dictionary[String, Dictionary] = {} +var _prepared_item_placements: Dictionary[String, Dictionary] = {} var _reconciling: bool = false var _backpack_level: int = 0 @@ -72,7 +73,44 @@ func can_accept_item(item_id: StringName) -> bool: if not is_managed_item(item_id): return true var key := item_key(item_id) - return _placements.has(key) or _first_free_slot(InventoryContainer.INVENTORY) >= 0 + return ( + _placements.has(key) + or _is_prepared_item_placement_available(key) + or _first_free_slot(InventoryContainer.INVENTORY) >= 0 + ) + + +func prepare_new_item_placement( + item_id: StringName, + target_container: InventoryContainer, + target_slot: int, +) -> bool: + if not is_managed_item(item_id): + return false + var key := item_key(item_id) + if ( + _placements.has(key) + or not _is_slot_valid(target_container, target_slot) + or not _key_at(target_container, target_slot).is_empty() + or _prepared_slot_is_occupied(target_container, target_slot) + ): + return false + _prepared_item_placements[key] = { + "container": int(target_container), + "slot": target_slot, + } + return true + + +func cancel_prepared_item_placement(item_id: StringName) -> void: + _prepared_item_placements.erase(item_key(item_id)) + + +func can_accept_item_in_storage(item_id: StringName) -> bool: + if not is_managed_item(item_id): + return true + var key := item_key(item_id) + return _placements.has(key) or _first_free_slot(InventoryContainer.STORAGE) >= 0 func can_accept_catch(catch_id: StringName = StringName()) -> bool: @@ -332,6 +370,7 @@ func restore_from_save_data(data: Dictionary) -> bool: "container": container, "slot": slot, } + _prepared_item_placements.clear() _placements = restored _reconcile(true) return true @@ -340,6 +379,7 @@ func restore_from_save_data(data: Dictionary) -> bool: func reset_to_defaults() -> void: var capacity_changed := _backpack_level != 0 _backpack_level = 0 + _prepared_item_placements.clear() _placements.clear() _reconcile(true) if capacity_changed: @@ -402,8 +442,17 @@ func _reconcile(force_signal: bool) -> void: for key: String in expected: if _placements.has(key): continue + var prepared: Dictionary = _prepared_item_placements.get(key, {}) var target_container := InventoryContainer.INVENTORY - var target_slot := _first_free_slot(target_container) + var target_slot: int = -1 + if _is_prepared_item_placement_available(key): + target_container = int( + prepared["container"] + ) as InventoryContainer + target_slot = int(prepared["slot"]) + _prepared_item_placements.erase(key) + if target_slot < 0: + target_slot = _first_free_slot(target_container) if target_slot < 0: target_container = InventoryContainer.STORAGE target_slot = _first_free_slot(target_container) @@ -484,6 +533,37 @@ func _first_free_slot(container: InventoryContainer) -> int: return -1 +func _is_prepared_item_placement_available(key: String) -> bool: + var placement: Dictionary = _prepared_item_placements.get(key, {}) + if placement.is_empty(): + return false + var container: int = int(placement.get("container", -1)) + var slot: int = int(placement.get("slot", -1)) + if ( + container not in [ + InventoryContainer.INVENTORY, + InventoryContainer.STORAGE, + InventoryContainer.HOTBAR, + ] + or not _is_slot_valid(container as InventoryContainer, slot) + ): + return false + return _key_at(container as InventoryContainer, slot).is_empty() + + +func _prepared_slot_is_occupied( + container: InventoryContainer, + slot: int, +) -> bool: + for placement: Dictionary in _prepared_item_placements.values(): + if ( + int(placement.get("container", -1)) == int(container) + and int(placement.get("slot", -1)) == slot + ): + return true + return false + + func _key_at(container: InventoryContainer, slot: int) -> String: for key: String in _placements: var placement: Dictionary = _placements[key] diff --git a/items/catalog/basic_cube.tres b/items/catalog/basic_cube.tres new file mode 100644 index 0000000..86a2cf9 --- /dev/null +++ b/items/catalog/basic_cube.tres @@ -0,0 +1,17 @@ +[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3] + +[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"] +[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="2_icon"] + +[resource] +script = ExtResource("1_item") +item_id = &"basic_cube" +display_name = "basic cube" +description = "a simple piece of decor for testing your RV layout." +category = 6 +icon = ExtResource("2_icon") +stackable = true +max_stack = 32 +usable = false +equippable = true +hotbar_allowed = true diff --git a/items/catalog/item_catalog.tres b/items/catalog/item_catalog.tres index 63ef3ea..2f15e0a 100644 --- a/items/catalog/item_catalog.tres +++ b/items/catalog/item_catalog.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="ItemCatalog" load_steps=41 format=3] +[gd_resource type="Resource" script_class="ItemCatalog" load_steps=42 format=3] [ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"] [ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"] @@ -40,7 +40,8 @@ [ext_resource type="Resource" path="res://items/catalog/crab_net.tres" id="39_crab_net"] [ext_resource type="Resource" path="res://items/catalog/standard_shovel.tres" id="40_shovel"] [ext_resource type="Resource" path="res://items/catalog/fishing_net.tres" id="41_fishing_net"] +[ext_resource type="Resource" path="res://items/catalog/basic_cube.tres" id="42_basic_cube"] [resource] script = ExtResource("1_script") -items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net")] +items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("8_art_kit"), ExtResource("9_worms"), ExtResource("10_snails"), ExtResource("11_shrimp"), ExtResource("12_squid"), ExtResource("14_sardine"), ExtResource("13_anchovy"), ExtResource("15_roe"), ExtResource("16_standby"), ExtResource("17_batteries"), ExtResource("18_cardboard"), ExtResource("19_pond"), ExtResource("20_river"), ExtResource("21_lake"), ExtResource("22_salt"), ExtResource("23_whisker"), ExtResource("24_reef"), ExtResource("25_moonbeam"), ExtResource("26_sun"), ExtResource("27_rain"), ExtResource("28_fog"), ExtResource("29_guide"), ExtResource("30_small"), ExtResource("31_heavy"), ExtResource("32_rocket"), ExtResource("33_lucky"), ExtResource("34_showboat"), ExtResource("35_deep"), ExtResource("36_oddity"), ExtResource("37_aurora"), ExtResource("38_magnet"), ExtResource("39_crab_net"), ExtResource("40_shovel"), ExtResource("41_fishing_net"), ExtResource("42_basic_cube")] diff --git a/main/main.gd b/main/main.gd index e9dc78f..8335c69 100644 --- a/main/main.gd +++ b/main/main.gd @@ -37,6 +37,9 @@ const FishingShopType = preload("res://ui/fishing_shop.gd") const FishingShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) @@ -70,6 +73,7 @@ const NetworkFishingServiceType = preload( "res://network/network_fishing_service.gd" ) const PlayerHotbarType = preload("res://inventory/player_hotbar.gd") +const DecorCatalogType = preload("res://homes/decor_catalog.gd") const NetworkSaleServiceType = preload( "res://network/network_sale_service.gd" ) @@ -123,6 +127,14 @@ const GatheringControllerType = preload( const NetworkWorldSpawnServiceType = preload( "res://network/network_world_spawn_service.gd" ) +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") +const HomeWorldServiceType = preload("res://homes/home_world_service.gd") +const NetworkHomeServiceType = preload( + "res://network/network_home_service.gd" +) +const HomeDecorControllerType = preload( + "res://homes/home_decor_controller.gd" +) const TITLE_MUSIC_SILENCE_DB: float = -80.0 const TITLE_MUSIC_PATH: String = ( @@ -153,6 +165,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 @onready var _game_ui: GameUIType = %GameUI @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager +@onready var _home_state: PlayerHomeStateType = %PlayerHomeState @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager @onready var _controller_mapping_manager: ControllerMappingManagerType = ( %ControllerMappingManager @@ -212,6 +225,10 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 ) @onready var _network_sale: NetworkSaleServiceType = %NetworkSaleService @onready var _network_shop: NetworkShopServiceType = %NetworkShopService +@onready var _network_home: NetworkHomeServiceType = %NetworkHomeService +@onready var _home_decor_controller: HomeDecorControllerType = ( + %HomeDecorController +) @onready var _network_item_use: NetworkItemUseServiceType = ( %NetworkItemUseService ) @@ -234,6 +251,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 @onready var _players_root: Node3D = $Players @onready var _surface_drawings_root: Node3D = $SurfaceDrawings @onready var _world_gatherables_root: Node3D = $WorldGatherables +@onready var _home_world: HomeWorldServiceType = %HomeWorldService @onready var _title_background: ColorRect = %TitleBackground @onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop @onready var _shop_backdrop: ColorRect = %ShopBackdrop @@ -241,6 +259,7 @@ const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 var _gameplay_started: bool = false var _save_slots := PlayerSaveSlotCatalogType.new() var _shop_interaction: FishingShopInteractionType +var _decor_shop_interaction: DecorShopInteractionType var _storage_interaction: PlayerStorageInteractionType var _title_music_tween: Tween var _title_music_transition_generation: int = 0 @@ -740,6 +759,7 @@ func _initialize_application(dedicated: bool) -> void: _player.inventory_layout, ) _shop_interaction = _test_world.get_fishing_shop() + _decor_shop_interaction = _test_world.get_decor_shop() _storage_interaction = _test_world.get_player_storage() if not dedicated: _shop_interaction.setup_local_player(_player) @@ -749,6 +769,10 @@ func _initialize_application(dedicated: bool) -> void: _game_ui.set_shop_npc_player_in_range( _shop_interaction.is_local_player_in_range() ) + _decor_shop_interaction.setup_local_player(_player) + _decor_shop_interaction.local_player_range_changed.connect( + _on_decor_shop_range_changed + ) _storage_interaction.setup_local_player(_player) _storage_interaction.local_player_range_changed.connect( _on_storage_range_changed @@ -770,6 +794,23 @@ func _initialize_application(dedicated: bool) -> void: _world_weather, _player_jobs, _player, + _home_state, + ) + _home_world.setup( + _test_world, + _player_spawn_service, + _network_session, + dedicated, + ) + _network_home.setup( + _network_session, + _player_spawn_service, + _home_state, + _save_manager, + _home_world, + _player.bag, + _player.inventory_layout, + _player.hotbar, ) _player_jobs.set_save_manager(_save_manager) _save_manager.set_autosave_enabled(false) @@ -903,6 +944,9 @@ func _initialize_application(dedicated: bool) -> void: _save_manager, _asset_reservations, _player.inventory_layout, + _home_state, + _network_home, + _decor_shop_interaction, ) _fishing_spot.setup( _player, @@ -942,11 +986,13 @@ func _initialize_application(dedicated: bool) -> void: _player.fishing_upgrades, _shop_interaction, _storage_interaction, + _decor_shop_interaction, _player.item_effects, _player.cooler_capacity, _network_session, _network_sale, _network_shop, + _home_state, _network_chat, _network_profile, _player_spawn_service, @@ -964,6 +1010,21 @@ func _initialize_application(dedicated: bool) -> void: _test_world.get_world_environment(), _test_world.get_sun(), ) + _home_decor_controller.setup( + _player, + _player.hotbar, + _player.bag, + _home_state, + _network_home, + _home_world, + _fishing_spot, + _game_ui.get_home_decor_toolbar(), + ) + _game_ui.get_player_storage().decorate_requested.connect( + _home_decor_controller.begin_decorating + ) + _network_home.local_space_changed.connect(_on_local_home_space_changed) + _game_ui.rv_storage_requested.connect(_on_rv_storage_requested) _game_ui.setup_data_and_identity( _data_root, _identity_backups, @@ -1475,6 +1536,9 @@ func _input(event: InputEvent) -> void: if _game_ui.get_player_storage().consume_escape(): get_viewport().set_input_as_handled() return + if _game_ui.get_decor_shop().consume_escape(): + get_viewport().set_input_as_handled() + return if fishing_shop.consume_escape(): get_viewport().set_input_as_handled() return @@ -1573,19 +1637,50 @@ func _is_pause_open_request(event: InputEvent) -> bool: func _unhandled_input(event: InputEvent) -> void: + if ( + _gameplay_started + and event.is_action_pressed("fish_primary") + and not (event is InputEventKey and event.echo) + and _fishing_spot.try_use_active_non_fishing_item() + ): + get_viewport().set_input_as_handled() + return if ( not _gameplay_started or not event.is_action_pressed("interact") or (event is InputEventKey and event.echo) ): return + var home_owner: String = _network_home.get_local_home_owner_fingerprint() + if not home_owner.is_empty(): + if _home_world.is_avatar_near_interior_exit(_player, home_owner): + if _network_home.request_exit(): + _game_ui.set_storage_prompt_visible(false) + get_viewport().set_input_as_handled() + return + if ( + _network_home.can_local_decorate() + and _fishing_spot.can_open_fishing_shop() + and _game_ui.get_player_storage().open_storage_from_home(true) + ): + _game_ui.set_storage_prompt_visible(false) + get_viewport().set_input_as_handled() + return + var nearby_home_owner: String = _home_world.find_nearby_exterior_owner( + _player + ) + if not nearby_home_owner.is_empty(): + if _network_home.request_enter(nearby_home_owner): + _game_ui.set_storage_prompt_visible(false) + get_viewport().set_input_as_handled() + return if ( - _storage_interaction != null - and _storage_interaction.is_local_player_in_range() + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() and _fishing_spot.can_open_fishing_shop() - and _game_ui.get_player_storage().open_storage() + and _game_ui.get_decor_shop().open_shop() ): - _game_ui.set_storage_prompt_visible(false) + _game_ui.set_shop_prompt_visible(false) get_viewport().set_input_as_handled() return if ( @@ -1608,25 +1703,28 @@ func _process(delta: float) -> void: ) _print_dedicated_metrics() return - var show_shop_prompt := _can_show_shop_prompt() + var show_decor_shop_prompt := _can_show_decor_shop_prompt() + var show_shop_prompt := show_decor_shop_prompt or _can_show_shop_prompt() var shop_prompt_anchor := ( - _shop_interaction.get_prompt_anchor_position() - if _shop_interaction != null - else Vector3.ZERO + _decor_shop_interaction.get_prompt_anchor_position() + if show_decor_shop_prompt + else ( + _shop_interaction.get_prompt_anchor_position() + if _shop_interaction != null else Vector3.ZERO + ) ) _game_ui.set_shop_prompt_visible( show_shop_prompt, shop_prompt_anchor, ) - var show_storage_prompt := _can_show_storage_prompt() - var storage_prompt_anchor := ( - _storage_interaction.get_prompt_anchor_position() - if _storage_interaction != null - else Vector3.ZERO - ) + var home_prompt := _get_home_prompt() _game_ui.set_storage_prompt_visible( - show_storage_prompt, - storage_prompt_anchor, + not home_prompt.is_empty(), + home_prompt.get("anchor", Vector3.ZERO), + str(home_prompt.get("label", "enter RV")), + ) + _game_ui.set_rv_storage_button_visible( + _can_show_rv_storage_button() ) @@ -2421,6 +2519,7 @@ func _apply_world( if _application_initialized: _refresh_active_world_bindings() _network_world_spawns.refresh_world_context() + _network_home.refresh_world_presentations() if _application_initialized and not _dedicated_runtime: _water_recovery.update_world_context( spawn_transform, @@ -2450,10 +2549,19 @@ func _refresh_active_world_bindings() -> void: _storage_interaction.local_player_range_changed.disconnect( _on_storage_range_changed ) + if is_instance_valid(_decor_shop_interaction): + if _decor_shop_interaction.local_player_range_changed.is_connected( + _on_decor_shop_range_changed + ): + _decor_shop_interaction.local_player_range_changed.disconnect( + _on_decor_shop_range_changed + ) _shop_interaction = _test_world.get_fishing_shop() + _decor_shop_interaction = _test_world.get_decor_shop() _storage_interaction = _test_world.get_player_storage() _network_sale.set_shop_interaction(_shop_interaction) _network_shop.set_shop_interaction(_shop_interaction) + _network_shop.set_decor_shop_interaction(_decor_shop_interaction) if _dedicated_runtime: return if _shop_interaction != null: @@ -2466,9 +2574,15 @@ func _refresh_active_world_bindings() -> void: _storage_interaction.local_player_range_changed.connect( _on_storage_range_changed ) + if _decor_shop_interaction != null: + _decor_shop_interaction.setup_local_player(_player) + _decor_shop_interaction.local_player_range_changed.connect( + _on_decor_shop_range_changed + ) _game_ui.set_world_interactions( _shop_interaction, _storage_interaction, + _decor_shop_interaction, ) _on_shop_range_changed( _shop_interaction != null @@ -2478,6 +2592,10 @@ func _refresh_active_world_bindings() -> void: _storage_interaction != null and _storage_interaction.is_local_player_in_range() ) + _on_decor_shop_range_changed( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ) func _on_water_recovery_starting() -> void: @@ -2491,6 +2609,7 @@ func _on_water_recovery_starting() -> void: _game_ui.close_player_menu_for_water_recovery() _game_ui.get_pause_menu().close_for_water_recovery() _game_ui.get_fishing_shop().close_for_water_recovery() + _game_ui.get_decor_shop().close_for_water_recovery() _game_ui.get_player_storage().close_for_water_recovery() @@ -2552,6 +2671,12 @@ func _on_active_hotbar_item_changed( and item.is_available() and _player.bag.owns_item(item_id) ) + var active_is_decor: bool = ( + DecorCatalogType.has_product(item_id) + and item != null + and item.is_available() + and _player.bag.owns_item(item_id) + ) _player.set_active_fishing_rod( item as FishingRodDataType if active_is_rod else null, true, @@ -2562,6 +2687,7 @@ func _on_active_hotbar_item_changed( ) _player.set_active_catching_net(active_is_catching_net) _player.set_active_shovel(active_is_shovel) + _player.set_active_decor_package(active_is_decor) _game_ui.set_surface_drawing_hotbar_selected(active_is_art_kit) _network_item_use.submit_local_equipped(item_id, active_is_rod or ( item != null and _player.bag.owns_item(item_id) @@ -2698,7 +2824,13 @@ func _exit_tree() -> void: func _on_shop_range_changed(in_range: bool) -> void: - _game_ui.set_shop_npc_player_in_range(in_range) + _game_ui.set_shop_npc_player_in_range( + in_range + or ( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ) + ) if not in_range: _game_ui.get_fishing_shop().close_for_range_exit() _game_ui.set_shop_prompt_visible(_can_show_shop_prompt()) @@ -2707,7 +2839,50 @@ func _on_shop_range_changed(in_range: bool) -> void: func _on_storage_range_changed(in_range: bool) -> void: if not in_range: _game_ui.get_player_storage().close_for_range_exit() - _game_ui.set_storage_prompt_visible(_can_show_storage_prompt()) + var prompt := _get_home_prompt() + _game_ui.set_storage_prompt_visible( + not prompt.is_empty(), + prompt.get("anchor", Vector3.ZERO), + str(prompt.get("label", "enter RV")), + ) + _game_ui.set_rv_storage_button_visible( + _can_show_rv_storage_button() + ) + + +func _on_decor_shop_range_changed(in_range: bool) -> void: + _game_ui.set_shop_npc_player_in_range( + in_range + or ( + _shop_interaction != null + and _shop_interaction.is_local_player_in_range() + ) + ) + if not in_range: + _game_ui.get_decor_shop().close_for_range_exit() + _game_ui.set_shop_prompt_visible( + _can_show_shop_prompt() or _can_show_decor_shop_prompt() + ) + + +func _on_local_home_space_changed( + space_id: StringName, + _owner_fingerprint: String, +) -> void: + var inside_rv: bool = String(space_id).begins_with("rv:") + _fishing_spot.set_fishing_environment_enabled(not inside_rv) + _game_ui.set_rv_interior_camera_mode( + inside_rv + ) + _game_ui.set_rv_storage_button_visible( + _can_show_rv_storage_button() + ) + + +func _on_rv_storage_requested() -> void: + if not _can_show_rv_storage_button(): + return + _game_ui.get_player_storage().open_storage_from_home(true) func _can_show_shop_prompt() -> bool: @@ -2716,18 +2891,66 @@ func _can_show_shop_prompt() -> bool: and _shop_interaction != null and _shop_interaction.is_local_player_in_range() and not _game_ui.get_fishing_shop().visible + and not _game_ui.get_decor_shop().visible + and not _water_recovery.is_recovery_active() + and _fishing_spot.can_open_fishing_shop() + ) + + +func _can_show_decor_shop_prompt() -> bool: + return ( + _gameplay_started + and _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + and not _game_ui.get_decor_shop().visible + and not _game_ui.get_fishing_shop().visible and not _water_recovery.is_recovery_active() and _fishing_spot.can_open_fishing_shop() ) func _can_show_storage_prompt() -> bool: + return not _get_home_prompt().is_empty() + + +func _can_show_rv_storage_button() -> bool: return ( _gameplay_started - and _storage_interaction != null - and _storage_interaction.is_local_player_in_range() - and not _game_ui.get_player_storage().visible - and not _game_ui.get_fishing_shop().visible + and not _network_home.get_local_home_owner_fingerprint().is_empty() + and _network_home.can_local_decorate() and not _water_recovery.is_recovery_active() and _fishing_spot.can_open_fishing_shop() ) + + +func _get_home_prompt() -> Dictionary: + if ( + not _gameplay_started + or _game_ui.get_player_storage().visible + or _game_ui.get_fishing_shop().visible + or _game_ui.get_decor_shop().visible + or _water_recovery.is_recovery_active() + or not _fishing_spot.can_open_fishing_shop() + ): + return {} + var owner_fingerprint: String = ( + _network_home.get_local_home_owner_fingerprint() + ) + if not owner_fingerprint.is_empty(): + if _home_world.is_avatar_near_interior_exit( + _player, owner_fingerprint + ): + return { + "label": "leave RV", + "anchor": _home_world.get_interior_prompt_position( + owner_fingerprint + ), + } + return {} + var nearby_owner: String = _home_world.find_nearby_exterior_owner(_player) + if nearby_owner.is_empty(): + return {} + return { + "label": "enter RV", + "anchor": _home_world.get_exterior_prompt_position(nearby_owner), + } diff --git a/main/main.tscn b/main/main.tscn index c1380d6..778a1d4 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -58,6 +58,10 @@ [ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"] [ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"] [ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"] +[ext_resource type="Script" path="res://homes/player_home_state.gd" id="61_home_state"] +[ext_resource type="Script" path="res://homes/home_world_service.gd" id="62_home_world"] +[ext_resource type="Script" path="res://network/network_home_service.gd" id="63_network_home"] +[ext_resource type="Script" path="res://homes/home_decor_controller.gd" id="64_home_decor_controller"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_grass_native"] shader = ExtResource("42_menu_pattern") @@ -259,6 +263,14 @@ script = ExtResource("22_network_sale") unique_name_in_owner = true script = ExtResource("23_network_shop") +[node name="NetworkHomeService" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("63_network_home") + +[node name="HomeDecorController" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("64_home_decor_controller") + [node name="NetworkItemUseService" type="Node" parent="." unique_id=255154670] unique_name_in_owner = true script = ExtResource("24_network_item") @@ -322,6 +334,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 3.3999481, 12.07081) [node name="WorldGatherables" type="Node3D" parent="."] +[node name="HomeWorldService" type="Node3D" parent="."] +unique_name_in_owner = true +script = ExtResource("62_home_world") + [node name="GatheringController" type="Node3D" parent="."] unique_name_in_owner = true script = ExtResource("60_gathering_controller") @@ -338,6 +354,10 @@ script = ExtResource("8_recovery") unique_name_in_owner = true script = ExtResource("9_save") +[node name="PlayerHomeState" type="Node" parent="."] +unique_name_in_owner = true +script = ExtResource("61_home_state") + [node name="PlayerSettingsManager" type="Node" parent="." unique_id=906991991] unique_name_in_owner = true script = ExtResource("10_settings") diff --git a/network/network_home_protocol.gd b/network/network_home_protocol.gd new file mode 100644 index 0000000..62b8488 --- /dev/null +++ b/network/network_home_protocol.gd @@ -0,0 +1,176 @@ +class_name NetworkHomeProtocol +extends RefCounted + +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") + +const CAPABILITY: StringName = &"portable_home_v1" +const RELIABLE_CHANNEL: int = NetworkProtocol.HOME_RELIABLE_CHANNEL +const MAX_ID_LENGTH: int = 96 +const MAX_MESSAGE_LENGTH: int = 160 +const WORLD_SPACE: StringName = &"world" + +enum TransitionAction { + ENTER, + EXIT, +} + + +static func make_space_id(owner_fingerprint: String) -> StringName: + return StringName("rv:%s" % owner_fingerprint) + + +static func manifest_digest(home_data: Dictionary) -> String: + var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data(home_data) + if sanitized.is_empty(): + return "" + var canonical_owned: Array[Dictionary] = [] + var owned: Dictionary = sanitized["owned_decor"] + var owned_ids: Array[String] = [] + for value: Variant in owned.keys(): + owned_ids.append(str(value)) + owned_ids.sort() + for decor_id: String in owned_ids: + canonical_owned.append({ + "id": decor_id, + "quantity": int(owned[decor_id]), + }) + var placements: Array[Dictionary] = [] + for value: Variant in sanitized["placements"]: + placements.append((value as Dictionary).duplicate(true)) + placements.sort_custom(func(first: Dictionary, second: Dictionary) -> bool: + return str(first["instance_id"]) < str(second["instance_id"]) + ) + var canonical: Dictionary = { + "schema_version": int(sanitized["schema_version"]), + "tier": int(sanitized["tier"]), + "exterior_id": str(sanitized["exterior_id"]), + "privacy": int(sanitized["privacy"]), + "owned": canonical_owned, + "placements": placements, + "revision": int(sanitized["revision"]), + "next_instance_sequence": int(sanitized["next_instance_sequence"]), + } + return JSON.stringify(canonical).sha256_text() + + +static func manifest_signature_fields(data: Dictionary) -> Array: + return [ + str(data.get("session_id", "")), + str(data.get("request_id", "")), + str(data.get("owner_fingerprint", "")), + int((data.get("home", {}) as Dictionary).get("revision", -1)), + str(data.get("digest", "")), + ] + + +static func validate_manifest_request(value: Variant) -> bool: + if typeof(value) != TYPE_DICTIONARY: + return false + var data: Dictionary = value + if ( + not _valid_id(data.get("request_id")) + or not _valid_id(data.get("session_id")) + or not NetworkIdentityCrypto.valid_fingerprint( + data.get("owner_fingerprint") + ) + or typeof(data.get("home")) != TYPE_DICTIONARY + or typeof(data.get("digest")) != TYPE_STRING + or str(data["digest"]).length() != 64 + or typeof(data.get("sender_signature")) != TYPE_PACKED_BYTE_ARRAY + ): + return false + var sanitized: Dictionary = PlayerHomeStateType.sanitize_save_data( + data["home"] + ) + return ( + not sanitized.is_empty() + and manifest_digest(sanitized) == str(data["digest"]) + ) + + +static func validate_manifest_broadcast(value: Variant) -> bool: + if typeof(value) != TYPE_DICTIONARY: + return false + var data: Dictionary = value + return ( + typeof(data.get("owner_peer_id")) == TYPE_INT + and int(data["owner_peer_id"]) > 0 + and NetworkIdentityCrypto.valid_fingerprint( + data.get("owner_fingerprint") + ) + and typeof(data.get("home")) == TYPE_DICTIONARY + and not PlayerHomeStateType.sanitize_save_data(data["home"]).is_empty() + and _valid_transform(data.get("exterior_transform")) + and typeof(data.get("interior_index")) == TYPE_INT + and int(data["interior_index"]) >= 0 + and int(data["interior_index"]) < 128 + ) + + +static func transition_signature_fields(data: Dictionary) -> Array: + return [ + str(data.get("session_id", "")), + str(data.get("request_id", "")), + int(data.get("action", -1)), + str(data.get("owner_fingerprint", "")), + ] + + +static func validate_transition_request(value: Variant) -> bool: + if typeof(value) != TYPE_DICTIONARY: + return false + var data: Dictionary = value + return ( + _valid_id(data.get("request_id")) + and _valid_id(data.get("session_id")) + and typeof(data.get("action")) == TYPE_INT + and int(data["action"]) >= TransitionAction.ENTER + and int(data["action"]) <= TransitionAction.EXIT + and NetworkIdentityCrypto.valid_fingerprint( + data.get("owner_fingerprint") + ) + and typeof(data.get("sender_signature")) == TYPE_PACKED_BYTE_ARRAY + ) + + +static func transform_to_array(value: Transform3D) -> Array[float]: + return [ + value.basis.x.x, value.basis.x.y, value.basis.x.z, + value.basis.y.x, value.basis.y.y, value.basis.y.z, + value.basis.z.x, value.basis.z.y, value.basis.z.z, + value.origin.x, value.origin.y, value.origin.z, + ] + + +static func array_to_transform(value: Variant) -> Transform3D: + if not _valid_transform(value): + return Transform3D() + var fields: Array = value + return Transform3D( + Basis( + Vector3(float(fields[0]), float(fields[1]), float(fields[2])), + Vector3(float(fields[3]), float(fields[4]), float(fields[5])), + Vector3(float(fields[6]), float(fields[7]), float(fields[8])), + ), + Vector3(float(fields[9]), float(fields[10]), float(fields[11])), + ) + + +static func _valid_transform(value: Variant) -> bool: + if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12: + return false + for component: Variant in value: + if ( + typeof(component) not in [TYPE_FLOAT, TYPE_INT] + or not is_finite(float(component)) + ): + return false + return true + + +static func _valid_id(value: Variant) -> bool: + return ( + typeof(value) == TYPE_STRING + and not str(value).is_empty() + and str(value).length() <= MAX_ID_LENGTH + ) diff --git a/network/network_home_protocol.gd.uid b/network/network_home_protocol.gd.uid new file mode 100644 index 0000000..24b6aae --- /dev/null +++ b/network/network_home_protocol.gd.uid @@ -0,0 +1 @@ +uid://fl3qwiluejhm diff --git a/network/network_home_service.gd b/network/network_home_service.gd new file mode 100644 index 0000000..df4fcde --- /dev/null +++ b/network/network_home_service.gd @@ -0,0 +1,956 @@ +class_name NetworkHomeService +extends Node + +const NetworkHomeProtocolType = preload( + "res://network/network_home_protocol.gd" +) +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") + +signal local_transition_finished(accepted: bool, message: String) +signal local_space_changed(space_id: StringName, owner_fingerprint: String) +signal local_manifest_rejected(message: String) + +const MAX_MANIFESTS: int = 128 + +var _session: NetworkSession +var _spawn_service: PlayerSpawnService +var _local_home: PlayerHomeStateType +var _save_manager: PlayerSaveManager +var _world_service: HomeWorldService +var _bag: PlayerBag +var _inventory_layout: PlayerInventoryLayout +var _hotbar: PlayerHotbar +var _manifests_by_fingerprint: Dictionary[String, Dictionary] = {} +var _fingerprint_by_peer: Dictionary[int, String] = {} +var _authorized_owned_counts: Dictionary[int, Dictionary] = {} +var _authorization_result_owner: Dictionary[String, int] = {} +var _next_interior_index: int = 0 +var _suppress_local_submission: bool = false +var _publish_queued: bool = false + + +func setup( + session: NetworkSession, + spawn_service: PlayerSpawnService, + local_home: PlayerHomeStateType, + save_manager: PlayerSaveManager, + world_service: HomeWorldService, + bag: PlayerBag, + inventory_layout: PlayerInventoryLayout, + hotbar: PlayerHotbar, +) -> void: + _session = session + _spawn_service = spawn_service + _local_home = local_home + _save_manager = save_manager + _world_service = world_service + _bag = bag + _inventory_layout = inventory_layout + _hotbar = hotbar + if not _session.state_changed.is_connected(_on_session_state_changed): + _session.state_changed.connect(_on_session_state_changed) + if not _session.join_authenticated.is_connected(_on_join_authenticated): + _session.join_authenticated.connect(_on_join_authenticated) + if not _session.peer_authenticated.is_connected(_on_peer_authenticated): + _session.peer_authenticated.connect(_on_peer_authenticated) + if not _session.peer_removed.is_connected(_on_peer_removed): + _session.peer_removed.connect(_on_peer_removed) + if not _local_home.changed.is_connected(_on_local_home_changed): + _local_home.changed.connect(_on_local_home_changed) + + +func get_local_space() -> StringName: + if _session == null or not _session.is_gameplay_session_active(): + return NetworkHomeProtocolType.WORLD_SPACE + return _session.get_peer_space(_session.get_local_peer_id()) + + +func get_local_home_owner_fingerprint() -> String: + var space_id: String = String(get_local_space()) + return space_id.trim_prefix("rv:") if space_id.begins_with("rv:") else "" + + +func can_local_decorate() -> bool: + return ( + get_local_home_owner_fingerprint() + == _session.get_local_identity_fingerprint() + ) + + +func get_authoritative_owned_count( + peer_id: int, + decor_id: StringName, +) -> int: + var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {}) + if authorized.has(String(decor_id)): + return int(authorized[String(decor_id)]) + var fingerprint: String = _fingerprint_for_peer(peer_id) + var manifest: Dictionary = _manifests_by_fingerprint.get(fingerprint, {}) + if manifest.is_empty(): + return -1 + var home: Dictionary = manifest.get("home", {}) + var owned: Dictionary = home.get("owned_decor", {}) + return int(owned.get(String(decor_id), 0)) + + +func place_local_decor( + decor_id: StringName, + position: Vector3, + yaw_radians: float, + mode: PlayerHomeStateType.PlacementMode, +) -> String: + if ( + not can_local_decorate() + or _bag == null + or _bag.get_quantity(decor_id) <= 0 + or _world_service == null + ): + return "" + var owner_fingerprint: String = ( + _session.get_local_identity_fingerprint() + ) + var normalized: Dictionary = ( + _local_home.get_nearest_valid_placement_transform( + decor_id, + position, + yaw_radians, + mode, + "", + Callable( + self, "_candidate_clear_of_players" + ).bind(owner_fingerprint, decor_id), + ) + ) + if normalized.is_empty(): + return "" + var instance_id: String = _local_home.place_decor( + decor_id, + normalized["position"], + float(normalized["yaw"]), + mode, + ) + if instance_id.is_empty(): + return "" + if not _bag.remove_item(decor_id, 1): + _local_home.remove_placement(instance_id) + return "" + _save_manager.save_if_dirty() + return instance_id + + +func remove_local_decor(instance_id: String) -> bool: + if ( + not can_local_decorate() + or _bag == null + or _inventory_layout == null + or _hotbar == null + ): + return false + var placement: Dictionary = _local_home.get_placement(instance_id) + if placement.is_empty(): + return false + var decor_id := StringName(str(placement["decor_id"])) + var target_hotbar_slot: int = _hotbar.find_item_slot(decor_id) + if target_hotbar_slot < 0: + target_hotbar_slot = _hotbar.find_first_empty_slot() + var prepared_hotbar_placement: bool = false + if ( + target_hotbar_slot >= 0 + and _inventory_layout.get_container( + PlayerInventoryLayout.EntryKind.ITEM, + decor_id, + ) < 0 + ): + prepared_hotbar_placement = ( + _inventory_layout.prepare_new_item_placement( + decor_id, + PlayerInventoryLayout.InventoryContainer.HOTBAR, + target_hotbar_slot, + ) + ) + var returned_to_inventory: bool = _bag.add_item(decor_id, 1) + if not returned_to_inventory: + if prepared_hotbar_placement: + _inventory_layout.cancel_prepared_item_placement(decor_id) + returned_to_inventory = _bag.add_item_to_storage(decor_id, 1) + if not returned_to_inventory: + return false + if target_hotbar_slot >= 0: + _hotbar.assign_item(target_hotbar_slot, decor_id) + _hotbar.select_slot(target_hotbar_slot) + if not _local_home.remove_placement(instance_id): + _bag.remove_item(decor_id, 1) + return false + _save_manager.save_if_dirty() + return true + + +func update_local_decor( + instance_id: String, + position: Vector3, + yaw_radians: float, + mode: PlayerHomeStateType.PlacementMode, +) -> bool: + var normalized: Dictionary = get_valid_local_decor_transform( + instance_id, position, yaw_radians, mode + ) + if normalized.is_empty(): + return false + if not _local_home.update_placement( + instance_id, + normalized["position"], + float(normalized["yaw"]), + mode, + ): + return false + _save_manager.save_if_dirty() + return true + + +func get_valid_local_decor_transform( + instance_id: String, + position: Vector3, + yaw_radians: float, + mode: PlayerHomeStateType.PlacementMode, +) -> Dictionary: + if not can_local_decorate() or _world_service == null: + return {} + var placement: Dictionary = _local_home.get_placement(instance_id) + if placement.is_empty(): + return {} + var decor_id := StringName(str(placement.get("decor_id", ""))) + return _local_home.get_valid_update_transform( + instance_id, + position, + yaw_radians, + mode, + Callable( + self, "_candidate_clear_of_players" + ).bind( + _session.get_local_identity_fingerprint(), decor_id + ), + ) + + +func _candidate_clear_of_players( + candidate: Dictionary, + owner_fingerprint: String, + decor_id: StringName, +) -> bool: + return ( + _world_service != null + and _world_service.is_decor_placement_clear_of_players( + owner_fingerprint, + decor_id, + candidate["position"], + float(candidate["yaw"]), + ) + ) + + +func request_enter(owner_fingerprint: String) -> bool: + if ( + _session == null + or not _session.is_gameplay_session_active() + or not _manifests_by_fingerprint.has(owner_fingerprint) + or get_local_space() != NetworkHomeProtocolType.WORLD_SPACE + ): + return false + return _submit_transition( + NetworkHomeProtocolType.TransitionAction.ENTER, + owner_fingerprint, + ) + + +func request_exit() -> bool: + var owner_fingerprint: String = get_local_home_owner_fingerprint() + if owner_fingerprint.is_empty(): + return false + return _submit_transition( + NetworkHomeProtocolType.TransitionAction.EXIT, + owner_fingerprint, + ) + + +func authorize_decor_purchase( + peer_id: int, + product_id: StringName, + resulting_count: int, + result_id: String, +) -> void: + if not _session.is_host() or peer_id <= 0 or result_id.is_empty(): + return + var counts: Dictionary = _authorized_owned_counts.get(peer_id, {}) + counts[String(product_id)] = resulting_count + _authorized_owned_counts[peer_id] = counts + _authorization_result_owner[result_id] = peer_id + + +func finalize_decor_purchase( + peer_id: int, + result_id: String, + applied: bool, +) -> void: + if _authorization_result_owner.get(result_id, 0) != peer_id: + return + _authorization_result_owner.erase(result_id) + if applied: + return + _authorized_owned_counts.erase(peer_id) + + +func refresh_world_presentations() -> void: + if _world_service == null: + return + _world_service.clear_presentations() + if not _world_service.is_world_ready(): + return + if not _session.is_host(): + for manifest: Dictionary in _manifests_by_fingerprint.values(): + _world_service.apply_manifest(manifest) + return + var local_fingerprint: String = _session.get_local_identity_fingerprint() + if ( + not _session.is_dedicated_host() + and not _manifests_by_fingerprint.has(local_fingerprint) + ): + publish_local_home() + var fingerprints: Array[String] = [] + fingerprints.assign(_manifests_by_fingerprint.keys()) + fingerprints.sort() + var reserved: Array[Transform3D] = [] + for fingerprint: String in fingerprints: + var manifest: Dictionary = _manifests_by_fingerprint[fingerprint] + var transform: Transform3D = _world_service.choose_exterior_transform( + int(manifest["owner_peer_id"]), reserved + ) + reserved.append(transform) + manifest["exterior_transform"] = ( + NetworkHomeProtocolType.transform_to_array(transform) + ) + _manifests_by_fingerprint[fingerprint] = manifest + _broadcast_manifest(manifest) + for fingerprint: String in fingerprints: + var manifest: Dictionary = _manifests_by_fingerprint[fingerprint] + _spawn_peer_at_exterior( + int(manifest["owner_peer_id"]), fingerprint + ) + + +func publish_local_home() -> bool: + if ( + _session == null + or not _session.is_gameplay_session_active() + or _session.is_dedicated_host() + or _local_home == null + ): + return false + if _session.is_host() and ( + _world_service == null or not _world_service.is_world_ready() + ): + return false + var home_data: Dictionary = _local_home.to_save_data() + var request: Dictionary = { + "request_id": _new_id("home"), + "session_id": _session.get_session_id(), + "owner_fingerprint": _session.get_local_identity_fingerprint(), + "home": home_data, + "digest": NetworkHomeProtocolType.manifest_digest(home_data), + } + request["sender_signature"] = _session.sign_local_action( + "portable_home_manifest", + NetworkHomeProtocolType.manifest_signature_fields(request), + ) + if _session.is_host(): + _process_manifest_request(_session.get_local_peer_id(), request) + else: + submit_home_manifest.rpc_id(1, request) + return true + + +@rpc( + "any_peer", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func submit_home_manifest(data: Dictionary) -> void: + var sender_id: int = multiplayer.get_remote_sender_id() + if ( + _session == null + or not _session.is_host() + or not _session.is_authenticated_peer(sender_id) + ): + return + _process_manifest_request(sender_id, data) + + +func _process_manifest_request(peer_id: int, data: Dictionary) -> void: + if ( + not NetworkHomeProtocolType.validate_manifest_request(data) + or str(data["session_id"]) != _session.get_session_id() + ): + _reject_manifest(peer_id, "The RV update was invalid.") + return + var record := _session.get_peer_record(peer_id) + var fingerprint: String = str(data["owner_fingerprint"]) + if ( + record == null + or record.identity_fingerprint != fingerprint + or not _session.verify_peer_action( + peer_id, + "portable_home_manifest", + NetworkHomeProtocolType.manifest_signature_fields(data), + data["sender_signature"], + ) + ): + _reject_manifest(peer_id, "The RV owner could not be verified.") + return + var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data( + data["home"] + ) + var prior: Dictionary = _manifests_by_fingerprint.get(fingerprint, {}) + var is_first_manifest: bool = prior.is_empty() + if not _valid_manifest_revision(peer_id, prior, home_data): + _reject_manifest(peer_id, "The RV update was rejected by the host.") + return + if not _moved_decor_clear_of_players(fingerprint, prior, home_data): + _reject_manifest(peer_id, "A player is in the way of that decor.") + return + var manifest: Dictionary + if prior.is_empty(): + if _manifests_by_fingerprint.size() >= MAX_MANIFESTS: + _reject_manifest(peer_id, "This room cannot place another RV.") + return + var reserved: Array[Transform3D] = [] + for existing: Dictionary in _manifests_by_fingerprint.values(): + reserved.append(NetworkHomeProtocolType.array_to_transform( + existing["exterior_transform"] + )) + var exterior_transform: Transform3D = ( + _world_service.choose_exterior_transform(peer_id, reserved) + ) + manifest = { + "owner_peer_id": peer_id, + "owner_fingerprint": fingerprint, + "home": home_data, + "exterior_transform": ( + NetworkHomeProtocolType.transform_to_array(exterior_transform) + ), + "interior_index": _next_interior_index, + } + _next_interior_index += 1 + else: + manifest = prior.duplicate(true) + manifest["home"] = home_data + manifest["owner_peer_id"] = peer_id + _manifests_by_fingerprint[fingerprint] = manifest + _fingerprint_by_peer[peer_id] = fingerprint + _consume_owned_authorizations(peer_id, home_data) + _broadcast_manifest(manifest) + if is_first_manifest: + _spawn_peer_at_exterior(peer_id, fingerprint) + + +func _valid_manifest_revision( + peer_id: int, + prior_manifest: Dictionary, + home_data: Dictionary, +) -> bool: + if prior_manifest.is_empty(): + # A portable RV is owned by the player's signed local save. A new host has + # no prior room ledger with which to compare its first manifest. + return true + var prior_home: Dictionary = prior_manifest["home"] + if ( + int(home_data["revision"]) <= int(prior_home["revision"]) + or int(home_data["tier"]) != int(prior_home["tier"]) + or str(home_data["exterior_id"]) != str(prior_home["exterior_id"]) + ): + return false + var prior_owned: Dictionary = prior_home["owned_decor"] + var next_owned: Dictionary = home_data["owned_decor"] + var all_ids: Dictionary[String, bool] = {} + for value: Variant in prior_owned.keys(): + all_ids[str(value)] = true + for value: Variant in next_owned.keys(): + all_ids[str(value)] = true + var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {}) + for product_id: String in all_ids: + var prior_count: int = int(prior_owned.get(product_id, 0)) + var next_count: int = int(next_owned.get(product_id, 0)) + if prior_count == next_count: + continue + if next_count < prior_count or int(authorized.get(product_id, -1)) != next_count: + return false + return true + + +func _moved_decor_clear_of_players( + owner_fingerprint: String, + prior_manifest: Dictionary, + home_data: Dictionary, +) -> bool: + if _world_service == null: + return false + var prior_by_instance: Dictionary[String, Dictionary] = {} + if not prior_manifest.is_empty(): + var prior_home: Dictionary = prior_manifest.get("home", {}) + for value: Variant in prior_home.get("placements", []): + if typeof(value) != TYPE_DICTIONARY: + continue + var prior_placement: Dictionary = value + prior_by_instance[str( + prior_placement.get("instance_id", "") + )] = prior_placement + for value: Variant in home_data.get("placements", []): + if typeof(value) != TYPE_DICTIONARY: + return false + var placement: Dictionary = value + var instance_id: String = str(placement.get("instance_id", "")) + var prior_placement: Dictionary = prior_by_instance.get( + instance_id, {} + ) + if ( + not prior_placement.is_empty() + and _same_decor_transform(prior_placement, placement) + ): + continue + var decor_id := StringName(str(placement.get("decor_id", ""))) + var position: Vector3 = _placement_position(placement) + if ( + not position.is_finite() + or not _world_service.is_decor_placement_clear_of_players( + owner_fingerprint, + decor_id, + position, + float(placement.get("yaw", 0.0)), + ) + ): + return false + return true + + +static func _same_decor_transform( + first: Dictionary, + second: Dictionary, +) -> bool: + if str(first.get("decor_id", "")) != str(second.get("decor_id", "")): + return false + var first_position: Vector3 = _placement_position(first) + var second_position: Vector3 = _placement_position(second) + return ( + first_position.is_equal_approx(second_position) + and is_equal_approx( + float(first.get("yaw", 0.0)), + float(second.get("yaw", 0.0)), + ) + ) + + +static func _placement_position(placement: Dictionary) -> Vector3: + 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 _consume_owned_authorizations(peer_id: int, home_data: Dictionary) -> void: + var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {}) + if authorized.is_empty(): + return + var owned: Dictionary = home_data["owned_decor"] + for product_id: String in authorized.keys(): + if int(owned.get(product_id, -1)) == int(authorized[product_id]): + authorized.erase(product_id) + if authorized.is_empty(): + _authorized_owned_counts.erase(peer_id) + else: + _authorized_owned_counts[peer_id] = authorized + + +func _broadcast_manifest(manifest: Dictionary) -> void: + if _world_service != null and _world_service.is_world_ready(): + _world_service.apply_manifest(manifest) + for peer_id: int in _session.get_authenticated_peer_ids(): + if peer_id == 1: + continue + receive_home_manifest.rpc_id(peer_id, manifest) + + +@rpc( + "authority", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func receive_home_manifest(data: Dictionary) -> void: + if not NetworkHomeProtocolType.validate_manifest_broadcast(data): + return + var fingerprint: String = str(data["owner_fingerprint"]) + _manifests_by_fingerprint[fingerprint] = data.duplicate(true) + _fingerprint_by_peer[int(data["owner_peer_id"])] = fingerprint + if _world_service != null and _world_service.is_world_ready(): + _world_service.apply_manifest(data) + + +func _reject_manifest(peer_id: int, message: String) -> void: + var fingerprint: String = _fingerprint_for_peer(peer_id) + var canonical: Dictionary = _manifests_by_fingerprint.get(fingerprint, {}) + if peer_id == _session.get_local_peer_id(): + _apply_manifest_rejection(message, canonical) + else: + receive_home_manifest_rejection.rpc_id(peer_id, message, canonical) + + +@rpc( + "authority", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func receive_home_manifest_rejection( + message: String, + canonical: Dictionary, +) -> void: + _apply_manifest_rejection(message, canonical) + + +func _apply_manifest_rejection(message: String, canonical: Dictionary) -> void: + if ( + canonical.is_empty() + or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical) + ): + local_manifest_rejected.emit(message) + return + _suppress_local_submission = true + _local_home.restore_from_save_data(canonical["home"]) + _suppress_local_submission = false + _save_manager.save_if_dirty() + receive_home_manifest(canonical) + local_manifest_rejected.emit(message) + + +func _submit_transition(action: int, owner_fingerprint: String) -> bool: + var request: Dictionary = { + "request_id": _new_id("home_transition"), + "session_id": _session.get_session_id(), + "action": action, + "owner_fingerprint": owner_fingerprint, + } + request["sender_signature"] = _session.sign_local_action( + "portable_home_transition", + NetworkHomeProtocolType.transition_signature_fields(request), + ) + if _session.is_host(): + _process_transition_request(_session.get_local_peer_id(), request) + else: + submit_home_transition.rpc_id(1, request) + return true + + +@rpc( + "any_peer", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func submit_home_transition(data: Dictionary) -> void: + var sender_id: int = multiplayer.get_remote_sender_id() + if ( + not _session.is_host() + or not _session.is_authenticated_peer(sender_id) + ): + return + _process_transition_request(sender_id, data) + + +func _process_transition_request(peer_id: int, data: Dictionary) -> void: + if ( + not NetworkHomeProtocolType.validate_transition_request(data) + or str(data["session_id"]) != _session.get_session_id() + or not _session.verify_peer_action( + peer_id, + "portable_home_transition", + NetworkHomeProtocolType.transition_signature_fields(data), + data["sender_signature"], + ) + ): + _send_transition_rejection(peer_id, "The RV transition was invalid.") + return + var owner_fingerprint: String = str(data["owner_fingerprint"]) + var action: int = int(data["action"]) + var avatar: Player = _spawn_service.get_avatar(peer_id) + var manifest: Dictionary = _manifests_by_fingerprint.get( + owner_fingerprint, {} + ) + if avatar == null or manifest.is_empty(): + _send_transition_rejection(peer_id, "That RV is unavailable.") + return + if action == NetworkHomeProtocolType.TransitionAction.ENTER: + var record := _session.get_peer_record(peer_id) + var owner_is_local: bool = ( + record != null and record.identity_fingerprint == owner_fingerprint + ) + var privacy: int = int((manifest["home"] as Dictionary)["privacy"]) + if ( + (not owner_is_local and privacy != PlayerHomeStateType.Privacy.OPEN) + or _session.get_peer_space(peer_id) + != NetworkHomeProtocolType.WORLD_SPACE + or not _world_service.is_avatar_near_exterior( + avatar, owner_fingerprint + ) + ): + _send_transition_rejection(peer_id, "You cannot enter that RV.") + return + _apply_host_transition( + peer_id, + NetworkHomeProtocolType.make_space_id(owner_fingerprint), + owner_fingerprint, + _world_service.get_interior_spawn_transform( + owner_fingerprint, peer_id + ), + ) + return + if ( + _session.get_peer_space(peer_id) + != NetworkHomeProtocolType.make_space_id(owner_fingerprint) + or not _world_service.is_avatar_near_interior_exit( + avatar, owner_fingerprint + ) + ): + _send_transition_rejection(peer_id, "Move closer to the RV door.") + return + _apply_host_transition( + peer_id, + NetworkHomeProtocolType.WORLD_SPACE, + owner_fingerprint, + _world_service.get_exterior_spawn_transform(owner_fingerprint), + ) + + +func _spawn_peer_at_exterior( + peer_id: int, + owner_fingerprint: String, +) -> void: + if ( + not _session.is_host() + or _session.get_peer_space(peer_id) + != NetworkHomeProtocolType.WORLD_SPACE + or _spawn_service.get_avatar(peer_id) == null + ): + return + _apply_host_transition( + peer_id, + NetworkHomeProtocolType.WORLD_SPACE, + owner_fingerprint, + _world_service.get_exterior_spawn_transform(owner_fingerprint), + ) + + +func _apply_host_transition( + peer_id: int, + space_id: StringName, + owner_fingerprint: String, + transform: Transform3D, +) -> void: + var result: Dictionary = { + "peer_id": peer_id, + "space_id": String(space_id), + "owner_fingerprint": owner_fingerprint, + "transform": NetworkHomeProtocolType.transform_to_array(transform), + } + _apply_transition_result(result) + for recipient_id: int in _session.get_authenticated_peer_ids(): + if recipient_id == 1: + continue + receive_home_transition.rpc_id(recipient_id, result) + _session.publish_authoritative_teleport(peer_id) + + +@rpc( + "authority", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func receive_home_transition(data: Dictionary) -> void: + if ( + typeof(data.get("peer_id")) != TYPE_INT + or typeof(data.get("space_id")) not in [TYPE_STRING, TYPE_STRING_NAME] + or not NetworkIdentityCrypto.valid_fingerprint( + data.get("owner_fingerprint") + ) + ): + return + var transform: Transform3D = NetworkHomeProtocolType.array_to_transform( + data.get("transform") + ) + if transform == Transform3D(): + return + _apply_transition_result(data) + + +func _apply_transition_result(data: Dictionary) -> void: + var peer_id: int = int(data["peer_id"]) + var space_id := StringName(str(data["space_id"])) + if not _session.set_peer_space(peer_id, space_id): + return + var avatar: Player = _spawn_service.get_avatar(peer_id) + if avatar != null: + avatar.global_transform = NetworkHomeProtocolType.array_to_transform( + data["transform"] + ) + avatar.velocity = Vector3.ZERO + if peer_id == _session.get_local_peer_id(): + _world_service.set_local_space(space_id) + local_space_changed.emit( + space_id, str(data["owner_fingerprint"]) + ) + local_transition_finished.emit(true, "") + _refresh_peer_visibility() + + +func _send_transition_rejection(peer_id: int, message: String) -> void: + if peer_id == _session.get_local_peer_id(): + local_transition_finished.emit(false, message) + else: + receive_home_transition_rejection.rpc_id(peer_id, message) + + +@rpc( + "authority", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func receive_home_transition_rejection(message: String) -> void: + local_transition_finished.emit( + false, message.left(NetworkHomeProtocolType.MAX_MESSAGE_LENGTH) + ) + + +func _refresh_peer_visibility() -> void: + var local_peer_id: int = _session.get_local_peer_id() + if local_peer_id <= 0: + return + for peer_id: int in _spawn_service.get_peer_ids(): + if peer_id == local_peer_id: + continue + _spawn_service.set_peer_presentation_visible( + peer_id, _session.peers_share_space(local_peer_id, peer_id) + ) + + +func _on_local_home_changed() -> void: + if _suppress_local_submission or _publish_queued: + return + _publish_queued = true + call_deferred("_publish_queued_home") + + +func _publish_queued_home() -> void: + _publish_queued = false + if _save_manager != null: + _save_manager.save_if_dirty() + publish_local_home() + + +func _on_join_authenticated() -> void: + publish_local_home() + + +func _on_peer_authenticated(peer_id: int, _display_name: String) -> void: + if not _session.is_host(): + return + for manifest: Dictionary in _manifests_by_fingerprint.values(): + receive_home_manifest.rpc_id(peer_id, manifest) + for existing_peer_id: int in _session.get_authenticated_peer_ids(): + if existing_peer_id == peer_id: + continue + var avatar: Player = _spawn_service.get_avatar(existing_peer_id) + if avatar == null: + continue + var space_id: StringName = _session.get_peer_space(existing_peer_id) + var owner_fingerprint: String = ( + String(space_id).trim_prefix("rv:") + if String(space_id).begins_with("rv:") + else _fingerprint_for_peer(existing_peer_id) + ) + if owner_fingerprint.is_empty(): + continue + receive_home_transition.rpc_id(peer_id, { + "peer_id": existing_peer_id, + "space_id": String(space_id), + "owner_fingerprint": owner_fingerprint, + "transform": NetworkHomeProtocolType.transform_to_array( + avatar.global_transform + ), + }) + + +func _on_peer_removed(peer_id: int) -> void: + var fingerprint: String = _fingerprint_by_peer.get(peer_id, "") + _fingerprint_by_peer.erase(peer_id) + _authorized_owned_counts.erase(peer_id) + if fingerprint.is_empty(): + return + if _session.is_host(): + for occupant_id: int in _session.get_authenticated_peer_ids(): + if ( + occupant_id == peer_id + or _session.get_peer_space(occupant_id) + != NetworkHomeProtocolType.make_space_id(fingerprint) + ): + continue + _apply_host_transition( + occupant_id, + NetworkHomeProtocolType.WORLD_SPACE, + fingerprint, + _world_service.get_exterior_spawn_transform(fingerprint), + ) + _manifests_by_fingerprint.erase(fingerprint) + _world_service.remove_presentation(fingerprint) + if _session.is_host(): + for recipient_id: int in _session.get_authenticated_peer_ids(): + if recipient_id != 1: + receive_home_removed.rpc_id(recipient_id, fingerprint) + + +@rpc( + "authority", "call_remote", "reliable", + NetworkHomeProtocolType.RELIABLE_CHANNEL, +) +func receive_home_removed(owner_fingerprint: String) -> void: + if not NetworkIdentityCrypto.valid_fingerprint(owner_fingerprint): + return + _manifests_by_fingerprint.erase(owner_fingerprint) + _world_service.remove_presentation(owner_fingerprint) + + +func _on_session_state_changed(state: NetworkSession.State) -> void: + if state in [NetworkSession.State.PRIVATE_HOST, NetworkSession.State.OPEN_HOST]: + if ( + not _session.is_dedicated_host() + and _world_service != null + and _world_service.is_world_ready() + ): + publish_local_home() + return + if state in [ + NetworkSession.State.INACTIVE, + NetworkSession.State.DISCONNECTING, + NetworkSession.State.CONNECTION_FAILED, + NetworkSession.State.SERVER_LOST, + ]: + _manifests_by_fingerprint.clear() + _fingerprint_by_peer.clear() + _authorized_owned_counts.clear() + _authorization_result_owner.clear() + _next_interior_index = 0 + _world_service.clear_presentations() + _world_service.set_local_space(NetworkHomeProtocolType.WORLD_SPACE) + local_space_changed.emit(NetworkHomeProtocolType.WORLD_SPACE, "") + + +func _fingerprint_for_peer(peer_id: int) -> String: + if _fingerprint_by_peer.has(peer_id): + return _fingerprint_by_peer[peer_id] + var record := _session.get_peer_record(peer_id) + return record.identity_fingerprint if record != null else "" + + +func _new_id(prefix: String) -> String: + return "%s:%s" % [ + prefix, Crypto.new().generate_random_bytes(16).hex_encode(), + ] diff --git a/network/network_home_service.gd.uid b/network/network_home_service.gd.uid new file mode 100644 index 0000000..be233ed --- /dev/null +++ b/network/network_home_service.gd.uid @@ -0,0 +1 @@ +uid://ong8kug36ku7 diff --git a/network/network_item_use_service.gd b/network/network_item_use_service.gd index cf2d5f9..024eca9 100644 --- a/network/network_item_use_service.gd +++ b/network/network_item_use_service.gd @@ -5,6 +5,7 @@ const FishingRodDataType = preload("res://items/fishing_rod_data.gd") const ArtShopStockType = preload("res://economy/art_shop_stock.gd") const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") +const DecorCatalogType = preload("res://homes/decor_catalog.gd") const MAX_LEDGER_ENTRIES: int = 64 @@ -394,6 +395,10 @@ func _apply_equipped(data: Dictionary) -> void: item_id == FishingShopStockType.STANDARD_SHOVEL_ID and bool(data["owns_item"]), ) + avatar.set_active_decor_package( + DecorCatalogType.has_product(item_id) + and bool(data["owns_item"]), + ) equipped_state_changed.emit( peer_id, StringName(str(data["item_id"])), int(data["category"]) ) diff --git a/network/network_protocol.gd b/network/network_protocol.gd index 20be022..49aca95 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -3,7 +3,7 @@ extends RefCounted const WorldLayoutType = preload("res://world/world_layout.gd") -const PROTOCOL_VERSION: int = 11 +const PROTOCOL_VERSION: int = 12 const GAME_BUILD: String = "prealpha" const MAX_GAME_VERSION_LENGTH: int = 64 const MAX_DISPLAY_NAME_LENGTH: int = 24 @@ -17,9 +17,9 @@ const MAX_SIGNATURE_LENGTH: int = 2048 # 8 reliable ordered session chat, 9 reliable private session mail, # 10 observer-only fishing summaries, 11 reliable movement animation state, # 12 reliable fish showcase state, 13 reliable world-spawn events, -# 14 reliable artwork state, 15 world-spawn snapshots, and 16 reliable -# fishing lifecycle. Bulk world/art payloads must not head-of-line block held -# items, fishing, or animation presentation. +# 14 reliable artwork state, 15 world-spawn snapshots, 16 reliable fishing +# lifecycle, and 17 reliable portable-home state. Bulk world/art payloads must +# not head-of-line block held items, fishing, animation, or home transitions. const SALE_RELIABLE_CHANNEL: int = 5 const SHOP_RELIABLE_CHANNEL: int = 6 const ITEM_RELIABLE_CHANNEL: int = 7 @@ -30,7 +30,8 @@ const SHOWCASE_RELIABLE_CHANNEL: int = 12 const WORLD_SPAWN_RELIABLE_CHANNEL: int = 13 const DRAWING_RELIABLE_CHANNEL: int = 14 const FISHING_RELIABLE_CHANNEL: int = 16 -const ENET_CHANNEL_COUNT: int = 17 +const HOME_RELIABLE_CHANNEL: int = 17 +const ENET_CHANNEL_COUNT: int = 18 const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2" const ART_SHOP_CAPABILITY: String = "art_shop_v1" const BACKPACK_SHOP_CAPABILITY: String = "backpack_shop_v1" @@ -45,6 +46,7 @@ const WORLD_LAYOUT_CAPABILITY: String = "world_layout_v1" const FRIENDS_CAPABILITY: String = "friends_v1" const MOVEMENT_RECONCILIATION_CAPABILITY: String = "movement_reconciliation_v2" const FISHING_REPLICATION_CAPABILITY: String = "fishing_replication_v2" +const HOME_CAPABILITY: String = "portable_home_v1" const DEFAULT_WORLD_SEED: int = 13001 const MAX_WORLD_SEED: int = 2147483646 @@ -202,6 +204,7 @@ static func make_client_hello( FRIENDS_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, + HOME_CAPABILITY, ]), "cosmetic_snapshot": cosmetic_snapshot, "identity_fingerprint": identity_fingerprint, @@ -342,6 +345,7 @@ static func make_server_hello( FRIENDS_CAPABILITY, MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, + HOME_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", diff --git a/network/network_session.gd b/network/network_session.gd index 8b99fd6..8add90f 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -141,6 +141,7 @@ var _last_server_protocol_version: int = 0 var _last_server_world_seed: int = NetworkProtocol.DEFAULT_WORLD_SEED var _last_server_world_layout: StringName = WorldLayout.GENERATED var _server_capabilities: PackedStringArray = PackedStringArray() +var _peer_space_ids: Dictionary[int, StringName] = {} var _profile_ready: bool = false var _player_identity: PlayerIdentityStore var _host_identity: HostIdentityStore @@ -334,6 +335,7 @@ func _register_player_host() -> void: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, ]), ) _registry.update_appearance(1, _local_appearance_snapshot) @@ -661,6 +663,7 @@ func supports_server_capability(capability: StringName) -> bool: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", @@ -685,6 +688,21 @@ func get_authenticated_peer_ids() -> Array[int]: return _registry.get_peer_ids() +func set_peer_space(peer_id: int, space_id: StringName) -> bool: + if not _registry.has_peer(peer_id) or space_id.is_empty(): + return false + _peer_space_ids[peer_id] = space_id + return true + + +func get_peer_space(peer_id: int) -> StringName: + return _peer_space_ids.get(peer_id, &"world") + + +func peers_share_space(first_peer_id: int, second_peer_id: int) -> bool: + return get_peer_space(first_peer_id) == get_peer_space(second_peer_id) + + func get_peer_rtt_ms(peer_id: int) -> int: if peer_id == 1: return 0 @@ -1030,6 +1048,7 @@ func _on_peer_disconnected(peer_id: int) -> void: _pending_identity_challenges.erase(peer_id) _authenticated_identity_cache.erase(peer_id) _operator_peer_ids.erase(peer_id) + _peer_space_ids.erase(peer_id) _last_animation_state_by_peer.erase(peer_id) _pending_animation_state_by_peer.erase(peer_id) var recovery_attempt: String = _recovery_attempts.get(peer_id, "") @@ -1313,6 +1332,7 @@ func submit_client_hello(data: Dictionary) -> void: for required_capability: String in [ NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, ]: if required_capability not in client_capabilities: _reject_peer( @@ -1546,6 +1566,7 @@ func receive_server_hello(data: Dictionary) -> void: for required_capability: String in [ NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, ]: if required_capability not in _server_capabilities: _teardown_peer() @@ -1578,6 +1599,7 @@ func receive_server_hello(data: Dictionary) -> void: NetworkProtocol.FRIENDS_CAPABILITY, NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, + NetworkProtocol.HOME_CAPABILITY, ]), ) _registry.update_appearance(local_peer_id, _local_appearance_snapshot) @@ -1623,6 +1645,7 @@ func receive_peer_despawn(peer_id: int) -> void: if state != State.JOINED_CLIENT: return _operator_peer_ids.erase(peer_id) + _peer_space_ids.erase(peer_id) _last_animation_state_by_peer.erase(peer_id) _pending_animation_state_by_peer.erase(peer_id) _registry.remove_peer(peer_id) @@ -2247,6 +2270,8 @@ func _broadcast_movement_snapshots() -> void: continue var snapshots: Array = [] for subject_id: int in peer_ids: + if not peers_share_space(recipient_id, subject_id): + continue var subject_avatar: Player = _spawn_service.get_avatar(subject_id) if subject_avatar == null: continue @@ -2315,7 +2340,7 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void: _animation_refresh_accumulator, ANIMATION_REFRESH_INTERVAL, ) - var updates: Array = [] + var updates_by_subject: Dictionary[int, Array] = {} for subject_id: int in peer_ids: var avatar: Player = _spawn_service.get_avatar(subject_id) if avatar == null: @@ -2333,12 +2358,18 @@ func _broadcast_movement_animation_updates(peer_ids: Array[int]) -> void: _last_animation_state_by_peer[subject_id] = state.duplicate(true) var encoded: Array = _encode_movement_animation(subject_id, state) if not encoded.is_empty(): - updates.append(encoded) - if updates.is_empty(): + updates_by_subject[subject_id] = encoded + if updates_by_subject.is_empty(): return for recipient_id: int in peer_ids: if recipient_id == 1: continue + var updates: Array = [] + for subject_id: int in updates_by_subject: + if peers_share_space(recipient_id, subject_id): + updates.append(updates_by_subject[subject_id]) + if updates.is_empty(): + continue receive_movement_animations.rpc_id(recipient_id, updates) _movement_animation_packets_sent += 1 _movement_animation_states_sent += updates.size() @@ -2888,6 +2919,7 @@ func _teardown_peer() -> void: _pending_animation_state_by_peer.clear() _last_local_animation_action_signature.clear() _server_capabilities = PackedStringArray() + _peer_space_ids.clear() _server_identity_fingerprint = "" _server_identity_public_key = "" _session_identity_keys.clear() diff --git a/network/network_shop_protocol.gd b/network/network_shop_protocol.gd index 2de9cc1..da9419a 100644 --- a/network/network_shop_protocol.gd +++ b/network/network_shop_protocol.gd @@ -4,6 +4,7 @@ extends RefCounted const CAPABILITY: StringName = &"shop_v1" const ART_CAPABILITY: StringName = &"art_shop_v1" const BACKPACK_CAPABILITY: StringName = &"backpack_shop_v1" +const DECOR_CAPABILITY: StringName = &"portable_home_v1" const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL const MAX_ID_LENGTH: int = 96 const MAX_MESSAGE_LENGTH: int = 160 @@ -18,6 +19,7 @@ enum ProductCategory { ART_KIT, ART_UPGRADE, BACKPACK_CAPACITY_UPGRADE, + DECOR, } diff --git a/network/network_shop_service.gd b/network/network_shop_service.gd index 6f31c40..8294b53 100644 --- a/network/network_shop_service.gd +++ b/network/network_shop_service.gd @@ -8,8 +8,10 @@ const ItemDataType = preload("res://items/item_data.gd") const FishingRodDataType = preload("res://items/fishing_rod_data.gd") const OwnedItemType = preload("res://items/owned_item.gd") const ArtShopStockType = preload("res://economy/art_shop_stock.gd") +const DecorCatalogType = preload("res://homes/decor_catalog.gd") -const SHOP_ID: StringName = &"main_fishing_shop" +const FISHING_SHOP_ID: StringName = &"main_fishing_shop" +const DECOR_SHOP_ID: StringName = &"rv_decor_shop" const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade" const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade" const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade" @@ -31,6 +33,7 @@ var _session: NetworkSession var _spawn_service: PlayerSpawnService var _network_fishing: NetworkFishingService var _interaction: FishingShopInteraction +var _decor_interaction: DecorShopInteraction var _wallet: PlayerWallet var _bag: PlayerBag var _item_catalog: ItemCatalog @@ -47,6 +50,8 @@ var _received_results: Dictionary[String, bool] = {} var _applied_results: Dictionary[String, bool] = {} var _pending_local_request: Dictionary = {} var _reservations: PlayerAssetReservationService +var _home_state: PlayerHomeState +var _network_home: NetworkHomeService func setup( @@ -63,6 +68,9 @@ func setup( save_manager: PlayerSaveManager, reservations: PlayerAssetReservationService, inventory_layout: PlayerInventoryLayout = null, + home_state: PlayerHomeState = null, + network_home: NetworkHomeService = null, + decor_interaction: DecorShopInteraction = null, ) -> void: _session = session _spawn_service = spawn_service @@ -77,6 +85,9 @@ func setup( _save_manager = save_manager _reservations = reservations _inventory_layout = inventory_layout + _home_state = home_state + _network_home = network_home + _decor_interaction = decor_interaction if not _session.peer_removed.is_connected(_on_peer_removed): _session.peer_removed.connect(_on_peer_removed) if not _session.state_changed.is_connected(_on_session_state_changed): @@ -87,6 +98,12 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void: _interaction = interaction +func set_decor_shop_interaction( + interaction: DecorShopInteraction, +) -> void: + _decor_interaction = interaction + + func can_request_purchase() -> bool: return ( _session != null @@ -124,6 +141,19 @@ func can_request_backpack_purchase() -> bool: ) +func can_request_decor_purchase() -> bool: + return ( + can_request_purchase() + and _home_state != null + and ( + _session.is_host() + or _session.supports_server_capability( + NetworkShopProtocol.DECOR_CAPABILITY + ) + ) + ) + + func is_local_purchase_pending() -> bool: return not _pending_local_request.is_empty() @@ -193,6 +223,15 @@ func request_backpack_capacity_upgrade() -> String: ) +func request_decor(product_id: StringName) -> String: + return _request_purchase( + product_id, + NetworkShopProtocol.ProductCategory.DECOR, + 1, + _home_state.get_owned_count(product_id) if _home_state != null else 0, + ) + + func request_art_kit() -> String: return _request_purchase( ArtShopStockType.ART_KIT_ITEM_ID, @@ -269,11 +308,25 @@ func _request_purchase( product_id, category, 0, 0, ) return "" + if ( + category == NetworkShopProtocol.ProductCategory.DECOR + and not can_request_decor_purchase() + ): + local_purchase_finished.emit( + "", false, "Decor require a newer server.", + product_id, category, 0, 0, + ) + return "" var request_id: String = _new_id("shop") + var shop_id: StringName = ( + DECOR_SHOP_ID + if category == NetworkShopProtocol.ProductCategory.DECOR + else FISHING_SHOP_ID + ) var request: Dictionary = { "request_id": request_id, "session_id": _session.get_session_id(), - "shop_id": str(SHOP_ID), + "shop_id": str(shop_id), "product_id": str(product_id), "category": category, "quantity": quantity, @@ -345,16 +398,34 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void: _rejected_result(data, "A purchase is already pending.") ) return - if not _is_shop_available_for_peer(peer_id): + var requested_shop_id := StringName(str(data["shop_id"])) + if not _is_shop_available_for_peer(peer_id, requested_shop_id): var avatar: Player = _spawn_service.get_avatar(peer_id) + var requested_interaction: FishingShopInteraction = ( + _decor_interaction + if requested_shop_id == DECOR_SHOP_ID + else _interaction + ) var message: String = ( "The shop is unavailable." - if avatar == null or _interaction == null + if avatar == null or requested_interaction == null else "Move closer to the shop." ) _record_and_send(peer_id, _rejected_result(data, message)) return var result: Dictionary = _build_authoritative_result(peer_id, data) + if ( + bool(result.get("accepted", false)) + and int(result.get("category", -1)) + == NetworkShopProtocol.ProductCategory.DECOR + and _network_home != null + ): + _network_home.authorize_decor_purchase( + peer_id, + StringName(str(result["product_id"])), + int(result["resulting_state"]), + str(result["result_id"]), + ) _pending_by_peer[peer_id] = request_id _record_and_send(peer_id, result) @@ -368,10 +439,24 @@ func _build_authoritative_result( var quantity: int = request["quantity"] var wallet_balance: int = request["wallet_balance"] var current_state: int = request["current_state"] + if ( + category == NetworkShopProtocol.ProductCategory.DECOR + and _network_home != null + ): + var authoritative_count: int = ( + _network_home.get_authoritative_owned_count(peer_id, product_id) + ) + if authoritative_count >= 0: + current_state = authoritative_count var cost: int = -1 var resulting_state: int = current_state var rejection: String = "" - if StringName(str(request["shop_id"])) != SHOP_ID: + var expected_shop_id: StringName = ( + DECOR_SHOP_ID + if category == NetworkShopProtocol.ProductCategory.DECOR + else FISHING_SHOP_ID + ) + if StringName(str(request["shop_id"])) != expected_shop_id: rejection = "The shop is unavailable." elif quantity < 1: rejection = "Purchase could not be completed." @@ -545,6 +630,19 @@ func _build_authoritative_result( resulting_state = PlayerArtUnlocks.resulting_mask( current_state, product_id ) + NetworkShopProtocol.ProductCategory.DECOR: + cost = DecorCatalogType.get_price(product_id) + if ( + not DecorCatalogType.has_product(product_id) + or quantity != 1 + or current_state < 0 + or current_state + >= DecorCatalogType.MAX_OWNED_PER_PRODUCT + or cost < 0 + ): + rejection = "Decor is unavailable." + else: + resulting_state = current_state + 1 if rejection.is_empty() and (cost < 0 or wallet_balance < cost): rejection = "Insufficient funds." if not rejection.is_empty(): @@ -667,6 +765,9 @@ func _apply_purchase_result(data: Dictionary) -> void: if _inventory_layout != null else 0 ) var art_snapshot: int = _art_unlocks.get_unlock_mask() + var home_snapshot: Dictionary = ( + _home_state.to_save_data() if _home_state != null else {} + ) var applied: bool = _apply_local_product(data) if not applied or not _save_manager.save_if_dirty(): _bag.replace_all_items(bag_snapshot) @@ -676,6 +777,8 @@ func _apply_purchase_result(data: Dictionary) -> void: if _inventory_layout != null: _inventory_layout.restore_backpack_level(backpack_snapshot) _art_unlocks.restore_mask(art_snapshot) + if _home_state != null and not home_snapshot.is_empty(): + _home_state.restore_from_save_data(home_snapshot) _wallet.restore_balance(wallet_snapshot) _save_manager.save_if_dirty() _fail_local_apply(data, "Purchase could not be completed.") @@ -696,6 +799,7 @@ func _validate_local_result(data: Dictionary) -> String: or _cooler_capacity == null or _art_unlocks == null or _save_manager == null + or _home_state == null or _wallet.get_balance() != int(data["expected_wallet"]) or str(data["product_id"]) != str(_pending_local_request.get("product_id", "")) @@ -806,6 +910,20 @@ func _validate_local_result(data: Dictionary) -> String: or cost != ArtShopStockType.UPGRADE_PRICE ): return "Purchase could not be completed." + NetworkShopProtocol.ProductCategory.DECOR: + if ( + not DecorCatalogType.has_product(product_id) + or _home_state.get_owned_count(product_id) != expected_state + or resulting_state != expected_state + 1 + or cost != DecorCatalogType.get_price(product_id) + or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT + or not _bag.can_add_item(product_id, 1) + ): + return ( + "Your inventory is full." + if not _bag.can_add_item(product_id, 1) + else "Purchase could not be completed." + ) _: return "Purchase could not be completed." return "" @@ -845,6 +963,12 @@ func _apply_local_product(data: Dictionary) -> bool: return _art_unlocks.purchase_product( product_id, _wallet, int(data["total_cost"]) ) + NetworkShopProtocol.ProductCategory.DECOR: + return ( + _wallet.debit(int(data["total_cost"])) + and _bag.add_item(product_id, 1) + and _home_state.add_owned_decor(product_id, 1) + ) return false @@ -921,7 +1045,7 @@ func _handle_acknowledgement( peer_id: int, result_id: String, request_id: String, - _applied: bool, + applied: bool, message: String, ) -> void: if ( @@ -933,20 +1057,32 @@ func _handle_acknowledgement( or _result_owners.get(result_id, 0) != peer_id ): return + if _network_home != null: + _network_home.finalize_decor_purchase( + peer_id, result_id, applied + ) _acknowledged_results[result_id] = true _bound_dictionary(_acknowledged_results) if _pending_by_peer.get(peer_id, "") == request_id: _pending_by_peer.erase(peer_id) -func _is_shop_available_for_peer(peer_id: int) -> bool: +func _is_shop_available_for_peer( + peer_id: int, + shop_id: StringName, +) -> bool: + var interaction: FishingShopInteraction = ( + _decor_interaction + if shop_id == DECOR_SHOP_ID + else _interaction + ) if ( _session == null or not _session.is_host() or not _session.is_gameplay_session_active() or _spawn_service == null - or _interaction == null - or not is_instance_valid(_interaction) + or interaction == null + or not is_instance_valid(interaction) ): return false var avatar: Player = _spawn_service.get_avatar(peer_id) @@ -957,7 +1093,7 @@ func _is_shop_available_for_peer(peer_id: int) -> bool: _network_fishing == null or not _network_fishing.has_peer_attempt(peer_id) ) - and _interaction.is_avatar_in_range(avatar) + and interaction.is_avatar_in_range(avatar) ) diff --git a/player/player.gd b/player/player.gd index 64d4ffe..30807be 100644 --- a/player/player.gd +++ b/player/player.gd @@ -427,6 +427,9 @@ class ShowcaseCameraSnapshot: @export_range(0.0, 1.0, 0.01) var controller_camera_deadzone: float = 0.2 @export var invert_camera_y: bool = false var _swap_hotbar_camera_scroll: bool = false +var _rv_interior_camera_mode: bool = false +var _rv_interior_environment: Environment +var _rv_previous_camera_environment: Environment @export_range(-89.0, 0.0, 0.5) var minimum_pitch_degrees: float = -65.0 @export_range(0.0, 89.0, 0.5) var maximum_pitch_degrees: float = 45.0 @export var minimum_zoom: float = 2.0 @@ -471,6 +474,8 @@ var _held_fish_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _held_fish_sprite: Sprite3D var _held_art_kit_display: Node3D var _held_art_kit_sprite: Sprite3D +var _held_decor_package_display: Node3D +var _held_decor_package_visible: bool = false var _catch_attachment_offset: Vector3 = Vector3(0.0, 0.08, 0.04) var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity")) @@ -992,6 +997,7 @@ func _initialize_held_item_attachment() -> void: _held_art_kit_sprite = attachment.get_node( "HeldArtKitDisplay/HeldArtKitSprite" ) as Sprite3D + _held_decor_package_display = _build_decor_package_display(attachment) _catch_display = attachment.get_node("CatchDisplay") as Node3D _catch_attachment_offset = _catch_display.position _catch_sprite = attachment.get_node("CatchDisplay/CatchSprite") as Sprite3D @@ -1322,9 +1328,7 @@ func _update_character_animation() -> void: and supports_network_animation_action(animation_action_id) and _character_animation_player.has_animation(animation_action_id) ) - var held_show_item_visible: bool = ( - _held_fish_visible or _held_art_kit_visible - ) + var held_show_item_visible: bool = _has_held_show_item() var requested_animation: Array[StringName] = [] if _pocket_visual_active: requested_animation = [_pocket_visual_animation] @@ -1766,7 +1770,11 @@ func _unhandled_input(event: InputEvent) -> void: var mouse_zoom_in: bool = ( event is InputEventMouseButton - and event.shift_pressed != _swap_hotbar_camera_scroll + and ( + (not event.shift_pressed) + if _rv_interior_camera_mode + else event.shift_pressed != _swap_hotbar_camera_scroll + ) and event.is_action_pressed("camera_zoom_in") ) var controller_zoom_in: bool = ( @@ -1775,7 +1783,11 @@ func _unhandled_input(event: InputEvent) -> void: ) var mouse_zoom_out: bool = ( event is InputEventMouseButton - and event.shift_pressed != _swap_hotbar_camera_scroll + and ( + (not event.shift_pressed) + if _rv_interior_camera_mode + else event.shift_pressed != _swap_hotbar_camera_scroll + ) and event.is_action_pressed("camera_zoom_out") ) var controller_zoom_out: bool = ( @@ -1963,7 +1975,11 @@ func get_active_gameplay_camera() -> Camera3D: func _set_target_zoom(value: float) -> void: - _target_zoom = clampf(value, minimum_zoom, maximum_zoom) + _target_zoom = clampf( + value, + 1.25 if _rv_interior_camera_mode else minimum_zoom, + 16.0 if _rv_interior_camera_mode else maximum_zoom, + ) func _apply_axis_deadzone(value: float) -> float: @@ -3021,6 +3037,49 @@ func apply_camera_settings( _swap_hotbar_camera_scroll = swap_hotbar_camera_scroll +func set_rv_interior_camera_mode( + enabled: bool, + excluded_wall_rids: Array[RID] = [], +) -> void: + if enabled != _rv_interior_camera_mode: + if enabled: + _rv_previous_camera_environment = _camera.environment + _camera.environment = _get_rv_interior_environment() + if _free_camera != null: + _free_camera.environment = _camera.environment + else: + _camera.environment = _rv_previous_camera_environment + if _free_camera != null: + _free_camera.environment = _camera.environment + _rv_previous_camera_environment = null + _rv_interior_camera_mode = enabled + if _spring_arm == null: + return + _spring_arm.clear_excluded_objects() + if enabled: + for collider_rid: RID in excluded_wall_rids: + if collider_rid.is_valid(): + _spring_arm.add_excluded_object(collider_rid) + _set_target_zoom(_target_zoom) + + +func _get_rv_interior_environment() -> Environment: + if _rv_interior_environment != null: + return _rv_interior_environment + _rv_interior_environment = Environment.new() + _rv_interior_environment.background_mode = Environment.BG_COLOR + _rv_interior_environment.background_color = Color("020506") + _rv_interior_environment.ambient_light_source = ( + Environment.AMBIENT_SOURCE_COLOR + ) + _rv_interior_environment.ambient_light_color = Color("d6caaa") + _rv_interior_environment.ambient_light_energy = 1.0 + _rv_interior_environment.reflected_light_source = ( + Environment.REFLECTION_SOURCE_DISABLED + ) + return _rv_interior_environment + + func is_camera_input_enabled() -> bool: return _camera_input_enabled @@ -3147,6 +3206,23 @@ func set_active_shovel(should_show: bool) -> void: ) +func set_active_decor_package(should_show: bool) -> void: + var changed: bool = _held_decor_package_visible != should_show + _held_decor_package_visible = should_show + if _held_decor_package_display != null: + _held_decor_package_display.visible = should_show + if should_show: + _fishing_rod.visible = false + if _catching_net != null: + _catching_net.visible = false + if _shovel != null: + _shovel.visible = false + elif changed: + _apply_active_rod_visibility() + if changed: + _update_character_animation() + + func _apply_fishing_rod_model(rod: FishingRodDataType) -> void: var next_id: StringName = rod.item_id if rod != null else StringName() if next_id == _active_fishing_rod_id: @@ -3319,7 +3395,52 @@ func set_held_fish( func _has_held_show_item() -> bool: - return _held_fish_visible or _held_art_kit_visible + return ( + _held_fish_visible + or _held_art_kit_visible + or _held_decor_package_visible + ) + + +func _build_decor_package_display(parent: Node3D) -> Node3D: + var display := Node3D.new() + display.name = "HeldDecorPackageDisplay" + display.position = Vector3(0.0, 0.1, 0.04) + display.rotation = Vector3(0.15, 0.0, -0.55) + display.visible = false + parent.add_child(display) + + var sack := MeshInstance3D.new() + sack.name = "Sack" + var sack_mesh := SphereMesh.new() + sack_mesh.radius = 0.22 + sack_mesh.height = 0.48 + sack_mesh.radial_segments = 8 + sack_mesh.rings = 4 + sack_mesh.material = _decor_package_material(Color("a87843")) + sack.mesh = sack_mesh + display.add_child(sack) + + var tie := MeshInstance3D.new() + tie.name = "Tie" + var tie_mesh := CylinderMesh.new() + tie_mesh.top_radius = 0.1 + tie_mesh.bottom_radius = 0.12 + tie_mesh.height = 0.08 + tie_mesh.radial_segments = 8 + tie_mesh.material = _decor_package_material(Color("513829")) + tie.mesh = tie_mesh + tie.position = Vector3(0.0, 0.22, 0.0) + display.add_child(tie) + return display + + +func _decor_package_material(color: Color) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.roughness = 1.0 + material.shading_mode = BaseMaterial3D.SHADING_MODE_PER_VERTEX + return material func _clear_held_fish() -> void: diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 1ad74a1..56fd49c 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -34,8 +34,9 @@ const WorldWeatherServiceType = preload( const WorldLayoutType = preload("res://world/world_layout.gd") const PlayerJobServiceType = preload("res://jobs/player_job_service.gd") const PlayerType = preload("res://player/player.gd") +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") -const SAVE_VERSION: int = 10 +const SAVE_VERSION: int = 11 const LEGACY_SAVE_FILENAME := "player_save.json" const ARCHIVE_EXTENSION := ".nfsave" const BASIC_ROD_ID: StringName = &"basic_fishing_rod" @@ -76,6 +77,7 @@ class LoadSnapshot: WorldWeatherServiceType.SUNNY_DURATION_RANGE.x ) var jobs_data: Dictionary = PlayerJobServiceType.default_save_data() + var home_data: Dictionary = PlayerHomeStateType.default_save_data() @export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5 @@ -96,6 +98,7 @@ var _world_time: WorldTimeServiceType var _world_weather: WorldWeatherServiceType var _jobs: PlayerJobServiceType var _player: PlayerType +var _home_state: PlayerHomeStateType var _autosave_timer: Timer var _is_configured: bool = false var _is_restoring: bool = false @@ -175,6 +178,7 @@ func setup( world_weather: WorldWeatherServiceType, jobs: PlayerJobServiceType, player: PlayerType, + home_state: PlayerHomeStateType, ) -> void: _inventory = inventory _collection_log = collection_log @@ -192,6 +196,7 @@ func setup( _world_weather = world_weather _jobs = jobs _player = player + _home_state = home_state _is_configured = ( _inventory != null and _collection_log != null @@ -209,6 +214,7 @@ func setup( and _world_weather != null and _jobs != null and _player != null + and _home_state != null ) if not _is_configured: push_error("PlayerSaveManager setup is missing required references.") @@ -255,6 +261,8 @@ func setup( _player.active_bait_changed.connect(_on_active_tackle_changed) if not _player.active_lure_changed.is_connected(_on_active_tackle_changed): _player.active_lure_changed.connect(_on_active_tackle_changed) + if not _home_state.changed.is_connected(_mark_dirty): + _home_state.changed.connect(_mark_dirty) func load_player_data() -> bool: @@ -360,6 +368,9 @@ func load_player_data() -> bool: snapshot.world_weather_seconds_remaining, ) var jobs_restored: bool = _jobs.restore_from_save_data(snapshot.jobs_data) + var home_restored: bool = _home_state.restore_from_save_data( + snapshot.home_data + ) _is_restoring = false if ( not inventory_restored @@ -376,13 +387,14 @@ func load_player_data() -> bool: or not world_time_restored or not world_weather_restored or not jobs_restored + or not home_restored ): push_error( ( "Validated player save could not be restored: " + "inventory=%s collection=%s wallet=%s bag=%s tackle=%s hotbar=%s " + "upgrades=%s cooler=%s layout=%s art=%s experience=%s " - + "time=%s weather=%s jobs=%s" + + "time=%s weather=%s jobs=%s home=%s" ) % [ inventory_restored, @@ -399,6 +411,7 @@ func load_player_data() -> bool: world_time_restored, world_weather_restored, jobs_restored, + home_restored, ] ) return false @@ -899,6 +912,7 @@ func _build_save_dictionary() -> Dictionary: ), }, "jobs": _jobs.to_save_data(), + "home": _home_state.to_save_data(), } @@ -913,6 +927,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: or typeof(save_data.get("inventory_layout")) != TYPE_DICTIONARY or typeof(save_data.get("experience")) != TYPE_DICTIONARY or typeof(save_data.get("jobs")) != TYPE_DICTIONARY + or typeof(save_data.get("home")) != TYPE_DICTIONARY ): return null var wallet_data: Dictionary = save_data["wallet"] @@ -924,6 +939,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: var inventory_layout_data: Dictionary = save_data["inventory_layout"] var experience_data: Dictionary = save_data["experience"] var jobs_data: Dictionary = save_data["jobs"] + var home_data: Dictionary = save_data["home"] var world_data: Dictionary = {} if typeof(save_data.get("world")) == TYPE_DICTIONARY: world_data = save_data["world"] @@ -1006,6 +1022,12 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: if not PlayerJobServiceType.validate_save_data(jobs_data): return null snapshot.jobs_data = jobs_data.duplicate(true) + var sanitized_home: Dictionary = PlayerHomeStateType.sanitize_save_data( + home_data + ) + if sanitized_home.is_empty(): + return null + snapshot.home_data = sanitized_home if world_data.has("time_hours"): snapshot.world_time_hours = _read_world_time_hours( world_data["time_hours"] @@ -1340,6 +1362,8 @@ func _migrate_save( migrated = _migrate_version_8_to_9(migrated) 9: migrated = _migrate_version_9_to_10(migrated) + 10: + migrated = _migrate_version_10_to_11(migrated) _: return {} if migrated.is_empty(): @@ -1597,6 +1621,13 @@ func _migrate_version_9_to_10(data: Dictionary) -> Dictionary: return migrated +func _migrate_version_10_to_11(data: Dictionary) -> Dictionary: + var migrated: Dictionary = data.duplicate(true) + migrated["home"] = PlayerHomeStateType.default_save_data() + migrated["save_version"] = 11 + return migrated + + func _mark_dirty() -> void: if ( _is_restoring @@ -1866,6 +1897,7 @@ func _restore_defaults() -> void: _world_layout = WorldLayoutType.GENERATED _world_seed = DEFAULT_WORLD_SEED _jobs.reset_to_defaults() + _home_state.reset_to_defaults() _is_restoring = false _is_dirty = false diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 4bfab98..146fb4e 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -19,9 +19,15 @@ func _run() -> void: for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(bool(main.call( + "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1 + ))) assert(bool(main.call("_prepare_private_host"))) var save_manager := main.get("_save_manager") as PlayerSaveManager - assert(save_manager.initialize_new_game()) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) main.call("_enter_gameplay") await physics_frame await physics_frame @@ -94,11 +100,7 @@ func _run() -> void: assert(keyboard_mapping.reset_mapping()) await process_frame var shop_prompt_key := game_ui.get_node("%ShopPromptKey") as Label - var storage_prompt_message := game_ui.get_node( - "%StoragePromptMessage" - ) as Label assert(shop_prompt_key.text == "E") - assert(storage_prompt_message.text == "E open storage") var remapped_interact := InputEventKey.new() remapped_interact.physical_keycode = KEY_F remapped_interact.pressed = true @@ -108,7 +110,6 @@ func _run() -> void: )) await process_frame assert(shop_prompt_key.text == "F") - assert(storage_prompt_message.text == "F open storage") assert(keyboard_mapping.reset_mapping()) await process_frame assert(toolbar.get_parent() == chat_ui.get_parent()) diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index 753f480..9f4623a 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -104,7 +104,10 @@ func _run() -> void: var hotbar_data: Dictionary = parsed["hotbar"] assert(typeof(hotbar_data.get("fish_slots")) == TYPE_ARRAY) assert(str((hotbar_data["fish_slots"] as Array)[1]) == fish_catch.catch_id) - assert(int(parsed["save_version"]) == 10) + assert(int(parsed["save_version"]) == 11) + assert(not PlayerHomeState.sanitize_save_data( + parsed.get("home", {}) + ).is_empty()) assert( int(parsed["experience"]["total_experience"]) == 125 diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index b6e6bfd..d8e482b 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -485,7 +485,7 @@ func _validate_version_four_migration() -> void: version_four, 4, ) - assert(int(migrated.get("save_version", -1)) == 10) + assert(int(migrated.get("save_version", -1)) == 11) assert(int((migrated["experience"] as Dictionary)["total_experience"]) == 0) assert( is_equal_approx( @@ -509,4 +509,7 @@ func _validate_version_four_migration() -> void: assert( PlayerJobService.validate_save_data(migrated.get("jobs", {})) ) + assert(not PlayerHomeState.sanitize_save_data( + migrated.get("home", {}) + ).is_empty()) manager.queue_free() diff --git a/tests/home_spawn_multiplayer_validation.gd b/tests/home_spawn_multiplayer_validation.gd new file mode 100644 index 0000000..1f16147 --- /dev/null +++ b/tests/home_spawn_multiplayer_validation.gd @@ -0,0 +1,178 @@ +extends SceneTree + +const MainScene: PackedScene = preload("res://main/main.tscn") +const TEST_PORT: int = 18222 + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var arguments: PackedStringArray = OS.get_cmdline_user_args() + if arguments.has("host"): + await _run_host() + return + if arguments.has("client"): + await _run_client() + return + push_error("Home spawn validation needs host or client mode.") + quit(1) + + +func _run_host() -> void: + var main: Node = await _create_initialized_main() + var session := main.get_node("%NetworkSession") as NetworkSession + var network_home := main.get_node( + "%NetworkHomeService" + ) as NetworkHomeService + var home_world := main.get_node("%HomeWorldService") as HomeWorldService + var spawn_service := main.get_node( + "%PlayerSpawnService" + ) as PlayerSpawnService + var save_manager := main.get("_save_manager") as PlayerSaveManager + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(bool(main.call( + "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1 + ))) + assert(save_manager.initialize_new_game(1, WorldLayout.STARTER_ISLAND)) + assert(session.start_private_host(TEST_PORT)) + main.call("_enter_gameplay") + assert(session.set_host_open(true)) + + var host_fingerprint: String = session.get_local_identity_fingerprint() + assert(await _wait_for_home(network_home, host_fingerprint)) + var host_avatar: Player = spawn_service.get_avatar(1) + var host_spawn: Transform3D = home_world.get_exterior_spawn_transform( + host_fingerprint + ) + assert(_same_door_position(host_avatar.global_position, host_spawn.origin)) + assert(network_home.request_enter(host_fingerprint)) + await physics_frame + host_avatar.global_position = home_world.get_interior_exit_position( + host_fingerprint + ) + assert(network_home.request_exit()) + await physics_frame + assert(_same_door_position(host_avatar.global_position, host_spawn.origin)) + + var remote_peer_id: int = await _wait_for_remote_peer(session) + assert(remote_peer_id > 1) + var remote_record := session.get_peer_record(remote_peer_id) + var remote_fingerprint: String = remote_record.identity_fingerprint + assert(await _wait_for_home(network_home, remote_fingerprint)) + var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id) + var remote_spawn: Transform3D = home_world.get_exterior_spawn_transform( + remote_fingerprint + ) + assert(_same_door_position( + remote_avatar.global_position, remote_spawn.origin + )) + + var disconnect_deadline: int = Time.get_ticks_msec() + 12000 + while ( + Time.get_ticks_msec() < disconnect_deadline + and session.is_authenticated_peer(remote_peer_id) + ): + await process_frame + assert(not session.is_authenticated_peer(remote_peer_id)) + print("Home spawn multiplayer host validation: PASS") + await _cleanup(main, session) + + +func _run_client() -> void: + var main: Node = await _create_initialized_main() + main.call( + "_on_title_join_game_requested", + "127.0.0.1:%d" % TEST_PORT, + ) + var session := main.get_node("%NetworkSession") as NetworkSession + var network_home := main.get_node( + "%NetworkHomeService" + ) as NetworkHomeService + var home_world := main.get_node("%HomeWorldService") as HomeWorldService + var deadline: int = Time.get_ticks_msec() + 20000 + while Time.get_ticks_msec() < deadline: + await process_frame + if session.state == NetworkSession.State.VERIFYING_SERVER_IDENTITY: + main.call("_confirm_server_trust") + if session.is_joined_client() and bool(main.get("_gameplay_started")): + break + assert(session.is_joined_client()) + var fingerprint: String = session.get_local_identity_fingerprint() + assert(await _wait_for_home(network_home, fingerprint)) + var player := main.get("_player") as Player + var exterior_spawn: Transform3D = ( + home_world.get_exterior_spawn_transform(fingerprint) + ) + deadline = Time.get_ticks_msec() + 5000 + while ( + Time.get_ticks_msec() < deadline + and not _same_door_position( + player.global_position, exterior_spawn.origin + ) + ): + await physics_frame + assert(_same_door_position( + player.global_position, exterior_spawn.origin + )) + print("Home spawn multiplayer client validation: PASS") + await _cleanup(main, session) + + +func _create_initialized_main() -> Node: + root.size = Vector2i(1280, 720) + var main: Node = MainScene.instantiate() + root.add_child(main) + for _frame: int in 4: + await process_frame + if not bool(main.get("_application_initialized")): + main.call("_activate_selected_data_path", "", true) + for _frame: int in 8: + await process_frame + assert(bool(main.get("_application_initialized"))) + return main + + +func _wait_for_home( + network_home: NetworkHomeService, + fingerprint: String, +) -> bool: + var deadline: int = Time.get_ticks_msec() + 10000 + while Time.get_ticks_msec() < deadline: + await process_frame + var manifests: Dictionary = network_home.get( + "_manifests_by_fingerprint" + ) + if manifests.has(fingerprint): + return true + return false + + +func _wait_for_remote_peer(session: NetworkSession) -> int: + var deadline: int = Time.get_ticks_msec() + 20000 + while Time.get_ticks_msec() < deadline: + await process_frame + for peer_id: int in session.get_authenticated_peer_ids(): + if peer_id != session.get_local_peer_id(): + return peer_id + return 0 + + +func _same_door_position(first: Vector3, second: Vector3) -> bool: + return ( + Vector2(first.x, first.z).distance_to(Vector2(second.x, second.z)) + < 0.08 + and absf(first.y - second.y) < 0.3 + ) + + +func _cleanup(main: Node, session: NetworkSession) -> void: + session.disconnect_session("") + main.queue_free() + for _frame: int in 4: + await process_frame + await create_timer(0.1).timeout + quit() diff --git a/tests/home_spawn_multiplayer_validation.gd.uid b/tests/home_spawn_multiplayer_validation.gd.uid new file mode 100644 index 0000000..06150f2 --- /dev/null +++ b/tests/home_spawn_multiplayer_validation.gd.uid @@ -0,0 +1 @@ +uid://bndtkip5p6xma diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd index 1fe5ceb..89e46f6 100644 --- a/tests/job_system_validation.gd +++ b/tests/job_system_validation.gd @@ -205,8 +205,11 @@ func _run() -> void: ) assert(bool(decoded.get("ok", false))) var save_data: Dictionary = decoded["data"] - assert(int(save_data.get("save_version", -1)) == 10) + assert(int(save_data.get("save_version", -1)) == 11) assert(PlayerJobService.validate_save_data(save_data.get("jobs", {}))) + assert(not PlayerHomeState.sanitize_save_data( + save_data.get("home", {}) + ).is_empty()) _validate_pause_session_switch(main, session) main.queue_free() diff --git a/tests/player_experience_validation.gd b/tests/player_experience_validation.gd index 5e9dbef..a76a3dd 100644 --- a/tests/player_experience_validation.gd +++ b/tests/player_experience_validation.gd @@ -143,7 +143,7 @@ func _validate_save_migration() -> void: version_five, 5, ) - assert(int(migrated.get("save_version", -1)) == 10) + assert(int(migrated.get("save_version", -1)) == 11) var experience_data: Dictionary = migrated.get("experience", {}) assert(int(experience_data.get("total_experience", -1)) == 0) var world_data: Dictionary = migrated.get("world", {}) @@ -155,6 +155,9 @@ func _validate_save_migration() -> void: assert( PlayerJobService.validate_save_data(migrated.get("jobs", {})) ) + assert(not PlayerHomeState.sanitize_save_data( + migrated.get("home", {}) + ).is_empty()) manager.queue_free() diff --git a/tests/progression_archive_validation.gd b/tests/progression_archive_validation.gd index c3f9569..30f0df6 100644 --- a/tests/progression_archive_validation.gd +++ b/tests/progression_archive_validation.gd @@ -22,11 +22,24 @@ func _run() -> void: var save_manager := main.get("_save_manager") as PlayerSaveManager var player := main.get("_player") as Player var data_root := main.get("_data_root") as PlayerDataRoot - assert(save_manager != null and player != null and data_root != null) + var home_state := main.get("_home_state") as PlayerHomeState + assert( + save_manager != null + and player != null + and data_root != null + and home_state != null + ) assert(save_manager.initialize_new_game(864209)) save_manager.set_autosave_enabled(true) assert(player.wallet.restore_balance(4321)) assert(player.bag.add_item(&"coffee")) + assert(home_state.add_owned_decor(&"basic_cube")) + assert(not home_state.place_decor( + &"basic_cube", + Vector3.ZERO, + 0.0, + PlayerHomeState.PlacementMode.GRID, + ).is_empty()) assert(save_manager.save_now()) var save_path: String = str(save_manager.get("_save_path")) @@ -38,7 +51,14 @@ func _run() -> void: _assert_opaque(save_path) var decoded: Dictionary = ProgressionSaveCodec.read_local_save(save_path) assert(bool(decoded.get("ok", false))) - assert(int((decoded["data"] as Dictionary)["save_version"]) == 10) + assert(int((decoded["data"] as Dictionary)["save_version"]) == 11) + assert(not PlayerHomeState.sanitize_save_data( + (decoded["data"] as Dictionary).get("home", {}) + ).is_empty()) + assert( + ((decoded["data"] as Dictionary)["home"] as Dictionary) + .get("placements", []).size() == 1 + ) var legacy_container_path: String = data_root.progression_backup_directory().path_join( "netfishing-container.nfsave" ) @@ -74,6 +94,8 @@ func _run() -> void: assert(save_manager.load_player_data()) assert(player.wallet.get_balance() == 4321) assert(save_manager.get_world_seed() == 864209) + assert(home_state.get_owned_count(&"basic_cube") == 1) + assert(home_state.get_placed_count(&"basic_cube") == 1) var legacy_data: Dictionary = ( ProgressionSaveCodec.read_local_save(save_path)["data"] diff --git a/tests/unified_inventory_validation.gd b/tests/unified_inventory_validation.gd index e9a0d85..6928969 100644 --- a/tests/unified_inventory_validation.gd +++ b/tests/unified_inventory_validation.gd @@ -35,6 +35,41 @@ func _run() -> void: hotbar.setup(bag, ItemCatalogResource, catches, layout) assert(PlayerInventoryLayout.INVENTORY_CAPACITIES == [9, 18, 27, 36]) assert(PlayerCoolerCapacity.CAPACITIES == [9, 18, 27, 36, 45, 54, 63, 72]) + var basic_cube: ItemData = ItemCatalogResource.get_item_by_id(&"basic_cube") + assert(basic_cube != null) + assert(basic_cube.icon != null) + assert(basic_cube.hotbar_allowed) + assert(bag.add_item(&"basic_cube")) + assert(layout.is_item_in_inventory(&"basic_cube")) + assert(hotbar.assign_item(0, &"basic_cube")) + assert(hotbar.get_item_id(0) == &"basic_cube") + assert( + layout.get_container( + PlayerInventoryLayout.EntryKind.ITEM, + &"basic_cube", + ) == PlayerInventoryLayout.InventoryContainer.HOTBAR + ) + assert(hotbar.clear_slot(0)) + assert(layout.is_item_in_inventory(&"basic_cube")) + assert(bag.remove_item(&"basic_cube")) + var packed_decor_slot: int = hotbar.find_first_empty_slot() + assert(packed_decor_slot == 0) + assert(layout.prepare_new_item_placement( + &"basic_cube", + PlayerInventoryLayout.InventoryContainer.HOTBAR, + packed_decor_slot, + )) + assert(bag.add_item(&"basic_cube")) + assert( + layout.get_container( + PlayerInventoryLayout.EntryKind.ITEM, + &"basic_cube", + ) == PlayerInventoryLayout.InventoryContainer.HOTBAR + ) + assert(hotbar.assign_item(packed_decor_slot, &"basic_cube")) + assert(hotbar.select_slot(packed_decor_slot)) + assert(hotbar.get_selected_item_id() == &"basic_cube") + assert(bag.remove_item(&"basic_cube")) assert(bag.add_item(&"coffee", 3)) assert(layout.get_inventory_count() == 1) diff --git a/ui/decor_shop.gd b/ui/decor_shop.gd new file mode 100644 index 0000000..9292806 --- /dev/null +++ b/ui/decor_shop.gd @@ -0,0 +1,323 @@ +class_name DecorShop +extends Control + +const INPUT_OWNER: StringName = &"decor_shop" +const DecorCatalogType = preload("res://homes/decor_catalog.gd") +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") + +signal menu_visibility_changed(is_open: bool) +signal menu_exit_started + +var _player: Player +var _wallet: PlayerWallet +var _fishing_spot: FishingSpot +var _interaction: DecorShopInteraction +var _bag: PlayerBag +var _home_state: PlayerHomeState +var _network_shop: NetworkShopService +var _wallet_label: Label +var _owned_label: Label +var _feedback: Label +var _buy_button: Button +var _close_button: Button +var _transaction_pending: bool = false +var _prior_movement_enabled: bool = true +var _prior_camera_enabled: bool = true +var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE + + +func _ready() -> void: + UtilityPageStyleType.apply_page(self) + _build_ui() + hide() + + +func setup( + player: Player, + wallet: PlayerWallet, + fishing_spot: FishingSpot, + interaction: DecorShopInteraction, + bag: PlayerBag, + home_state: PlayerHomeState, + network_shop: NetworkShopService, +) -> void: + _player = player + _wallet = wallet + _fishing_spot = fishing_spot + _interaction = interaction + _bag = bag + _home_state = home_state + _network_shop = network_shop + if not _wallet.balance_changed.is_connected(_on_wallet_changed): + _wallet.balance_changed.connect(_on_wallet_changed) + if not _bag.contents_changed.is_connected(_refresh): + _bag.contents_changed.connect(_refresh) + if not _home_state.changed.is_connected(_refresh): + _home_state.changed.connect(_refresh) + if ( + _network_shop != null + and not _network_shop.local_purchase_pending.is_connected( + _on_purchase_pending + ) + ): + _network_shop.local_purchase_pending.connect(_on_purchase_pending) + _network_shop.local_purchase_finished.connect(_on_purchase_finished) + _refresh() + + +func set_world_interaction(interaction: DecorShopInteraction) -> void: + _interaction = interaction + + +func open_shop() -> bool: + if ( + visible + or _player == null + or _wallet == null + or _fishing_spot == null + or _interaction == null + or not _interaction.is_local_player_in_range() + or not _fishing_spot.can_open_fishing_shop() + ): + return false + _prior_movement_enabled = _player.is_movement_enabled() + _prior_camera_enabled = _player.is_camera_input_enabled() + _prior_mouse_mode = Input.mouse_mode + _player.set_movement_enabled(false) + _player.set_camera_input_enabled(false) + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _transaction_pending = ( + _network_shop != null + and _network_shop.is_local_purchase_pending() + ) + _feedback.text = "" + _refresh() + show() + _buy_button.call_deferred("grab_focus") + menu_visibility_changed.emit(true) + return true + + +func close_shop(restore_controls: bool = true) -> void: + if not visible: + return + menu_exit_started.emit() + get_viewport().gui_release_focus() + hide() + if _fishing_spot != null: + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) + if restore_controls and _player != null: + _player.set_movement_enabled(_prior_movement_enabled) + _player.set_camera_input_enabled(_prior_camera_enabled) + Input.mouse_mode = _prior_mouse_mode + menu_visibility_changed.emit(false) + + +func consume_escape() -> bool: + if not visible: + return false + close_shop() + return true + + +func close_for_range_exit() -> void: + close_shop() + + +func close_for_water_recovery() -> void: + close_shop(false) + + +func close_for_session_end() -> void: + close_shop(false) + + +func _unhandled_input(event: InputEvent) -> void: + if not visible or not event.is_action_pressed("ui_cancel"): + return + close_shop() + get_viewport().set_input_as_handled() + + +func _purchase_cube() -> void: + if _network_shop == null or _transaction_pending: + return + var request_id: String = _network_shop.request_decor( + DecorCatalogType.BASIC_CUBE_ID + ) + if request_id.is_empty(): + _refresh() + + +func _on_purchase_pending(_request_id: String) -> void: + _transaction_pending = true + _feedback.text = "purchasing…" + _refresh() + + +func _on_purchase_finished( + _request_id: String, + accepted: bool, + message: String, + product_id: StringName, + category: int, + _quantity: int, + total_cost: int, +) -> void: + if ( + category != NetworkShopProtocol.ProductCategory.DECOR + or product_id != DecorCatalogType.BASIC_CUBE_ID + ): + return + _transaction_pending = false + _feedback.text = ( + "basic cube purchased · %d fishcoin spent" % total_cost + if accepted + else message.to_lower() + ) + _refresh() + + +func _on_wallet_changed(_balance: int, _delta: int) -> void: + _refresh() + + +func _refresh() -> void: + if _wallet_label == null: + return + var balance: int = _wallet.get_balance() if _wallet != null else 0 + var owned: int = ( + _home_state.get_owned_count(DecorCatalogType.BASIC_CUBE_ID) + if _home_state != null else 0 + ) + var carried: int = ( + _bag.get_quantity(DecorCatalogType.BASIC_CUBE_ID) + if _bag != null else 0 + ) + _wallet_label.text = "%d fishcoins" % balance + _owned_label.text = "owned %d · packed %d" % [owned, carried] + _buy_button.disabled = ( + _transaction_pending + or _wallet == null + or not _wallet.can_afford( + DecorCatalogType.get_price(DecorCatalogType.BASIC_CUBE_ID) + ) + or _bag == null + or not _bag.can_add_item(DecorCatalogType.BASIC_CUBE_ID, 1) + or owned >= DecorCatalogType.MAX_OWNED_PER_PRODUCT + ) + + +func _build_ui() -> void: + set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + z_index = 83 + mouse_filter = Control.MOUSE_FILTER_STOP + var blocker := ColorRect.new() + blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + blocker.color = Color(0.02, 0.08, 0.10, 0.55) + blocker.mouse_filter = Control.MOUSE_FILTER_STOP + add_child(blocker) + + var panel := PanelContainer.new() + panel.set_anchors_preset(Control.PRESET_CENTER) + panel.position = Vector2(-440.0, -250.0) + panel.size = Vector2(880.0, 500.0) + panel.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_PANEL_DEEP, 24 + ), + ) + add_child(panel) + var margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + margin.add_theme_constant_override(side, 28) + panel.add_child(margin) + var layout := VBoxContainer.new() + layout.add_theme_constant_override("separation", 18) + margin.add_child(layout) + + var header := HBoxContainer.new() + header.add_theme_constant_override("separation", 12) + layout.add_child(header) + var title := Label.new() + title.text = "decor" + title.add_theme_font_size_override("font_size", 38) + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(title) + _wallet_label = Label.new() + _wallet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + _wallet_label.add_theme_font_size_override("font_size", 22) + header.add_child(_wallet_label) + + var item_panel := PanelContainer.new() + item_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL + item_panel.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_FIELD, 18 + ), + ) + layout.add_child(item_panel) + var item_margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + item_margin.add_theme_constant_override(side, 24) + item_panel.add_child(item_margin) + var item_row := HBoxContainer.new() + item_row.add_theme_constant_override("separation", 28) + item_margin.add_child(item_row) + var preview := Panel.new() + preview.custom_minimum_size = Vector2(150.0, 150.0) + preview.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style(Color("b77a55"), 12), + ) + item_row.add_child(preview) + var details := VBoxContainer.new() + details.size_flags_horizontal = Control.SIZE_EXPAND_FILL + details.add_theme_constant_override("separation", 10) + item_row.add_child(details) + var item_name := Label.new() + item_name.text = DecorCatalogType.get_display_name( + DecorCatalogType.BASIC_CUBE_ID + ) + item_name.add_theme_font_size_override("font_size", 32) + details.add_child(item_name) + var description := Label.new() + description.text = DecorCatalogType.get_description( + DecorCatalogType.BASIC_CUBE_ID + ) + description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.add_theme_font_size_override("font_size", 20) + description.size_flags_vertical = Control.SIZE_EXPAND_FILL + details.add_child(description) + _owned_label = Label.new() + _owned_label.add_theme_font_size_override("font_size", 18) + details.add_child(_owned_label) + _buy_button = Button.new() + _buy_button.text = "buy · 1 fishcoin" + _buy_button.custom_minimum_size = Vector2(260.0, 58.0) + UtilityPageStyleType.apply_ocean_button(_buy_button) + _buy_button.pressed.connect(_purchase_cube) + details.add_child(_buy_button) + + var footer := HBoxContainer.new() + footer.add_theme_constant_override("separation", 12) + layout.add_child(footer) + _feedback = Label.new() + _feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL + _feedback.add_theme_font_size_override("font_size", 18) + footer.add_child(_feedback) + _close_button = Button.new() + _close_button.text = "close" + _close_button.custom_minimum_size = Vector2(150.0, 50.0) + UtilityPageStyleType.apply_ocean_button(_close_button) + _close_button.pressed.connect(close_shop) + footer.add_child(_close_button) + _buy_button.focus_neighbor_bottom = _buy_button.get_path_to(_close_button) + _close_button.focus_neighbor_top = _close_button.get_path_to(_buy_button) diff --git a/ui/decor_shop.gd.uid b/ui/decor_shop.gd.uid new file mode 100644 index 0000000..ed65203 --- /dev/null +++ b/ui/decor_shop.gd.uid @@ -0,0 +1 @@ +uid://810ck3mvbn5v diff --git a/ui/decor_shop.tscn b/ui/decor_shop.tscn new file mode 100644 index 0000000..2b26f04 --- /dev/null +++ b/ui/decor_shop.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/decor_shop.gd" id="1_script"] + +[node name="DecorShop" type="Control"] +unique_name_in_owner = true +visible = false +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_script") diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 3ce0d17..585efc9 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -20,6 +20,7 @@ const TitleScreenType = preload("res://ui/title_screen.gd") const PauseMenuType = preload("res://ui/pause_menu.gd") const NetworkSessionType = preload("res://network/network_session.gd") const FishingShopType = preload("res://ui/fishing_shop.gd") +const DecorShopType = preload("res://ui/decor_shop.gd") const SettingsPanelType = preload("res://ui/settings_panel.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" @@ -27,7 +28,11 @@ const PlayerFishingUpgradesType = preload( const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) const PlayerStorageType = preload("res://ui/player_storage.gd") +const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd") const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) @@ -71,6 +76,9 @@ const MAIN_BUBBLE_PROFILE: BubbleMenuProfile = preload( const ACTIVE_BAIT_EMPTY_ICON: Texture2D = preload( "res://ui/icons/pictograms/x_dark.png" ) +const RV_STORAGE_ICON: Texture2D = preload( + "res://ui/icons/tab_menu/inventory_simple.png" +) signal pixelation_settings_visibility_changed(is_visible: bool) signal crisp_reset_focus_requested @@ -82,6 +90,7 @@ signal virtual_pointer_mode_changed(is_active: bool) signal social_prompt_accepted signal social_prompt_declined signal data_root_changed +signal rv_storage_requested const VIRTUAL_MOUSE_INPUT_OWNER: StringName = &"controller_virtual_mouse" const EMOTE_RADIAL_CAMERA_OWNER: StringName = &"emote_radial_menu" @@ -145,7 +154,10 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 @onready var _social_prompt: BubbleConfirmationPage = %SocialPrompt @onready var _hotbar_ui: HotbarUIType = %Hotbar @onready var _fishing_shop: FishingShopType = %FishingShop +@onready var _decor_shop: DecorShopType = %DecorShop @onready var _player_storage: PlayerStorageType = %PlayerStorage +@onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar +@onready var _rv_storage_button: Button = %RVStorageButton @onready var _storage_prompt: PanelContainer = %StoragePrompt @onready var _storage_prompt_message: Label = %StoragePromptMessage @onready var _shop_prompt: Control = %ShopPrompt @@ -195,6 +207,7 @@ var _chat_input_open: bool = false var _player_menu_hotbar_visible: bool = false var _main_shop_buyer: FishBuyerProfileType var _shop_interaction: ShopInteractionType +var _decor_shop_interaction: DecorShopInteractionType var _storage_interaction: PlayerStorageInteractionType var _surface_drawing: NetworkSurfaceDrawingService var _surface_drawing_hotbar_selected: bool = false @@ -221,14 +234,17 @@ var _controller_text_entry_request: Callable var _controller_text_entry_is_open: Callable var _controller_text_entry_close: Callable var _restore_chat_keyboard_after_fishing: bool = false +var _rv_storage_button_requested_visible: bool = false func _ready() -> void: _prioritize_surface_drawing_pointer_input() _bite_prompt_button.pressed.connect(_on_bite_prompt_pressed) + _rv_storage_button.pressed.connect(_on_rv_storage_button_pressed) _social_prompt.confirmed.connect(_accept_social_prompt) _social_prompt.cancelled.connect(_decline_social_prompt) _apply_active_bait_indicator_style() + _apply_rv_storage_button_style() _refresh_active_bait_indicator() _refresh_interaction_prompt_bindings() # Reward feedback must remain above full-screen canonical menus. Keeping the @@ -346,11 +362,13 @@ func setup( fishing_upgrades: PlayerFishingUpgradesType, shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, + decor_shop_interaction: DecorShopInteractionType, item_effects: PlayerItemEffectsType, cooler_capacity: PlayerCoolerCapacityType, network_session: NetworkSessionType, network_sale_service: NetworkSaleService, network_shop_service: NetworkShopService, + home_state: PlayerHomeState, network_chat_service: NetworkChatService, network_profile: NetworkProfilePreferences, spawn_service: PlayerSpawnService, @@ -494,6 +512,15 @@ func setup( network_sale_service, reservations, ) + _decor_shop.setup( + player, + wallet, + fishing_spot, + decor_shop_interaction, + bag, + home_state, + network_shop_service, + ) _player_storage.setup( player, fishing_spot, @@ -509,6 +536,8 @@ func setup( ) _fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) _fishing_shop.menu_exit_started.connect(_on_shop_exit_started) + _decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _decor_shop.menu_exit_started.connect(_on_shop_exit_started) _fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested) _fishing_shop.shop_cooler_return_requested.connect( _on_shop_cooler_return_requested @@ -518,6 +547,7 @@ func setup( ) _main_shop_buyer = main_shop_buyer _shop_interaction = shop_interaction + _decor_shop_interaction = decor_shop_interaction _storage_interaction = storage_interaction _surface_drawing = surface_drawing if ( @@ -540,11 +570,14 @@ func setup( func set_world_interactions( shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, + decor_shop_interaction: DecorShopInteractionType, ) -> void: _shop_interaction = shop_interaction _storage_interaction = storage_interaction + _decor_shop_interaction = decor_shop_interaction _fishing_shop.set_world_interaction(shop_interaction) _player_storage.set_world_interaction(storage_interaction) + _decor_shop.set_world_interaction(decor_shop_interaction) func _prioritize_surface_drawing_pointer_input() -> void: @@ -1655,6 +1688,34 @@ func _apply_active_bait_indicator_style() -> void: _active_bait_button.add_theme_color_override(state, Color.WHITE) +func _apply_rv_storage_button_style() -> void: + var normal_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_normal_style() + var hover_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_hover_style() + var pressed_style: StyleBoxFlat = MAIN_BUBBLE_PROFILE.make_pressed_style() + for style: StyleBoxFlat in [normal_style, hover_style, pressed_style]: + style.set_corner_radius_all(29) + style.content_margin_left = 9.0 + style.content_margin_top = 9.0 + style.content_margin_right = 9.0 + style.content_margin_bottom = 9.0 + _rv_storage_button.icon = RV_STORAGE_ICON + _rv_storage_button.add_theme_stylebox_override("normal", normal_style) + _rv_storage_button.add_theme_stylebox_override("hover", hover_style) + _rv_storage_button.add_theme_stylebox_override("focus", hover_style) + _rv_storage_button.add_theme_stylebox_override("pressed", pressed_style) + for state: StringName in [ + &"icon_normal_color", + &"icon_hover_color", + &"icon_focus_color", + &"icon_pressed_color", + ]: + _rv_storage_button.add_theme_color_override(state, Color.WHITE) + + +func _on_rv_storage_button_pressed() -> void: + rv_storage_requested.emit() + + func _refresh_active_bait_indicator() -> void: _refresh_active_bait_indicator_visibility() if not is_node_ready(): @@ -1718,17 +1779,20 @@ func set_gameplay_ui_enabled(enabled: bool) -> void: if not enabled: _gameplay_hud_hidden = false _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() if enabled: _try_start_shop_npc_speech() _refresh_gameplay_hud_visibility() _refresh_chat_availability() if not enabled: + _home_decor_toolbar.set_active(false) _end_virtual_mouse() _close_radial_menus() if _surface_drawing != null: _surface_drawing.deactivate() close_player_menu_for_session_end() _fishing_shop.close_for_session_end() + _decor_shop.close_for_session_end() _player_storage.close_for_session_end() _fishing_panel.visible = false _shop_prompt.hide() @@ -1749,6 +1813,7 @@ func set_gameplay_hud_hidden(hidden: bool) -> void: _quick_radial_menu.set_hud_hidden(hidden) _refresh_gameplay_hud_visibility() _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() _refresh_hotbar_visibility() _refresh_chat_availability() @@ -1864,13 +1929,43 @@ func get_fishing_shop() -> FishingShopType: return _fishing_shop +func get_decor_shop() -> DecorShopType: + return _decor_shop + + func get_player_storage() -> PlayerStorageType: return _player_storage +func get_home_decor_toolbar() -> HomeDecorToolbarType: + return _home_decor_toolbar + + +func set_rv_interior_camera_mode(enabled: bool) -> void: + _hotbar_ui.set_rv_interior_camera_mode(enabled) + + +func set_rv_storage_button_visible(requested_visible: bool) -> void: + _rv_storage_button_requested_visible = requested_visible + _refresh_rv_storage_button_visibility() + + +func _refresh_rv_storage_button_visibility() -> void: + _rv_storage_button.visible = ( + _rv_storage_button_requested_visible + and _gameplay_ui_enabled + and not _gameplay_hud_hidden + and not _system_menu_open + and not _player_menu_open + and not _shop_open + and not _storage_open + ) + + func set_storage_prompt_visible( requested_visible: bool, world_anchor: Vector3 = Vector3(0.0, INF, 0.0), + action_text: String = "open storage", ) -> void: if not _storage_prompt.has_meta(&"styled"): _storage_prompt.set_meta(&"styled", true) @@ -1879,6 +1974,7 @@ func set_storage_prompt_visible( ) style.anti_aliasing = false _storage_prompt.add_theme_stylebox_override("panel", style) + _storage_prompt_message.text = action_text _storage_prompt.visible = ( requested_visible and _gameplay_ui_enabled @@ -2762,6 +2858,7 @@ func _refresh_hotbar_visibility() -> void: func _emit_interactive_pointer_ui_changed() -> void: _refresh_active_bait_indicator_visibility() + _refresh_rv_storage_button_visibility() interactive_pointer_ui_changed.emit( _system_menu_open or _player_menu_open diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 483433a..c219c84 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=22 format=3] +[gd_scene load_steps=24 format=3] [ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"] [ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"] @@ -15,6 +15,8 @@ [ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"] [ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"] [ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"] +[ext_resource type="PackedScene" path="res://ui/decor_shop.tscn" id="16_decor_shop"] +[ext_resource type="PackedScene" path="res://ui/home_decor_toolbar.tscn" id="17_decor_toolbar"] [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] bg_color = Color(0.032, 0.118, 0.15, 1) @@ -472,9 +474,37 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10) [node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")] unique_name_in_owner = true +[node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")] +unique_name_in_owner = true + +[node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")] +unique_name_in_owner = true + [node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")] unique_name_in_owner = true +[node name="RVStorageButton" type="Button" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +unique_name_in_owner = true +visible = false +z_index = 57 +layout_mode = 1 +anchors_preset = 1 +anchor_left = 1.0 +anchor_right = 1.0 +offset_left = -74.0 +offset_top = 16.0 +offset_right = -16.0 +offset_bottom = 74.0 +grow_horizontal = 0 +mouse_default_cursor_shape = 2 +focus_mode = 0 +tooltip_text = "open RV storage" +theme = ExtResource("3_theme") +texture_filter = 1 +expand_icon = true +icon_alignment = 1 +vertical_icon_alignment = 1 + [node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false diff --git a/ui/home_decor_toolbar.gd b/ui/home_decor_toolbar.gd new file mode 100644 index 0000000..6f73219 --- /dev/null +++ b/ui/home_decor_toolbar.gd @@ -0,0 +1,126 @@ +class_name HomeDecorToolbar +extends PanelContainer + +signal placement_mode_requested(mode: int) +signal rotate_requested +signal store_requested +signal cancel_requested + +var _mode_button: Button +var _selection_label: Label +var _rotate_button: Button +var _store_button: Button +var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID + + +func _ready() -> void: + _build_ui() + hide() + + +func set_active(active: bool) -> void: + if visible == active: + return + visible = active + if active and _mode_button != null: + _mode_button.call_deferred("grab_focus") + + +func set_mode(mode: PlayerHomeState.PlacementMode) -> void: + _mode = mode + if _mode_button != null: + _mode_button.text = ( + "placement: grid" + if mode == PlayerHomeState.PlacementMode.GRID + else "placement: free" + ) + + +func set_selection(instance_id: String, display_name: String = "") -> void: + var selected: bool = not instance_id.is_empty() + if _selection_label != null: + _selection_label.text = ( + "selected: %s" % display_name + if selected else "click decor to select; drag to move" + ) + if _rotate_button != null: + _rotate_button.disabled = not selected + if _store_button != null: + _store_button.disabled = not selected + + +func report_status(message: String) -> void: + if _selection_label != null: + _selection_label.text = message + + +func _build_ui() -> void: + custom_minimum_size = Vector2(330.0, 0.0) + position = Vector2(925.0, 540.0) + z_index = 58 + mouse_filter = Control.MOUSE_FILTER_STOP + add_theme_stylebox_override( + "panel", + UtilityPageStyle.rounded_style( + Color(UtilityPageStyle.OCEAN_PANEL_DEEP, 0.96), 16 + ), + ) + var margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + margin.add_theme_constant_override(side, 12) + add_child(margin) + var layout := VBoxContainer.new() + layout.add_theme_constant_override("separation", 8) + margin.add_child(layout) + var title := Label.new() + title.text = "RV decor" + title.add_theme_font_size_override("font_size", 24) + layout.add_child(title) + _selection_label = Label.new() + _selection_label.text = "click decor to select; drag to move" + _selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _selection_label.add_theme_font_size_override("font_size", 17) + layout.add_child(_selection_label) + var row := HBoxContainer.new() + row.add_theme_constant_override("separation", 8) + layout.add_child(row) + _mode_button = Button.new() + _mode_button.text = "placement: grid" + _mode_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(_mode_button) + _mode_button.pressed.connect(_toggle_mode) + row.add_child(_mode_button) + _rotate_button = Button.new() + _rotate_button.text = "rotate" + _rotate_button.disabled = true + UtilityPageStyle.apply_ocean_button(_rotate_button) + _rotate_button.pressed.connect(rotate_requested.emit) + row.add_child(_rotate_button) + var action_row := HBoxContainer.new() + action_row.add_theme_constant_override("separation", 8) + layout.add_child(action_row) + _store_button = Button.new() + _store_button.text = "pack up" + _store_button.disabled = true + _store_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(_store_button) + _store_button.pressed.connect(store_requested.emit) + action_row.add_child(_store_button) + var cancel_button := Button.new() + cancel_button.text = "clear selection" + cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(cancel_button) + cancel_button.pressed.connect(cancel_requested.emit) + action_row.add_child(cancel_button) + + +func _toggle_mode() -> void: + _mode = ( + PlayerHomeState.PlacementMode.FREE + if _mode == PlayerHomeState.PlacementMode.GRID + else PlayerHomeState.PlacementMode.GRID + ) + set_mode(_mode) + placement_mode_requested.emit(int(_mode)) diff --git a/ui/home_decor_toolbar.gd.uid b/ui/home_decor_toolbar.gd.uid new file mode 100644 index 0000000..b1bb23b --- /dev/null +++ b/ui/home_decor_toolbar.gd.uid @@ -0,0 +1 @@ +uid://bmtlixw3wief3 diff --git a/ui/home_decor_toolbar.tscn b/ui/home_decor_toolbar.tscn new file mode 100644 index 0000000..671308d --- /dev/null +++ b/ui/home_decor_toolbar.tscn @@ -0,0 +1,8 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/home_decor_toolbar.gd" id="1_script"] + +[node name="HomeDecorToolbar" type="PanelContainer"] +unique_name_in_owner = true +visible = false +script = ExtResource("1_script") diff --git a/ui/hotbar.gd b/ui/hotbar.gd index 24a9aa8..5f54217 100644 --- a/ui/hotbar.gd +++ b/ui/hotbar.gd @@ -37,6 +37,7 @@ var _slots: Array[BubbleHotbarSlotType] = [] var _gameplay_input_enabled: bool = false var _drag_enabled: bool = false var _swap_hotbar_camera_scroll: bool = false +var _rv_interior_camera_mode: bool = false var _motion_elapsed: float = 0.0 var _compact_layout: bool = false var _player_menu_context: bool = false @@ -99,6 +100,10 @@ func set_swap_hotbar_camera_scroll(enabled: bool) -> void: _swap_hotbar_camera_scroll = enabled +func set_rv_interior_camera_mode(enabled: bool) -> void: + _rv_interior_camera_mode = enabled + + func set_drag_enabled(enabled: bool) -> void: _drag_enabled = enabled for slot: BubbleHotbarSlotType in _slots: @@ -308,7 +313,11 @@ func _unhandled_input(event: InputEvent) -> void: if ( event is InputEventMouseButton and event.pressed - and event.shift_pressed == _swap_hotbar_camera_scroll + and ( + event.shift_pressed + if _rv_interior_camera_mode + else event.shift_pressed == _swap_hotbar_camera_scroll + ) ): if event.button_index == MOUSE_BUTTON_WHEEL_UP: _hotbar.cycle_selection(-1) diff --git a/ui/player_storage.gd b/ui/player_storage.gd index 25eb0d4..1f553ea 100644 --- a/ui/player_storage.gd +++ b/ui/player_storage.gd @@ -4,6 +4,7 @@ extends Control const INPUT_OWNER: StringName = &"player_storage" signal menu_visibility_changed(is_open: bool) +signal decorate_requested var _player: Player var _fishing_spot: FishingSpot @@ -14,6 +15,9 @@ var _storage_grid: GeneralInventoryGrid var _inventory_count: Label var _storage_count: Label var _feedback: Label +var _title: Label +var _decorate_button: Button +var _home_access: bool = false var _prior_movement_enabled: bool = true var _prior_camera_enabled: bool = true var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE @@ -75,15 +79,34 @@ func set_world_interaction(interaction: PlayerStorageInteraction) -> void: func open_storage() -> bool: + return _open_storage(false) + + +func open_storage_from_home(allow_decorating: bool = true) -> bool: + return _open_storage(true, allow_decorating) + + +func _open_storage( + home_access: bool, + allow_decorating: bool = false, +) -> bool: if ( visible or _player == null - or _interaction == null - or not _interaction.is_local_player_in_range() or _fishing_spot == null or not _fishing_spot.can_open_fishing_shop() + or ( + not home_access + and ( + _interaction == null + or not _interaction.is_local_player_in_range() + ) + ) ): return false + _home_access = home_access + _title.text = "RV storage" if home_access else "private storage" + _decorate_button.visible = home_access and allow_decorating _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_enabled = _player.is_camera_input_enabled() _prior_mouse_mode = Input.mouse_mode @@ -112,10 +135,13 @@ func close_storage(restore_controls: bool = true) -> void: _player.set_movement_enabled(_prior_movement_enabled) _player.set_camera_input_enabled(_prior_camera_enabled) Input.mouse_mode = _prior_mouse_mode + _home_access = false menu_visibility_changed.emit(false) func close_for_range_exit() -> void: + if _home_access: + return close_storage() @@ -248,12 +274,20 @@ func _build_ui() -> void: var header := HBoxContainer.new() layout.add_child(header) - var title := Label.new() - title.text = "private storage" - title.add_theme_font_size_override("font_size", 32) - title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY) - title.size_flags_horizontal = Control.SIZE_EXPAND_FILL - header.add_child(title) + _title = Label.new() + _title.text = "private storage" + _title.add_theme_font_size_override("font_size", 32) + _title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY) + _title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(_title) + _decorate_button = Button.new() + _decorate_button.text = "decorate RV" + _decorate_button.tooltip_text = "Place decor inside your RV" + _decorate_button.custom_minimum_size = Vector2(160.0, 48.0) + UtilityPageStyle.apply_ocean_button(_decorate_button) + _decorate_button.hide() + _decorate_button.pressed.connect(_request_decorating) + header.add_child(_decorate_button) var close := Button.new() close.text = "×" close.tooltip_text = "Close storage" @@ -285,6 +319,13 @@ func _build_ui() -> void: layout.add_child(_feedback) +func _request_decorating() -> void: + if not _home_access: + return + close_storage() + decorate_requested.emit() + + func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary: var panel := PanelContainer.new() panel.custom_minimum_size = Vector2(550.0, 0.0) diff --git a/world/decor_shop_interaction.gd b/world/decor_shop_interaction.gd new file mode 100644 index 0000000..049bfbe --- /dev/null +++ b/world/decor_shop_interaction.gd @@ -0,0 +1,2 @@ +class_name DecorShopInteraction +extends FishingShopInteraction diff --git a/world/decor_shop_interaction.gd.uid b/world/decor_shop_interaction.gd.uid new file mode 100644 index 0000000..955738c --- /dev/null +++ b/world/decor_shop_interaction.gd.uid @@ -0,0 +1 @@ +uid://u70jgwedk7ym diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 780ddb8..4e15251 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -9,6 +9,9 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) @@ -80,8 +83,12 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [ @onready var _player_storage: PlayerStorageInteractionType = ( $Interactables/PlayerStorageBox/InteractionArea ) +@onready var _decor_shop: DecorShopInteractionType = ( + $Interactables/DecorShopWorld/InteractionArea +) @onready var _shop_root: Node3D = %FishingShopWorld @onready var _storage_root: Node3D = %PlayerStorageBox +@onready var _decor_shop_root: Node3D = %DecorShopWorld @onready var _decorations: Node3D = %Decorations @onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks @onready var _diggable_beach: DiggableArea3D = %DiggableBeach @@ -148,6 +155,10 @@ func get_player_storage() -> PlayerStorageInteractionType: return _player_storage +func get_decor_shop() -> DecorShopInteractionType: + return _decor_shop + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return _shoreline_reference @@ -428,6 +439,8 @@ func _place_spawn_amenities(center: Vector3) -> void: _shop_root.rotation.y = PI _storage_root.position = center + Vector3(-2.4, 0.0, 1.2) _storage_root.rotation.y = PI * 0.5 + _decor_shop_root.position = center + Vector3(-2.4, 0.0, 1.2) + _decor_shop_root.rotation.y = PI * 0.5 func _configure_static_water() -> void: diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index 47ee6bc..04602e1 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=14 format=3] +[gd_scene load_steps=15 format=3] [ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"] [ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"] @@ -13,6 +13,7 @@ [ext_resource type="Resource" path="res://fish/pools/starter_pond_pool.tres" id="11_pond_pool"] [ext_resource type="Resource" path="res://world/generation/props/terrain_prop_catalog.tres" id="12_prop_catalog"] [ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"] +[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="14_decor_shop"] [node name="GeneratedWorldRegion" type="Node3D"] script = ExtResource("1_region") @@ -98,6 +99,9 @@ unique_name_in_owner = true [node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("6_storage")] unique_name_in_owner = true +[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("14_decor_shop")] +unique_name_in_owner = true + [node name="Decorations" type="Node3D" parent="."] unique_name_in_owner = true diff --git a/world/interactables/decor_shop_world.tscn b/world/interactables/decor_shop_world.tscn new file mode 100644 index 0000000..6b735e2 --- /dev/null +++ b/world/interactables/decor_shop_world.tscn @@ -0,0 +1,35 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://world/decor_shop_interaction.gd" id="1_shop"] +[ext_resource type="PackedScene" path="res://art/exported/characters/base/straywild_base_character.glb" id="2_character"] +[ext_resource type="Script" path="res://world/world_character_display.gd" id="3_display"] + +[sub_resource type="SphereShape3D" id="InteractionShape"] +resource_local_to_scene = true +radius = 3.0 + +[node name="DecorShopWorld" type="Node3D"] + +[node name="Shopkeeper" parent="." instance=ExtResource("2_character")] +script = ExtResource("3_display") +species_id = "round" +fur_color_id = "orange" +ears_id = "pointy_short" +tail_id = "cat" +eyes_id = "simple_shine" +nose_id = "dog_round" +mouth_id = "three" +head_look_enabled = true + +[node name="AnimationPlayer" parent="Shopkeeper"] +autoplay = "idle" + +[node name="InteractionArea" type="Area3D" parent="."] +unique_name_in_owner = true +position = Vector3(0, 1, -0.3) +collision_layer = 0 +collision_mask = 2 +script = ExtResource("1_shop") + +[node name="InteractionShape" type="CollisionShape3D" parent="InteractionArea"] +shape = SubResource("InteractionShape") diff --git a/world/interactables/player_storage_box.tscn b/world/interactables/player_storage_box.tscn index 4bf9a8e..008f0ac 100644 --- a/world/interactables/player_storage_box.tscn +++ b/world/interactables/player_storage_box.tscn @@ -27,6 +27,8 @@ size = Vector3(1.62, 0.86, 1.02) radius = 2.3 [node name="PlayerStorageBox" type="Node3D"] +process_mode = 4 +visible = false [node name="Body" type="MeshInstance3D" parent="."] position = Vector3(0, 0.35, 0) @@ -44,6 +46,7 @@ collision_mask = 0 [node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"] position = Vector3(0, 0.43, 0) +disabled = true shape = SubResource("CollisionShape") [node name="InteractionArea" type="Area3D" parent="."] @@ -54,6 +57,7 @@ collision_mask = 2 script = ExtResource("1_interaction") [node name="CollisionShape3D" type="CollisionShape3D" parent="InteractionArea"] +disabled = true shape = SubResource("InteractionShape") [node name="PromptAnchor" type="Marker3D" parent="."] diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index 3af3010..b418292 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -8,6 +8,9 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) const FOLIAGE_WIND_SHADER: Shader = preload( "res://world/materials/foliage_wind.gdshader" ) @@ -49,6 +52,10 @@ var fishing_shop_path: NodePath = ( var player_storage_path: NodePath = ( ^"Interactables/PlayerStorageBox/InteractionArea" ) +@export_node_path("Area3D") +var decor_shop_path: NodePath = ( + ^"Interactables/DecorShopWorld/InteractionArea" +) @export_group("Terrain Collision") # The imported GLB is the collision authority. Rebuild once per region load so # newly authored terrain and props cannot retain a stale saved fallback shape. @@ -84,6 +91,10 @@ func get_player_storage() -> PlayerStorageInteractionType: return get_node_or_null(player_storage_path) as PlayerStorageInteractionType +func get_decor_shop() -> DecorShopInteractionType: + return get_node_or_null(decor_shop_path) as DecorShopInteractionType + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D diff --git a/world/regions/starter_island_region.tscn b/world/regions/starter_island_region.tscn index 8f4bc4b..9b95dfa 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -18,6 +18,7 @@ [ext_resource type="Script" path="res://world/water/shoreline_ribbon_config.gd" id="22_shoreline_config"] [ext_resource type="Script" path="res://world/digging/diggable_area_3d.gd" id="23_diggable_area"] [ext_resource type="Script" path="res://world/gathering/gatherable_anchor_set_3d.gd" id="24_gatherable_anchors"] +[ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="25_decor_shop"] [sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_4e4k5"] data = PackedVector3Array(10.033899, 3.3913999, 3.3446, 13.3786, 3.3913999, 0, 13.3786, 3.3913999, 3.3446, 10.033899, 3.3913999, 3.3446, 10.033899, 3.3913999, 0, 13.3786, 3.3913999, 0, 6.6893, 3.3913999, 3.3446, 10.033899, 3.3913999, 0, 10.033899, 3.3913999, 3.3446, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 0, 10.033899, 3.3913999, 0, 6.6893, 3.3913999, 0, 10.033899, 3.3913999, -3.3446, 10.033899, 3.3913999, 0, 6.6893, 3.3913999, 6.6893, 6.6893, 3.3913999, 3.3446, 10.033899, 3.3913999, 3.3446, 6.6893, 3.3913999, 6.6893, 10.033899, 3.3913999, 3.3446, 10.033899, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 6.6893, 3.3913999, 0, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 10.033899, 6.6893, 3.3913999, 6.6893, 10.033899, 3.3913999, 6.6893, 6.6893, 3.3913999, 10.033899, 10.033899, 3.3913999, 6.6893, 10.033899, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 6.6893, 3.3913999, 3.3446, 6.6893, 3.3913999, 6.6893, 3.3446, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 6.6893, 3.3913999, 3.3446, 3.3446, 3.3913999, 10.033899, 6.6893, 3.3913999, 6.6893, 6.6893, 3.3913999, 10.033899, 3.3446, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 6.6893, 3.3913999, 6.6893, 0, 3.3913999, 6.6893, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 6.6893, 0, 3.3913999, 6.6893, 0, 3.3913999, 3.3446, 3.3446, 3.3913999, 3.3446, 0, 3.3913999, 10.033899, 0, 3.3913999, 6.6893, 3.3446, 3.3913999, 6.6893, 0, 3.3913999, 10.033899, 3.3446, 3.3913999, 6.6893, 3.3446, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, -3.3446, 3.3913999, 10.033899, 0, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, 0, 3.3913999, 10.033899, 0, 3.3913999, 13.3786, 0, 3.3913999, 13.3786, 0, 3.3913999, 10.033899, 3.3446, 3.3913999, 10.033899, 0, 3.3913999, 13.3786, 3.3446, 3.3913999, 10.033899, 3.3446, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, 3.3446, 3.3913999, 10.033899, 6.6893, 3.3913999, 10.033899, 3.3446, 3.3913999, 13.3786, 6.6893, 3.3913999, 10.033899, 6.6893, 3.3913999, 13.3786, 3.9148, 3.3913999, 16.514599, 3.3446, 3.3913999, 13.3786, 6.6893, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, 3.9148, 3.3913999, 16.514599, 3.6399, 3.3913999, 16.2405, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 13.3786, 6.6893, 3.3913999, 16.7232, 6.6893, 3.3913999, 16.7232, 6.6949, 3.3913999, 18.9914, 6.6893, 3.3913999, 18.9861, 6.6949, 3.3913999, 18.9914, 6.6893, 3.3913999, 16.7232, 10.3743, 3.3913999, 16.7232, 6.6949, 3.3913999, 18.9914, 10.3743, 3.3913999, 16.7232, 9.1275, 3.3913999, 18.830599, 10.033899, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 13.3786, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 19.798899, 3.3913999, -13.1096, 19.798899, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 16.7232, 3.3913999, -13.1096, 19.798899, 3.3913999, -13.1096, 16.7232, 3.3913999, -10.033899, 16.4543, 3.3913999, -13.3786, 16.7232, 3.3913999, -13.1096, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -13.3786, 16.4543, 3.3913999, -13.3786, 13.3786, 3.3913999, -6.6893, 13.3786, 3.3913999, -10.033899, 16.7232, 3.3913999, -10.033899, 13.3786, 3.3913999, -6.6893, 16.7232, 3.3913999, -10.033899, 16.7232, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 13.3786, 3.3913999, -10.033899, 13.3786, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 10.033899, 3.3913999, -10.033899, 13.3786, 3.3913999, -10.033899, 16.4543, 3.3913999, -13.3786, 16.495699, 3.3913999, -16.459, 16.495699, 3.3913999, -13.42, 16.4543, 3.3913999, -13.3786, 12.738999, 3.3913999, -15.894899, 16.495699, 3.3913999, -16.459, 12.738999, 3.3913999, -15.894899, 16.4543, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 12.460899, 3.3913999, -16.1674, 12.738999, 3.3913999, -15.894899, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 12.460899, 3.3913999, -16.1674, 10.033899, 3.3913999, -10.033899, 10.033899, 3.3913999, -13.3786, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 13.3786, 3.3913999, -13.3786, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 10.033899, 3.3913999, -16.7232, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 6.6893, 3.3913999, -16.7232, 10.033899, 3.3913999, -16.7232, 6.6893, 3.3913999, -10.033899, 10.033899, 3.3913999, -13.3786, 10.033899, 3.3913999, -10.033899, 6.6893, 3.3913999, -10.033899, 6.6893, 3.3913999, -13.3786, 10.033899, 3.3913999, -13.3786, 6.6893, 3.3913999, -6.6893, 6.6893, 3.3913999, -10.033899, 10.033899, 3.3913999, -10.033899, 6.6893, 3.3913999, -6.6893, 10.033899, 3.3913999, -10.033899, 10.033899, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 6.6893, 3.3913999, -13.3786, 6.6893, 3.3913999, -10.033899, 4.6821, 3.3913999, -13.3786, 6.6893, 3.3913999, -16.7232, 6.6893, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 4.6821, 3.3913999, -13.3786, 6.6893, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 3.8388, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 4.6821, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 4.6821, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 4.4131, 3.2091, -13.357699, 3.3446, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 6.6893, 3.3913999, -10.033899, 3.3446, 3.3913999, -6.6893, 6.6893, 3.3913999, -10.033899, 6.6893, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, 4.0225, 3.3913999, -8.7143, 3.3446, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, 0, 3.3913999, -8.7143, 4.0225, 3.3913999, -8.7143, 3.3446, 3.3913999, -3.3446, 3.3446, 3.3913999, -6.6893, 6.6893, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, 0, 3.3913999, -3.3446, 0, 3.3913999, -6.6893, 3.3446, 3.3913999, -6.6893, 0, 3.3913999, -3.3446, 3.3446, 3.3913999, -6.6893, 3.3446, 3.3913999, -3.3446, -3.6378, 3.3913999, -2.0792, 0, 3.3913999, -6.6893, 0, 3.3913999, -3.3446, -2.9566998, 3.3913999, 0, -3.6378, 3.3913999, -2.0792, 0, 3.3913999, -3.3446, -3.6378, 3.3913999, -2.0792, -3.3446, 3.3913999, -6.6893, 0, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -6.6893, -3.3446, 3.3913999, -6.6893, -6.6893, 3.3913999, -3.7326, -3.3446, 3.3913999, -6.6893, -3.6378, 3.3913999, -2.0792, -2.9566998, 3.3913999, 0, 0, 3.3913999, -3.3446, 0, 3.3913999, 0, 0, 3.3913999, 0, 0, 3.3913999, -3.3446, 3.3446, 3.3913999, -3.3446, -2.9566998, 3.3913999, 3.3446, -2.9566998, 3.3913999, 0, 0, 3.3913999, 0, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 0, 0, 3.3913999, 3.3446, -6.6893, 3.3913999, -3.7326, -6.6893, 3.3913999, -6.6893, -3.3446, 3.3913999, -6.6893, -6.6893, 3.3913999, -6.6893, -3.9101999, 3.3913999, -9.8592, -3.3446, 3.3913999, -6.6893, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -6.6893, -6.6893, 3.3913999, -3.7326, 0, 3.3913999, 3.3446, 0, 3.3913999, 0, 3.3446, 3.3913999, 0, 0, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 3.3446, 3.3913999, 0, 0, 3.3913999, 3.3446, 3.3446, 3.3913999, 0, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 3.3446, 3.3446, 3.3913999, 0, 6.6893, 3.3913999, 0, 3.3446, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 6.6893, 3.3913999, -3.3446, 3.3446, 3.3913999, 0, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, 0, 3.3446, 3.3913999, -3.3446, 6.6893, 3.3913999, -6.6893, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, 0, 6.6893, 3.3913999, -3.3446, 10.033899, 3.3913999, -3.3446, 6.6893, 3.3913999, -3.3446, 6.6893, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 6.6893, 3.3913999, -3.3446, 10.033899, 3.3913999, -6.6893, 10.033899, 3.3913999, -3.3446, -6.6893, 3.3913999, -6.6893, -6.6893, 3.3913999, -10.033899, -3.9101999, 3.3913999, -9.8592, -6.6893, 3.3913999, -10.033899, -3.9101999, 3.3913999, -12.538199, -3.9101999, 3.3913999, -9.8592, -10.033899, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -6.6893, 3.3913999, -6.6893, -10.033899, 3.3913999, -6.6893, -6.6893, 3.3913999, -10.033899, -6.6893, 3.3913999, -6.6893, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -10.033899, -6.6893, 3.3913999, -12.538199, -3.9101999, 3.3913999, -12.538199, -13.3786, 3.3913999, -3.7326, -13.3786, 3.3913999, -6.6893, -10.033899, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -6.6893, -10.033899, 3.3913999, -10.033899, -6.6893, 3.3913999, -10.033899, -13.3786, 3.3913999, -6.6893, -10.033899, 3.3913999, -10.033899, -10.033899, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -16.7232, 3.3913999, -6.6893, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -6.6893, -13.3786, 3.3913999, -10.033899, -10.033899, 3.3913999, -10.033899, -19.7545, 3.3913999, -2.7851999, -16.7232, 3.3913999, -6.6893, -16.191599, 3.3913999, -3.0971, -19.7545, 3.3913999, -2.7851999, -19.280699, 3.3913999, -6.6893, -16.7232, 3.3913999, -6.6893, -17.1112, 3.3913999, 0, -19.7545, 3.3913999, -2.7851999, -16.191599, 3.3913999, -3.0971, -17.1112, 3.3913999, 0, -19.9511, 3.3913999, -2.5834, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -17.1112, 3.3913999, 0, -20.0679, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -20.0679, 3.3913999, 0, -17.1112, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -17.1112, 3.3913999, 0, -17.1112, 3.3913999, 3.3044999, -13.3786, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -10.033899, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -10.033899, -10.033899, 3.3913999, -10.033899, -13.3786, 3.3913999, -10.033899, -13.3786, 3.3913999, -13.3786, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -10.033899, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -12.538199, -13.3786, 3.3913999, -13.3786, -10.699599, 3.3913999, -15.835, -10.699599, 3.3913999, -13.175799, -13.3786, 3.3913999, -13.3786, -13.3786, 3.3913999, -15.4154, -10.699599, 3.3913999, -15.835, -6.6893, 3.3913999, -12.538199, -10.699599, 3.3913999, -13.175799, -6.6893, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -6.6893, 3.175, -12.867, -20.0679, 3.3913999, 6.6893, -20.0679, 3.3913999, 3.3446, -17.1112, 3.3913999, 3.3044999, -22.5541, 3.3913999, 9.2058, -22.3151, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -22.5541, 3.3913999, 9.2058, -20.0679, 3.3913999, 6.6893, -20.0679, 3.3913999, 9.167399, -20.0679, 3.3913999, 9.167399, -20.0679, 3.3913999, 6.6893, -16.7232, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -17.1112, 3.3913999, 3.3044999, -16.7232, 3.3913999, 6.6893, -20.0679, 3.3913999, 9.167399, -16.7232, 3.3913999, 6.6893, -16.285099, 3.3913999, 9.5865, -16.7232, 3.3913999, 6.6893, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -16.7232, 3.3913999, 6.6893, -13.1355, 3.3913999, 6.7549996, -17.1112, 3.3913999, 3.3044999, -13.1355, 3.3913999, 6.7549996, -16.7232, 3.3913999, 6.6893, -16.053999, 3.3913999, 9.8082, -13.1355, 3.3913999, 6.7549996, -13.3786, 3.3913999, 10.033899, -13.1355, 3.3913999, 6.7549996, -17.1112, 3.3913999, 3.3044999, -14.877199, 3.3913999, 4.6657, -14.877199, 3.3913999, 4.6657, -17.1112, 3.3913999, 3.3044999, -15.582299, 3.3913999, 4.0366, -13.1355, 3.3913999, 6.7549996, -14.877199, 3.3913999, 4.6657, -14.0948, 3.3913999, 5.5073, -13.3786, 3.3913999, 10.033899, -13.1355, 3.3913999, 6.7549996, -10.033899, 3.3913999, 7.0772, -13.3786, 3.3913999, 10.033899, -10.033899, 3.3913999, 7.0772, -10.033899, 3.3913999, 10.033899, -10.033899, 3.3913999, 10.033899, -10.033899, 3.3913999, 7.0772, -6.6893, 3.3913999, 7.0772, -10.033899, 3.3913999, 10.033899, -6.6893, 3.3913999, 7.0772, -6.6893, 3.3913999, 10.033899, -10.033899, 3.0033998, 6.6893, -13.1355, 3.3913999, 6.7549996, -12.747499, 3.0033998, 6.3671, -10.033899, 3.0033998, 6.6893, -10.033899, 3.3913999, 7.0772, -13.1355, 3.3913999, 6.7549996, -2.5674999, 2.4164, 7.6215, -6.6893, 3.3913999, 7.0772, -6.6893, 3.0033998, 6.6893, -2.5674999, 2.4164, 7.6215, -2.1794999, 2.8042998, 8.009399, -6.6893, 3.3913999, 7.0772, -12.747499, 3.0033998, 6.3671, -14.0948, 3.3913999, 5.5073, -13.6942, 3.0033998, 5.1798, -12.747499, 3.0033998, 6.3671, -13.1355, 3.3913999, 6.7549996, -14.0948, 3.3913999, 5.5073, -6.6893, 3.0033998, 6.6893, -10.033899, 3.3913999, 7.0772, -10.033899, 3.0033998, 6.6893, -6.6893, 3.0033998, 6.6893, -6.6893, 3.3913999, 7.0772, -10.033899, 3.3913999, 7.0772, -16.7232, 3.0033998, 0, -16.191599, 3.3913999, -3.0971, -15.8036995, 3.0033998, -2.7091, -16.7232, 3.0033998, 0, -17.1112, 3.3913999, 0, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -3.7326, -15.8036995, 3.0033998, -2.7091, -16.191599, 3.3913999, -3.0971, -13.3786, 3.3913999, -3.7326, -13.3786, 3.0033998, -3.3446, -15.8036995, 3.0033998, -2.7091, -2.9566998, 3.3913999, 0, -4.0257998, 3.0033998, -1.6911999, -3.6378, 3.3913999, -2.0792, -2.9566998, 3.3913999, 0, -3.3446, 3.0033998, 0, -4.0257998, 3.0033998, -1.6911999, -3.6378, 3.3913999, -2.0792, -6.6893, 3.0033998, -3.3446, -6.6893, 3.3913999, -3.7326, -3.6378, 3.3913999, -2.0792, -4.0257998, 3.0033998, -1.6911999, -6.6893, 3.0033998, -3.3446, -10.033899, 3.3913999, -3.7326, -13.3786, 3.0033998, -3.3446, -13.3786, 3.3913999, -3.7326, -10.033899, 3.3913999, -3.7326, -10.033899, 3.0033998, -3.3446, -13.3786, 3.0033998, -3.3446, -6.6893, 3.3913999, -3.7326, -10.033899, 3.0033998, -3.3446, -10.033899, 3.3913999, -3.7326, -6.6893, 3.3913999, -3.7326, -6.6893, 3.0033998, -3.3446, -10.033899, 3.0033998, -3.3446, -16.7232, 3.0033998, 2.9166, -17.1112, 3.3913999, 0, -16.7232, 3.0033998, 0, -16.7232, 3.0033998, 2.9166, -17.1112, 3.3913999, 3.3044999, -17.1112, 3.3913999, 0, -14.4892, 3.0033998, 4.2778, -15.582299, 3.3913999, 4.0366, -15.372, 3.0033998, 3.6132998, -14.4892, 3.0033998, 4.2778, -14.877199, 3.3913999, 4.6657, -15.582299, 3.3913999, 4.0366, -2.9566998, 3.3913999, 3.3446, -3.3446, 3.0033998, 0, -2.9566998, 3.3913999, 0, -2.9566998, 3.3913999, 3.3446, -3.3446, 3.0033998, 3.3446, -3.3446, 3.0033998, 0, -2.1794999, 2.8042998, 8.009399, -2.1397, 3.0033998, 5.1795, -1.7832999, 3.3913999, 5.2162, -2.1794999, 2.8042998, 8.009399, -2.5674999, 2.4164, 7.6215, -2.1397, 3.0033998, 5.1795, -6.6893, 3.3913999, 10.033899, -2.1794999, 2.8042998, 8.009399, -3.3446, 3.3913999, 10.033899, -6.6893, 3.3913999, 10.033899, -6.6893, 3.3913999, 7.0772, -2.1794999, 2.8042998, 8.009399, -1.7832999, 3.3913999, 5.2162, 0, 3.3913999, 6.6893, -2.1794999, 2.8042998, 8.009399, 0, 3.3913999, 6.6893, -1.7832999, 3.3913999, 5.2162, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 6.6893, -2.9566998, 3.3913999, 3.3446, 0, 3.3913999, 3.3446, -3.3446, 3.3913999, 10.033899, 0, 3.3913999, 6.6893, 0, 3.3913999, 10.033899, -3.3446, 3.3913999, 10.033899, -2.1794999, 2.8042998, 8.009399, 0, 3.3913999, 6.6893, -2.1397, 3.0033998, 5.1795, -2.9566998, 3.3913999, 3.3446, -1.7832999, 3.3913999, 5.2162, -2.1397, 3.0033998, 5.1795, -3.3446, 3.0033998, 3.3446, -2.9566998, 3.3913999, 3.3446, -14.0948, 3.3913999, 5.5073, -14.4892, 3.0033998, 4.2778, -13.6942, 3.0033998, 5.1798, -14.0948, 3.3913999, 5.5073, -14.877199, 3.3913999, 4.6657, -14.4892, 3.0033998, 4.2778, -15.582299, 3.3913999, 4.0366, -16.7232, 3.0033998, 2.9166, -15.372, 3.0033998, 3.6132998, -15.582299, 3.3913999, 4.0366, -17.1112, 3.3913999, 3.3044999, -16.7232, 3.0033998, 2.9166, 20.198, 3.1490998, -10.033899, 19.798899, 3.3913999, -13.1096, 20.1539, 3.1299, -13.399799, 20.198, 3.1490998, -10.033899, 19.798899, 3.3913999, -10.033899, 19.798899, 3.3913999, -13.1096, 20.1539, 3.1299, -13.399799, 16.7232, 3.3913999, -13.1096, 16.8204, 3.138, -13.447599, 20.1539, 3.1299, -13.399799, 19.798899, 3.3913999, -13.1096, 16.7232, 3.3913999, -13.1096, 16.756899, 3.1650999, -16.7286, 12.738999, 3.3913999, -15.894899, 12.7471, 3.1766999, -16.1988, 16.756899, 3.1650999, -16.7286, 16.495699, 3.3913999, -16.459, 12.738999, 3.3913999, -15.894899, 16.8051, 3.1778, -13.479, 16.495699, 3.3913999, -16.459, 16.756899, 3.1650999, -16.7286, 16.8051, 3.1778, -13.479, 16.495699, 3.3913999, -13.42, 16.495699, 3.3913999, -16.459, 16.4543, 3.3913999, -13.3786, 16.8204, 3.138, -13.447599, 16.7232, 3.3913999, -13.1096, 16.8204, 3.138, -13.447599, 16.4543, 3.3913999, -13.3786, 16.495699, 3.3913999, -13.42, 16.8204, 3.138, -13.447599, 16.495699, 3.3913999, -13.42, 16.8051, 3.1778, -13.479, 10.033899, 3.0523999, -19.0294, 6.6893, 3.2782998, -18.727999, 6.6893, 3.0440998, -19.036999, 10.033899, 3.0523999, -19.0294, 10.033899, 3.2784998, -18.6997, 6.6893, 3.2782998, -18.727999, 12.723599, 2.9213, -19.2902, 10.033899, 3.2784998, -18.6997, 10.033899, 3.0523999, -19.0294, 12.723599, 2.9213, -19.2902, 12.446099, 3.1699998, -18.9401, 10.033899, 3.2784998, -18.6997, 12.446099, 3.1699998, -18.9401, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 12.446099, 3.1699998, -18.9401, 12.723599, 2.9213, -19.2902, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 10.033899, 3.2784998, -18.6997, 12.446099, 3.1699998, -18.9401, 10.033899, 3.3913999, -16.7232, 10.033899, 3.2784998, -18.6997, 12.460899, 3.3913999, -16.1674, 10.033899, 3.3913999, -16.7232, 12.460899, 3.3913999, -16.1674, 12.460899, 3.3913999, -16.1674, 6.6893, 3.3913999, -16.7232, 10.033899, 3.2784998, -18.6997, 10.033899, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 6.6893, 3.2782998, -18.727999, 10.033899, 3.2784998, -18.6997, 12.738999, 3.3913999, -15.894899, 12.747, 3.1771, -16.1991, 12.7471, 3.1766999, -16.1988, 12.738999, 3.3913999, -15.894899, 12.460899, 3.3913999, -16.1674, 12.747, 3.1771, -16.1991, 12.460899, 3.3913999, -16.1674, 12.738999, 3.3913999, -15.894899, 12.460899, 3.3913999, -16.1674, 3.8388, 3.3913999, -16.7232, 4.4783998, 3.1067, -18.2665, 4.6443, 3.2897, -17.9799, 3.8388, 3.3913999, -16.7232, 3.5895, 3.2033, -16.7232, 4.4783998, 3.1067, -18.2665, 4.6821, 3.3913999, -13.3786, 3.5895, 3.2033, -16.7232, 3.8388, 3.3913999, -16.7232, 4.6821, 3.3913999, -13.3786, 4.4131, 3.2091, -13.357699, 3.5895, 3.2033, -16.7232, 6.6640997, 3.0702, -19.018099, 4.6443, 3.2897, -17.9799, 4.4783998, 3.1067, -18.2665, 6.6640997, 3.0702, -19.018099, 6.6705, 3.2772, -18.741, 4.6443, 3.2897, -17.9799, 4.6443, 3.2897, -17.9799, 6.6893, 3.3913999, -16.7232, 3.8388, 3.3913999, -16.7232, 6.6893, 3.3913999, -16.7232, 4.6443, 3.2897, -17.9799, 6.6705, 3.2772, -18.741, 6.6893, 3.3913999, -16.7232, 6.6705, 3.2772, -18.741, 6.6893, 3.2782998, -18.727999, 6.6893, 3.2782998, -18.727999, 6.6640997, 3.0702, -19.018099, 6.6893, 3.0440998, -19.036999, 6.6893, 3.2782998, -18.727999, 6.6705, 3.2772, -18.741, 6.6640997, 3.0702, -19.018099, 3.7529, 3.1712, -9.0494995, 0, 3.3913999, -8.7143, 0, 3.179, -9.0652, 3.7529, 3.1712, -9.0494995, 4.0225, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, 4.0225, 3.3913999, -8.7143, 4.4131, 3.2091, -13.357699, 4.6821, 3.3913999, -13.3786, 4.0225, 3.3913999, -8.7143, 3.7529, 3.1712, -9.0494995, 4.4131, 3.2091, -13.357699, -3.6538, 3.1585999, -12.813399, -6.6893, 3.3913999, -12.538199, -6.6893, 3.175, -12.867, -3.6538, 3.1585999, -12.813399, -3.9101999, 3.3913999, -12.538199, -6.6893, 3.3913999, -12.538199, -3.6076, 3.1676, -10.0696, -3.9101999, 3.3913999, -12.538199, -3.6538, 3.1585999, -12.813399, -3.6076, 3.1676, -10.0696, -3.9101999, 3.3913999, -9.8592, -3.9101999, 3.3913999, -12.538199, -0.0081, 3.1864998, -9.063499, -3.9101999, 3.3913999, -9.8592, -3.6076, 3.1676, -10.0696, -0.0081, 3.1864998, -9.063499, 0, 3.3913999, -8.7143, -3.9101999, 3.3913999, -9.8592, 0, 3.3913999, -8.7143, -0.0081, 3.1864998, -9.063499, 0, 3.179, -9.0652, 0, 3.3913999, -8.7143, 0, 3.3913999, -8.7143, -0.0081, 3.1864998, -9.063499, -10.4043, 3.1608999, -13.4295, -10.699599, 3.3913999, -15.835, -10.4319, 3.1557999, -16.1556, -10.4043, 3.1608999, -13.4295, -10.699599, 3.3913999, -13.175799, -10.699599, 3.3913999, -15.835, -10.4319, 3.1557999, -16.1556, -13.3786, 3.3913999, -15.4154, -13.3786, 3.1771998, -15.7625, -10.4319, 3.1557999, -16.1556, -10.699599, 3.3913999, -15.835, -13.3786, 3.3913999, -15.4154, -10.699599, 3.3913999, -13.175799, -6.6893, 3.175, -12.867, -6.6893, 3.3913999, -12.538199, -10.699599, 3.3913999, -13.175799, -10.4043, 3.1608999, -13.4295, -6.6893, 3.175, -12.867, -16.130499, 3.6227, -13.623799, -16.0243, 3.3509998, -15.752, -15.7941, 3.5591, -15.4641, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -16.0243, 3.3509998, -15.752, -13.3911, 3.1892, -15.757, -15.7941, 3.5591, -15.4641, -16.0243, 3.3509998, -15.752, -13.3911, 3.1892, -15.757, -13.396, 3.3925, -15.4333, -15.7941, 3.5591, -15.4641, -15.7941, 3.5591, -15.4641, -13.3786, 3.3913999, -13.3786, -16.130499, 3.6227, -13.623799, -13.3786, 3.3913999, -13.3786, -15.7941, 3.5591, -15.4641, -13.396, 3.3925, -15.4333, -13.3786, 3.3913999, -13.3786, -13.396, 3.3925, -15.4333, -13.3786, 3.3913999, -15.4154, -13.3786, 3.3913999, -15.4154, -13.3911, 3.1892, -15.757, -13.3786, 3.1771998, -15.7625, -13.3786, 3.3913999, -15.4154, -13.396, 3.3925, -15.4333, -13.3911, 3.1892, -15.757, -16.3753, 3.4225, -13.6277, -16.1241, 3.3832998, -9.5097, -16.4131, 3.1706, -9.788899, -16.3753, 3.4225, -13.6277, -16.130499, 3.6227, -13.623799, -16.1241, 3.3832998, -9.5097, -16.7232, 3.3913999, -6.6893, -13.3786, 3.3913999, -10.033899, -13.3786, 3.3913999, -6.6893, -16.7232, 3.3913999, -6.6893, -16.1241, 3.3832998, -9.5097, -13.3786, 3.3913999, -10.033899, -16.1241, 3.3832998, -9.5097, -13.3786, 3.3913999, -13.3786, -13.3786, 3.3913999, -10.033899, -16.1241, 3.3832998, -9.5097, -16.130499, 3.6227, -13.623799, -13.3786, 3.3913999, -13.3786, -16.130499, 3.6227, -13.623799, -16.1241, 3.3832998, -9.5097, -16.130499, 3.6227, -13.623799, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -16.3753, 3.4225, -13.6277, -16.130499, 3.6227, -13.623799, -16.130499, 3.6227, -13.623799, -16.3753, 3.4225, -13.6277, -19.7545, 3.3913999, -2.7851999, -19.515, 3.2122, -6.6893, -19.280699, 3.3913999, -6.6893, -19.7545, 3.3913999, -2.7851999, -19.9489, 3.2063, -2.8082, -19.515, 3.2122, -6.6893, -19.280699, 3.3913999, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.3965, 3.3909, -9.5092, -19.280699, 3.3913999, -6.6893, -19.515, 3.2122, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.280699, 3.3913999, -6.6893, -16.1241, 3.3832998, -9.5097, -16.7232, 3.3913999, -6.6893, -19.280699, 3.3913999, -6.6893, -19.3965, 3.3909, -9.5092, -16.1241, 3.3832998, -9.5097, -16.1241, 3.3832998, -9.5097, -19.6237, 3.1655998, -9.757899, -16.4131, 3.1706, -9.788899, -19.6237, 3.1655998, -9.757899, -16.1241, 3.3832998, -9.5097, -19.3965, 3.3909, -9.5092, -19.6237, 3.1655998, -9.757899, -19.3965, 3.3909, -9.5092, -19.6161, 3.2075999, -9.7116995, -22.8657, 3.4937, 3.3613, -23.3676, 3.4459999, 0.1372, -23.170399, 3.6251, 0.122999996, -22.8657, 3.4937, 3.3613, -23.0809, 3.3083, 3.3615, -23.3676, 3.4459999, 0.1372, -23.170399, 3.6251, 0.122999996, -23.390299, 3.3349998, -2.6938999, -23.192099, 3.5300999, -2.4668999, -23.170399, 3.6251, 0.122999996, -23.3676, 3.4459999, 0.1372, -23.390299, 3.3349998, -2.6938999, -23.192099, 3.5300999, -2.4668999, -19.9489, 3.2063, -2.8082, -19.9511, 3.3913999, -2.5834, -23.192099, 3.5300999, -2.4668999, -23.390299, 3.3349998, -2.6938999, -19.9489, 3.2063, -2.8082, -23.192099, 3.5300999, -2.4668999, -20.0679, 3.3913999, 0, -23.170399, 3.6251, 0.122999996, -20.0679, 3.3913999, 0, -23.192099, 3.5300999, -2.4668999, -19.9511, 3.3913999, -2.5834, -19.9511, 3.3913999, -2.5834, -23.192099, 3.5300999, -2.4668999, -19.9511, 3.3913999, -2.5834, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 0, -20.0679, 3.3913999, 3.3446, -22.8657, 3.4937, 3.3613, -23.170399, 3.6251, 0.122999996, -20.0679, 3.3913999, 0, -19.7545, 3.3913999, -2.7851999, -19.9489, 3.2063, -2.8082, -19.9489, 3.2063, -2.8082, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -19.9489, 3.2063, -2.8082, -19.9511, 3.3913999, -2.5834, -19.7545, 3.3913999, -2.7851999, -19.9511, 3.3913999, -2.5834, -23.0777, 3.3197, 3.3734, -22.3151, 3.3913999, 6.6893, -22.563799, 3.2219, 6.6893, -23.0777, 3.3197, 3.3734, -22.8764, 3.4937, 3.3741, -22.3151, 3.3913999, 6.6893, -22.8764, 3.4937, 3.3741, -20.0679, 3.3913999, 6.6893, -22.3151, 3.3913999, 6.6893, -20.0679, 3.3913999, 6.6893, -22.8764, 3.4937, 3.3741, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 6.6893, -22.8657, 3.4937, 3.3613, -20.0679, 3.3913999, 3.3446, -22.8657, 3.4937, 3.3613, -23.0777, 3.3197, 3.3734, -23.0809, 3.3083, 3.3615, -22.8657, 3.4937, 3.3613, -22.8764, 3.4937, 3.3741, -23.0777, 3.3197, 3.3734, -16.285099, 3.3913999, 9.5865, -20.0679, 3.1943, 9.407499, -20.0679, 3.3913999, 9.167399, -16.285099, 3.3913999, 9.5865, -16.2818, 3.1913998, 9.8056, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -20.0679, 3.3913999, 9.167399, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -22.5541, 3.3913999, 9.2058, -20.0679, 3.3913999, 9.167399, -22.5541, 3.3913999, 9.2058, -22.563799, 3.2219, 6.6893, -22.3151, 3.3913999, 6.6893, -22.5541, 3.3913999, 9.2058, -22.791399, 3.2124999, 9.4432, -22.563799, 3.2219, 6.6893, -22.791399, 3.2124999, 9.4432, -22.5541, 3.3913999, 9.2058, -22.791399, 3.2124999, 9.4432, -13.3786, 3.3922, 12.571899, -16.076399, 3.04, 12.735, -15.847199, 3.2614, 12.4973, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, -16.076399, 3.04, 12.735, -15.847199, 3.2614, 12.4973, -16.2818, 3.1913998, 9.8056, -16.053999, 3.3913999, 9.8082, -15.847199, 3.2614, 12.4973, -16.076399, 3.04, 12.735, -16.2818, 3.1913998, 9.8056, -15.847199, 3.2614, 12.4973, -13.3786, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -15.847199, 3.2614, 12.4973, -16.053999, 3.3913999, 9.8082, -13.3786, 3.3913999, 10.033899, -16.053999, 3.3913999, 9.8082, -15.847199, 3.2614, 12.4973, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.2818, 3.1913998, 9.8056, -16.2818, 3.1913998, 9.8056, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -16.2818, 3.1913998, 9.8056, -16.053999, 3.3913999, 9.8082, -16.285099, 3.3913999, 9.5865, -16.053999, 3.3913999, 9.8082, -5.9742, 3.3994, 12.8817, -10.033899, 2.9384, 12.4537, -10.033899, 3.2142, 12.1061, -5.9742, 3.3994, 12.8817, -6.2748, 3.126, 13.122, -10.033899, 2.9384, 12.4537, -10.033899, 3.2142, 12.1061, -13.3786, 3.194, 12.8089, -13.3786, 3.3922, 12.571899, -10.033899, 3.2142, 12.1061, -10.033899, 2.9384, 12.4537, -13.3786, 3.194, 12.8089, -10.033899, 3.2142, 12.1061, -6.6893, 3.3913999, 10.033899, -5.9742, 3.3994, 12.8817, -10.033899, 3.2142, 12.1061, -10.033899, 3.3913999, 10.033899, -6.6893, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -10.033899, 3.3913999, 10.033899, -10.033899, 3.2142, 12.1061, -13.3786, 3.3922, 12.571899, -13.3786, 3.3913999, 10.033899, -10.033899, 3.3913999, 10.033899, -13.3786, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -13.3786, 3.3922, 12.571899, -5.9742, 3.3994, 12.8817, -3.3446, 3.3913999, 10.033899, -3.3446, 3.3913999, 13.3786, -5.9742, 3.3994, 12.8817, -6.6893, 3.3913999, 10.033899, -3.3446, 3.3913999, 10.033899, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, -13.3786, 3.194, 12.8089, -13.3786, 3.3922, 12.571899, -13.3786, 3.3922, 12.571899, -13.3786, 3.194, 12.8089, 3.6399, 3.3913999, 16.2405, 0, 2.9636998, 16.0179, 0, 3.2243998, 15.7093, 3.6399, 3.3913999, 16.2405, 3.6425998, 3.1413999, 16.5127, 0, 2.9636998, 16.0179, 0, 3.2243998, 15.7093, -3.3446, 3.1497998, 15.914599, -3.3446, 3.3913999, 15.6011, 0, 3.2243998, 15.7093, 0, 2.9636998, 16.0179, -3.3446, 3.1497998, 15.914599, -6.2558, 3.1427999, 16.4098, -3.3446, 3.3913999, 15.6011, -3.3446, 3.1497998, 15.914599, -6.2558, 3.1427999, 16.4098, -5.9737997, 3.392, 16.087599, -3.3446, 3.3913999, 15.6011, -3.3446, 3.3913999, 15.6011, 0, 3.3913999, 13.3786, 0, 3.2243998, 15.7093, -3.3446, 3.3913999, 15.6011, -3.3446, 3.3913999, 13.3786, 0, 3.3913999, 13.3786, 0, 3.2243998, 15.7093, 3.3446, 3.3913999, 13.3786, 3.6399, 3.3913999, 16.2405, 0, 3.2243998, 15.7093, 0, 3.3913999, 13.3786, 3.3446, 3.3913999, 13.3786, -5.9737997, 3.392, 16.087599, -3.3446, 3.3913999, 13.3786, -3.3446, 3.3913999, 15.6011, -5.9737997, 3.392, 16.087599, -5.9742, 3.3994, 12.8817, -3.3446, 3.3913999, 13.3786, -6.2748, 3.1260998, 16.4145, -5.9737997, 3.392, 16.087599, -6.2558, 3.1427999, 16.4098, -5.9737997, 3.392, 16.087599, -6.2748, 3.1260998, 16.4145, -6.2748, 3.126, 13.122, -5.9737997, 3.392, 16.087599, -6.2748, 3.126, 13.122, -5.9742, 3.3994, 12.8817, 6.6893, 3.3913999, 18.9861, 3.9266999, 3.5967, 19.6223, 4.1273, 3.7335, 19.3804, 6.6893, 3.3913999, 18.9861, 6.6893, 3.2050998, 19.228699, 3.9266999, 3.5967, 19.6223, 4.1273, 3.7335, 19.3804, 3.6536, 3.2078998, 16.5669, 3.8651, 3.3981998, 16.5647, 4.1273, 3.7335, 19.3804, 3.9266999, 3.5967, 19.6223, 3.6536, 3.2078998, 16.5669, 3.8651, 3.3981998, 16.5647, 6.6893, 3.3913999, 18.9861, 4.1273, 3.7335, 19.3804, 6.6893, 3.3913999, 18.9861, 3.8651, 3.3981998, 16.5647, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 18.9861, 3.9148, 3.3913999, 16.514599, 6.6893, 3.3913999, 16.7232, 3.9148, 3.3913999, 16.514599, 3.6425998, 3.1413999, 16.5127, 3.6399, 3.3913999, 16.2405, 3.6425998, 3.1413999, 16.5127, 3.9148, 3.3913999, 16.514599, 3.8651, 3.3981998, 16.5647, 3.6425998, 3.1413999, 16.5127, 3.8651, 3.3981998, 16.5647, 3.6536, 3.2078998, 16.5669, 9.3066, 3.2186, 19.0653, 10.3743, 3.3913999, 16.7232, 10.5913, 3.1985998, 16.7232, 9.3066, 3.2186, 19.0653, 9.1275, 3.3913999, 18.830599, 10.3743, 3.3913999, 16.7232, 9.1275, 3.3913999, 18.830599, 6.6991997, 3.2148, 19.2255, 6.6949, 3.3913999, 18.9914, 9.1275, 3.3913999, 18.830599, 9.3066, 3.2186, 19.0653, 6.6991997, 3.2148, 19.2255, 6.6893, 3.3913999, 18.9861, 6.6991997, 3.2148, 19.2255, 6.6893, 3.2050998, 19.228699, 6.6893, 3.3913999, 18.9861, 6.6949, 3.3913999, 18.9914, 6.6991997, 3.2148, 19.2255, 9.8131, 3.3546, 13.1979, 10.5893, 3.2117, 16.7106, 10.403999, 3.3911998, 16.6938, 9.8131, 3.3546, 13.1979, 10.0546, 3.1885, 13.4071, 10.5893, 3.2117, 16.7106, 6.6893, 3.3913999, 13.3786, 10.3743, 3.3913999, 16.7232, 6.6893, 3.3913999, 16.7232, 10.3743, 3.3913999, 16.7232, 6.6893, 3.3913999, 13.3786, 9.8131, 3.3546, 13.1979, 10.3743, 3.3913999, 16.7232, 9.8131, 3.3546, 13.1979, 10.403999, 3.3911998, 16.6938, 6.6893, 3.3913999, 13.3786, 10.033899, 3.3913999, 10.033899, 9.8131, 3.3546, 13.1979, 6.6893, 3.3913999, 13.3786, 6.6893, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 10.3743, 3.3913999, 16.7232, 10.5893, 3.2117, 16.7106, 10.5913, 3.1985998, 16.7232, 10.3743, 3.3913999, 16.7232, 10.403999, 3.3911998, 16.6938, 10.5893, 3.2117, 16.7106, 13.1839, 3.2744, 6.6893, 13.5511, 3.1797, 6.6893, 13.4036, 3.0749998, 6.6893, 13.1839, 3.2744, 6.6893, 13.3786, 3.2744, 6.496, 13.5511, 3.1797, 6.6893, 12.9758, 3.2036, 10.033899, 13.1839, 3.2744, 6.6893, 13.4036, 3.0749998, 6.6893, 12.9758, 3.2036, 10.033899, 12.7325, 3.3913999, 10.033899, 13.1839, 3.2744, 6.6893, 13.2631, 3.5611, 13.4043, 12.7325, 3.3913999, 10.033899, 12.9758, 3.2036, 10.033899, 13.2631, 3.5611, 13.4043, 13.025499, 3.6999, 13.1985, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 13.1839, 3.2744, 6.6893, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 10.033899, 10.033899, 3.3913999, 6.6893, 13.1839, 3.2744, 6.6893, 9.8131, 3.3546, 13.1979, 12.7325, 3.3913999, 10.033899, 13.025499, 3.6999, 13.1985, 9.8131, 3.3546, 13.1979, 10.033899, 3.3913999, 10.033899, 12.7325, 3.3913999, 10.033899, 10.033899, 3.3913999, 3.3446, 13.1839, 3.2744, 6.6893, 10.033899, 3.3913999, 6.6893, 13.1839, 3.2744, 6.6893, 10.033899, 3.3913999, 3.3446, 13.3786, 3.3913999, 3.3446, 13.1839, 3.2744, 6.6893, 13.3786, 3.3913999, 3.3446, 13.3786, 3.2744, 6.496, 13.025499, 3.6999, 13.1985, 10.0546, 3.1885, 13.4071, 9.8131, 3.3546, 13.1979, 13.025499, 3.6999, 13.1985, 13.2631, 3.5611, 13.4043, 10.0546, 3.1885, 13.4071, 13.2631, 3.5611, 13.4043, 13.2631, 3.5611, 13.4043, 13.025499, 3.6999, 13.1985, 14.0489, 3.0042, -3.3446, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3028998, -3.6027, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.0865998, 13.1206, 3.3028998, -3.3446, 16.7232, 3.1513, -3.602, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.6027, 16.7232, 3.1513, -3.602, 16.7127, 2.8734, -3.3446, 14.0489, 3.0042, -3.3446, 13.3786, 3.3028998, -3.6027, 16.7232, 3.3913999, -6.6893, 16.7232, 3.1513, -3.602, 13.3786, 3.3028998, -3.6027, 13.3786, 3.3913999, -6.6893, 16.7232, 3.3913999, -6.6893, 10.033899, 3.3913999, -6.6893, 13.1206, 3.3028998, -3.3446, 10.033899, 3.3913999, -3.3446, 13.1206, 3.3028998, -3.3446, 10.033899, 3.3913999, -6.6893, 13.3786, 3.3913999, -6.6893, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3913999, -6.6893, 13.3786, 3.3028998, -3.6027, 20.0679, 0, 0, 14.0489, 3.0042, -3.3446, 20.961199, 0, -3.3446, 20.0679, 0, 3.3446, 14.0489, 3.0042, -3.3446, 20.0679, 0, 0, 14.0489, 3.0042, -3.3446, 13.3786, 3.3913999, 0, 13.3786, 3.3028998, -3.0865998, 14.0489, 3.0042, -3.3446, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 0, 14.0489, 3.0042, -3.3446, 20.0679, 0, 3.3446, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 0, 13.3786, 3.2744, 6.496, 13.3786, 3.3913999, 3.3446, 13.3786, 3.2744, 6.496, 20.0679, 0, 3.3446, 16.7232, 1.5715, 6.6893, 16.7232, 1.5715, 6.6893, 20.0679, 0, 3.3446, 20.0679, -0.124199994, 6.6893, 13.3786, 3.2744, 6.496, 16.7232, 1.5715, 6.6893, 13.5511, 3.1797, 6.6893, 10.033899, 3.3913999, -3.3446, 13.3786, 3.3913999, 0, 10.033899, 3.3913999, 0, 13.3786, 3.3913999, 0, 10.033899, 3.3913999, -3.3446, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3913999, 0, 13.1206, 3.3028998, -3.3446, 13.3786, 3.3028998, -3.0865998, 19.831999, 3.3902998, -10.0008, 20.147, 3.0588999, -6.6893, 19.8322, 3.2891998, -6.6893, 19.831999, 3.3902998, -10.0008, 20.1796, 3.182, -9.995999, 20.147, 3.0588999, -6.6893, 19.8322, 3.2891998, -6.6893, 20.1349, 2.7974, -3.3783998, 19.8322, 3.0491, -3.602, 19.8322, 3.2891998, -6.6893, 20.147, 3.0588999, -6.6893, 20.1349, 2.7974, -3.3783998, 16.7232, 3.1513, -3.602, 19.8322, 3.2891998, -6.6893, 19.8322, 3.0491, -3.602, 16.7232, 3.1513, -3.602, 16.7232, 3.3913999, -6.6893, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -10.033899, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -6.6893, 19.8322, 3.2891998, -6.6893, 16.7232, 3.3913999, -10.033899, 19.798899, 3.3913999, -10.033899, 19.8322, 3.2891998, -6.6893, 19.798899, 3.3913999, -10.033899, 19.831999, 3.3902998, -10.0008, 20.198, 3.1490998, -10.033899, 19.831999, 3.3902998, -10.0008, 19.798899, 3.3913999, -10.033899, 20.198, 3.1490998, -10.033899, 20.1796, 3.182, -9.995999, 19.831999, 3.3902998, -10.0008, 20.144999, 2.7605999, -3.3446, 19.8322, 3.0491, -3.602, 20.1349, 2.7974, -3.3783998, 19.8322, 3.0491, -3.602, 20.144999, 2.7605999, -3.3446, 16.7127, 2.8734, -3.3446, 19.8322, 3.0491, -3.602, 16.7127, 2.8734, -3.3446, 16.7232, 3.1513, -3.602, 15.0508995, 0, 6.6893, 16.7232, 1.5715, 6.6893, 16.7232, 0, 6.6893, 16.7232, 0, 6.6893, 16.7232, 1.5715, 6.6893, 20.0679, -0.124199994, 6.6893, 15.0508995, 0, 6.6893, 13.5511, 3.1797, 6.6893, 16.7232, 1.5715, 6.6893, 15.0508995, 0, 6.6893, 13.4036, 3.0749998, 6.6893, 13.5511, 3.1797, 6.6893, 13.4036, 3.0749998, 6.6893, 15.0508995, 0, 6.6893, 13.8046, 0, 6.6893, -16.7232, 0.6137, 0, -15.7546, 0.6137, 3.5372, -16.7232, 0.6137, 2.9166, -15.7546, 0.6137, 3.5372, -16.7232, 0.6137, 0, -13.3786, 0.6137, 0, -16.7232, 0.6137, 0, -15.8036995, 0.6137, -2.7091, -13.3786, 0.6137, -3.3446, -16.7232, 0.6137, 0, -13.3786, 0.6137, -3.3446, -13.3786, 0.6137, 0, -13.3786, 0.6137, 0, -13.3786, 0.6137, -3.3446, -10.033899, 0.6137, -3.3446, -15.7546, 0.6137, 3.5372, -13.3786, 0.6137, 0, -14.4892, 0.6137, 4.2778, -13.3786, 0.6137, 0, -10.033899, 0.6137, -3.3446, -10.033899, 0.6137, 0, -14.4892, 0.6137, 4.2778, -13.3786, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, 0, -10.033899, 0.6137, -3.3446, -6.6893, 0.6137, -3.3446, -10.033899, 0.6137, 0, -6.6893, 0.6137, -3.3446, -6.6893, 0.6137, 0, -6.6893, 0.6137, 0, -6.6893, 0.6137, -3.3446, -4.0257998, 0.6137, -1.6911999, -6.6893, 0.6137, 0, -4.0257998, 0.6137, -1.6911999, -3.3446, 0.6137, 0, -10.033899, 0.6137, 3.3446, -10.033899, 0.6137, 0, -6.6893, 0.6137, 0, -14.4892, 0.6137, 4.2778, -10.033899, 0.6137, 0, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 0, -3.3446, 0.6137, 0, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 0, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -3.3446, 0.6137, 0, -3.3446, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -14.4892, 0.6137, 4.2778, -10.033899, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -10.033899, 0.6137, 3.3446, -6.6893, 0.6137, 3.3446, -10.033899, 0.6137, 6.6893, -6.6893, 0.6137, 3.3446, -6.6893, 0.6137, 6.6893, -10.033899, 0.6137, 6.6893, -13.677099, 0.6137, 5.2616, -14.4892, 0.6137, 4.2778, -13.677099, 0.6137, 5.2616, -10.033899, 0.6137, 6.6893, -12.747499, 0.6137, 6.3671, -6.6893, 0.6137, 3.3446, -2.5674999, 0.0266, 7.6215, -6.6893, 0.6137, 6.6893, -2.5674999, 0.0266, 7.6215, -6.6893, 0.6137, 3.3446, -3.3446, 0.6137, 3.3446, -2.5674999, 0.0266, 7.6215, -3.3446, 0.6137, 3.3446, -2.1193, 0.6137, 5.2736998, -16.7232, 3.0033998, 2.9166, -15.7546, 0.6137, 3.5372, -15.372, 3.0033998, 3.6132998, -16.7232, 3.0033998, 2.9166, -16.7232, 0.6137, 2.9166, -15.7546, 0.6137, 3.5372, -14.4892, 3.0033998, 4.2778, -13.677099, 0.6137, 5.2616, -13.6942, 3.0033998, 5.1798, -14.4892, 3.0033998, 4.2778, -14.4892, 0.6137, 4.2778, -13.677099, 0.6137, 5.2616, -15.8036995, 3.0033998, -2.7091, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -15.8036995, 3.0033998, -2.7091, -15.8036995, 0.6137, -2.7091, -16.7232, 0.6137, 0, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 2.9166, -16.7232, 3.0033998, 2.9166, -16.7232, 3.0033998, 0, -16.7232, 0.6137, 0, -16.7232, 0.6137, 2.9166, -3.3446, 3.0033998, 0, -4.0257998, 0.6137, -1.6911999, -4.0257998, 3.0033998, -1.6911999, -3.3446, 3.0033998, 0, -3.3446, 0.6137, 0, -4.0257998, 0.6137, -1.6911999, -3.3446, 3.0033998, 3.3446, -3.3446, 0.6137, 0, -3.3446, 3.0033998, 0, -3.3446, 3.0033998, 3.3446, -3.3446, 0.6137, 3.3446, -3.3446, 0.6137, 0, -12.747499, 3.0033998, 6.3671, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, 6.6893, -12.747499, 3.0033998, 6.3671, -12.747499, 0.6137, 6.3671, -10.033899, 0.6137, 6.6893, -10.033899, 3.0033998, -3.3446, -13.3786, 0.6137, -3.3446, -13.3786, 3.0033998, -3.3446, -10.033899, 3.0033998, -3.3446, -10.033899, 0.6137, -3.3446, -13.3786, 0.6137, -3.3446, -6.6893, 3.0033998, -3.3446, -10.033899, 0.6137, -3.3446, -10.033899, 3.0033998, -3.3446, -6.6893, 3.0033998, -3.3446, -6.6893, 0.6137, -3.3446, -10.033899, 0.6137, -3.3446, -13.3786, 3.0033998, -3.3446, -15.8036995, 0.6137, -2.7091, -15.8036995, 3.0033998, -2.7091, -13.3786, 3.0033998, -3.3446, -13.3786, 0.6137, -3.3446, -15.8036995, 0.6137, -2.7091, -4.0257998, 3.0033998, -1.6911999, -6.6893, 0.6137, -3.3446, -6.6893, 3.0033998, -3.3446, -4.0257998, 3.0033998, -1.6911999, -4.0257998, 0.6137, -1.6911999, -6.6893, 0.6137, -3.3446, -6.6893, 3.0033998, 6.6893, -2.5674999, 0.0266, 7.6215, -2.5674999, 2.4164, 7.6215, -6.6893, 3.0033998, 6.6893, -6.6893, 0.6137, 6.6893, -2.5674999, 0.0266, 7.6215, -10.033899, 3.0033998, 6.6893, -6.6893, 0.6137, 6.6893, -6.6893, 3.0033998, 6.6893, -10.033899, 3.0033998, 6.6893, -10.033899, 0.6137, 6.6893, -6.6893, 0.6137, 6.6893, -2.5674999, 2.4164, 7.6215, -2.1193, 0.6137, 5.2736998, -2.1397, 3.0033998, 5.1795, -2.5674999, 2.4164, 7.6215, -2.5674999, 0.0266, 7.6215, -2.1193, 0.6137, 5.2736998, -2.1193, 0.6137, 5.2736998, -3.3446, 3.0033998, 3.3446, -2.1397, 3.0033998, 5.1795, -2.1193, 0.6137, 5.2736998, -3.3446, 0.6137, 3.3446, -3.3446, 3.0033998, 3.3446, -13.677099, 0.6137, 5.2616, -12.747499, 3.0033998, 6.3671, -13.6942, 3.0033998, 5.1798, -13.677099, 0.6137, 5.2616, -12.747499, 0.6137, 6.3671, -12.747499, 3.0033998, 6.3671, -15.7546, 0.6137, 3.5372, -14.4892, 3.0033998, 4.2778, -15.372, 3.0033998, 3.6132998, -15.7546, 0.6137, 3.5372, -14.4892, 0.6137, 4.2778, -14.4892, 3.0033998, 4.2778, 21.1831, 0, -13.6544, 16.8204, 3.138, -13.447599, 18.0241, 0, -14.3019, 21.1831, 0, -13.6544, 20.1539, 3.1299, -13.399799, 16.8204, 3.138, -13.447599, 21.8888, 0, -10.033899, 20.1539, 3.1299, -13.399799, 21.1831, 0, -13.6544, 21.8888, 0, -10.033899, 20.198, 3.1490998, -10.033899, 20.1539, 3.1299, -13.399799, 17.2285, 0, -16.8036, 12.7471, 3.1766999, -16.1988, 13.7014, 0, -17.416399, 17.2285, 0, -16.8036, 16.756899, 3.1650999, -16.7286, 12.7471, 3.1766999, -16.1988, 16.8204, 3.138, -13.447599, 17.2285, 0, -16.8036, 18.0241, 0, -14.3019, 17.2285, 0, -16.8036, 16.8204, 3.138, -13.447599, 16.8051, 3.1778, -13.479, 17.2285, 0, -16.8036, 16.8051, 3.1778, -13.479, 16.756899, 3.1650999, -16.7286, 12.7471, 3.1766999, -16.1988, 13.3786, 0, -20.5992, 13.7014, 0, -17.416399, 13.3786, 0, -20.5992, 12.7471, 3.1766999, -16.1988, 12.747, 3.1771, -16.1991, 13.3786, 0, -20.5992, 12.747, 3.1771, -16.1991, 12.723599, 2.9213, -19.2902, 13.3786, 0, -20.5992, 10.033899, 3.0523999, -19.0294, 10.033899, 0, -20.467, 13.3786, 0, -20.5992, 12.723599, 2.9213, -19.2902, 10.033899, 3.0523999, -19.0294, 10.033899, 0, -20.467, 6.6893, 3.0440998, -19.036999, 6.6893, 0, -20.1318, 10.033899, 0, -20.467, 10.033899, 3.0523999, -19.0294, 6.6893, 3.0440998, -19.036999, 3.0140998, 0, -16.7232, 4.4131, 3.2091, -13.357699, 3.3446, -0.3989, -12.9443, 3.0140998, 0, -16.7232, 3.5895, 3.2033, -16.7232, 4.4131, 3.2091, -13.357699, 6.6893, 3.0440998, -19.036999, 3.3446, 0, -20.5992, 6.6893, 0, -20.1318, 3.3446, 0, -20.5992, 6.6893, 3.0440998, -19.036999, 6.6640997, 3.0702, -19.018099, 3.3446, 0, -20.5992, 6.6640997, 3.0702, -19.018099, 4.4783998, 3.1067, -18.2665, 3.3446, 0, -20.5992, 3.5895, 3.2033, -16.7232, 3.0140998, 0, -16.7232, 3.3446, 0, -20.5992, 4.4783998, 3.1067, -18.2665, 3.5895, 3.2033, -16.7232, 2.8241, 0, -10.4594, 0, 3.179, -9.0652, 0, 0, -10.7654, 2.8241, 0, -10.4594, 3.7529, 3.1712, -9.0494995, 0, 3.179, -9.0652, 3.3446, -0.3989, -12.9443, 3.7529, 3.1712, -9.0494995, 2.8241, 0, -10.4594, 3.3446, -0.3989, -12.9443, 4.4131, 3.2091, -13.357699, 3.7529, 3.1712, -9.0494995, 4.4131, 3.2091, -13.357699, 3.3446, -0.3989, -12.9443, 4.4131, 3.2091, -13.357699, -2.6297, 0, -10.5745, -3.6538, 3.1585999, -12.813399, -3.3446, 0, -13.3786, -2.6297, 0, -10.5745, -3.6076, 3.1676, -10.0696, -3.6538, 3.1585999, -12.813399, -3.3446, 0, -13.3786, -6.6893, 3.175, -12.867, -6.6893, 0, -14.2637, -3.3446, 0, -13.3786, -3.6538, 3.1585999, -12.813399, -6.6893, 3.175, -12.867, 0, 3.179, -9.0652, -2.6297, 0, -10.5745, 0, 0, -10.7654, -2.6297, 0, -10.5745, 0, 3.179, -9.0652, -0.0081, 3.1864998, -9.063499, -2.6297, 0, -10.5745, -0.0081, 3.1864998, -9.063499, -3.6076, 3.1676, -10.0696, -6.6893, 0, -14.2637, -10.4043, 3.1608999, -13.4295, -9.6168995, 0, -14.127899, -6.6893, 0, -14.2637, -6.6893, 3.175, -12.867, -10.4043, 3.1608999, -13.4295, -6.6893, 3.175, -12.867, -6.6893, 0, -14.2637, -6.6893, 3.175, -12.867, -9.6168995, 0, -14.127899, -10.4319, 3.1557999, -16.1556, -10.033899, 0, -16.7232, -9.6168995, 0, -14.127899, -10.4043, 3.1608999, -13.4295, -10.4319, 3.1557999, -16.1556, -10.033899, 0, -16.7232, -13.3786, 3.1771998, -15.7625, -13.3786, 0, -17.3364, -10.033899, 0, -16.7232, -10.4319, 3.1557999, -16.1556, -13.3786, 3.1771998, -15.7625, -16.7232, 0, -16.7232, -16.3753, 3.4225, -13.6277, -16.7232, 0, -13.3786, -16.7232, 0, -16.7232, -16.0243, 3.3509998, -15.752, -16.3753, 3.4225, -13.6277, -13.3786, 3.1771998, -15.7625, -16.7232, 0, -16.7232, -13.3786, 0, -17.3364, -16.7232, 0, -16.7232, -13.3786, 3.1771998, -15.7625, -13.3911, 3.1892, -15.757, -16.7232, 0, -16.7232, -13.3911, 3.1892, -15.757, -16.0243, 3.3509998, -15.752, -17.275, 0, -10.5169, -19.6237, 3.1655998, -9.757899, -20.0679, 0, -10.033899, -17.275, 0, -10.5169, -16.4131, 3.1706, -9.788899, -19.6237, 3.1655998, -9.757899, -16.7232, 0, -13.3786, -16.4131, 3.1706, -9.788899, -17.275, 0, -10.5169, -16.7232, 0, -13.3786, -16.3753, 3.4225, -13.6277, -16.4131, 3.1706, -9.788899, -16.3753, 3.4225, -13.6277, -16.7232, 0, -13.3786, -16.3753, 3.4225, -13.6277, -20.3764, 0, -6.6893, -19.9489, 3.2063, -2.8082, -20.0679, -0.3484, -3.5669, -20.3764, 0, -6.6893, -19.515, 3.2122, -6.6893, -19.9489, 3.2063, -2.8082, -19.6237, 3.1655998, -9.757899, -20.3764, 0, -6.6893, -20.0679, 0, -10.033899, -20.3764, 0, -6.6893, -19.6237, 3.1655998, -9.757899, -19.6161, 3.2075999, -9.7116995, -20.3764, 0, -6.6893, -19.6161, 3.2075999, -9.7116995, -19.515, 3.2122, -6.6893, -23.412498, -0.2913, 0.26119998, -23.0809, 3.3083, 3.3615, -23.412498, 0, 3.3446, -23.412498, -0.2913, 0.26119998, -23.3676, 3.4459999, 0.1372, -23.0809, 3.3083, 3.3615, -23.412498, 0, -3.3446, -23.3676, 3.4459999, 0.1372, -23.412498, -0.2913, 0.26119998, -23.412498, 0, -3.3446, -23.390299, 3.3349998, -2.6938999, -23.3676, 3.4459999, 0.1372, -20.0679, -0.3484, -3.5669, -23.390299, 3.3349998, -2.6938999, -23.412498, 0, -3.3446, -20.0679, -0.3484, -3.5669, -19.9489, 3.2063, -2.8082, -23.390299, 3.3349998, -2.6938999, -19.9489, 3.2063, -2.8082, -20.0679, -0.3484, -3.5669, -19.9489, 3.2063, -2.8082, -23.0809, 3.3083, 3.3615, -23.8078, 0, 6.6893, -23.412498, 0, 3.3446, -23.8078, 0, 6.6893, -23.0809, 3.3083, 3.3615, -23.0777, 3.3197, 3.3734, -23.8078, 0, 6.6893, -23.0777, 3.3197, 3.3734, -22.563799, 3.2219, 6.6893, -23.8078, 0, 6.6893, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -23.8078, 0, 6.6893, -22.563799, 3.2219, 6.6893, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -20.0679, 3.1943, 9.407499, -20.0679, 0, 10.033899, -23.412498, 0, 10.033899, -22.791399, 3.2124999, 9.4432, -20.0679, 3.1943, 9.407499, -22.791399, 3.2124999, 9.4432, -23.412498, 0, 10.033899, -22.791399, 3.2124999, 9.4432, -20.0679, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 10.033899, -20.0679, 0, 10.033899, -20.0679, 3.1943, 9.407499, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 13.3786, -13.3786, 3.194, 12.8089, -13.3786, 0, 13.3786, -16.7232, 0, 13.3786, -16.076399, 3.04, 12.735, -13.3786, 3.194, 12.8089, -16.7232, 0, 10.033899, -16.076399, 3.04, 12.735, -16.7232, 0, 13.3786, -16.7232, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -16.076399, 3.04, 12.735, -16.2818, 3.1913998, 9.8056, -16.7232, 0, 10.033899, -16.2818, 3.1913998, 9.8056, -13.3786, 0, 13.3786, -10.033899, 2.9384, 12.4537, -10.033899, 0, 13.3786, -13.3786, 0, 13.3786, -13.3786, 3.194, 12.8089, -10.033899, 2.9384, 12.4537, -13.3786, 3.194, 12.8089, -13.3786, 0, 13.3786, -13.3786, 3.194, 12.8089, -10.033899, 0, 13.3786, -6.2748, 3.126, 13.122, -6.6893, 0, 13.3786, -10.033899, 0, 13.3786, -10.033899, 2.9384, 12.4537, -6.2748, 3.126, 13.122, -6.6893, 0, 13.3786, -6.2748, 3.1260998, 16.4145, -6.6893, 0, 16.7232, -6.6893, 0, 13.3786, -6.2748, 3.126, 13.122, -6.2748, 3.1260998, 16.4145, -3.3446, 0, 16.7232, 0, 2.9636998, 16.0179, 0, 0, 16.7232, -3.3446, 0, 16.7232, -3.3446, 3.1497998, 15.914599, 0, 2.9636998, 16.0179, 0, 0, 16.7232, 3.6425998, 3.1413999, 16.5127, 3.3446, 0, 16.7232, 0, 0, 16.7232, 0, 2.9636998, 16.0179, 3.6425998, 3.1413999, 16.5127, -6.2748, 3.1260998, 16.4145, -3.3446, 0, 16.7232, -6.6893, 0, 16.7232, -3.3446, 0, 16.7232, -6.2748, 3.1260998, 16.4145, -6.2558, 3.1427999, 16.4098, -3.3446, 0, 16.7232, -6.2558, 3.1427999, 16.4098, -3.3446, 3.1497998, 15.914599, 3.3446, 0, 20.0679, 6.6893, 3.2050998, 19.228699, 6.6893, 0, 20.0679, 3.3446, 0, 20.0679, 3.9266999, 3.5967, 19.6223, 6.6893, 3.2050998, 19.228699, 3.6425998, 3.1413999, 16.5127, 3.3446, 0, 20.0679, 3.3446, 0, 16.7232, 3.3446, 0, 20.0679, 3.6425998, 3.1413999, 16.5127, 3.6536, 3.2078998, 16.5669, 3.3446, 0, 20.0679, 3.6536, 3.2078998, 16.5669, 3.9266999, 3.5967, 19.6223, 6.6893, 3.2050998, 19.228699, 10.572599, 0, 20.0679, 6.6893, 0, 20.0679, 10.572599, 0, 20.0679, 6.6893, 3.2050998, 19.228699, 6.6991997, 3.2148, 19.2255, 10.572599, 0, 20.0679, 6.6991997, 3.2148, 19.2255, 9.3066, 3.2186, 19.0653, 10.572599, 0, 20.0679, 10.5913, 3.1985998, 16.7232, 10.572599, 0, 16.7232, 10.572599, 0, 20.0679, 9.3066, 3.2186, 19.0653, 10.5913, 3.1985998, 16.7232, 10.5913, 3.1985998, 16.7232, 10.572599, 0, 13.918799, 10.572599, 0, 16.7232, 10.572599, 0, 13.918799, 10.5913, 3.1985998, 16.7232, 10.5893, 3.2117, 16.7106, 10.572599, 0, 13.918799, 10.5893, 3.2117, 16.7106, 10.0546, 3.1885, 13.4071, 10.572599, 0, 13.918799, 13.2631, 3.5611, 13.4043, 13.8046, 0, 13.918799, 10.572599, 0, 13.918799, 10.0546, 3.1885, 13.4071, 13.2631, 3.5611, 13.4043, 13.8046, 0, 13.918799, 12.9758, 3.2036, 10.033899, 13.8046, 0, 10.033899, 13.8046, 0, 13.918799, 13.2631, 3.5611, 13.4043, 12.9758, 3.2036, 10.033899, 13.8046, 0, 13.918799, 13.2631, 3.5611, 13.4043, 13.2631, 3.5611, 13.4043, 13.8046, 0, 10.033899, 13.4036, 3.0749998, 6.6893, 13.8046, 0, 6.6893, 13.8046, 0, 10.033899, 12.9758, 3.2036, 10.033899, 13.4036, 3.0749998, 6.6893, 20.961199, 0, -3.3446, 16.7127, 2.8734, -3.3446, 20.144999, 2.7605999, -3.3446, 20.961199, 0, -3.3446, 14.0489, 3.0042, -3.3446, 16.7127, 2.8734, -3.3446, 20.144999, 2.7605999, -3.3446, 21.2349, 0, -6.6893, 20.961199, 0, -3.3446, 21.2349, 0, -6.6893, 20.144999, 2.7605999, -3.3446, 20.1349, 2.7974, -3.3783998, 21.2349, 0, -6.6893, 20.1349, 2.7974, -3.3783998, 20.147, 3.0588999, -6.6893, 20.147, 3.0588999, -6.6893, 21.8888, 0, -10.033899, 21.2349, 0, -6.6893, 21.8888, 0, -10.033899, 20.147, 3.0588999, -6.6893, 20.1796, 3.182, -9.995999, 21.8888, 0, -10.033899, 20.1796, 3.182, -9.995999, 20.198, 3.1490998, -10.033899, 23.412498, 0, -26.7572, 26.7572, -0.2593, -23.412498, 23.412498, 0, -23.412498, 26.7572, -0.2593, -23.412498, 23.412498, 0, -26.7572, 26.7572, -0.2593, -25.084799, 26.7572, -0.2593, -25.084799, 23.412498, 0, -26.7572, 26.7572, -0.2593, -26.7572, 26.7572, -0.2593, -21.7402, 23.412498, 0, -23.412498, 26.7572, -0.2593, -23.412498, 26.7572, -0.2593, -20.0679, 23.412498, 0, -23.412498, 26.7572, -0.2593, -21.7402, 23.412498, 0, -23.412498, 26.7572, -0.2593, -20.0679, 23.412498, 0, -20.0679, 26.7572, -0.2593, -18.3955, 23.412498, 0, -20.0679, 26.7572, -0.2593, -20.0679, 26.7572, -0.2593, -16.7232, 23.412498, 0, -20.0679, 26.7572, -0.2593, -18.3955, 23.412498, 0, -20.0679, 26.7572, -0.2593, -16.7232, 23.412498, 0, -16.7232, 26.7572, -0.2593, -15.0508995, 23.412498, 0, -16.7232, 26.7572, -0.2593, -16.7232, 26.7572, -0.2593, -13.3786, 23.412498, 0, -16.7232, 26.7572, -0.2593, -15.0508995, 23.412498, 0, -16.7232, 26.7572, -0.2593, -13.3786, 23.412498, 0, -13.3786, 26.7572, -0.2593, -11.7063, 23.412498, 0, -13.3786, 26.7572, -0.2593, -13.3786, 26.7572, -0.2593, -10.033899, 23.412498, 0, -13.3786, 26.7572, -0.2593, -11.7063, 23.412498, 0, -13.3786, 26.7572, -0.2593, -10.033899, 23.412498, 0, -10.033899, 26.7572, -0.2593, -8.3616, 23.412498, 0, -10.033899, 26.7572, -0.2593, -10.033899, 26.7572, -0.2593, -6.6893, 23.412498, 0, -10.033899, 26.7572, -0.2593, -8.3616, 23.412498, 0, -10.033899, 26.7572, -0.2593, -6.6893, 23.412498, 0, -6.6893, 26.7572, -0.2593, -5.0169997, 23.412498, 0, -6.6893, 26.7572, -0.2593, -6.6893, 26.7572, -0.2593, -3.3446, 23.412498, 0, -6.6893, 26.7572, -0.2593, -5.0169997, 23.412498, 0, -6.6893, 26.7572, -0.2593, -3.3446, 23.412498, 0, -3.3446, 26.7572, -0.2593, -1.6723, 23.412498, 0, -3.3446, 26.7572, -0.2593, -3.3446, 26.7572, -0.2593, 0, 23.412498, 0, -3.3446, 26.7572, -0.2593, -1.6723, 23.412498, 0, -3.3446, 26.7572, -0.2593, 0, 23.412498, 0, 0, 26.7572, -0.2593, 1.6723, 23.412498, 0, 0, 26.7572, -0.2593, 0, 26.7572, -0.2593, 3.3446, 23.412498, 0, 0, 26.7572, -0.2593, 1.6723, 23.412498, 0, 0, 26.7572, -0.2593, 3.3446, 23.412498, 0, 3.3446, 26.7572, -0.2593, 5.0169997, 23.412498, 0, 3.3446, 26.7572, -0.2593, 3.3446, 26.7572, -0.2593, 6.6893, 23.412498, 0, 3.3446, 26.7572, -0.2593, 5.0169997, 23.412498, 0, 3.3446, 26.7572, -0.2593, 6.6893, 23.412498, 0, 6.6893, 26.7572, -0.2593, 8.3616, 23.412498, 0, 6.6893, 26.7572, -0.2593, 6.6893, 26.7572, -0.2593, 10.033899, 23.412498, 0, 6.6893, 26.7572, -0.2593, 8.3616, 23.412498, 0, 6.6893, 26.7572, -0.2593, 10.033899, 23.412498, 0, 10.033899, 26.7572, -0.2593, 11.7063, 23.412498, 0, 10.033899, 26.7572, -0.2593, 10.033899, 26.7572, -0.2593, 13.3786, 23.412498, 0, 10.033899, 26.7572, -0.2593, 11.7063, 23.412498, 0, 10.033899, 26.7572, -0.2593, 13.3786, 23.412498, 0, 13.3786, 26.7572, -0.2593, 15.0508995, 23.412498, 0, 13.3786, 26.7572, -0.2593, 13.3786, 26.7572, -0.2593, 16.7232, 23.412498, 0, 13.3786, 26.7572, -0.2593, 15.0508995, 23.412498, 0, 13.3786, 26.7572, -0.2593, 16.7232, 23.412498, 0, 16.7232, 26.7572, -0.2593, 18.3955, 23.412498, 0, 16.7232, 26.7572, -0.2593, 16.7232, 26.7572, -0.2593, 20.0679, 23.412498, 0, 16.7232, 26.7572, -0.2593, 18.3955, 23.412498, 0, 16.7232, 26.7572, -0.2593, 20.0679, 23.412498, 0, 20.0679, 26.7572, -0.2593, 21.7402, 23.412498, 0, 20.0679, 26.7572, -0.2593, 20.0679, 26.7572, -0.2593, 23.412498, 23.412498, 0, 20.0679, 26.7572, -0.2593, 21.7402, 23.412498, 0, 20.0679, 26.7572, -0.2593, 23.412498, 23.412498, 0, 23.412498, 26.7572, -0.2593, 25.084799, 23.412498, 0, 23.412498, 26.7572, -0.2593, 23.412498, 26.7572, -0.2593, 26.7572, 23.412498, 0, 23.412498, 26.7572, -0.2593, 25.084799, 23.412498, 0, 23.412498, 26.7572, -0.2593, 26.7572, 23.412498, 0, 26.7572, -3.3446, 0, -23.412498, 0, 0, -26.7572, 0, 0, -23.412498, 0, 0, -23.412498, 0, 0, -26.7572, 3.3446, 0, -26.7572, -3.3446, 0, -23.412498, -3.3446, 0, -26.7572, 0, 0, -26.7572, 0, 0, -23.412498, 3.3446, 0, -26.7572, 3.3446, 0, -23.412498, 3.3446, 0, -23.412498, 3.3446, 0, -26.7572, 6.6893, 0, -26.7572, -3.3446, 0, -20.0679, -3.3446, 0, -23.412498, 0, 0, -23.412498, 3.3446, 0, -23.412498, 6.6893, 0, -26.7572, 6.6893, 0, -23.412498, 6.6893, 0, -23.412498, 6.6893, 0, -26.7572, 10.033899, 0, -26.7572, 0, 0, -20.0679, 0, 0, -23.412498, 3.3446, 0, -23.412498, -3.3446, 0, -20.0679, 0, 0, -23.412498, 0, 0, -20.0679, 3.3446, 0, -20.5992, 3.3446, 0, -23.412498, 6.6893, 0, -23.412498, 0, 0, -20.0679, 3.3446, 0, -23.412498, 3.3446, 0, -20.5992, 3.3446, 0, -20.5992, 6.6893, 0, -23.412498, 6.6893, 0, -20.1318, 6.6893, 0, -23.412498, 10.033899, 0, -26.7572, 10.033899, 0, -23.412498, 6.6893, 0, -20.1318, 6.6893, 0, -23.412498, 10.033899, 0, -23.412498, 10.033899, 0, -23.412498, 10.033899, 0, -26.7572, 13.3786, 0, -26.7572, 6.6893, 0, -20.1318, 10.033899, 0, -23.412498, 10.033899, 0, -20.467, 10.033899, 0, -23.412498, 13.3786, 0, -26.7572, 13.3786, 0, -23.412498, 10.033899, 0, -20.467, 10.033899, 0, -23.412498, 13.3786, 0, -23.412498, 13.3786, 0, -23.412498, 13.3786, 0, -26.7572, 16.7232, 0, -26.7572, 10.033899, 0, -20.467, 13.3786, 0, -23.412498, 13.3786, 0, -20.5992, 13.3786, 0, -23.412498, 16.7232, 0, -26.7572, 16.7232, 0, -23.412498, 13.3786, 0, -20.5992, 13.3786, 0, -23.412498, 16.7232, 0, -23.412498, 16.7232, 0, -23.412498, 16.7232, 0, -26.7572, 20.0679, 0, -26.7572, 13.3786, 0, -20.5992, 16.7232, 0, -23.412498, 16.7232, 0, -20.0679, 13.7014, 0, -17.416399, 13.3786, 0, -20.5992, 16.7232, 0, -20.0679, 13.7014, 0, -17.416399, 16.7232, 0, -20.0679, 17.2285, 0, -16.8036, 16.7232, 0, -23.412498, 20.0679, 0, -26.7572, 20.0679, 0, -23.412498, 16.7232, 0, -20.0679, 16.7232, 0, -23.412498, 20.0679, 0, -23.412498, 20.0679, 0, -23.412498, 20.0679, 0, -26.7572, 23.412498, 0, -26.7572, 20.0679, 0, -23.412498, 23.412498, 0, -26.7572, 23.412498, 0, -23.412498, 16.7232, 0, -20.0679, 20.0679, 0, -23.412498, 20.0679, 0, -20.0679, 20.0679, 0, -20.0679, 20.0679, 0, -23.412498, 23.412498, 0, -23.412498, 17.2285, 0, -16.8036, 16.7232, 0, -20.0679, 20.0679, 0, -20.0679, 20.0679, 0, -20.0679, 23.412498, 0, -23.412498, 23.412498, 0, -20.0679, 17.2285, 0, -16.8036, 20.0679, 0, -20.0679, 20.0679, 0, -16.7232, 20.0679, 0, -16.7232, 20.0679, 0, -20.0679, 23.412498, 0, -20.0679, 18.0241, 0, -14.3019, 17.2285, 0, -16.8036, 20.0679, 0, -16.7232, 20.0679, 0, -16.7232, 23.412498, 0, -20.0679, 23.412498, 0, -16.7232, 18.0241, 0, -14.3019, 20.0679, 0, -16.7232, 21.1831, 0, -13.6544, 21.1831, 0, -13.6544, 20.0679, 0, -16.7232, 23.412498, 0, -16.7232, 21.1831, 0, -13.6544, 23.412498, 0, -16.7232, 23.412498, 0, -13.3786, 21.8888, 0, -10.033899, 21.1831, 0, -13.6544, 23.412498, 0, -13.3786, 21.8888, 0, -10.033899, 23.412498, 0, -13.3786, 23.412498, 0, -10.033899, 21.2349, 0, -6.6893, 21.8888, 0, -10.033899, 23.412498, 0, -10.033899, 21.2349, 0, -6.6893, 23.412498, 0, -10.033899, 23.412498, 0, -6.6893, 20.961199, 0, -3.3446, 21.2349, 0, -6.6893, 23.412498, 0, -6.6893, 20.961199, 0, -3.3446, 23.412498, 0, -6.6893, 23.412498, 0, -3.3446, 20.0679, 0, 0, 20.961199, 0, -3.3446, 23.412498, 0, -3.3446, 20.0679, 0, 0, 23.412498, 0, -3.3446, 23.412498, 0, 0, 20.0679, 0, 3.3446, 20.0679, 0, 0, 23.412498, 0, 0, 20.0679, 0, 3.3446, 23.412498, 0, 0, 23.412498, 0, 3.3446, 10.572599, 0, 16.7232, 13.8046, 0, 13.918799, 13.3786, 0, 16.7232, 10.572599, 0, 16.7232, 10.572599, 0, 13.918799, 13.8046, 0, 13.918799, 10.572599, 0, 20.0679, 10.572599, 0, 16.7232, 13.3786, 0, 16.7232, 13.3786, 0, 16.7232, 13.8046, 0, 13.918799, 15.0508995, 0, 13.3786, 10.572599, 0, 20.0679, 13.3786, 0, 16.7232, 13.3786, 0, 20.0679, 13.8046, 0, 13.918799, 15.0508995, 0, 10.033899, 15.0508995, 0, 13.3786, 13.8046, 0, 13.918799, 13.8046, 0, 10.033899, 15.0508995, 0, 10.033899, 13.8046, 0, 10.033899, 15.0508995, 0, 6.6893, 15.0508995, 0, 10.033899, 13.8046, 0, 10.033899, 13.8046, 0, 6.6893, 15.0508995, 0, 6.6893, 15.0508995, 0, 10.033899, 15.0508995, 0, 6.6893, 16.7232, 0, 6.6893, 15.0508995, 0, 10.033899, 16.7232, 0, 6.6893, 16.7232, 0, 10.033899, 15.0508995, 0, 13.3786, 15.0508995, 0, 10.033899, 16.7232, 0, 10.033899, 13.3786, 0, 16.7232, 15.0508995, 0, 13.3786, 15.0508995, 0, 16.7232, 13.3786, 0, 20.0679, 13.3786, 0, 16.7232, 15.0508995, 0, 16.7232, 15.0508995, 0, 13.3786, 16.7232, 0, 10.033899, 16.7232, 0, 13.3786, 15.0508995, 0, 16.7232, 15.0508995, 0, 13.3786, 16.7232, 0, 13.3786, 16.7232, 0, 13.3786, 16.7232, 0, 10.033899, 20.0679, 0, 10.033899, 16.7232, 0, 13.3786, 20.0679, 0, 10.033899, 20.0679, 0, 13.3786, 20.0679, 0, 13.3786, 20.0679, 0, 10.033899, 23.412498, 0, 10.033899, 20.0679, 0, 13.3786, 23.412498, 0, 10.033899, 23.412498, 0, 13.3786, 16.7232, 0, 16.7232, 16.7232, 0, 13.3786, 20.0679, 0, 13.3786, 15.0508995, 0, 16.7232, 16.7232, 0, 13.3786, 16.7232, 0, 16.7232, 20.0679, 0, 16.7232, 20.0679, 0, 13.3786, 23.412498, 0, 13.3786, 16.7232, 0, 16.7232, 20.0679, 0, 13.3786, 20.0679, 0, 16.7232, 20.0679, 0, 16.7232, 23.412498, 0, 13.3786, 23.412498, 0, 16.7232, 15.0508995, 0, 20.0679, 15.0508995, 0, 16.7232, 16.7232, 0, 16.7232, 13.3786, 0, 20.0679, 15.0508995, 0, 16.7232, 15.0508995, 0, 20.0679, 16.7232, 0, 20.0679, 16.7232, 0, 16.7232, 20.0679, 0, 16.7232, 15.0508995, 0, 20.0679, 16.7232, 0, 16.7232, 16.7232, 0, 20.0679, 20.0679, 0, 20.0679, 20.0679, 0, 16.7232, 23.412498, 0, 16.7232, 16.7232, 0, 20.0679, 20.0679, 0, 16.7232, 20.0679, 0, 20.0679, 20.0679, 0, 20.0679, 23.412498, 0, 16.7232, 23.412498, 0, 20.0679, 20.0679, 0, 23.412498, 20.0679, 0, 20.0679, 23.412498, 0, 20.0679, 20.0679, 0, 23.412498, 23.412498, 0, 20.0679, 23.412498, 0, 23.412498, 20.0679, 0, 26.7572, 20.0679, 0, 23.412498, 23.412498, 0, 23.412498, 20.0679, 0, 26.7572, 23.412498, 0, 23.412498, 23.412498, 0, 26.7572, 16.7232, 0, 26.7572, 20.0679, 0, 23.412498, 20.0679, 0, 26.7572, 16.7232, 0, 23.412498, 20.0679, 0, 20.0679, 20.0679, 0, 23.412498, 16.7232, 0, 26.7572, 16.7232, 0, 23.412498, 20.0679, 0, 23.412498, 16.7232, 0, 23.412498, 16.7232, 0, 20.0679, 20.0679, 0, 20.0679, 15.0508995, 0, 26.7572, 16.7232, 0, 23.412498, 16.7232, 0, 26.7572, 15.0508995, 0, 23.412498, 16.7232, 0, 20.0679, 16.7232, 0, 23.412498, 15.0508995, 0, 26.7572, 15.0508995, 0, 23.412498, 16.7232, 0, 23.412498, 15.0508995, 0, 23.412498, 15.0508995, 0, 20.0679, 16.7232, 0, 20.0679, 13.3786, 0, 26.7572, 15.0508995, 0, 23.412498, 15.0508995, 0, 26.7572, 13.3786, 0, 23.412498, 15.0508995, 0, 20.0679, 15.0508995, 0, 23.412498, 13.3786, 0, 26.7572, 13.3786, 0, 23.412498, 15.0508995, 0, 23.412498, 13.3786, 0, 23.412498, 13.3786, 0, 20.0679, 15.0508995, 0, 20.0679, 10.033899, 0, 26.7572, 13.3786, 0, 23.412498, 13.3786, 0, 26.7572, 10.033899, 0, 23.412498, 13.3786, 0, 20.0679, 13.3786, 0, 23.412498, 10.033899, 0, 26.7572, 10.033899, 0, 23.412498, 13.3786, 0, 23.412498, 10.033899, 0, 23.412498, 10.572599, 0, 20.0679, 13.3786, 0, 20.0679, 6.6893, 0, 26.7572, 10.033899, 0, 23.412498, 10.033899, 0, 26.7572, 6.6893, 0, 23.412498, 10.572599, 0, 20.0679, 10.033899, 0, 23.412498, 6.6893, 0, 26.7572, 6.6893, 0, 23.412498, 10.033899, 0, 23.412498, 6.6893, 0, 23.412498, 6.6893, 0, 20.0679, 10.572599, 0, 20.0679, 3.3446, 0, 26.7572, 6.6893, 0, 23.412498, 6.6893, 0, 26.7572, 3.3446, 0, 23.412498, 6.6893, 0, 20.0679, 6.6893, 0, 23.412498, 3.3446, 0, 26.7572, 3.3446, 0, 23.412498, 6.6893, 0, 23.412498, 3.3446, 0, 23.412498, 3.3446, 0, 20.0679, 6.6893, 0, 20.0679, 0, 0, 26.7572, 3.3446, 0, 23.412498, 3.3446, 0, 26.7572, 0, 0, 26.7572, 0, 0, 23.412498, 3.3446, 0, 23.412498, 0, 0, 23.412498, 3.3446, 0, 20.0679, 3.3446, 0, 23.412498, -3.3446, 0, 26.7572, 0, 0, 23.412498, 0, 0, 26.7572, 0, 0, 23.412498, 0, 0, 20.0679, 3.3446, 0, 20.0679, 0, 0, 20.0679, 3.3446, 0, 16.7232, 3.3446, 0, 20.0679, 0, 0, 20.0679, 0, 0, 16.7232, 3.3446, 0, 16.7232, -3.3446, 0, 26.7572, -3.3446, 0, 23.412498, 0, 0, 23.412498, -3.3446, 0, 23.412498, 0, 0, 20.0679, 0, 0, 23.412498, -6.6893, 0, 26.7572, -3.3446, 0, 23.412498, -3.3446, 0, 26.7572, -3.3446, 0, 20.0679, 0, 0, 16.7232, 0, 0, 20.0679, -3.3446, 0, 23.412498, -3.3446, 0, 20.0679, 0, 0, 20.0679, -3.3446, 0, 20.0679, -3.3446, 0, 16.7232, 0, 0, 16.7232, -6.6893, 0, 26.7572, -6.6893, 0, 23.412498, -3.3446, 0, 23.412498, -6.6893, 0, 23.412498, -3.3446, 0, 20.0679, -3.3446, 0, 23.412498, -10.033899, 0, 26.7572, -6.6893, 0, 23.412498, -6.6893, 0, 26.7572, -6.6893, 0, 20.0679, -3.3446, 0, 16.7232, -3.3446, 0, 20.0679, -6.6893, 0, 23.412498, -6.6893, 0, 20.0679, -3.3446, 0, 20.0679, -6.6893, 0, 20.0679, -6.6893, 0, 16.7232, -3.3446, 0, 16.7232, -10.033899, 0, 26.7572, -10.033899, 0, 23.412498, -6.6893, 0, 23.412498, -10.033899, 0, 23.412498, -6.6893, 0, 20.0679, -6.6893, 0, 23.412498, -13.3786, 0, 26.7572, -10.033899, 0, 23.412498, -10.033899, 0, 26.7572, -10.033899, 0, 20.0679, -6.6893, 0, 16.7232, -6.6893, 0, 20.0679, -10.033899, 0, 23.412498, -10.033899, 0, 20.0679, -6.6893, 0, 20.0679, -13.3786, 0, 26.7572, -13.3786, 0, 23.412498, -10.033899, 0, 23.412498, -13.3786, 0, 23.412498, -10.033899, 0, 20.0679, -10.033899, 0, 23.412498, -16.7232, 0, 26.7572, -13.3786, 0, 23.412498, -13.3786, 0, 26.7572, -10.033899, 0, 20.0679, -10.033899, 0, 16.7232, -6.6893, 0, 16.7232, -10.033899, 0, 16.7232, -6.6893, 0, 13.3786, -6.6893, 0, 16.7232, -10.033899, 0, 16.7232, -10.033899, 0, 13.3786, -6.6893, 0, 13.3786, -13.3786, 0, 20.0679, -10.033899, 0, 16.7232, -10.033899, 0, 20.0679, -13.3786, 0, 23.412498, -13.3786, 0, 20.0679, -10.033899, 0, 20.0679, -13.3786, 0, 16.7232, -10.033899, 0, 13.3786, -10.033899, 0, 16.7232, -13.3786, 0, 20.0679, -13.3786, 0, 16.7232, -10.033899, 0, 16.7232, -13.3786, 0, 16.7232, -13.3786, 0, 13.3786, -10.033899, 0, 13.3786, -16.7232, 0, 26.7572, -16.7232, 0, 23.412498, -13.3786, 0, 23.412498, -16.7232, 0, 23.412498, -13.3786, 0, 20.0679, -13.3786, 0, 23.412498, -20.0679, 0, 26.7572, -16.7232, 0, 23.412498, -16.7232, 0, 26.7572, -16.7232, 0, 20.0679, -13.3786, 0, 16.7232, -13.3786, 0, 20.0679, -16.7232, 0, 23.412498, -16.7232, 0, 20.0679, -13.3786, 0, 20.0679, -16.7232, 0, 16.7232, -13.3786, 0, 13.3786, -13.3786, 0, 16.7232, -16.7232, 0, 20.0679, -16.7232, 0, 16.7232, -13.3786, 0, 16.7232, -16.7232, 0, 16.7232, -16.7232, 0, 13.3786, -13.3786, 0, 13.3786, -20.0679, 0, 26.7572, -20.0679, 0, 23.412498, -16.7232, 0, 23.412498, -20.0679, 0, 23.412498, -16.7232, 0, 20.0679, -16.7232, 0, 23.412498, -23.412498, 0, 26.7572, -20.0679, 0, 23.412498, -20.0679, 0, 26.7572, -20.0679, 0, 20.0679, -16.7232, 0, 16.7232, -16.7232, 0, 20.0679, -20.0679, 0, 23.412498, -20.0679, 0, 20.0679, -16.7232, 0, 20.0679, -23.412498, 0, 26.7572, -23.412498, 0, 23.412498, -20.0679, 0, 23.412498, -23.412498, 0, 23.412498, -20.0679, 0, 20.0679, -20.0679, 0, 23.412498, -26.7572, 0, 26.7572, -23.412498, 0, 23.412498, -23.412498, 0, 26.7572, -26.7572, 0, 26.7572, -26.7572, 0, 23.412498, -23.412498, 0, 23.412498, -26.7572, 0, 23.412498, -23.412498, 0, 20.0679, -23.412498, 0, 23.412498, -23.412498, 0, 23.412498, -23.412498, 0, 20.0679, -20.0679, 0, 20.0679, -26.7572, 0, 23.412498, -26.7572, 0, 20.0679, -23.412498, 0, 20.0679, -20.0679, 0, 20.0679, -20.0679, 0, 16.7232, -16.7232, 0, 16.7232, -23.412498, 0, 20.0679, -20.0679, 0, 16.7232, -20.0679, 0, 20.0679, -20.0679, 0, 16.7232, -16.7232, 0, 13.3786, -16.7232, 0, 16.7232, -26.7572, 0, 20.0679, -23.412498, 0, 16.7232, -23.412498, 0, 20.0679, -23.412498, 0, 20.0679, -23.412498, 0, 16.7232, -20.0679, 0, 16.7232, -26.7572, 0, 20.0679, -26.7572, 0, 16.7232, -23.412498, 0, 16.7232, -20.0679, 0, 16.7232, -20.0679, 0, 13.3786, -16.7232, 0, 13.3786, -23.412498, 0, 16.7232, -20.0679, 0, 13.3786, -20.0679, 0, 16.7232, -20.0679, 0, 13.3786, -16.7232, 0, 10.033899, -16.7232, 0, 13.3786, -20.0679, 0, 13.3786, -20.0679, 0, 10.033899, -16.7232, 0, 10.033899, -23.412498, 0, 16.7232, -23.412498, 0, 13.3786, -20.0679, 0, 13.3786, -23.412498, 0, 13.3786, -20.0679, 0, 10.033899, -20.0679, 0, 13.3786, -26.7572, 0, 16.7232, -23.412498, 0, 13.3786, -23.412498, 0, 16.7232, -23.412498, 0, 13.3786, -23.412498, 0, 10.033899, -20.0679, 0, 10.033899, -26.7572, 0, 16.7232, -26.7572, 0, 13.3786, -23.412498, 0, 13.3786, -26.7572, 0, 13.3786, -23.412498, 0, 10.033899, -23.412498, 0, 13.3786, -26.7572, 0, 13.3786, -26.7572, 0, 10.033899, -23.412498, 0, 10.033899, -26.7572, 0, 10.033899, -23.8078, 0, 6.6893, -23.412498, 0, 10.033899, -26.7572, 0, 10.033899, -26.7572, 0, 6.6893, -23.8078, 0, 6.6893, -26.7572, 0, 6.6893, -23.412498, 0, 3.3446, -23.8078, 0, 6.6893, -26.7572, 0, 6.6893, -26.7572, 0, 3.3446, -23.412498, 0, 3.3446, -16.7232, 0, -23.412498, -13.3786, 0, -26.7572, -13.3786, 0, -23.412498, -13.3786, 0, -23.412498, -13.3786, 0, -26.7572, -10.033899, 0, -26.7572, -16.7232, 0, -23.412498, -16.7232, 0, -26.7572, -13.3786, 0, -26.7572, -13.3786, 0, -23.412498, -10.033899, 0, -26.7572, -10.033899, 0, -23.412498, -10.033899, 0, -23.412498, -10.033899, 0, -26.7572, -6.6893, 0, -26.7572, -16.7232, 0, -20.0679, -16.7232, 0, -23.412498, -13.3786, 0, -23.412498, -10.033899, 0, -23.412498, -6.6893, 0, -26.7572, -6.6893, 0, -23.412498, -6.6893, 0, -23.412498, -6.6893, 0, -26.7572, -3.3446, 0, -26.7572, -6.6893, 0, -23.412498, -3.3446, 0, -26.7572, -3.3446, 0, -23.412498, -13.3786, 0, -20.0679, -13.3786, 0, -23.412498, -10.033899, 0, -23.412498, -16.7232, 0, -20.0679, -13.3786, 0, -23.412498, -13.3786, 0, -20.0679, -10.033899, 0, -20.0679, -10.033899, 0, -23.412498, -6.6893, 0, -23.412498, -13.3786, 0, -20.0679, -10.033899, 0, -23.412498, -10.033899, 0, -20.0679, -6.6893, 0, -20.0679, -6.6893, 0, -23.412498, -3.3446, 0, -23.412498, -10.033899, 0, -20.0679, -6.6893, 0, -23.412498, -6.6893, 0, -20.0679, -6.6893, 0, -20.0679, -3.3446, 0, -23.412498, -3.3446, 0, -20.0679, -13.3786, 0, -17.3364, -13.3786, 0, -20.0679, -10.033899, 0, -20.0679, -13.3786, 0, -17.3364, -10.033899, 0, -20.0679, -10.033899, 0, -16.7232, -10.033899, 0, -16.7232, -10.033899, 0, -20.0679, -6.6893, 0, -20.0679, -16.7232, 0, -16.7232, -13.3786, 0, -20.0679, -13.3786, 0, -17.3364, -16.7232, 0, -16.7232, -16.7232, 0, -20.0679, -13.3786, 0, -20.0679, -10.033899, 0, -16.7232, -6.6893, 0, -20.0679, -6.6893, 0, -16.7232, -6.6893, 0, -16.7232, -6.6893, 0, -20.0679, -3.3446, 0, -20.0679, -9.6168995, 0, -14.127899, -10.033899, 0, -16.7232, -6.6893, 0, -16.7232, -9.6168995, 0, -14.127899, -6.6893, 0, -16.7232, -6.6893, 0, -14.2637, -6.6893, 0, -16.7232, -3.3446, 0, -20.0679, -3.3446, 0, -16.7232, -6.6893, 0, -14.2637, -6.6893, 0, -16.7232, -3.3446, 0, -16.7232, -3.3446, 0, -16.7232, -3.3446, 0, -20.0679, 0, 0, -20.0679, -6.6893, 0, -14.2637, -3.3446, 0, -16.7232, -3.3446, 0, -13.3786, -3.3446, 0, -16.7232, 0, 0, -20.0679, 0, 0, -16.7232, -3.3446, 0, -13.3786, -3.3446, 0, -16.7232, 0, 0, -16.7232, 0, 0, -16.7232, 0, 0, -20.0679, 3.3446, 0, -20.5992, 0, 0, -16.7232, 3.3446, 0, -20.5992, 3.0140998, 0, -16.7232, -3.3446, 0, -13.3786, 0, 0, -16.7232, 0, 0, -13.3786, -2.6297, 0, -10.5745, -3.3446, 0, -13.3786, 0, 0, -13.3786, -2.6297, 0, -10.5745, 0, 0, -13.3786, 0, 0, -10.7654, -20.0679, 0, -16.7232, -16.7232, 0, -20.0679, -16.7232, 0, -16.7232, -20.0679, 0, -16.7232, -20.0679, 0, -20.0679, -16.7232, 0, -20.0679, -20.0679, 0, -20.0679, -16.7232, 0, -23.412498, -16.7232, 0, -20.0679, -20.0679, 0, -13.3786, -20.0679, 0, -16.7232, -16.7232, 0, -16.7232, -20.0679, 0, -13.3786, -16.7232, 0, -16.7232, -16.7232, 0, -13.3786, -23.412498, 0, -16.7232, -20.0679, 0, -20.0679, -20.0679, 0, -16.7232, -20.0679, 0, -10.033899, -20.0679, 0, -13.3786, -16.7232, 0, -13.3786, -20.0679, 0, -10.033899, -16.7232, 0, -13.3786, -17.275, 0, -10.5169, -23.412498, 0, -13.3786, -20.0679, 0, -16.7232, -20.0679, 0, -13.3786, -23.412498, 0, -13.3786, -23.412498, 0, -16.7232, -20.0679, 0, -16.7232, -23.412498, 0, -10.033899, -20.0679, 0, -13.3786, -20.0679, 0, -10.033899, -23.412498, 0, -10.033899, -23.412498, 0, -13.3786, -20.0679, 0, -13.3786, -26.7572, 0, -10.033899, -23.412498, 0, -13.3786, -23.412498, 0, -10.033899, -26.7572, 0, -10.033899, -26.7572, 0, -13.3786, -23.412498, 0, -13.3786, -26.7572, 0, -13.3786, -23.412498, 0, -16.7232, -23.412498, 0, -13.3786, -26.7572, 0, -6.6893, -26.7572, 0, -10.033899, -23.412498, 0, -10.033899, -26.7572, 0, -13.3786, -26.7572, 0, -16.7232, -23.412498, 0, -16.7232, -26.7572, 0, -6.6893, -23.412498, 0, -10.033899, -23.412498, 0, -6.6893, -23.412498, 0, -6.6893, -23.412498, 0, -10.033899, -20.0679, 0, -10.033899, -23.412498, 0, -6.6893, -20.0679, 0, -10.033899, -20.3764, 0, -6.6893, -26.7572, 0, -3.3446, -26.7572, 0, -6.6893, -23.412498, 0, -6.6893, -26.7572, 0, -3.3446, -23.412498, 0, -6.6893, -23.412498, 0, -3.3446, -26.7572, 0, -16.7232, -23.412498, 0, -20.0679, -23.412498, 0, -16.7232, -23.412498, 0, -16.7232, -23.412498, 0, -20.0679, -20.0679, 0, -20.0679, -26.7572, 0, -16.7232, -26.7572, 0, -20.0679, -23.412498, 0, -20.0679, -23.412498, 0, -20.0679, -20.0679, 0, -23.412498, -20.0679, 0, -20.0679, -20.0679, 0, -20.0679, -20.0679, 0, -23.412498, -16.7232, 0, -23.412498, -20.0679, 0, -23.412498, -16.7232, 0, -26.7572, -16.7232, 0, -23.412498, -20.0679, 0, -23.412498, -20.0679, 0, -26.7572, -16.7232, 0, -26.7572, -23.412498, 0, -20.0679, -23.412498, 0, -23.412498, -20.0679, 0, -23.412498, -23.412498, 0, -23.412498, -20.0679, 0, -26.7572, -20.0679, 0, -23.412498, -26.7572, 0, -20.0679, -23.412498, 0, -23.412498, -23.412498, 0, -20.0679, -23.412498, 0, -23.412498, -23.412498, 0, -26.7572, -20.0679, 0, -26.7572, -26.7572, 0, -20.0679, -26.7572, 0, -23.412498, -23.412498, 0, -23.412498, -26.7572, 0, -23.412498, -23.412498, 0, -26.7572, -23.412498, 0, -23.412498, -26.7572, 0, -23.412498, -26.7572, 0, -26.7572, -23.412498, 0, -26.7572, -23.412498, 0, -3.3446, -20.3764, 0, -6.6893, -20.0679, -0.3484, -3.5669, -23.412498, 0, -3.3446, -23.412498, 0, -6.6893, -20.3764, 0, -6.6893, 16.7232, 0, 10.033899, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 10.033899, 16.7232, 0, 10.033899, 16.7232, 0, 6.6893, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 10.033899, 23.412498, 0, 6.6893, 23.412498, 0, 10.033899, 20.0679, 0, 10.033899, 20.0679, -0.124199994, 6.6893, 23.412498, 0, 6.6893, -26.7572, 0, 3.3446, -23.412498, -0.2913, 0.26119998, -23.412498, 0, 3.3446, -26.7572, 0, 3.3446, -26.7572, 0, 0, -23.412498, -0.2913, 0.26119998, -26.7572, 0, 0, -23.412498, 0, -3.3446, -23.412498, -0.2913, 0.26119998, -26.7572, 0, 0, -26.7572, 0, -3.3446, -23.412498, 0, -3.3446, 0, 0, -13.3786, 3.0140998, 0, -16.7232, 3.3446, -0.3989, -12.9443, 0, 0, -13.3786, 0, 0, -16.7232, 3.0140998, 0, -16.7232, 0, 0, -10.7654, 3.3446, -0.3989, -12.9443, 2.8241, 0, -10.4594, 0, 0, -10.7654, 0, 0, -13.3786, 3.3446, -0.3989, -12.9443, 20.0679, -0.124199994, 6.6893, 23.412498, 0, 3.3446, 23.412498, 0, 6.6893, 20.0679, -0.124199994, 6.6893, 20.0679, 0, 3.3446, 23.412498, 0, 3.3446, 51.2184, -3.9413, -15.0508995, 54.072197, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 54.072197, -3.9413, -13.3786, 54.072197, -3.9413, -11.7063, 51.2184, -3.9413, -15.0508995, 54.072197, -3.9413, -15.0508995, 54.072197, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 54.072197, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 54.072197, -3.9413, -11.7063, 54.072197, -3.9413, -10.033899, 48.3645, -3.9413, -15.0508995, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 54.072197, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 54.072197, -3.9413, -10.033899, 54.072197, -3.9413, -8.3616, 48.3645, -3.9413, -13.3786, 51.2184, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 48.3645, -3.9413, -15.0508995, 51.2184, -3.9413, -13.3786, 48.3645, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 48.3645, -3.9413, -13.3786, 51.2184, -3.9413, -11.7063, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 54.072197, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 54.072197, -3.9413, -8.3616, 54.072197, -3.9413, -6.6893, 48.3645, -3.9413, -11.7063, 51.2184, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 51.2184, -3.9413, -10.033899, 51.2184, -3.9413, -8.3616, 45.5107, -3.9413, -13.3786, 48.3645, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 48.3645, -3.9413, -11.7063, 48.3645, -3.9413, -10.033899, 45.5107, -3.9413, -13.3786, 48.3645, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 51.2184, -3.9413, -8.3616, 54.072197, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 54.072197, -3.9413, -6.6893, 54.072197, -3.9413, -5.0169997, 48.3645, -3.9413, -10.033899, 51.2184, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 45.5107, -3.9413, -11.7063, 48.3645, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 48.3645, -3.9413, -10.033899, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 54.072197, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 54.072197, -3.9413, -5.0169997, 54.072197, -3.9413, -3.3446, 48.3645, -3.9413, -8.3616, 51.2184, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 45.5107, -3.9413, -10.033899, 48.3645, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 48.3645, -3.9413, -8.3616, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 54.072197, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 54.072197, -3.9413, -3.3446, 54.072197, -3.9413, -1.6723, 48.3645, -3.9413, -6.6893, 51.2184, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 45.5107, -3.9413, -8.3616, 48.3645, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 48.3645, -3.9413, -6.6893, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 54.072197, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 54.072197, -3.9413, -1.6723, 54.072197, -3.9413, 0, 48.3645, -3.9413, -5.0169997, 51.2184, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 45.5107, -3.9413, -6.6893, 48.3645, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 48.3645, -3.9413, -5.0169997, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 54.072197, -3.9413, 0, 51.2184, -3.9413, 0, 51.2184, -3.9413, 0, 54.072197, -3.9413, 0, 54.072197, -3.9413, 1.6723, 48.3645, -3.9413, -3.3446, 51.2184, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, -1.6723, 51.2184, -3.9413, 0, 45.5107, -3.9413, -5.0169997, 48.3645, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 48.3645, -3.9413, -3.3446, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, 0, 54.072197, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 54.072197, -3.9413, 1.6723, 54.072197, -3.9413, 3.3446, 48.3645, -3.9413, -1.6723, 51.2184, -3.9413, 0, 48.3645, -3.9413, 0, 48.3645, -3.9413, 0, 51.2184, -3.9413, 0, 51.2184, -3.9413, 1.6723, 45.5107, -3.9413, -3.3446, 48.3645, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 48.3645, -3.9413, -1.6723, 48.3645, -3.9413, 0, 51.2184, -3.9413, 1.6723, 54.072197, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 54.072197, -3.9413, 3.3446, 54.072197, -3.9413, 5.0169997, 48.3645, -3.9413, 0, 51.2184, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 45.5107, -3.9413, -1.6723, 48.3645, -3.9413, 0, 45.5107, -3.9413, 0, 45.5107, -3.9413, 0, 48.3645, -3.9413, 0, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 54.072197, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 54.072197, -3.9413, 5.0169997, 54.072197, -3.9413, 6.6893, 48.3645, -3.9413, 1.6723, 51.2184, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 45.5107, -3.9413, 0, 48.3645, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 48.3645, -3.9413, 1.6723, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 54.072197, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 54.072197, -3.9413, 6.6893, 54.072197, -3.9413, 8.3616, 48.3645, -3.9413, 3.3446, 51.2184, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 45.5107, -3.9413, 1.6723, 48.3645, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 48.3645, -3.9413, 3.3446, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 54.072197, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 54.072197, -3.9413, 8.3616, 54.072197, -3.9413, 10.033899, 48.3645, -3.9413, 5.0169997, 51.2184, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 45.5107, -3.9413, 3.3446, 48.3645, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 48.3645, -3.9413, 5.0169997, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 54.072197, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 54.072197, -3.9413, 10.033899, 54.072197, -3.9413, 11.7063, 48.3645, -3.9413, 6.6893, 51.2184, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 45.5107, -3.9413, 5.0169997, 48.3645, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 48.3645, -3.9413, 6.6893, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 54.072197, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 54.072197, -3.9413, 11.7063, 54.072197, -3.9413, 13.3786, 48.3645, -3.9413, 8.3616, 51.2184, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 45.5107, -3.9413, 6.6893, 48.3645, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 48.3645, -3.9413, 8.3616, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 54.072197, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 54.072197, -3.9413, 13.3786, 54.072197, -3.9413, 15.0508995, 48.3645, -3.9413, 10.033899, 51.2184, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 45.5107, -3.9413, 8.3616, 48.3645, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 48.3645, -3.9413, 10.033899, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 54.072197, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 54.072197, -3.9413, 15.0508995, 54.072197, -3.9413, 16.7232, 48.3645, -3.9413, 11.7063, 51.2184, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 45.5107, -3.9413, 10.033899, 48.3645, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 48.3645, -3.9413, 11.7063, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 54.072197, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 54.072197, -3.9413, 16.7232, 54.072197, -3.9413, 18.3955, 48.3645, -3.9413, 13.3786, 51.2184, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 45.5107, -3.9413, 11.7063, 48.3645, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 48.3645, -3.9413, 13.3786, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 54.072197, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 54.072197, -3.9413, 18.3955, 54.072197, -3.9413, 20.0679, 48.3645, -3.9413, 15.0508995, 51.2184, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 45.5107, -3.9413, 13.3786, 48.3645, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 48.3645, -3.9413, 15.0508995, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 54.072197, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 54.072197, -3.9413, 20.0679, 54.072197, -3.9413, 21.7402, 48.3645, -3.9413, 16.7232, 51.2184, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 45.5107, -3.9413, 15.0508995, 48.3645, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 48.3645, -3.9413, 16.7232, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 54.072197, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 54.072197, -3.9413, 21.7402, 54.072197, -3.9413, 23.412498, 48.3645, -3.9413, 18.3955, 51.2184, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 45.5107, -3.9413, 16.7232, 48.3645, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 48.3645, -3.9413, 18.3955, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 54.072197, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 54.072197, -3.9413, 23.412498, 54.072197, -3.9413, 25.084799, 48.3645, -3.9413, 20.0679, 51.2184, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 45.5107, -3.9413, 18.3955, 48.3645, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 48.3645, -3.9413, 20.0679, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 54.072197, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 54.072197, -3.9413, 25.084799, 54.072197, -3.9413, 26.7572, 51.2184, -3.9413, 25.084799, 54.072197, -3.9413, 26.7572, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 23.412498, 51.2184, -3.9413, 23.412498, 51.2184, -3.9413, 25.084799, 48.3645, -3.9413, 21.7402, 51.2184, -3.9413, 23.412498, 48.3645, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 51.2184, -3.9413, 25.084799, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 23.412498, 51.2184, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 51.2184, -3.9413, 26.7572, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 21.7402, 48.3645, -3.9413, 21.7402, 48.3645, -3.9413, 23.412498, 45.5107, -3.9413, 20.0679, 48.3645, -3.9413, 21.7402, 45.5107, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 48.3645, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 45.5107, -3.9413, 21.7402, 48.3645, -3.9413, 23.412498, 45.5107, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 48.3645, -3.9413, 25.084799, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 23.412498, 48.3645, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 48.3645, -3.9413, 26.7572, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 25.084799, 45.5107, -3.9413, 25.084799, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 25.084799, 45.5107, -3.9413, 26.7572, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 25.084799, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 26.7572, 39.7973, -3.9413, 26.7572, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 25.084799, 39.7973, -3.9413, 25.084799, 42.6568, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 42.6568, -3.9413, 25.084799, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 23.412498, 42.6568, -3.9413, 25.084799, 42.6568, -3.9413, 23.412498, 45.5107, -3.9413, 23.412498, 45.5107, -3.9413, 25.084799, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 23.412498, 39.7973, -3.9413, 23.412498, 42.6568, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 42.6568, -3.9413, 23.412498, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 21.7402, 42.6568, -3.9413, 23.412498, 42.6568, -3.9413, 21.7402, 45.5107, -3.9413, 21.7402, 45.5107, -3.9413, 23.412498, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 21.7402, 39.7973, -3.9413, 21.7402, 42.6568, -3.9413, 20.0679, 45.5107, -3.9413, 21.7402, 42.6568, -3.9413, 21.7402, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 20.0679, 42.6568, -3.9413, 21.7402, 42.6568, -3.9413, 20.0679, 45.5107, -3.9413, 20.0679, 45.5107, -3.9413, 21.7402, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 20.0679, 39.7973, -3.9413, 20.0679, 42.6568, -3.9413, 18.3955, 45.5107, -3.9413, 20.0679, 42.6568, -3.9413, 20.0679, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 18.3955, 42.6568, -3.9413, 20.0679, 42.6568, -3.9413, 18.3955, 45.5107, -3.9413, 18.3955, 45.5107, -3.9413, 20.0679, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 18.3955, 39.7973, -3.9413, 18.3955, 42.6568, -3.9413, 16.7232, 45.5107, -3.9413, 18.3955, 42.6568, -3.9413, 18.3955, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 16.7232, 42.6568, -3.9413, 18.3955, 42.6568, -3.9413, 16.7232, 45.5107, -3.9413, 16.7232, 45.5107, -3.9413, 18.3955, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 16.7232, 39.7973, -3.9413, 16.7232, 42.6568, -3.9413, 15.0508995, 45.5107, -3.9413, 16.7232, 42.6568, -3.9413, 16.7232, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 15.0508995, 42.6568, -3.9413, 16.7232, 42.6568, -3.9413, 15.0508995, 45.5107, -3.9413, 15.0508995, 45.5107, -3.9413, 16.7232, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 15.0508995, 39.7973, -3.9413, 15.0508995, 42.6568, -3.9413, 13.3786, 45.5107, -3.9413, 15.0508995, 42.6568, -3.9413, 15.0508995, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 13.3786, 42.6568, -3.9413, 15.0508995, 42.6568, -3.9413, 13.3786, 45.5107, -3.9413, 13.3786, 45.5107, -3.9413, 15.0508995, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 13.3786, 39.7973, -3.9413, 13.3786, 42.6568, -3.9413, 11.7063, 45.5107, -3.9413, 13.3786, 42.6568, -3.9413, 13.3786, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 11.7063, 42.6568, -3.9413, 13.3786, 42.6568, -3.9413, 11.7063, 45.5107, -3.9413, 11.7063, 45.5107, -3.9413, 13.3786, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 11.7063, 39.7973, -3.9413, 11.7063, 42.6568, -3.9413, 10.033899, 45.5107, -3.9413, 11.7063, 42.6568, -3.9413, 11.7063, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 10.033899, 42.6568, -3.9413, 11.7063, 42.6568, -3.9413, 10.033899, 45.5107, -3.9413, 10.033899, 45.5107, -3.9413, 11.7063, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 10.033899, 39.7973, -3.9413, 10.033899, 42.6568, -3.9413, 8.3616, 45.5107, -3.9413, 10.033899, 42.6568, -3.9413, 10.033899, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 8.3616, 42.6568, -3.9413, 10.033899, 42.6568, -3.9413, 8.3616, 45.5107, -3.9413, 8.3616, 45.5107, -3.9413, 10.033899, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 8.3616, 39.7973, -3.9413, 8.3616, 42.6568, -3.9413, 6.6893, 45.5107, -3.9413, 8.3616, 42.6568, -3.9413, 8.3616, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 6.6893, 42.6568, -3.9413, 8.3616, 42.6568, -3.9413, 6.6893, 45.5107, -3.9413, 6.6893, 45.5107, -3.9413, 8.3616, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 6.6893, 39.7973, -3.9413, 6.6893, 42.6568, -3.9413, 5.0169997, 45.5107, -3.9413, 6.6893, 42.6568, -3.9413, 6.6893, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 5.0169997, 42.6568, -3.9413, 6.6893, 42.6568, -3.9413, 5.0169997, 45.5107, -3.9413, 5.0169997, 45.5107, -3.9413, 6.6893, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 5.0169997, 39.7973, -3.9413, 5.0169997, 42.6568, -3.9413, 3.3446, 45.5107, -3.9413, 5.0169997, 42.6568, -3.9413, 5.0169997, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 3.3446, 42.6568, -3.9413, 5.0169997, 42.6568, -3.9413, 3.3446, 45.5107, -3.9413, 3.3446, 45.5107, -3.9413, 5.0169997, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 3.3446, 39.7973, -3.9413, 3.3446, 42.6568, -3.9413, 1.6723, 45.5107, -3.9413, 3.3446, 42.6568, -3.9413, 3.3446, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 1.6723, 42.6568, -3.9413, 3.3446, 42.6568, -3.9413, 1.6723, 45.5107, -3.9413, 1.6723, 45.5107, -3.9413, 3.3446, 39.7973, -3.9413, 0, 42.6568, -3.9413, 1.6723, 39.7973, -3.9413, 1.6723, 42.6568, -3.9413, 0, 45.5107, -3.9413, 1.6723, 42.6568, -3.9413, 1.6723, 39.7973, -3.9413, 0, 42.6568, -3.9413, 0, 42.6568, -3.9413, 1.6723, 42.6568, -3.9413, 0, 45.5107, -3.9413, 0, 45.5107, -3.9413, 1.6723, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, 0, 39.7973, -3.9413, 0, 42.6568, -3.9413, -1.6723, 45.5107, -3.9413, 0, 42.6568, -3.9413, 0, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, -1.6723, 42.6568, -3.9413, 0, 42.6568, -3.9413, -1.6723, 45.5107, -3.9413, -1.6723, 45.5107, -3.9413, 0, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -1.6723, 39.7973, -3.9413, -1.6723, 42.6568, -3.9413, -3.3446, 45.5107, -3.9413, -1.6723, 42.6568, -3.9413, -1.6723, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -3.3446, 42.6568, -3.9413, -1.6723, 42.6568, -3.9413, -3.3446, 45.5107, -3.9413, -3.3446, 45.5107, -3.9413, -1.6723, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -3.3446, 39.7973, -3.9413, -3.3446, 42.6568, -3.9413, -5.0169997, 45.5107, -3.9413, -3.3446, 42.6568, -3.9413, -3.3446, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -5.0169997, 42.6568, -3.9413, -3.3446, 42.6568, -3.9413, -5.0169997, 45.5107, -3.9413, -5.0169997, 45.5107, -3.9413, -3.3446, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -5.0169997, 39.7973, -3.9413, -5.0169997, 42.6568, -3.9413, -6.6893, 45.5107, -3.9413, -5.0169997, 42.6568, -3.9413, -5.0169997, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -6.6893, 42.6568, -3.9413, -5.0169997, 42.6568, -3.9413, -6.6893, 45.5107, -3.9413, -6.6893, 45.5107, -3.9413, -5.0169997, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -6.6893, 39.7973, -3.9413, -6.6893, 42.6568, -3.9413, -8.3616, 45.5107, -3.9413, -6.6893, 42.6568, -3.9413, -6.6893, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -8.3616, 42.6568, -3.9413, -6.6893, 42.6568, -3.9413, -8.3616, 45.5107, -3.9413, -8.3616, 45.5107, -3.9413, -6.6893, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -8.3616, 39.7973, -3.9413, -8.3616, 42.6568, -3.9413, -10.033899, 45.5107, -3.9413, -8.3616, 42.6568, -3.9413, -8.3616, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -10.033899, 42.6568, -3.9413, -8.3616, 42.6568, -3.9413, -10.033899, 45.5107, -3.9413, -10.033899, 45.5107, -3.9413, -8.3616, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -10.033899, 39.7973, -3.9413, -10.033899, 42.6568, -3.9413, -11.7063, 45.5107, -3.9413, -10.033899, 42.6568, -3.9413, -10.033899, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -11.7063, 42.6568, -3.9413, -10.033899, 42.6568, -3.9413, -11.7063, 45.5107, -3.9413, -11.7063, 45.5107, -3.9413, -10.033899, 39.7973, -3.9413, -13.3786, 42.6568, -3.9413, -11.7063, 39.7973, -3.9413, -11.7063, 42.6568, -3.9413, -13.3786, 45.5107, -3.9413, -11.7063, 42.6568, -3.9413, -11.7063, 39.7973, -3.9413, -13.3786, 42.6568, -3.9413, -13.3786, 42.6568, -3.9413, -11.7063, 42.6568, -3.9413, -13.3786, 45.5107, -3.9413, -13.3786, 45.5107, -3.9413, -11.7063, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -13.3786, 39.7973, -3.9413, -13.3786, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 42.6568, -3.9413, -13.3786, 42.6568, -3.9413, -15.0508995, 45.5107, -3.9413, -13.3786, 42.6568, -3.9413, -13.3786, 39.7973, -3.9413, -16.7232, 42.6568, -3.9413, -15.0508995, 39.7973, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 45.5107, -3.9413, -15.0508995, 45.5107, -3.9413, -13.3786, 45.5107, -3.9413, -15.0508995, 48.3645, -3.9413, -13.3786, 45.5107, -3.9413, -13.3786, 39.7973, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 42.6568, -3.9413, -15.0508995, 42.6568, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 42.6568, -3.9413, -15.0508995, 39.7973, -3.9413, -18.3955, 42.6568, -3.9413, -16.7232, 39.7973, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 48.3645, -3.9413, -15.0508995, 48.3645, -3.9413, -13.3786, 39.7973, -3.9413, -18.3955, 42.6568, -3.9413, -18.3955, 42.6568, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 45.5107, -3.9413, -16.7232, 45.5107, -3.9413, -15.0508995, 45.5107, -3.9413, -16.7232, 48.3645, -3.9413, -15.0508995, 45.5107, -3.9413, -15.0508995, 42.6568, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 42.6568, -3.9413, -16.7232, 39.7973, -3.9413, -20.0679, 42.6568, -3.9413, -18.3955, 39.7973, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 48.3645, -3.9413, -16.7232, 48.3645, -3.9413, -15.0508995, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -15.0508995, 48.3645, -3.9413, -15.0508995, 42.6568, -3.9413, -18.3955, 45.5107, -3.9413, -18.3955, 45.5107, -3.9413, -16.7232, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -16.7232, 45.5107, -3.9413, -16.7232, 39.7973, -3.9413, -20.0679, 42.6568, -3.9413, -20.0679, 42.6568, -3.9413, -18.3955, 42.6568, -3.9413, -20.0679, 45.5107, -3.9413, -18.3955, 42.6568, -3.9413, -18.3955, 39.7973, -3.9413, -21.7402, 42.6568, -3.9413, -20.0679, 39.7973, -3.9413, -20.0679, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -16.7232, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -16.7232, 54.072197, -3.9413, -15.0508995, 51.2184, -3.9413, -15.0508995, 51.2184, -3.9413, -16.7232, 54.072197, -3.9413, -16.7232, 54.072197, -3.9413, -15.0508995, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -16.7232, 48.3645, -3.9413, -16.7232, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -18.3955, 48.3645, -3.9413, -16.7232, 51.2184, -3.9413, -18.3955, 54.072197, -3.9413, -16.7232, 51.2184, -3.9413, -16.7232, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -18.3955, 51.2184, -3.9413, -16.7232, 51.2184, -3.9413, -18.3955, 54.072197, -3.9413, -18.3955, 54.072197, -3.9413, -16.7232, 45.5107, -3.9413, -20.0679, 48.3645, -3.9413, -18.3955, 45.5107, -3.9413, -18.3955, 42.6568, -3.9413, -20.0679, 45.5107, -3.9413, -20.0679, 45.5107, -3.9413, -18.3955, 48.3645, -3.9413, -20.0679, 51.2184, -3.9413, -18.3955, 48.3645, -3.9413, -18.3955, 45.5107, -3.9413, -20.0679, 48.3645, -3.9413, -20.0679, 48.3645, -3.9413, -18.3955, 51.2184, -3.9413, -20.0679, 54.072197, -3.9413, -18.3955, 51.2184, -3.9413, -18.3955, 48.3645, -3.9413, -20.0679, 51.2184, -3.9413, -20.0679, 51.2184, -3.9413, -18.3955, 51.2184, -3.9413, -20.0679, 54.072197, -3.9413, -20.0679, 54.072197, -3.9413, -18.3955, 39.7973, -3.9413, -21.7402, 42.6568, -3.9413, -21.7402, 42.6568, -3.9413, -20.0679, 42.6568, -3.9413, -21.7402, 45.5107, -3.9413, -20.0679, 42.6568, -3.9413, -20.0679, 39.7973, -3.9413, -23.412498, 42.6568, -3.9413, -21.7402, 39.7973, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 48.3645, -3.9413, -20.0679, 45.5107, -3.9413, -20.0679, 42.6568, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 45.5107, -3.9413, -20.0679, 39.7973, -3.9413, -23.412498, 42.6568, -3.9413, -23.412498, 42.6568, -3.9413, -21.7402, 42.6568, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 42.6568, -3.9413, -21.7402, 39.7973, -3.9413, -25.084799, 42.6568, -3.9413, -23.412498, 39.7973, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 48.3645, -3.9413, -21.7402, 48.3645, -3.9413, -20.0679, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -20.0679, 48.3645, -3.9413, -20.0679, 42.6568, -3.9413, -23.412498, 45.5107, -3.9413, -23.412498, 45.5107, -3.9413, -21.7402, 45.5107, -3.9413, -23.412498, 48.3645, -3.9413, -21.7402, 45.5107, -3.9413, -21.7402, 39.7973, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -23.412498, 42.6568, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 42.6568, -3.9413, -23.412498, 39.7973, -3.9413, -26.7572, 42.6568, -3.9413, -25.084799, 39.7973, -3.9413, -25.084799, 39.7973, -3.9413, -26.7572, 42.6568, -3.9413, -26.7572, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -26.7572, 45.5107, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 42.6568, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 42.6568, -3.9413, -26.7572, 45.5107, -3.9413, -26.7572, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -23.412498, 48.3645, -3.9413, -23.412498, 48.3645, -3.9413, -21.7402, 45.5107, -3.9413, -25.084799, 48.3645, -3.9413, -23.412498, 45.5107, -3.9413, -23.412498, 45.5107, -3.9413, -26.7572, 48.3645, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 45.5107, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -23.412498, 45.5107, -3.9413, -26.7572, 48.3645, -3.9413, -26.7572, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 51.2184, -3.9413, -20.0679, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -21.7402, 48.3645, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 54.072197, -3.9413, -20.0679, 51.2184, -3.9413, -20.0679, 51.2184, -3.9413, -21.7402, 54.072197, -3.9413, -21.7402, 54.072197, -3.9413, -20.0679, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 51.2184, -3.9413, -21.7402, 51.2184, -3.9413, -23.412498, 54.072197, -3.9413, -21.7402, 51.2184, -3.9413, -21.7402, 48.3645, -3.9413, -25.084799, 51.2184, -3.9413, -23.412498, 48.3645, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 54.072197, -3.9413, -23.412498, 54.072197, -3.9413, -21.7402, 48.3645, -3.9413, -26.7572, 51.2184, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 48.3645, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -23.412498, 51.2184, -3.9413, -25.084799, 54.072197, -3.9413, -23.412498, 51.2184, -3.9413, -23.412498, 48.3645, -3.9413, -26.7572, 51.2184, -3.9413, -26.7572, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 54.072197, -3.9413, -25.084799, 54.072197, -3.9413, -23.412498, 51.2184, -3.9413, -26.7572, 54.072197, -3.9413, -25.084799, 51.2184, -3.9413, -25.084799, 51.2184, -3.9413, -26.7572, 54.072197, -3.9413, -26.7572, 54.072197, -3.9413, -25.084799, 29.679499, -0.6044, -15.0508995, 32.4649, -1.1213, -13.3786, 29.679499, -0.6044, -13.3786, 29.679499, -0.6044, -13.3786, 32.4649, -1.1213, -13.3786, 32.4649, -1.1213, -11.7063, 29.679499, -0.6044, -15.0508995, 32.4649, -1.1213, -15.0508995, 32.4649, -1.1213, -13.3786, 29.679499, -0.6044, -13.3786, 32.4649, -1.1213, -11.7063, 29.679499, -0.6044, -11.7063, 29.679499, -0.6044, -16.7232, 32.4649, -1.1213, -15.0508995, 29.679499, -0.6044, -15.0508995, 29.679499, -0.6044, -11.7063, 32.4649, -1.1213, -11.7063, 32.4649, -1.1213, -10.033899, 29.679499, -0.6044, -16.7232, 32.4649, -1.1213, -16.7232, 32.4649, -1.1213, -15.0508995, 29.679499, -0.6044, -11.7063, 32.4649, -1.1213, -10.033899, 29.679499, -0.6044, -10.033899, 29.679499, -0.6044, -18.3955, 32.4649, -1.1213, -16.7232, 29.679499, -0.6044, -16.7232, 29.679499, -0.6044, -10.033899, 32.4649, -1.1213, -10.033899, 32.4649, -1.1213, -8.3616, 29.679499, -0.6044, -18.3955, 32.4649, -1.1213, -18.3955, 32.4649, -1.1213, -16.7232, 29.679499, -0.6044, -10.033899, 32.4649, -1.1213, -8.3616, 29.679499, -0.6044, -8.3616, 29.679499, -0.6044, -20.0679, 32.4649, -1.1213, -18.3955, 29.679499, -0.6044, -18.3955, 29.679499, -0.6044, -8.3616, 32.4649, -1.1213, -8.3616, 32.4649, -1.1213, -6.6893, 29.679499, -0.6044, -20.0679, 32.4649, -1.1213, -20.0679, 32.4649, -1.1213, -18.3955, 29.679499, -0.6044, -8.3616, 32.4649, -1.1213, -6.6893, 29.679499, -0.6044, -6.6893, 29.679499, -0.6044, -21.7402, 32.4649, -1.1213, -20.0679, 29.679499, -0.6044, -20.0679, 29.679499, -0.6044, -6.6893, 32.4649, -1.1213, -6.6893, 32.4649, -1.1213, -5.0169997, 29.679499, -0.6044, -21.7402, 32.4649, -1.1213, -21.7402, 32.4649, -1.1213, -20.0679, 29.679499, -0.6044, -6.6893, 32.4649, -1.1213, -5.0169997, 29.679499, -0.6044, -5.0169997, 29.679499, -0.6044, -23.412498, 32.4649, -1.1213, -21.7402, 29.679499, -0.6044, -21.7402, 29.679499, -0.6044, -5.0169997, 32.4649, -1.1213, -5.0169997, 32.4649, -1.1213, -3.3446, 29.679499, -0.6044, -23.412498, 32.4649, -1.1213, -23.412498, 32.4649, -1.1213, -21.7402, 29.679499, -0.6044, -5.0169997, 32.4649, -1.1213, -3.3446, 29.679499, -0.6044, -3.3446, 29.679499, -0.6044, -25.084799, 32.4649, -1.1213, -23.412498, 29.679499, -0.6044, -23.412498, 29.679499, -0.6044, -3.3446, 32.4649, -1.1213, -3.3446, 32.4649, -1.1213, -1.6723, 29.679499, -0.6044, -25.084799, 32.4649, -1.1213, -25.084799, 32.4649, -1.1213, -23.412498, 29.679499, -0.6044, -26.7572, 32.4649, -1.1213, -25.084799, 29.679499, -0.6044, -25.084799, 29.679499, -0.6044, -26.7572, 32.4649, -1.1213, -26.7572, 32.4649, -1.1213, -25.084799, 29.679499, -0.6044, -3.3446, 32.4649, -1.1213, -1.6723, 29.679499, -0.6044, -1.6723, 29.679499, -0.6044, -1.6723, 32.4649, -1.1213, -1.6723, 32.4649, -1.1213, 0, 29.679499, -0.6044, -1.6723, 32.4649, -1.1213, 0, 29.679499, -0.6044, 0, 29.679499, -0.6044, 0, 32.4649, -1.1213, 0, 32.4649, -1.1213, 1.6723, 29.679499, -0.6044, 0, 32.4649, -1.1213, 1.6723, 29.679499, -0.6044, 1.6723, 29.679499, -0.6044, 1.6723, 32.4649, -1.1213, 1.6723, 32.4649, -1.1213, 3.3446, 29.679499, -0.6044, 1.6723, 32.4649, -1.1213, 3.3446, 29.679499, -0.6044, 3.3446, 29.679499, -0.6044, 3.3446, 32.4649, -1.1213, 3.3446, 32.4649, -1.1213, 5.0169997, 29.679499, -0.6044, 3.3446, 32.4649, -1.1213, 5.0169997, 29.679499, -0.6044, 5.0169997, 29.679499, -0.6044, 5.0169997, 32.4649, -1.1213, 5.0169997, 32.4649, -1.1213, 6.6893, 29.679499, -0.6044, 5.0169997, 32.4649, -1.1213, 6.6893, 29.679499, -0.6044, 6.6893, 29.679499, -0.6044, 6.6893, 32.4649, -1.1213, 6.6893, 32.4649, -1.1213, 8.3616, 29.679499, -0.6044, 6.6893, 32.4649, -1.1213, 8.3616, 29.679499, -0.6044, 8.3616, 29.679499, -0.6044, 8.3616, 32.4649, -1.1213, 8.3616, 32.4649, -1.1213, 10.033899, 29.679499, -0.6044, 8.3616, 32.4649, -1.1213, 10.033899, 29.679499, -0.6044, 10.033899, 29.679499, -0.6044, 10.033899, 32.4649, -1.1213, 10.033899, 32.4649, -1.1213, 11.7063, 29.679499, -0.6044, 10.033899, 32.4649, -1.1213, 11.7063, 29.679499, -0.6044, 11.7063, 29.679499, -0.6044, 11.7063, 32.4649, -1.1213, 11.7063, 32.4649, -1.1213, 13.3786, 29.679499, -0.6044, 11.7063, 32.4649, -1.1213, 13.3786, 29.679499, -0.6044, 13.3786, 29.679499, -0.6044, 13.3786, 32.4649, -1.1213, 13.3786, 32.4649, -1.1213, 15.0508995, 29.679499, -0.6044, 13.3786, 32.4649, -1.1213, 15.0508995, 29.679499, -0.6044, 15.0508995, 29.679499, -0.6044, 15.0508995, 32.4649, -1.1213, 15.0508995, 32.4649, -1.1213, 16.7232, 29.679499, -0.6044, 15.0508995, 32.4649, -1.1213, 16.7232, 29.679499, -0.6044, 16.7232, 29.679499, -0.6044, 16.7232, 32.4649, -1.1213, 16.7232, 32.4649, -1.1213, 18.3955, 29.679499, -0.6044, 16.7232, 32.4649, -1.1213, 18.3955, 29.679499, -0.6044, 18.3955, 29.679499, -0.6044, 18.3955, 32.4649, -1.1213, 18.3955, 32.4649, -1.1213, 20.0679, 29.679499, -0.6044, 18.3955, 32.4649, -1.1213, 20.0679, 29.679499, -0.6044, 20.0679, 29.679499, -0.6044, 20.0679, 32.4649, -1.1213, 20.0679, 32.4649, -1.1213, 21.7402, 29.679499, -0.6044, 20.0679, 32.4649, -1.1213, 21.7402, 29.679499, -0.6044, 21.7402, 29.679499, -0.6044, 21.7402, 32.4649, -1.1213, 21.7402, 32.4649, -1.1213, 23.412498, 29.679499, -0.6044, 21.7402, 32.4649, -1.1213, 23.412498, 29.679499, -0.6044, 23.412498, 29.679499, -0.6044, 23.412498, 32.4649, -1.1213, 23.412498, 32.4649, -1.1213, 25.084799, 29.679499, -0.6044, 23.412498, 32.4649, -1.1213, 25.084799, 29.679499, -0.6044, 25.084799, 29.679499, -0.6044, 25.084799, 32.4649, -1.1213, 25.084799, 32.4649, -1.1213, 26.7572, 29.679499, -0.6044, 25.084799, 32.4649, -1.1213, 26.7572, 29.679499, -0.6044, 26.7572, 32.4649, -1.1213, -15.0508995, 39.7973, -3.9413, -13.3786, 32.4649, -1.1213, -13.3786, 32.4649, -1.1213, -13.3786, 39.7973, -3.9413, -13.3786, 39.7973, -3.9413, -11.7063, 32.4649, -1.1213, -15.0508995, 39.7973, -3.9413, -15.0508995, 39.7973, -3.9413, -13.3786, 32.4649, -1.1213, -13.3786, 39.7973, -3.9413, -11.7063, 32.4649, -1.1213, -11.7063, 32.4649, -1.1213, -16.7232, 39.7973, -3.9413, -15.0508995, 32.4649, -1.1213, -15.0508995, 32.4649, -1.1213, -11.7063, 39.7973, -3.9413, -11.7063, 39.7973, -3.9413, -10.033899, 32.4649, -1.1213, -16.7232, 39.7973, -3.9413, -16.7232, 39.7973, -3.9413, -15.0508995, 32.4649, -1.1213, -11.7063, 39.7973, -3.9413, -10.033899, 32.4649, -1.1213, -10.033899, 32.4649, -1.1213, -18.3955, 39.7973, -3.9413, -16.7232, 32.4649, -1.1213, -16.7232, 32.4649, -1.1213, -10.033899, 39.7973, -3.9413, -10.033899, 39.7973, -3.9413, -8.3616, 32.4649, -1.1213, -18.3955, 39.7973, -3.9413, -18.3955, 39.7973, -3.9413, -16.7232, 32.4649, -1.1213, -10.033899, 39.7973, -3.9413, -8.3616, 32.4649, -1.1213, -8.3616, 32.4649, -1.1213, -20.0679, 39.7973, -3.9413, -18.3955, 32.4649, -1.1213, -18.3955, 32.4649, -1.1213, -8.3616, 39.7973, -3.9413, -8.3616, 39.7973, -3.9413, -6.6893, 32.4649, -1.1213, -20.0679, 39.7973, -3.9413, -20.0679, 39.7973, -3.9413, -18.3955, 32.4649, -1.1213, -8.3616, 39.7973, -3.9413, -6.6893, 32.4649, -1.1213, -6.6893, 32.4649, -1.1213, -21.7402, 39.7973, -3.9413, -20.0679, 32.4649, -1.1213, -20.0679, 32.4649, -1.1213, -6.6893, 39.7973, -3.9413, -6.6893, 39.7973, -3.9413, -5.0169997, 32.4649, -1.1213, -21.7402, 39.7973, -3.9413, -21.7402, 39.7973, -3.9413, -20.0679, 32.4649, -1.1213, -6.6893, 39.7973, -3.9413, -5.0169997, 32.4649, -1.1213, -5.0169997, 32.4649, -1.1213, -23.412498, 39.7973, -3.9413, -21.7402, 32.4649, -1.1213, -21.7402, 32.4649, -1.1213, -5.0169997, 39.7973, -3.9413, -5.0169997, 39.7973, -3.9413, -3.3446, 32.4649, -1.1213, -23.412498, 39.7973, -3.9413, -23.412498, 39.7973, -3.9413, -21.7402, 32.4649, -1.1213, -5.0169997, 39.7973, -3.9413, -3.3446, 32.4649, -1.1213, -3.3446, 32.4649, -1.1213, -25.084799, 39.7973, -3.9413, -23.412498, 32.4649, -1.1213, -23.412498, 32.4649, -1.1213, -3.3446, 39.7973, -3.9413, -3.3446, 39.7973, -3.9413, -1.6723, 32.4649, -1.1213, -25.084799, 39.7973, -3.9413, -25.084799, 39.7973, -3.9413, -23.412498, 32.4649, -1.1213, -26.7572, 39.7973, -3.9413, -25.084799, 32.4649, -1.1213, -25.084799, 32.4649, -1.1213, -26.7572, 39.7973, -3.9413, -26.7572, 39.7973, -3.9413, -25.084799, 32.4649, -1.1213, -3.3446, 39.7973, -3.9413, -1.6723, 32.4649, -1.1213, -1.6723, 32.4649, -1.1213, -1.6723, 39.7973, -3.9413, -1.6723, 39.7973, -3.9413, 0, 32.4649, -1.1213, -1.6723, 39.7973, -3.9413, 0, 32.4649, -1.1213, 0, 32.4649, -1.1213, 0, 39.7973, -3.9413, 0, 39.7973, -3.9413, 1.6723, 32.4649, -1.1213, 0, 39.7973, -3.9413, 1.6723, 32.4649, -1.1213, 1.6723, 32.4649, -1.1213, 1.6723, 39.7973, -3.9413, 1.6723, 39.7973, -3.9413, 3.3446, 32.4649, -1.1213, 1.6723, 39.7973, -3.9413, 3.3446, 32.4649, -1.1213, 3.3446, 32.4649, -1.1213, 3.3446, 39.7973, -3.9413, 3.3446, 39.7973, -3.9413, 5.0169997, 32.4649, -1.1213, 3.3446, 39.7973, -3.9413, 5.0169997, 32.4649, -1.1213, 5.0169997, 32.4649, -1.1213, 5.0169997, 39.7973, -3.9413, 5.0169997, 39.7973, -3.9413, 6.6893, 32.4649, -1.1213, 5.0169997, 39.7973, -3.9413, 6.6893, 32.4649, -1.1213, 6.6893, 32.4649, -1.1213, 6.6893, 39.7973, -3.9413, 6.6893, 39.7973, -3.9413, 8.3616, 32.4649, -1.1213, 6.6893, 39.7973, -3.9413, 8.3616, 32.4649, -1.1213, 8.3616, 32.4649, -1.1213, 8.3616, 39.7973, -3.9413, 8.3616, 39.7973, -3.9413, 10.033899, 32.4649, -1.1213, 8.3616, 39.7973, -3.9413, 10.033899, 32.4649, -1.1213, 10.033899, 32.4649, -1.1213, 10.033899, 39.7973, -3.9413, 10.033899, 39.7973, -3.9413, 11.7063, 32.4649, -1.1213, 10.033899, 39.7973, -3.9413, 11.7063, 32.4649, -1.1213, 11.7063, 32.4649, -1.1213, 11.7063, 39.7973, -3.9413, 11.7063, 39.7973, -3.9413, 13.3786, 32.4649, -1.1213, 11.7063, 39.7973, -3.9413, 13.3786, 32.4649, -1.1213, 13.3786, 32.4649, -1.1213, 13.3786, 39.7973, -3.9413, 13.3786, 39.7973, -3.9413, 15.0508995, 32.4649, -1.1213, 13.3786, 39.7973, -3.9413, 15.0508995, 32.4649, -1.1213, 15.0508995, 32.4649, -1.1213, 15.0508995, 39.7973, -3.9413, 15.0508995, 39.7973, -3.9413, 16.7232, 32.4649, -1.1213, 15.0508995, 39.7973, -3.9413, 16.7232, 32.4649, -1.1213, 16.7232, 32.4649, -1.1213, 16.7232, 39.7973, -3.9413, 16.7232, 39.7973, -3.9413, 18.3955, 32.4649, -1.1213, 16.7232, 39.7973, -3.9413, 18.3955, 32.4649, -1.1213, 18.3955, 32.4649, -1.1213, 18.3955, 39.7973, -3.9413, 18.3955, 39.7973, -3.9413, 20.0679, 32.4649, -1.1213, 18.3955, 39.7973, -3.9413, 20.0679, 32.4649, -1.1213, 20.0679, 32.4649, -1.1213, 20.0679, 39.7973, -3.9413, 20.0679, 39.7973, -3.9413, 21.7402, 32.4649, -1.1213, 20.0679, 39.7973, -3.9413, 21.7402, 32.4649, -1.1213, 21.7402, 32.4649, -1.1213, 21.7402, 39.7973, -3.9413, 21.7402, 39.7973, -3.9413, 23.412498, 32.4649, -1.1213, 21.7402, 39.7973, -3.9413, 23.412498, 32.4649, -1.1213, 23.412498, 32.4649, -1.1213, 23.412498, 39.7973, -3.9413, 23.412498, 39.7973, -3.9413, 25.084799, 32.4649, -1.1213, 23.412498, 39.7973, -3.9413, 25.084799, 32.4649, -1.1213, 25.084799, 32.4649, -1.1213, 25.084799, 39.7973, -3.9413, 25.084799, 39.7973, -3.9413, 26.7572, 32.4649, -1.1213, 25.084799, 39.7973, -3.9413, 26.7572, 32.4649, -1.1213, 26.7572, 26.7572, -0.2593, 16.7232, 29.679499, -0.6044, 18.3955, 26.7572, -0.2593, 18.3955, 26.7572, -0.2593, 18.3955, 29.679499, -0.6044, 18.3955, 29.679499, -0.6044, 20.0679, 26.7572, -0.2593, 16.7232, 29.679499, -0.6044, 16.7232, 29.679499, -0.6044, 18.3955, 26.7572, -0.2593, 18.3955, 29.679499, -0.6044, 20.0679, 26.7572, -0.2593, 20.0679, 26.7572, -0.2593, 15.0508995, 29.679499, -0.6044, 16.7232, 26.7572, -0.2593, 16.7232, 26.7572, -0.2593, 20.0679, 29.679499, -0.6044, 20.0679, 29.679499, -0.6044, 21.7402, 26.7572, -0.2593, 15.0508995, 29.679499, -0.6044, 15.0508995, 29.679499, -0.6044, 16.7232, 26.7572, -0.2593, 20.0679, 29.679499, -0.6044, 21.7402, 26.7572, -0.2593, 21.7402, 26.7572, -0.2593, 13.3786, 29.679499, -0.6044, 15.0508995, 26.7572, -0.2593, 15.0508995, 26.7572, -0.2593, 21.7402, 29.679499, -0.6044, 21.7402, 29.679499, -0.6044, 23.412498, 26.7572, -0.2593, 13.3786, 29.679499, -0.6044, 13.3786, 29.679499, -0.6044, 15.0508995, 26.7572, -0.2593, 21.7402, 29.679499, -0.6044, 23.412498, 26.7572, -0.2593, 23.412498, 26.7572, -0.2593, 11.7063, 29.679499, -0.6044, 13.3786, 26.7572, -0.2593, 13.3786, 26.7572, -0.2593, 23.412498, 29.679499, -0.6044, 23.412498, 29.679499, -0.6044, 25.084799, 26.7572, -0.2593, 11.7063, 29.679499, -0.6044, 11.7063, 29.679499, -0.6044, 13.3786, 26.7572, -0.2593, 23.412498, 29.679499, -0.6044, 25.084799, 26.7572, -0.2593, 25.084799, 26.7572, -0.2593, 25.084799, 29.679499, -0.6044, 25.084799, 29.679499, -0.6044, 26.7572, 26.7572, -0.2593, 25.084799, 29.679499, -0.6044, 26.7572, 26.7572, -0.2593, 26.7572, 26.7572, -0.2593, 10.033899, 29.679499, -0.6044, 11.7063, 26.7572, -0.2593, 11.7063, 26.7572, -0.2593, 10.033899, 29.679499, -0.6044, 10.033899, 29.679499, -0.6044, 11.7063, 26.7572, -0.2593, 8.3616, 29.679499, -0.6044, 10.033899, 26.7572, -0.2593, 10.033899, 26.7572, -0.2593, 8.3616, 29.679499, -0.6044, 8.3616, 29.679499, -0.6044, 10.033899, 26.7572, -0.2593, 6.6893, 29.679499, -0.6044, 8.3616, 26.7572, -0.2593, 8.3616, 26.7572, -0.2593, 6.6893, 29.679499, -0.6044, 6.6893, 29.679499, -0.6044, 8.3616, 26.7572, -0.2593, 5.0169997, 29.679499, -0.6044, 6.6893, 26.7572, -0.2593, 6.6893, 26.7572, -0.2593, 5.0169997, 29.679499, -0.6044, 5.0169997, 29.679499, -0.6044, 6.6893, 26.7572, -0.2593, 3.3446, 29.679499, -0.6044, 5.0169997, 26.7572, -0.2593, 5.0169997, 26.7572, -0.2593, 3.3446, 29.679499, -0.6044, 3.3446, 29.679499, -0.6044, 5.0169997, 26.7572, -0.2593, 1.6723, 29.679499, -0.6044, 3.3446, 26.7572, -0.2593, 3.3446, 26.7572, -0.2593, 1.6723, 29.679499, -0.6044, 1.6723, 29.679499, -0.6044, 3.3446, 26.7572, -0.2593, 0, 29.679499, -0.6044, 1.6723, 26.7572, -0.2593, 1.6723, 26.7572, -0.2593, 0, 29.679499, -0.6044, 0, 29.679499, -0.6044, 1.6723, 26.7572, -0.2593, -1.6723, 29.679499, -0.6044, 0, 26.7572, -0.2593, 0, 26.7572, -0.2593, -1.6723, 29.679499, -0.6044, -1.6723, 29.679499, -0.6044, 0, 26.7572, -0.2593, -3.3446, 29.679499, -0.6044, -1.6723, 26.7572, -0.2593, -1.6723, 26.7572, -0.2593, -3.3446, 29.679499, -0.6044, -3.3446, 29.679499, -0.6044, -1.6723, 26.7572, -0.2593, -5.0169997, 29.679499, -0.6044, -3.3446, 26.7572, -0.2593, -3.3446, 26.7572, -0.2593, -5.0169997, 29.679499, -0.6044, -5.0169997, 29.679499, -0.6044, -3.3446, 26.7572, -0.2593, -6.6893, 29.679499, -0.6044, -5.0169997, 26.7572, -0.2593, -5.0169997, 26.7572, -0.2593, -6.6893, 29.679499, -0.6044, -6.6893, 29.679499, -0.6044, -5.0169997, 26.7572, -0.2593, -8.3616, 29.679499, -0.6044, -6.6893, 26.7572, -0.2593, -6.6893, 26.7572, -0.2593, -8.3616, 29.679499, -0.6044, -8.3616, 29.679499, -0.6044, -6.6893, 26.7572, -0.2593, -10.033899, 29.679499, -0.6044, -8.3616, 26.7572, -0.2593, -8.3616, 26.7572, -0.2593, -10.033899, 29.679499, -0.6044, -10.033899, 29.679499, -0.6044, -8.3616, 26.7572, -0.2593, -11.7063, 29.679499, -0.6044, -10.033899, 26.7572, -0.2593, -10.033899, 26.7572, -0.2593, -11.7063, 29.679499, -0.6044, -11.7063, 29.679499, -0.6044, -10.033899, 26.7572, -0.2593, -13.3786, 29.679499, -0.6044, -11.7063, 26.7572, -0.2593, -11.7063, 26.7572, -0.2593, -13.3786, 29.679499, -0.6044, -13.3786, 29.679499, -0.6044, -11.7063, 26.7572, -0.2593, -15.0508995, 29.679499, -0.6044, -13.3786, 26.7572, -0.2593, -13.3786, 26.7572, -0.2593, -15.0508995, 29.679499, -0.6044, -15.0508995, 29.679499, -0.6044, -13.3786, 26.7572, -0.2593, -16.7232, 29.679499, -0.6044, -15.0508995, 26.7572, -0.2593, -15.0508995, 26.7572, -0.2593, -16.7232, 29.679499, -0.6044, -16.7232, 29.679499, -0.6044, -15.0508995, 26.7572, -0.2593, -18.3955, 29.679499, -0.6044, -16.7232, 26.7572, -0.2593, -16.7232, 26.7572, -0.2593, -18.3955, 29.679499, -0.6044, -18.3955, 29.679499, -0.6044, -16.7232, 26.7572, -0.2593, -20.0679, 29.679499, -0.6044, -18.3955, 26.7572, -0.2593, -18.3955, 26.7572, -0.2593, -20.0679, 29.679499, -0.6044, -20.0679, 29.679499, -0.6044, -18.3955, 26.7572, -0.2593, -21.7402, 29.679499, -0.6044, -20.0679, 26.7572, -0.2593, -20.0679, 26.7572, -0.2593, -21.7402, 29.679499, -0.6044, -21.7402, 29.679499, -0.6044, -20.0679, 26.7572, -0.2593, -23.412498, 29.679499, -0.6044, -21.7402, 26.7572, -0.2593, -21.7402, 26.7572, -0.2593, -23.412498, 29.679499, -0.6044, -23.412498, 29.679499, -0.6044, -21.7402, 26.7572, -0.2593, -25.084799, 29.679499, -0.6044, -23.412498, 26.7572, -0.2593, -23.412498, 26.7572, -0.2593, -25.084799, 29.679499, -0.6044, -25.084799, 29.679499, -0.6044, -23.412498, 26.7572, -0.2593, -26.7572, 29.679499, -0.6044, -25.084799, 26.7572, -0.2593, -25.084799, 26.7572, -0.2593, -26.7572, 29.679499, -0.6044, -26.7572, 29.679499, -0.6044, -25.084799) @@ -288,3 +289,7 @@ transform = Transform3D(-0.9976046, 0, -0.06917416, 0, 1, 0, 0.06917416, 0, -0.9 [node name="PlayerStorageBox" parent="Interactables" instance=ExtResource("8_storage")] position = Vector3(-3.25, 3.42, 11.25) + +[node name="DecorShopWorld" parent="Interactables" instance=ExtResource("25_decor_shop")] +position = Vector3(-3.25, 3.42, 11.25) +rotation = Vector3(0, 1.5707964, 0) diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index 792e4ed..85f6e1b 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -18,6 +18,10 @@ func get_fishing_shop() -> FishingShopInteraction: return null +func get_decor_shop() -> DecorShopInteraction: + return null + + func get_player_storage() -> PlayerStorageInteraction: return null diff --git a/world/test_world.gd b/world/test_world.gd index 9f4da5e..1b0a813 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -7,6 +7,9 @@ const FishingShopInteractionType = preload( const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) +const DecorShopInteractionType = preload( + "res://world/decor_shop_interaction.gd" +) const WorldLayoutType = preload("res://world/world_layout.gd") const GENERATED_REGION_SCENE: PackedScene = preload( "res://world/generation/generated_world_region.tscn" @@ -31,6 +34,7 @@ var _world_layout: StringName = WorldLayoutType.GENERATED var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED var _light_performance_profile: bool = false var _dedicated_simulation: bool = false +var _local_home_interior_presentation_active: bool = false func get_player_water_triggers() -> Array[PlayerWaterTrigger]: @@ -67,6 +71,10 @@ func get_player_storage() -> PlayerStorageInteractionType: return _active_region.get_player_storage() +func get_decor_shop() -> DecorShopInteractionType: + return _active_region.get_decor_shop() + + func get_player_spawn_transform() -> Transform3D: return _active_region.get_player_spawn_transform() @@ -85,6 +93,12 @@ func set_light_performance_profile(enabled: bool) -> void: _active_region.set_light_performance_profile(enabled) +func set_local_home_interior_presentation_active(enabled: bool) -> void: + _local_home_interior_presentation_active = enabled + if _active_region != null and not _dedicated_simulation: + _active_region.visible = not enabled + + func set_dedicated_simulation(enabled: bool) -> void: _dedicated_simulation = enabled if _active_region != null: @@ -272,6 +286,8 @@ func _replace_active_region(layout: StringName, seed: int) -> bool: _active_region.set_light_performance_profile(_light_performance_profile) if _dedicated_simulation: _set_region_presentation_enabled(_active_region, false) + else: + _active_region.visible = not _local_home_interior_presentation_active return true