From f910e6bdbbecad3fd1dd50218ecf6e04cd3d8601 Mon Sep 17 00:00:00 2001 From: Voyager Date: Wed, 26 Aug 2026 00:05:51 -0400 Subject: [PATCH 01/17] Update Forgejo links for Straywild rename --- README.md | 4 ++-- docs/README.md | 4 ++-- scripts/build_playtest.sh | 2 +- scripts/build_portmaster.sh | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 536afab..e228e7b 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ Discord: https://discord.gg/5gP22447kc Matrix: https://matrix.to/#/#straywild:matrix.makearmy.io -Repo: https://forge.makearmy.io/woofmeow/netfishing +Repo: https://forge.makearmy.io/woofmeow/straywild -Releases: https://forge.makearmy.io/woofmeow/netfishing/releases +Releases: https://forge.makearmy.io/woofmeow/straywild/releases ## Installing release builds diff --git a/docs/README.md b/docs/README.md index 5327b30..444ebc7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,10 +17,10 @@ for local README files. ## Related repositories -- [Discovery service API](https://forge.makearmy.io/woofmeow/netfishing-discovery-server/src/branch/main/docs/API.md) +- [Discovery service API](https://forge.makearmy.io/woofmeow/straywild-discovery-server/src/branch/main/docs/API.md) stays with the service implementation and defines its versioned HTTP contract. -- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/netfishing-dedicated-server/src/branch/main/README.md) +- [Dedicated-server packaging](https://forge.makearmy.io/woofmeow/straywild-dedicated-server/src/branch/main/README.md) stays with its installer and update scripts. It consumes this repository's attribution and legal notices from the exact pinned game commit instead of maintaining copies. diff --git a/scripts/build_playtest.sh b/scripts/build_playtest.sh index 1c74943..2347d3d 100755 --- a/scripts/build_playtest.sh +++ b/scripts/build_playtest.sh @@ -9,7 +9,7 @@ readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64" readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64" readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt" readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)" -readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/netfishing" +readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild" readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-windows-x86_64.zip" readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-linux-x86_64.zip" readonly GODOT_BIN="${GODOT_BIN:-godot}" diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index 0487b5b..a40e0cd 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -193,7 +193,7 @@ cat >"${GAME_ROOT}/licenses/SOURCE-CODE.md" < Date: Sat, 29 Aug 2026 17:41:10 -0400 Subject: [PATCH 02/17] Fix clam spurt origin artifact (#118) --- gathering/world_gatherable.gd | 7 +++-- tests/world_spawn_protocol_validation.gd | 39 ++++++++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/gathering/world_gatherable.gd b/gathering/world_gatherable.gd index 8a07ba0..1a71477 100644 --- a/gathering/world_gatherable.gd +++ b/gathering/world_gatherable.gd @@ -29,12 +29,16 @@ func configure( entity_id = configured_entity_id data = configured_data type_id = data.type_id if data != null else StringName() + # Establish the authoritative transform before constructing transient visuals. + # Effects emitted before this point would begin at the default world origin. + apply_network_state(position, yaw, true) if data != null and data.is_stationary_hotspot(): _ensure_water_spurt_visual() _water_spurt_elapsed = ( float(abs(hash(entity_id)) % 1000) / 1000.0 * WATER_SPURT_INTERVAL_SECONDS ) + _emit_water_spurt() else: _ensure_visual() if data != null and data.catch_data != null and _sprite != null: @@ -50,7 +54,6 @@ func configure( ) ) _sprite.rotation_degrees.x = data.sprite_tilt_degrees - apply_network_state(position, yaw, true) func apply_network_state( @@ -147,7 +150,6 @@ func _ensure_water_spurt_visual() -> void: hole.mesh = hole_mesh hole.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF _water_spurt_root.add_child(hole) - _emit_water_spurt() func _emit_water_spurt() -> void: @@ -159,6 +161,7 @@ func _emit_water_spurt() -> void: particles.amount = 6 particles.lifetime = 0.42 particles.one_shot = true + particles.local_coords = true particles.explosiveness = 1.0 particles.direction = Vector3.UP particles.spread = 32.0 diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index a8365a1..d100260 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -16,6 +16,10 @@ const CalendarSeasonType = preload("res://world/calendar_season.gd") func _initialize() -> void: + call_deferred(&"_run") + + +func _run() -> void: assert(NetworkProtocol.PROTOCOL_VERSION == 11) assert( NetworkWorldSpawnProtocol.CAPABILITY @@ -23,7 +27,7 @@ func _initialize() -> void: ) assert(NetworkWorldSpawnProtocol.SNAPSHOT_ENTITIES_PER_ENVELOPE <= 4) _validate_catalog_statuses() - _validate_billboard_presentation() + await _validate_billboard_presentation() _validate_envelopes() print("World spawn protocol validation: PASS") quit() @@ -148,8 +152,27 @@ func _validate_billboard_presentation() -> void: assert(not sprite.shaded) gatherable.free() + var unconfigured_hotspot := WorldGatherableType.new() + unconfigured_hotspot.call("_ensure_water_spurt_visual") + assert( + unconfigured_hotspot.get_node_or_null( + "WaterSpurt/WaterDroplets" + ) == null + ) + unconfigured_hotspot.free() + + var clam: GatherableData = Gatherables.get_entry(&"clam_manila") + assert(clam != null) var hotspot := WorldGatherableType.new() - hotspot.call("_ensure_water_spurt_visual") + root.add_child(hotspot) + var configured_position := Vector3(24.0, 0.3, -11.0) + hotspot.configure( + "world:clam-presentation-regression", + clam, + configured_position, + 0.0, + ) + assert(hotspot.global_position.is_equal_approx(configured_position)) var hole := hotspot.get_node("WaterSpurt/BurrowMark") as MeshInstance3D assert(hole != null) assert(hole.cast_shadow == GeometryInstance3D.SHADOW_CASTING_SETTING_OFF) @@ -158,6 +181,18 @@ func _validate_billboard_presentation() -> void: assert(hole_material.shading_mode == BaseMaterial3D.SHADING_MODE_UNSHADED) assert(hole_material.transparency == BaseMaterial3D.TRANSPARENCY_DISABLED) assert(is_equal_approx(hole_material.albedo_color.a, 1.0)) + var particles := hotspot.get_node( + "WaterSpurt/WaterDroplets" + ) as CPUParticles3D + assert(particles != null and particles.emitting) + assert(particles.local_coords) + assert(particles.global_position.is_equal_approx( + configured_position + Vector3.UP * 0.035 + )) + hotspot.set_process(false) + await create_timer(0.6).timeout + await process_frame + assert(hotspot.get_node_or_null("WaterSpurt/WaterDroplets") == null) hotspot.queue_free() From 4575830857c18dd6d9a3b4761c0df7c19cacf18c Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 29 Aug 2026 17:43:11 -0400 Subject: [PATCH 03/17] Improve nearby water recovery and camera prompts Recover players on generated worlds to the nearest safe grass or sand surface. Use the active gameplay camera when positioning shop and storage prompts. --- main/main.gd | 18 +- tests/art_tools_validation.gd | 6 + tests/fishing_authority_validation.gd | 9 + tests/generated_world_runtime_validation.gd | 75 ++++++ tests/world_layout_validation.gd | 8 + ui/game_ui.gd | 4 +- world/generation/generated_world_region.gd | 239 ++++++++++++++++++++ world/regions/world_region.gd | 18 ++ world/test_world.gd | 10 + world/water_recovery_controller.gd | 34 ++- 10 files changed, 396 insertions(+), 25 deletions(-) diff --git a/main/main.gd b/main/main.gd index 442c97c..7fae866 100644 --- a/main/main.gd +++ b/main/main.gd @@ -1006,7 +1006,8 @@ func _initialize_application(dedicated: bool) -> void: _game_ui, _game_ui.get_screen_fade(), _test_world.get_player_water_triggers(), - _test_world.get_safe_respawn_points() + _test_world.get_safe_respawn_points(), + Callable(_test_world, "get_water_recovery_position"), ) _ui_pixelation.effective_pixel_size_changed.connect( _on_effective_ui_pixel_size_changed @@ -2344,17 +2345,9 @@ func _on_remote_recovery_requested( peer_id, "Fishing attempt ended." ) - var target_position: Vector3 = _test_world.get_player_spawn_transform().origin - var nearest_distance: float = INF - for point: SafeRespawnPoint in _test_world.get_safe_respawn_points(): - if point == null or not point.enabled: - continue - var distance: float = point.get_horizontal_distance_squared( - entry_position - ) - if distance < nearest_distance: - nearest_distance = distance - target_position = point.global_position + var target_position := _test_world.get_water_recovery_position( + entry_position + ) target_position.y += _water_recovery.respawn_height_offset avatar.global_position = target_position avatar.velocity = Vector3.ZERO @@ -2432,6 +2425,7 @@ func _apply_world( spawn_transform, _test_world.get_player_water_triggers(), _test_world.get_safe_respawn_points(), + Callable(_test_world, "get_water_recovery_position"), ) _shoreline_ambience.configure( _player, diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 3b9d6a3..4bfab98 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -416,6 +416,12 @@ func _run() -> void: ) assert(normal_camera.is_position_behind(chat_anchor)) assert(not free_camera.is_position_behind(chat_anchor)) + var shop_prompt := game_ui.get_node("%ShopPrompt") as Control + var storage_prompt := game_ui.get_node("%StoragePrompt") as Control + game_ui.set_shop_prompt_visible(true, chat_anchor) + game_ui.set_storage_prompt_visible(true, chat_anchor) + assert(shop_prompt.visible) + assert(storage_prompt.visible) chat_ui.show_local_speech("hello there friend") await process_frame assert(bool(player.get("_speech_mouth_active"))) diff --git a/tests/fishing_authority_validation.gd b/tests/fishing_authority_validation.gd index e7c87bb..b3c17dd 100644 --- a/tests/fishing_authority_validation.gd +++ b/tests/fishing_authority_validation.gd @@ -265,6 +265,15 @@ func _run() -> void: assert(bool(water_recovery.get("_prior_movement_enabled"))) assert(bool(water_recovery.get("_prior_camera_input_enabled"))) assert(not pause_menu.visible) + var test_world := main.get_node("TestWorld") as TestWorld + assert(test_world != null) + var recovery_entry := water_recovery.get("_entry_position") as Vector3 + var resolved_recovery := water_recovery.call( + "_resolve_recovery_position" + ) as Vector3 + assert(resolved_recovery.is_equal_approx( + test_world.get_water_recovery_position(recovery_entry) + )) water_recovery.call("_finish_recovery") assert(player.is_movement_enabled()) diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index f487ae7..6a3f6c8 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -536,6 +536,11 @@ func _validate_generated_region( -INF, 0.35, ) + _validate_water_recovery_positions( + region, + grass_prop_surface_triangles, + sand_prop_surface_triangles, + ) var anchors := region.get_node( "GatherableAnchors/ReachableTreeTrunks" ) as GatherableAnchorSet3D @@ -1268,6 +1273,76 @@ func _triangle_surface_height_at( return highest +func _validate_water_recovery_positions( + region: GeneratedWorldRegion, + grass_triangles: Array[PackedVector3Array], + sand_triangles: Array[PackedVector3Array], +) -> void: + var fresh_water_root := region.get_node( + "WaterBodies/FreshWaterBodies" + ) as Node3D + assert(fresh_water_root != null and fresh_water_root.get_child_count() > 0) + var fresh_water := fresh_water_root.get_child(0) as WaterBodyAuthoring + assert(fresh_water != null) + var entry_position := fresh_water.global_position + var fallback_position := region.get_player_spawn_transform().origin + var recovery_position := region.get_water_recovery_position( + entry_position, + fallback_position, + ) + assert(recovery_position.is_finite()) + assert( + _horizontal_distance_squared(entry_position, recovery_position) + < _horizontal_distance_squared(entry_position, fallback_position), + "Fresh-water recovery returned the distant generated-world spawn.", + ) + assert( + recovery_position.y + > GeneratedWorldRegion.WATER_HEIGHT + + GeneratedWorldRegion.WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE + ) + var recovery_triangles: Array[PackedVector3Array] = [] + recovery_triangles.append_array(grass_triangles) + recovery_triangles.append_array(sand_triangles) + var center_height := _triangle_surface_height_at( + recovery_position, + recovery_triangles, + ) + assert(center_height > -INF) + assert(absf(center_height - recovery_position.y) <= 0.01) + for direction: Vector2 in GeneratedWorldRegion.PROP_SURFACE_SAMPLE_DIRECTIONS: + var footprint_position := recovery_position + Vector3( + direction.x * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS, + 0.0, + direction.y * GeneratedWorldRegion.WATER_RECOVERY_FOOTPRINT_RADIUS, + ) + var footprint_height := _triangle_surface_height_at( + footprint_position, + recovery_triangles, + ) + assert(footprint_height > -INF) + assert( + absf(footprint_height - recovery_position.y) + <= GeneratedWorldRegion.PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE + ) + assert( + region.get_water_recovery_position( + entry_position, + fallback_position, + ).is_equal_approx(recovery_position) + ) + assert( + region.get_water_recovery_position( + Vector3(INF, 0.0, 0.0), + fallback_position, + ).is_equal_approx(fallback_position) + ) + + +func _horizontal_distance_squared(a: Vector3, b: Vector3) -> float: + return Vector2(a.x - b.x, a.z - b.z).length_squared() + + func _validate_tree_gatherable_anchors( region: GeneratedWorldRegion, decorations: Node3D, diff --git a/tests/world_layout_validation.gd b/tests/world_layout_validation.gd index dd40fcf..e6eec39 100644 --- a/tests/world_layout_validation.gd +++ b/tests/world_layout_validation.gd @@ -100,6 +100,14 @@ func _validate_world_switching() -> void: assert(world.get_fishing_shop() != null) assert(world.get_player_storage() != null) assert(not world.get_fishable_water_regions().is_empty()) + var starter_safe_points := world.get_safe_respawn_points() + assert(not starter_safe_points.is_empty()) + var authored_safe_point: SafeRespawnPoint = starter_safe_points.back() + assert( + world.get_water_recovery_position( + authored_safe_point.global_position + ).is_equal_approx(authored_safe_point.global_position) + ) assert(world.activate_world(WorldLayout.GENERATED, 24680)) assert(world.get_world_layout() == WorldLayout.GENERATED) assert(world.get_generation_seed() == 24680) diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 6e4241b..7f8f0a6 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -1892,7 +1892,7 @@ func _position_storage_prompt(world_anchor: Vector3) -> void: if _player == null: _storage_prompt.hide() return - var camera := _player.get_gameplay_camera() + var camera := _player.get_active_gameplay_camera() if camera == null or camera.is_position_behind(world_anchor): _storage_prompt.hide() return @@ -2006,7 +2006,7 @@ func _position_shop_prompt(world_anchor: Vector3) -> void: if _player == null: _shop_prompt.hide() return - var camera: Camera3D = _player.get_gameplay_camera() + var camera: Camera3D = _player.get_active_gameplay_camera() if camera == null or camera.is_position_behind(world_anchor): _shop_prompt.hide() return diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index aa27a65..780ddb8 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -43,6 +43,12 @@ const PROP_MINIMUM_GROUND_CLEARANCE := 0.05 const PROP_MAXIMUM_SUPPORT_HEIGHT_DIFFERENCE := 0.35 const PROP_CHANCE_SCALE := 10000 const PROP_SELECTION_WEIGHT_SCALE := 1000 +const WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE := 0.05 +const WATER_RECOVERY_MINIMUM_UP_DOT := 0.6 +const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6 +const WATER_RECOVERY_INSET_STEP := 0.25 +const WATER_RECOVERY_MAXIMUM_INSET := 1.5 +const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0] const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [ Vector2(1.0, 0.0), Vector2(0.70710678, 0.70710678), @@ -90,6 +96,7 @@ var _placed_prop_clearance_radii: Array[float] = [] var _placed_prop_groups: Array[StringName] = [] var _placed_group_coordinates: Dictionary[StringName, Array] = {} var _biome_assignments: Dictionary[Vector2i, StringName] = {} +var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {} func _ready() -> void: @@ -190,6 +197,37 @@ func get_spawn_surface_triangles( return triangles +func get_water_recovery_position( + entry_position: Vector3, + fallback_position: Vector3, +) -> Vector3: + if ( + not entry_position.is_finite() + or _water_recovery_triangles_by_coordinate.is_empty() + ): + return super(entry_position, fallback_position) + var nearest_surface_distance := _nearest_recovery_surface_distance( + entry_position + ) + if not is_finite(nearest_surface_distance): + return super(entry_position, fallback_position) + for expansion: float in WATER_RECOVERY_SEARCH_EXPANSIONS: + var maximum_distance := nearest_surface_distance + expansion + var result: Variant = _nearest_safe_recovery_surface_position( + entry_position, + maximum_distance, + ) + if result is Vector3: + return result as Vector3 + var map_wide_result: Variant = _nearest_safe_recovery_surface_position( + entry_position, + get_playable_half_extents().length() * 2.0, + ) + if map_wide_result is Vector3: + return map_wide_result as Vector3 + return super(entry_position, fallback_position) + + func _on_generation_completed(summary: Dictionary) -> void: _diggable_beach.invalidate_surface_cache() var records: Array[Dictionary] = _generator.placement_records() @@ -325,6 +363,10 @@ func _on_generation_completed(summary: Dictionary) -> void: grass_surface_triangles_by_coordinate, sand_surface_triangles_by_coordinate, ) + _cache_water_recovery_surfaces( + grass_surface_triangles_by_coordinate, + sand_surface_triangles_by_coordinate, + ) _place_spawn_amenities(spawn_position) _configure_fresh_water(records) _configure_diggable_area() @@ -1261,6 +1303,203 @@ func _terrain_surface_triangles_by_coordinate( return triangles_by_coordinate +func _cache_water_recovery_surfaces( + grass_triangles: Dictionary[Vector2i, Array], + sand_triangles: Dictionary[Vector2i, Array], +) -> void: + _water_recovery_triangles_by_coordinate.clear() + _append_water_recovery_surfaces(grass_triangles) + _append_water_recovery_surfaces(sand_triangles) + + +func _append_water_recovery_surfaces( + source: Dictionary[Vector2i, Array], +) -> void: + for coordinate: Vector2i in source: + var eligible: Array = _water_recovery_triangles_by_coordinate.get( + coordinate, + [], + ) + for triangle: PackedVector3Array in source[coordinate]: + if triangle.size() != 3: + continue + if minf( + triangle[0].y, + minf(triangle[1].y, triangle[2].y), + ) <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE: + continue + var cross := (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ) + if ( + cross.length_squared() <= 0.0000001 + or absf(cross.normalized().dot(Vector3.UP)) + < WATER_RECOVERY_MINIMUM_UP_DOT + ): + continue + eligible.append(triangle) + if not eligible.is_empty(): + _water_recovery_triangles_by_coordinate[coordinate] = eligible + + +func _nearest_recovery_surface_distance(entry_position: Vector3) -> float: + var entry_horizontal := Vector2(entry_position.x, entry_position.z) + var nearest_distance_squared := INF + for coordinate: Vector2i in _water_recovery_triangles_by_coordinate: + for triangle: PackedVector3Array in ( + _water_recovery_triangles_by_coordinate[coordinate] + ): + var closest := _closest_horizontal_point_on_triangle( + entry_horizontal, + triangle, + ) + nearest_distance_squared = minf( + nearest_distance_squared, + entry_horizontal.distance_squared_to(closest), + ) + return sqrt(nearest_distance_squared) + + +func _nearest_safe_recovery_surface_position( + entry_position: Vector3, + maximum_distance: float, +) -> Variant: + var entry_horizontal := Vector2(entry_position.x, entry_position.z) + var maximum_distance_squared := maximum_distance * maximum_distance + var best_distance_squared := INF + var best_position: Variant = null + for coordinate: Vector2i in _water_recovery_triangles_by_coordinate: + for triangle: PackedVector3Array in ( + _water_recovery_triangles_by_coordinate[coordinate] + ): + var closest := _closest_horizontal_point_on_triangle( + entry_horizontal, + triangle, + ) + if ( + entry_horizontal.distance_squared_to(closest) + > maximum_distance_squared + ): + continue + var centroid := Vector2( + (triangle[0].x + triangle[1].x + triangle[2].x) / 3.0, + (triangle[0].z + triangle[1].z + triangle[2].z) / 3.0, + ) + var inset_distance := minf( + closest.distance_to(centroid), + WATER_RECOVERY_MAXIMUM_INSET, + ) + var inset_steps := ceili( + inset_distance / WATER_RECOVERY_INSET_STEP + ) + for inset_index: int in inset_steps + 1: + var candidate_horizontal := closest.move_toward( + centroid, + minf( + float(inset_index) * WATER_RECOVERY_INSET_STEP, + inset_distance, + ), + ) + var distance_squared := entry_horizontal.distance_squared_to( + candidate_horizontal + ) + if ( + distance_squared > maximum_distance_squared + or distance_squared >= best_distance_squared + ): + continue + var candidate := Vector3( + candidate_horizontal.x, + entry_position.y, + candidate_horizontal.y, + ) + var safe_position: Variant = _safe_recovery_surface_position( + candidate + ) + if safe_position is Vector3: + best_distance_squared = distance_squared + best_position = safe_position + return best_position + + +func _safe_recovery_surface_position(candidate: Vector3) -> Variant: + var nearby_triangles := _water_recovery_surface_triangles_near(candidate) + var surface_height := _surface_height_at(candidate, nearby_triangles) + if ( + surface_height <= WATER_HEIGHT + WATER_RECOVERY_MINIMUM_GROUND_CLEARANCE + or not _surface_supports_prop_footprint( + candidate, + surface_height, + WATER_RECOVERY_FOOTPRINT_RADIUS, + nearby_triangles, + ) + or not _has_prop_clearance( + candidate, + WATER_RECOVERY_FOOTPRINT_RADIUS, + ) + ): + return null + candidate.y = surface_height + return candidate + + +func _water_recovery_surface_triangles_near( + position: Vector3, +) -> Array[PackedVector3Array]: + var result: Array[PackedVector3Array] = [] + if _generator.catalog == null or _generator.catalog.chunk_size <= 0.0: + return result + var local_position := _generator.to_local(position) + var half_grid := Vector2( + float(_generator.grid_size.x - 1) * 0.5, + float(_generator.grid_size.y - 1) * 0.5, + ) + var coordinate := Vector2i( + roundi(local_position.x / _generator.catalog.chunk_size + half_grid.x), + roundi(local_position.z / _generator.catalog.chunk_size + half_grid.y), + ) + for offset_x: int in range(-1, 2): + for offset_y: int in range(-1, 2): + var nearby_coordinate := coordinate + Vector2i(offset_x, offset_y) + result.append_array( + _water_recovery_triangles_by_coordinate.get( + nearby_coordinate, + [], + ) + ) + return result + + +func _closest_horizontal_point_on_triangle( + point: Vector2, + triangle: PackedVector3Array, +) -> Vector2: + var polygon := PackedVector2Array([ + Vector2(triangle[0].x, triangle[0].z), + Vector2(triangle[1].x, triangle[1].z), + Vector2(triangle[2].x, triangle[2].z), + ]) + if Geometry2D.is_point_in_polygon(point, polygon): + return point + var closest := Geometry2D.get_closest_point_to_segment( + point, + polygon[0], + polygon[1], + ) + var nearest_distance_squared := point.distance_squared_to(closest) + for edge_index: int in range(1, 3): + var edge_closest := Geometry2D.get_closest_point_to_segment( + point, + polygon[edge_index], + polygon[(edge_index + 1) % 3], + ) + var distance_squared := point.distance_squared_to(edge_closest) + if distance_squared < nearest_distance_squared: + nearest_distance_squared = distance_squared + closest = edge_closest + return closest + + func _terrain_coordinate_for(mesh_instance: MeshInstance3D) -> Vector2i: var current: Node = mesh_instance while current != null and current != _generator: diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index a0730e4..792e4ed 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -72,6 +72,24 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]: return points +func get_water_recovery_position( + entry_position: Vector3, + fallback_position: Vector3, +) -> Vector3: + if not entry_position.is_finite(): + return fallback_position + var target_position := fallback_position + var nearest_distance := INF + for point: SafeRespawnPoint in get_safe_respawn_points(): + if point == null or not is_instance_valid(point) or not point.enabled: + continue + var distance := point.get_horizontal_distance_squared(entry_position) + if distance < nearest_distance: + nearest_distance = distance + target_position = point.global_position + return target_position + + func get_diggable_areas() -> Array[DiggableArea3D]: var areas: Array[DiggableArea3D] = [] var root: Node = get_node_or_null(diggable_area_root) diff --git a/world/test_world.gd b/world/test_world.gd index d9d18dc..9f4da5e 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -49,6 +49,16 @@ func get_safe_respawn_points() -> Array[SafeRespawnPoint]: ) +func get_water_recovery_position(entry_position: Vector3) -> Vector3: + if _active_region == null: + return global_position + var fallback_position := get_player_spawn_transform().origin + return _active_region.get_water_recovery_position( + entry_position, + fallback_position, + ) + + func get_fishing_shop() -> FishingShopInteractionType: return _active_region.get_fishing_shop() diff --git a/world/water_recovery_controller.gd b/world/water_recovery_controller.gd index 9626881..0d0c5aa 100644 --- a/world/water_recovery_controller.gd +++ b/world/water_recovery_controller.gd @@ -25,6 +25,7 @@ var _game_ui: GameUI var _screen_fade: ScreenFade var _water_triggers: Array[PlayerWaterTrigger] = [] var _safe_points: Array[SafeRespawnPoint] = [] +var _recovery_position_resolver := Callable() var _initial_spawn_transform: Transform3D var _entry_position: Vector3 var _bob_base_position: Vector3 @@ -43,6 +44,7 @@ func setup( screen_fade: ScreenFade, water_triggers: Array[PlayerWaterTrigger], safe_points: Array[SafeRespawnPoint], + recovery_position_resolver: Callable = Callable(), ) -> void: _player = player _fishing_spot = fishing_spot @@ -50,6 +52,7 @@ func setup( _screen_fade = screen_fade _water_triggers = water_triggers _safe_points = safe_points + _recovery_position_resolver = recovery_position_resolver _initial_spawn_transform = player.global_transform for water_trigger: PlayerWaterTrigger in _water_triggers: if ( @@ -73,6 +76,7 @@ func update_world_context( initial_spawn_transform: Transform3D, water_triggers: Array[PlayerWaterTrigger], safe_points: Array[SafeRespawnPoint], + recovery_position_resolver: Callable = Callable(), ) -> void: for water_trigger: PlayerWaterTrigger in _water_triggers: if ( @@ -88,6 +92,7 @@ func update_world_context( _initial_spawn_transform = initial_spawn_transform _water_triggers = water_triggers _safe_points = safe_points + _recovery_position_resolver = recovery_position_resolver for water_trigger: PlayerWaterTrigger in _water_triggers: if ( water_trigger != null @@ -183,17 +188,7 @@ func _on_fade_transition_completed( func _respawn_player() -> void: - var target_position: Vector3 = _initial_spawn_transform.origin - var nearest_distance: float = INF - for point: SafeRespawnPoint in _safe_points: - if point == null or not is_instance_valid(point) or not point.enabled: - continue - var distance: float = point.get_horizontal_distance_squared( - _entry_position - ) - if distance < nearest_distance: - nearest_distance = distance - target_position = point.global_position + var target_position := _resolve_recovery_position() target_position.y += respawn_height_offset var respawn_transform: Transform3D = _player.global_transform respawn_transform.basis = _recovery_root_basis @@ -204,6 +199,23 @@ func _respawn_player() -> void: local_respawn_completed.emit(_entry_position) +func _resolve_recovery_position() -> Vector3: + if _recovery_position_resolver.is_valid(): + var resolved: Variant = _recovery_position_resolver.call(_entry_position) + if resolved is Vector3 and (resolved as Vector3).is_finite(): + return resolved as Vector3 + var target_position := _initial_spawn_transform.origin + var nearest_distance := INF + for point: SafeRespawnPoint in _safe_points: + if point == null or not is_instance_valid(point) or not point.enabled: + continue + var distance := point.get_horizontal_distance_squared(_entry_position) + if distance < nearest_distance: + nearest_distance = distance + target_position = point.global_position + return target_position + + func _finish_recovery() -> void: _player.restore_gameplay_orientation_after_recovery() _player.set_water_recovery_active(false) From 81decf2b719c975332f0653e94be7511a1adfbc9 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sat, 29 Aug 2026 20:19:47 -0400 Subject: [PATCH 04/17] Add logbook catalog index filters --- fishing/fishing_spot.gd | 44 ++ main/main.gd | 1 + ...ontroller_menu_accessibility_validation.gd | 18 + tests/logbook_runtime_validation.gd | 39 +- tests/logbook_validation.gd | 441 ++++++++++++ ui/game_ui.gd | 4 + ui/logbook_catalog.gd | 338 +++++++++ ui/logbook_page.gd | 649 +++++++++++++++++- ui/player_menu.gd | 16 +- 9 files changed, 1540 insertions(+), 10 deletions(-) diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 2559af5..8e3e169 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -150,6 +150,7 @@ var _network_session: NetworkSessionType var _network_fishing: NetworkFishingServiceType var _world_time: WorldTimeServiceType var _world_weather: WorldWeatherServiceType +var _fishable_water_regions_resolver := Callable() var _active_player: PlayerType var _state_time_remaining: float = 0.0 var _cast_charge: float = 0.0 @@ -222,6 +223,7 @@ func setup( network_fishing: NetworkFishingServiceType = null, world_time: WorldTimeServiceType = null, world_weather: WorldWeatherServiceType = null, + fishable_water_regions_resolver: Callable = Callable(), ) -> void: _local_player = local_player _local_inventory = local_inventory @@ -242,6 +244,7 @@ func setup( _network_fishing = network_fishing _world_time = world_time _world_weather = world_weather + _fishable_water_regions_resolver = fishable_water_regions_resolver if ( _network_session != null and not _network_session.state_changed.is_connected( @@ -1664,6 +1667,47 @@ func build_network_context( return _build_fishing_context(region) +func is_species_available_now(fish: FishDataType) -> bool: + if fish == null or not fish.is_selectable(): + return false + var season := ( + _world_time.get_season() + if _world_time != null + else CalendarSeason.UNKNOWN + ) + if not fish.is_available_in_season(season): + return false + if fish.collection_method != FishDataType.CollectionMethod.FISHING: + return true + for region: FishableWaterRegionType in _get_logbook_water_regions(): + if ( + region == null + or region.fish_pool == null + or region.fish_pool.get_fish_by_id(fish.id) == null + ): + continue + var context := _build_fishing_context(region) + if not fish.is_allowed_in_water(context.water_type): + continue + if fish.availability == null or fish.availability.is_available(context): + return true + return false + + +func _get_logbook_water_regions() -> Array[FishableWaterRegionType]: + var regions: Array[FishableWaterRegionType] = [] + if not _fishable_water_regions_resolver.is_valid(): + return regions + var resolved: Variant = _fishable_water_regions_resolver.call() + if resolved is not Array: + return regions + for value: Variant in resolved: + var region := value as FishableWaterRegionType + if region != null and is_instance_valid(region): + regions.append(region) + return regions + + func _get_active_bait_tags() -> Array[StringName]: if _local_player == null or _item_catalog == null: return [] diff --git a/main/main.gd b/main/main.gd index 7fae866..e9dc78f 100644 --- a/main/main.gd +++ b/main/main.gd @@ -920,6 +920,7 @@ func _initialize_application(dedicated: bool) -> void: _network_fishing, _world_time, _world_weather, + Callable(_test_world, "get_fishable_water_regions"), ) if dedicated: return diff --git a/tests/controller_menu_accessibility_validation.gd b/tests/controller_menu_accessibility_validation.gd index aa2f9e2..37e43d5 100644 --- a/tests/controller_menu_accessibility_validation.gd +++ b/tests/controller_menu_accessibility_validation.gd @@ -1356,6 +1356,24 @@ func _validate_player_menu_nested_controller_back() -> void: == LogbookPageType.ControllerZone.ENTRIES, "Global player-menu Back did not return Logbook to creature selection.", ) + logbook_page.call("_set_index_open", true, false) + logbook_page.set( + "_controller_zone", + LogbookPageType.ControllerZone.INDEX, + ) + _expect( + bool(menu.call("consume_escape")), + "Global player-menu Back did not consume the Logbook index zone.", + ) + _expect( + menu.visible and not bool(logbook_page.get("_index_open")), + "Global player-menu Back did not return from the Logbook index page.", + ) + _expect( + int(logbook_page.get("_controller_zone")) + == LogbookPageType.ControllerZone.TABS, + "Global player-menu Back did not return Logbook index focus to tabs.", + ) var net_page := menu.get("_the_net_page") as Control net_page.set("_active", true) diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd index 75fc47f..47e795d 100644 --- a/tests/logbook_runtime_validation.gd +++ b/tests/logbook_runtime_validation.gd @@ -2,6 +2,7 @@ extends SceneTree const MainScene = preload("res://main/main.tscn") const FishCatchType = preload("res://fish/fish_catch.gd") +const MENU_TRANSITION_SETTLE_SECONDS := 3.0 func _initialize() -> void: @@ -36,12 +37,16 @@ func _run() -> void: var logbook := player_menu.get_node("%CatalogLogbook") as LogbookPage var backdrop := main.get_node("%PlayerMenuBackdrop") as ColorRect var hotbar := game_ui.get_node("%Hotbar") as Control + var fishing_spot := main.get("_fishing_spot") as FishingSpot + assert(logbook.get("_fishing_spot") == fishing_spot) + assert(logbook.get("_world_time") == main.get("_world_time")) + assert(logbook.get("_world_weather") == main.get("_world_weather")) var shortcut := InputEventKey.new() shortcut.pressed = true shortcut.physical_keycode = KEY_L assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut))) - await create_timer(2.2).timeout + await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout assert(player_menu.visible) assert(logbook.visible) assert(backdrop.visible) @@ -51,6 +56,18 @@ func _run() -> void: var entry_buttons: Dictionary = logbook.get("_entry_buttons") assert(entry_buttons.size() == 108) await _capture_if_requested("-unknown") + logbook.call("_set_index_open", true, false) + await process_frame + assert(bool(logbook.get("_index_open"))) + assert((logbook.get("_index_body") as Control).visible) + await _capture_if_requested("-index") + logbook.set_compact_presentation(true) + await process_frame + await _capture_if_requested("-index-compact") + logbook.set_compact_presentation(false) + await process_frame + logbook.call("_set_index_open", false, false) + await process_frame logbook.call( "_select_category", WaterType.Type.FRESH_WATER ) @@ -68,13 +85,29 @@ func _run() -> void: logbook.call("_select_entry", &"bluegill", &"bluegill") await create_timer(0.2).timeout await _capture_if_requested("-known") + var bluegill := ( + main.get("fish_catalog") as FishPool + ).get_fish_by_id(&"bluegill") + var bluegill_available_now := fishing_spot.is_species_available_now(bluegill) + var index_options := logbook.get("_index_options") as Dictionary + index_options["availability"] = ( + LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW + ) + logbook.notify_availability_context_changed() + await process_frame + assert( + (logbook.get("_entry_buttons") as Dictionary).has(&"bluegill") + == bluegill_available_now + ) + logbook.call("_reset_index") + await process_frame assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut))) - await create_timer(2.2).timeout + await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout assert(not player_menu.visible) assert(bool(player_menu.call("_handle_direct_page_shortcut", shortcut))) - await create_timer(2.2).timeout + await create_timer(MENU_TRANSITION_SETTLE_SECONDS).timeout player_menu.call( "_show_section_immediate", PlayerMenu.Section.PROFILE ) diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index 4810e0a..431ed2e 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -17,6 +17,8 @@ const CatalogResource: FishPoolType = preload( "res://fish/pools/fish_catalog.tres" ) +var _query_available_id: StringName + func _initialize() -> void: call_deferred("_run") @@ -35,6 +37,7 @@ func _run() -> void: quit() return _validate_catalog() + _validate_catalog_query() await _validate_page() print("Logbook validation: PASS") quit() @@ -68,6 +71,348 @@ func _validate_catalog() -> void: ) +func _validate_catalog_query() -> void: + var collection := CollectionLogType.new() + var inventory := FishInventoryType.new() + var fresh_species: Array[FishDataType] = [] + for fish: FishDataType in LogbookCatalog.ordered_species( + CatalogResource.candidates + ): + if LogbookCatalog.category_for(fish) == LogbookCatalog.Category.FRESH_WATER: + fresh_species.append(fish) + assert(fresh_species.size() == 108) + var first := fresh_species[0] + var second := fresh_species[1] + var unknown := fresh_species[2] + for quality: int in FishQualityType.TIER_COUNT: + collection.record_catch(first.id, quality) + collection.record_catch(second.id, FishQualityType.Tier.BORING) + var held := _make_test_catch(first, true) + assert(inventory.add_catch(held)) + + var options := _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + var filtered := LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered.size() == 2) + assert(first in filtered and second in filtered) + + options["show"] = LogbookCatalog.DiscoveryFilter.MISSING + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered.size() == fresh_species.size() - 2) + options["sort"] = LogbookCatalog.SortMode.CATALOG_NUMBER + options["descending"] = true + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered.front() == fresh_species.back()) + assert(filtered.back() == unknown) + + options["show"] = LogbookCatalog.DiscoveryFilter.INCOMPLETE + options["descending"] = false + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [second]) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["sort"] = LogbookCatalog.SortMode.CATCH_COUNT + options["descending"] = true + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [first, second]) + + options = _default_query_options() + options["search"] = unknown.display_name + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered.is_empty()) + options["search"] = str(unknown.catalog_number) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [unknown]) + + options = _default_query_options() + options["holding"] = LogbookCatalog.HoldingFilter.FAVORITED + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [first]) + options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [first]) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["group"] = String(first.collection_group) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(first in filtered) + for fish: FishDataType in filtered: + assert(fish.collection_group == first.collection_group) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["rarity"] = ( + int(first.rarity) + LogbookCatalog.RarityFilter.COMMON + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(first in filtered) + for fish: FishDataType in filtered: + assert(fish.rarity == first.rarity) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["method"] = ( + int(first.collection_method) + LogbookCatalog.MethodFilter.FISHING + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(first in filtered) + for fish: FishDataType in filtered: + assert(fish.collection_method == first.collection_method) + + var first_season := -1 + for season: int in 4: + if first.is_available_in_season(season): + first_season = season + break + assert(first_season >= 0) + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["season"] = ( + first_season + LogbookCatalog.SeasonFilter.SPRING + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(first in filtered) + for fish: FishDataType in filtered: + assert(fish.is_available_in_season(first_season)) + + var first_is_available_by_day := ( + first.availability == null or first.availability.allow_day + ) + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["time"] = ( + LogbookCatalog.TimeFilter.DAY + if first_is_available_by_day + else LogbookCatalog.TimeFilter.NIGHT + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(first in filtered) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.CAUGHT + options["sort"] = LogbookCatalog.SortMode.QUALITY_PROGRESS + options["descending"] = true + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered == [first, second]) + for sort_mode: LogbookCatalog.SortMode in [ + LogbookCatalog.SortMode.NAME, + LogbookCatalog.SortMode.RARITY, + LogbookCatalog.SortMode.VALUE, + LogbookCatalog.SortMode.WEIGHT, + ]: + options = _default_query_options() + options["sort"] = sort_mode + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(filtered[0] in [first, second]) + assert(filtered[1] in [first, second]) + assert(filtered[0] != filtered[1]) + assert(filtered[2] == unknown) + + _query_available_id = second.id + options = _default_query_options() + options["availability"] = ( + LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + Callable(self, "_query_species_available_now"), + ) + assert(filtered == [second]) + + _query_available_id = unknown.id + options["show"] = LogbookCatalog.DiscoveryFilter.MISSING + options["rarity"] = ( + int(unknown.rarity) + LogbookCatalog.RarityFilter.COMMON + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + Callable(self, "_query_species_available_now"), + ) + assert(filtered == [unknown]) + + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.MISSING + options["group"] = String(unknown.collection_group) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(unknown in filtered) + assert( + String(unknown.collection_group) + in LogbookCatalog.available_groups( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + ) + ) + + var unknown_is_available_by_day := ( + unknown.availability == null or unknown.availability.allow_day + ) + options = _default_query_options() + options["show"] = LogbookCatalog.DiscoveryFilter.MISSING + options["time"] = ( + LogbookCatalog.TimeFilter.DAY + if unknown_is_available_by_day + else LogbookCatalog.TimeFilter.NIGHT + ) + filtered = LogbookCatalog.filtered_species( + CatalogResource.candidates, + LogbookCatalog.Category.FRESH_WATER, + collection, + inventory, + options, + ) + assert(unknown in filtered) + inventory.free() + collection.free() + + +func _default_query_options() -> Dictionary: + return { + "search": "", + "show": LogbookCatalog.DiscoveryFilter.ALL, + "sort": LogbookCatalog.SortMode.CATALOG_NUMBER, + "descending": false, + "availability": LogbookCatalog.AvailabilityFilter.ANY, + "season": LogbookCatalog.SeasonFilter.ANY, + "time": LogbookCatalog.TimeFilter.ANY, + "rarity": LogbookCatalog.RarityFilter.ANY, + "method": LogbookCatalog.MethodFilter.ANY, + "holding": LogbookCatalog.HoldingFilter.ANY, + "group": "", + } + + +func _make_test_catch(fish: FishDataType, favorited: bool) -> FishCatchType: + var fish_catch := FishCatchType.new() + fish_catch.fish = fish + fish_catch.fish_id = fish.id + fish_catch.weight_lb = fish.get_minimum_weight() + fish_catch.display_scale = fish.get_display_scale_for_weight( + fish_catch.weight_lb + ) + fish_catch.quality = FishQualityType.Tier.BORING + fish_catch.sale_value = fish.get_sale_value_for_weight(fish_catch.weight_lb) + fish_catch.is_favorited = favorited + fish_catch.ensure_identity() + return fish_catch + + +func _query_species_available_now(fish: FishDataType) -> bool: + return fish != null and fish.id == _query_available_id + + func _validate_page() -> void: var collection := CollectionLogType.new() var inventory := FishInventoryType.new() @@ -91,6 +436,10 @@ func _validate_page() -> void: assert(outer_margin.get_theme_constant("margin_right") == 160) assert(outer_margin.get_theme_constant("margin_bottom") == 106) assert(is_equal_approx(book.offset_top, 65.0)) + page.call("_set_index_open", true, false) + await process_frame + _validate_index_content_bounds(page) + page.call("_set_index_open", false, false) page.set_compact_presentation(false) await process_frame assert(outer_margin.get_theme_constant("margin_left") == 52) @@ -158,6 +507,39 @@ func _validate_page() -> void: ) var initial_detail_body := page.get("_detail_body") as VBoxContainer assert(not initial_detail_body.get_parent() is ScrollContainer) + var index_tab := page.get("_index_tab") as Button + var index_body := page.get("_index_body") as VBoxContainer + assert(index_tab != null and index_body != null) + assert(index_tab.size == LogbookPage.INDEX_TAB_SIZE) + assert( + not index_tab.get_global_rect().intersects( + (category_tabs.back() as Button).get_global_rect() + ) + ) + page.call("_set_index_open", true, false) + await process_frame + assert(index_body.visible) + assert(not initial_detail_body.visible) + assert((page.get("_index_option_buttons") as Dictionary).size() == 10) + _validate_index_content_bounds(page) + page.call( + "_cycle_index_option", + &"show", + 1, + ) + await process_frame + assert((page.get("_entry_buttons") as Dictionary).is_empty()) + assert( + (page.get("_empty_state") as Label).text + == "No entries match this index." + ) + page.call("_reset_index") + await process_frame + assert((page.get("_entry_buttons") as Dictionary).size() == 108) + page.call("_set_index_open", false, false) + await process_frame + assert(not index_body.visible) + assert(initial_detail_body.visible) assert( page.call("_entry_label_text", "flathead catfish") == "flathead\ncatfish" @@ -458,8 +840,19 @@ func _validate_page() -> void: (page.get("_portrait_overlay_backdrop") as Button).pressed.emit() await process_frame + var live_index_options := page.get("_index_options") as Dictionary + live_index_options["holding"] = LogbookCatalog.HoldingFilter.IN_COOLER + page.call("_refresh_catalog") + await process_frame + assert( + (page.get("_entry_buttons") as Dictionary).keys() + == [&"bluegill"] + ) inventory.remove_catch_by_id(fish_catch.catch_id) await process_frame + assert((page.get("_entry_buttons") as Dictionary).is_empty()) + page.call("_reset_index") + await process_frame assert(not _detail_text(page).contains("number owned")) page.queue_free() collection.queue_free() @@ -474,6 +867,54 @@ func _detail_text(page: LogbookPage) -> String: return "\n".join(values) +func _validate_index_content_bounds(page: LogbookPage) -> void: + var body := page.get("_index_body") as VBoxContainer + assert(body != null and body.visible) + var right_page := body.get_parent() as Control + assert(right_page != null) + var page_rect := right_page.get_global_rect() + var body_rect := body.get_global_rect() + var protected_rect := Rect2( + page_rect.position + Vector2( + LogbookPage.INDEX_CONTENT_LEFT_INSET, + LogbookPage.INDEX_CONTENT_TOP_INSET, + ), + page_rect.size - Vector2( + LogbookPage.INDEX_CONTENT_LEFT_INSET + + LogbookPage.INDEX_CONTENT_RIGHT_INSET, + LogbookPage.INDEX_CONTENT_TOP_INSET + + LogbookPage.INDEX_CONTENT_BOTTOM_INSET, + ), + ) + assert( + body_rect.position.x + >= page_rect.position.x + LogbookPage.INDEX_CONTENT_LEFT_INSET - 1.0 + ) + assert( + body_rect.end.x + <= page_rect.end.x - LogbookPage.INDEX_CONTENT_RIGHT_INSET + 1.0 + ) + assert( + body_rect.position.y >= page_rect.position.y, + "Index starts above the authored page: body=%s page=%s" + % [body_rect, page_rect], + ) + assert( + body_rect.end.y <= page_rect.end.y, + "Index extends below the authored page: body=%s page=%s" + % [body_rect, page_rect], + ) + var controls: Array = page.get("_index_controls") as Array + for value: Variant in controls: + var control := value as Control + if control != null and control.visible: + assert( + protected_rect.grow(2.0).encloses(control.get_global_rect()), + "Index control crossed the authored right-page boundary: %s" + % control.name, + ) + + func _descendant_label_text(parent: Node) -> String: var values: PackedStringArray = [] for node: Node in parent.find_children("*", "Label", true, false): diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 7f8f0a6..3ce0d17 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -260,6 +260,9 @@ func _ready() -> void: _player_menu.controller_hotbar_management_ended.connect( _on_controller_hotbar_management_ended ) + _player_menu.controller_text_entry_requested.connect( + request_controller_text_entry_for + ) _title_settings_panel.panel_visibility_changed.connect( _on_settings_visibility_changed ) @@ -459,6 +462,7 @@ func setup( discovery, player_jobs, world_time, + world_weather, world_environment, world_sun, ) diff --git a/ui/logbook_catalog.gd b/ui/logbook_catalog.gd index d5dc639..ac7a910 100644 --- a/ui/logbook_catalog.gd +++ b/ui/logbook_catalog.gd @@ -2,6 +2,9 @@ class_name LogbookCatalog extends RefCounted const FishDataType = preload("res://fish/fish_data.gd") +const FishInventoryType = preload("res://inventory/fish_inventory.gd") +const CollectionLogType = preload("res://collection/collection_log.gd") +const CalendarSeasonType = preload("res://world/calendar_season.gd") enum Category { FRESH_WATER, @@ -10,6 +13,66 @@ enum Category { SHELLFISH, } +enum DiscoveryFilter { + ALL, + CAUGHT, + MISSING, + INCOMPLETE, +} + +enum SortMode { + CATALOG_NUMBER, + NAME, + RARITY, + CATCH_COUNT, + QUALITY_PROGRESS, + VALUE, + WEIGHT, +} + +enum AvailabilityFilter { + ANY, + AVAILABLE_NOW, +} + +enum SeasonFilter { + ANY, + CURRENT, + SPRING, + SUMMER, + FALL, + WINTER, +} + +enum TimeFilter { + ANY, + CURRENT, + DAY, + NIGHT, +} + +enum RarityFilter { + ANY, + COMMON, + UNCOMMON, + RARE, + EPIC, + LEGENDARY, +} + +enum MethodFilter { + ANY, + FISHING, + NET, + DIGGING, +} + +enum HoldingFilter { + ANY, + IN_COOLER, + FAVORITED, +} + static func category_for(fish: FishDataType) -> Category: if fish == null: @@ -73,3 +136,278 @@ static func ordered_species( return a.catalog_number < b.catalog_number ) return ordered + + +static func filtered_species( + candidates: Array[FishDataType], + category: Category, + collection_log: CollectionLogType, + inventory: FishInventoryType, + options: Dictionary, + available_now_resolver: Callable = Callable(), + current_season: int = CalendarSeasonType.UNKNOWN, + current_is_night: bool = false, +) -> Array[FishDataType]: + var filtered: Array[FishDataType] = [] + for fish: FishDataType in ordered_species(candidates): + if category_for(fish) != category: + continue + if not _matches_filters( + fish, + collection_log, + inventory, + options, + available_now_resolver, + current_season, + current_is_night, + ): + continue + filtered.append(fish) + var sort_mode := int(options.get("sort", SortMode.CATALOG_NUMBER)) + var descending := bool(options.get("descending", false)) + filtered.sort_custom( + func(first: FishDataType, second: FishDataType) -> bool: + return _species_less_than( + first, + second, + collection_log, + sort_mode, + descending, + ) + ) + return filtered + + +static func available_groups( + candidates: Array[FishDataType], + category: Category, + _collection_log: CollectionLogType, +) -> Array[String]: + var groups: Array[String] = [] + for fish: FishDataType in ordered_species(candidates): + if category_for(fish) != category: + continue + var group := _group_label(fish) + if not group.is_empty() and group not in groups: + groups.append(group) + groups.sort_custom( + func(first: String, second: String) -> bool: + return first.naturalnocasecmp_to(second) < 0 + ) + return groups + + +static func quality_progress( + collection_log: CollectionLogType, + fish_id: StringName, +) -> int: + if collection_log == null: + return 0 + var mask: int = collection_log.get_quality_mask(fish_id) + var count: int = 0 + while mask > 0: + count += mask & 1 + mask >>= 1 + return count + + +static func _matches_filters( + fish: FishDataType, + collection_log: CollectionLogType, + inventory: FishInventoryType, + options: Dictionary, + available_now_resolver: Callable, + current_season: int, + current_is_night: bool, +) -> bool: + var discovered := ( + collection_log != null and collection_log.has_discovered(fish.id) + ) + match int(options.get("show", DiscoveryFilter.ALL)): + DiscoveryFilter.CAUGHT: + if not discovered: + return false + DiscoveryFilter.MISSING: + if discovered: + return false + DiscoveryFilter.INCOMPLETE: + if ( + not discovered + or collection_log.has_mastered(fish.id) + ): + return false + + var search_text := str(options.get("search", "")).strip_edges().to_lower() + if not search_text.is_empty(): + var number_text := str(catalog_number(fish)) + var number_query := search_text.trim_prefix("#") + var number_matches := ( + number_query.is_valid_int() + and number_text == number_query + ) + var identity_matches := false + if discovered: + identity_matches = " ".join([ + fish.display_name, + _group_label(fish), + fish.get_habitat_label(), + ]).to_lower().contains(search_text) + if not number_matches and not identity_matches: + return false + + if ( + int(options.get("availability", AvailabilityFilter.ANY)) + == AvailabilityFilter.AVAILABLE_NOW + ): + if available_now_resolver.is_valid(): + var resolved: Variant = available_now_resolver.call(fish) + if typeof(resolved) != TYPE_BOOL or not bool(resolved): + return false + elif not _matches_season(fish, current_season): + return false + + var season_filter := int(options.get("season", SeasonFilter.ANY)) + if season_filter != SeasonFilter.ANY: + var requested_season := current_season + if season_filter >= SeasonFilter.SPRING: + requested_season = season_filter - SeasonFilter.SPRING + if not _matches_season(fish, requested_season): + return false + + var time_filter := int(options.get("time", TimeFilter.ANY)) + if time_filter != TimeFilter.ANY: + var requested_night := current_is_night + if time_filter == TimeFilter.DAY: + requested_night = false + elif time_filter == TimeFilter.NIGHT: + requested_night = true + if not _matches_time(fish, requested_night): + return false + + var rarity_filter := int(options.get("rarity", RarityFilter.ANY)) + if ( + rarity_filter != RarityFilter.ANY + and fish.rarity != rarity_filter - RarityFilter.COMMON + ): + return false + + var method_filter := int(options.get("method", MethodFilter.ANY)) + if ( + method_filter != MethodFilter.ANY + and fish.collection_method != method_filter - MethodFilter.FISHING + ): + return false + + var holding_filter := int(options.get("holding", HoldingFilter.ANY)) + if holding_filter != HoldingFilter.ANY: + var favorites_only := holding_filter == HoldingFilter.FAVORITED + if not _has_current_catch(inventory, fish.id, favorites_only): + return false + + var requested_group := str(options.get("group", "")) + if ( + not requested_group.is_empty() + and _group_label(fish).nocasecmp_to(requested_group) != 0 + ): + return false + return true + + +static func _species_less_than( + first: FishDataType, + second: FishDataType, + collection_log: CollectionLogType, + sort_mode: int, + descending: bool, +) -> bool: + var first_discovered := ( + collection_log != null and collection_log.has_discovered(first.id) + ) + var second_discovered := ( + collection_log != null and collection_log.has_discovered(second.id) + ) + if sort_mode != SortMode.CATALOG_NUMBER: + if first_discovered != second_discovered: + return first_discovered + if not first_discovered and not second_discovered: + return _catalog_less_than(first, second) + + var comparison: int = 0 + match sort_mode: + SortMode.NAME: + comparison = first.display_name.naturalnocasecmp_to( + second.display_name + ) + SortMode.RARITY: + comparison = first.rarity - second.rarity + SortMode.CATCH_COUNT: + comparison = ( + collection_log.get_catch_count(first.id) + - collection_log.get_catch_count(second.id) + ) + SortMode.QUALITY_PROGRESS: + comparison = ( + quality_progress(collection_log, first.id) + - quality_progress(collection_log, second.id) + ) + SortMode.VALUE: + comparison = first.sell_value_max - second.sell_value_max + SortMode.WEIGHT: + comparison = _compare_floats( + first.get_maximum_weight(), + second.get_maximum_weight(), + ) + _: + comparison = first.catalog_number - second.catalog_number + if comparison != 0: + return comparison > 0 if descending else comparison < 0 + return _catalog_less_than(first, second) + + +static func _catalog_less_than(first: FishDataType, second: FishDataType) -> bool: + if first.catalog_number == second.catalog_number: + return String(first.id) < String(second.id) + return first.catalog_number < second.catalog_number + + +static func _compare_floats(first: float, second: float) -> int: + if is_equal_approx(first, second): + return 0 + return -1 if first < second else 1 + + +static func _matches_season(fish: FishDataType, season: int) -> bool: + return fish.is_available_in_season(season) + + +static func _matches_time(fish: FishDataType, requested_night: bool) -> bool: + if ( + fish.collection_method != FishDataType.CollectionMethod.FISHING + or fish.availability == null + ): + return true + return ( + fish.availability.allow_night + if requested_night + else fish.availability.allow_day + ) + + +static func _has_current_catch( + inventory: FishInventoryType, + fish_id: StringName, + favorites_only: bool, +) -> bool: + if inventory == null: + return false + for fish_catch in inventory.get_catches_by_fish_id(fish_id): + if fish_catch != null and (not favorites_only or fish_catch.is_favorited): + return true + return false + + +static func _group_label(fish: FishDataType) -> String: + if fish == null: + return "" + var group := String(fish.collection_group).strip_edges() + return group if not group.is_empty() else fish.get_habitat_label() diff --git a/ui/logbook_page.gd b/ui/logbook_page.gd index 6319dd6..cc490a0 100644 --- a/ui/logbook_page.gd +++ b/ui/logbook_page.gd @@ -6,10 +6,21 @@ const FishDataType = preload("res://fish/fish_data.gd") const FishQualityType = preload("res://fish/fish_quality.gd") const FishInventoryType = preload("res://inventory/fish_inventory.gd") const FishPoolType = preload("res://fish/fish_pool.gd") +const FishingSpotType = preload("res://fishing/fishing_spot.gd") +const WorldTimeServiceType = preload("res://world/world_time_service.gd") +const WorldWeatherServiceType = preload( + "res://world/world_weather_service.gd" +) const SILHOUETTE_SHADER: Shader = preload( "res://ui/logbook_silhouette.gdshader" ) const OrganizerTabType = preload("res://ui/components/organizer_tab.gd") +const ControllerFocusNavigationType = preload( + "res://ui/controller_focus_navigation.gd" +) +const NOTEPAD_INK_ACTION_SCENE: PackedScene = preload( + "res://ui/components/bubble_menu/notepad_ink_action.tscn" +) const InventoryNotepadType = preload( "res://ui/components/inventory_notepad.gd" ) @@ -48,6 +59,8 @@ const LOGBOOK_TAB_LEFT_INSET: float = 92.0 const LOGBOOK_TAB_SIZE := Vector2(108.0, 38.0) const LOGBOOK_TAB_SEPARATION: float = 6.0 const LOGBOOK_TAB_TEXT_SIDE_INSET: float = 10.0 +const INDEX_TAB_SIZE := Vector2(88.0, 38.0) +const INDEX_TAB_RIGHT_INSET: float = 72.0 const PAGE_CONTENT_SCALE: float = 0.97 const WIDE_OUTER_MARGINS := Rect2(52.0, 85.0, 52.0, 18.0) const COMPACT_OUTER_MARGINS := Rect2(160.0, 85.0, 160.0, 106.0) @@ -61,17 +74,53 @@ const DETAIL_SECTION_SEPARATION: int = 10 const DETAIL_QUALITY_SECTION_SIZE := Vector2(300.0, 104.0) const DETAIL_STATS_SECTION_SIZE := Vector2(400.0, 160.0) const DETAIL_STATS_ROW_SEPARATION: int = 8 +const INDEX_OPTION_HEIGHT: float = 34.0 +const INDEX_CONTENT_LEFT_INSET: float = 44.0 +const INDEX_CONTENT_TOP_INSET: float = 4.0 +const INDEX_CONTENT_RIGHT_INSET: float = 22.0 +const INDEX_CONTENT_BOTTOM_INSET: float = 10.0 +const INDEX_SHOW_LABELS := [ + "all entries", "caught", "missing", "needs quality", +] +const INDEX_SORT_LABELS := [ + "catalog no.", "name", "rarity", "number caught", + "quality progress", "value", "weight", +] +const INDEX_AVAILABILITY_LABELS := [ + "any time", "available now", +] +const INDEX_SEASON_LABELS := [ + "any season", "current season", "spring", "summer", "fall", "winter", +] +const INDEX_TIME_LABELS := [ + "any time", "current time", "day", "night", +] +const INDEX_RARITY_LABELS := [ + "any rarity", "common", "uncommon", "rare", "epic", "legendary", +] +const INDEX_METHOD_LABELS := [ + "any method", "fishing", "netting", "digging", +] +const INDEX_HOLDING_LABELS := [ + "any ownership", "in cooler", "favorited", +] + +signal controller_text_entry_requested(control: Control) enum ControllerZone { TABS, ENTRIES, DETAILS, + INDEX, OVERLAY, } var _collection_log: CollectionLogType var _inventory: FishInventoryType var _catalog: FishPoolType +var _fishing_spot: FishingSpotType +var _world_time: WorldTimeServiceType +var _world_weather: WorldWeatherServiceType var _category: LogbookCatalog.Category = ( LogbookCatalog.Category.FRESH_WATER ) @@ -111,6 +160,31 @@ var _portrait_overlay_stats_view: Control var _portrait_overlay_stats: GridContainer var _overlay_return_focus: Control var _controller_zone: ControllerZone = ControllerZone.TABS +var _index_tab: OrganizerTabType +var _index_body: VBoxContainer +var _index_search: LineEdit +var _index_summary: Label +var _index_note: Label +var _index_reset: Button +var _index_option_buttons: Dictionary[StringName, Button] = {} +var _index_controls: Array[Control] = [] +var _index_open: bool = false +var _index_update_in_progress: bool = false +var _index_visible_count: int = 0 +var _index_category_total: int = 0 +var _index_options: Dictionary = { + "search": "", + "show": LogbookCatalog.DiscoveryFilter.ALL, + "sort": LogbookCatalog.SortMode.CATALOG_NUMBER, + "descending": false, + "availability": LogbookCatalog.AvailabilityFilter.ANY, + "season": LogbookCatalog.SeasonFilter.ANY, + "time": LogbookCatalog.TimeFilter.ANY, + "rarity": LogbookCatalog.RarityFilter.ANY, + "method": LogbookCatalog.MethodFilter.ANY, + "holding": LogbookCatalog.HoldingFilter.ANY, + "group": "", +} func _ready() -> void: @@ -124,10 +198,16 @@ func setup( collection_log: CollectionLogType, inventory: FishInventoryType, catalog: FishPoolType, + fishing_spot: FishingSpotType = null, + world_time: WorldTimeServiceType = null, + world_weather: WorldWeatherServiceType = null, ) -> void: _collection_log = collection_log _inventory = inventory _catalog = catalog + _fishing_spot = fishing_spot + _world_time = world_time + _world_weather = world_weather if not _collection_log.fish_discovered.is_connected( _on_fish_discovered ): @@ -138,12 +218,28 @@ func setup( _collection_log.collection_changed.connect(_on_collection_changed) if not _inventory.catches_changed.is_connected(_on_inventory_changed): _inventory.catches_changed.connect(_on_inventory_changed) + if ( + _world_time != null + and not _world_time.time_changed.is_connected( + _on_logbook_context_changed + ) + ): + _world_time.time_changed.connect(_on_logbook_context_changed) + _world_time.calendar_date_changed.connect(_on_logbook_context_changed) + if ( + _world_weather != null + and not _world_weather.weather_changed.is_connected( + _on_logbook_context_changed + ) + ): + _world_weather.weather_changed.connect(_on_logbook_context_changed) _refresh_catalog() func set_compact_presentation(compact: bool) -> void: _compact_presentation = compact _apply_presentation_margins() + _apply_index_presentation() func activate() -> void: @@ -180,6 +276,17 @@ func set_interactive(value: bool) -> void: if _interactive and not tab.button_pressed else Control.MOUSE_FILTER_IGNORE ) + if _index_tab != null: + _index_tab.focus_mode = ( + Control.FOCUS_ALL + if _interactive and _controller_zone == ControllerZone.TABS + else Control.FOCUS_NONE + ) + _index_tab.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if _interactive + else Control.MOUSE_FILTER_IGNORE + ) for entry: Button in _entry_buttons.values(): entry.focus_mode = ( Control.FOCUS_ALL @@ -199,6 +306,19 @@ func set_interactive(value: bool) -> void: if _interactive and _controller_zone == ControllerZone.DETAILS else Control.FOCUS_NONE ) + for index_control: Control in _index_controls: + if not is_instance_valid(index_control): + continue + index_control.focus_mode = ( + Control.FOCUS_ALL + if _interactive and _controller_zone == ControllerZone.INDEX + else Control.FOCUS_NONE + ) + index_control.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if _interactive and _index_open + else Control.MOUSE_FILTER_IGNORE + ) if _portrait_overlay_backdrop != null: _portrait_overlay_backdrop.focus_mode = ( Control.FOCUS_ALL @@ -214,6 +334,9 @@ func focus_initial() -> void: _portrait_overlay_backdrop.grab_focus() return if _controller_zone == ControllerZone.TABS: + if _index_open and _index_tab != null: + _index_tab.grab_focus() + return var category_tab_index: int = _category_tab_categories.find(_category) if category_tab_index >= 0: _category_tabs[category_tab_index].grab_focus() @@ -222,6 +345,10 @@ func focus_initial() -> void: if not _detail_buttons.is_empty(): _detail_buttons.front().grab_focus() return + if _controller_zone == ControllerZone.INDEX: + if _index_search != null: + _index_search.grab_focus() + return var selected := _entry_buttons.get(_selected_entry_key) as Button if selected != null: selected.grab_focus() @@ -247,6 +374,8 @@ func handle_controller_input(event: InputEvent) -> bool: _hide_portrait_overlay() ControllerZone.DETAILS: _set_controller_zone(ControllerZone.ENTRIES) + ControllerZone.INDEX: + _set_index_open(false) ControllerZone.ENTRIES: _set_controller_zone(ControllerZone.TABS) ControllerZone.TABS: @@ -254,15 +383,43 @@ func handle_controller_input(event: InputEvent) -> bool: return true if _controller_zone == ControllerZone.TABS: if event.is_action_pressed("ui_left"): + if _index_open: + _set_index_open(false, false) + _category_tabs.back().grab_focus() + return true _select_adjacent_controller_category(-1) return true if event.is_action_pressed("ui_right"): + if ( + _category_tab_categories.find(_category) + == _category_tab_categories.size() - 1 + ): + _set_index_open(true, false) + _index_tab.grab_focus() + return true _select_adjacent_controller_category(1) return true if accept_pressed: - if not _entry_buttons.is_empty(): + if _index_open: + _set_controller_zone(ControllerZone.INDEX) + elif not _entry_buttons.is_empty(): _set_controller_zone(ControllerZone.ENTRIES) return true + if _controller_zone == ControllerZone.INDEX: + var focus_owner := get_viewport().gui_get_focus_owner() + if accept_pressed and focus_owner == _index_search: + controller_text_entry_requested.emit(_index_search) + return true + if event.is_action_pressed("ui_left"): + var previous_key := _index_key_for_control(focus_owner) + if not previous_key.is_empty(): + _cycle_index_option(previous_key, -1) + return true + if event.is_action_pressed("ui_right") or accept_pressed: + var next_key := _index_key_for_control(focus_owner) + if not next_key.is_empty(): + _cycle_index_option(next_key, 1) + return true if _controller_zone == ControllerZone.ENTRIES and accept_pressed: if not _selected_entry_key.is_empty() and not _detail_buttons.is_empty(): _set_controller_zone(ControllerZone.DETAILS) @@ -329,6 +486,20 @@ func _build_interface() -> void: tabs.add_child(tab) _category_tabs.append(tab) _category_tab_categories.append(category) + _index_tab = OrganizerTabType.new() + _index_tab.name = "CatalogIndexTab" + _index_tab.anchor_left = 1.0 + _index_tab.anchor_right = 1.0 + _index_tab.offset_left = -INDEX_TAB_RIGHT_INSET - INDEX_TAB_SIZE.x + _index_tab.offset_top = 37.0 + _index_tab.offset_right = -INDEX_TAB_RIGHT_INSET + _index_tab.offset_bottom = 37.0 + INDEX_TAB_SIZE.y + _index_tab.text = "index" + _index_tab.palette_index = 0 + _index_tab.tooltip_text = "Open catalog index" + _index_tab.accessibility_name = "Catalog index" + _index_tab.pressed.connect(_toggle_index_page) + stack.add_child(_index_tab) _configure_category_focus() var book := Control.new() @@ -418,9 +589,176 @@ func _build_interface() -> void: right_page.add_child(_detail_body) _apply_page_content_scale(_detail_body) _show_no_selection() + _build_index_page(right_page) _build_portrait_overlay() +func _build_index_page(right_page: Control) -> void: + _index_body = VBoxContainer.new() + _index_body.name = "CatalogIndex" + _index_body.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + _index_body.offset_left = INDEX_CONTENT_LEFT_INSET + _index_body.offset_top = INDEX_CONTENT_TOP_INSET + _index_body.offset_right = -INDEX_CONTENT_RIGHT_INSET + _index_body.offset_bottom = -INDEX_CONTENT_BOTTOM_INSET + _index_body.add_theme_constant_override("separation", 4) + _index_body.visible = false + right_page.add_child(_index_body) + _apply_page_content_scale(_index_body) + _apply_index_presentation() + + var heading := _label("catalog index", 28) + heading.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_body.add_child(heading) + _index_summary = _field_label("", 14) + _index_summary.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_summary.add_theme_color_override("font_color", MUTED_INK) + _index_body.add_child(_index_summary) + + var search_stack := VBoxContainer.new() + search_stack.add_theme_constant_override("separation", 0) + _index_body.add_child(search_stack) + var search_label := _field_label("find by name, group, or catalog no.", 13) + search_label.add_theme_color_override("font_color", MUTED_INK) + search_stack.add_child(search_label) + _index_search = LineEdit.new() + _index_search.name = "CatalogIndexSearch" + _index_search.custom_minimum_size.y = 34.0 + _index_search.placeholder_text = "write here..." + _index_search.max_length = 48 + _index_search.add_theme_font_override( + "font", InventoryNotepadType.HANDWRITTEN_FONT + ) + _index_search.add_theme_font_size_override("font_size", 18) + _index_search.add_theme_color_override("font_color", INK) + _index_search.add_theme_color_override("caret_color", INK) + _index_search.add_theme_color_override( + "font_placeholder_color", Color(MUTED_INK, 0.72) + ) + for style_name: StringName in [ + &"normal", &"focus", &"read_only", + ]: + _index_search.add_theme_stylebox_override( + style_name, + _index_search_style(style_name == &"focus"), + ) + _index_search.text_changed.connect(_on_index_search_changed) + search_stack.add_child(_index_search) + _index_controls.append(_index_search) + + var options_grid := GridContainer.new() + options_grid.columns = 2 + options_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL + options_grid.add_theme_constant_override("h_separation", 18) + options_grid.add_theme_constant_override("v_separation", 2) + _index_body.add_child(options_grid) + for descriptor: Dictionary in [ + {"key": &"show", "label": "show"}, + {"key": &"rarity", "label": "rarity"}, + {"key": &"sort", "label": "sort by"}, + {"key": &"availability", "label": "availability"}, + {"key": &"descending", "label": "order"}, + {"key": &"season", "label": "season"}, + {"key": &"method", "label": "method"}, + {"key": &"time", "label": "time of day"}, + {"key": &"holding", "label": "ownership"}, + {"key": &"group", "label": "collection group"}, + ]: + options_grid.add_child(_make_index_option( + descriptor["key"], + str(descriptor["label"]), + )) + + var footer := HBoxContainer.new() + footer.alignment = BoxContainer.ALIGNMENT_CENTER + _index_body.add_child(footer) + _index_reset = NOTEPAD_INK_ACTION_SCENE.instantiate() as Button + _index_reset.name = "CatalogIndexReset" + _index_reset.custom_minimum_size = Vector2(126.0, 34.0) + _index_reset.text = "clear index" + _apply_index_value_font(_index_reset) + _index_reset.tooltip_text = "Clear every catalog filter" + _index_reset.accessibility_name = "Clear catalog index filters" + _index_reset.pressed.connect(_reset_index) + footer.add_child(_index_reset) + _index_controls.append(_index_reset) + + _index_note = _field_label( + "silhouettes keep names hidden in filtered results", + 12, + ) + _index_note.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _index_note.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _index_note.add_theme_color_override("font_color", Color(MUTED_INK, 0.82)) + _index_body.add_child(_index_note) + _update_index_controls() + _configure_index_focus.call_deferred() + + +func _make_index_option(key: StringName, label_text: String) -> Control: + var stack := VBoxContainer.new() + stack.size_flags_horizontal = Control.SIZE_EXPAND_FILL + stack.add_theme_constant_override("separation", -3) + var label := _field_label(label_text, 12) + label.add_theme_color_override("font_color", MUTED_INK) + stack.add_child(label) + var button := NOTEPAD_INK_ACTION_SCENE.instantiate() as Button + button.name = "CatalogIndex%s" % String(key).capitalize().replace(" ", "") + button.custom_minimum_size = Vector2(0.0, INDEX_OPTION_HEIGHT) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + button.clip_text = true + _apply_index_value_font(button) + button.tooltip_text = "Change %s; use left or right on a controller" % label_text + button.pressed.connect(_cycle_index_option.bind(key, 1)) + button.gui_input.connect(_on_index_option_gui_input.bind(key)) + stack.add_child(button) + _index_option_buttons[key] = button + _index_controls.append(button) + return stack + + +func _apply_index_value_font(button: Button) -> void: + button.add_theme_font_override( + "font", InventoryNotepadType.HANDWRITTEN_FONT + ) + button.add_theme_font_size_override("font_size", 18) + for color_name: StringName in [ + &"font_color", + &"font_hover_color", + &"font_focus_color", + &"font_pressed_color", + ]: + button.add_theme_color_override(color_name, INK) + + +func _index_search_style(focused: bool) -> StyleBoxFlat: + var style := StyleBoxFlat.new() + style.bg_color = Color(1.0, 0.97, 0.82, 0.12 if focused else 0.0) + style.border_color = Color(INK, 0.72 if focused else 0.42) + style.border_width_bottom = 2 + style.content_margin_left = 6 + style.content_margin_right = 22 + style.content_margin_top = 2 + style.content_margin_bottom = 3 + return style + + +func _configure_index_focus() -> void: + var controls: Array[Control] = [] + for control: Control in _index_controls: + if control != null and is_instance_valid(control): + controls.append(control) + if not controls.is_empty(): + ControllerFocusNavigationType.configure_spatial_neighbors(controls) + + +func _apply_index_presentation() -> void: + if _index_body == null: + return + var scale_value := 0.82 if _compact_presentation else PAGE_CONTENT_SCALE + _index_body.scale = Vector2.ONE * scale_value + + func _apply_presentation_margins() -> void: if _outer_margin == null: return @@ -548,11 +886,30 @@ func _refresh_catalog() -> void: var preserved_entry_key: StringName = _selected_entry_key _clear_entries() var species: Array[FishDataType] = [] + var category_total: int = 0 if _catalog != null: - species = LogbookCatalog.ordered_species(_catalog.candidates) + for fish: FishDataType in LogbookCatalog.ordered_species( + _catalog.candidates + ): + if LogbookCatalog.category_for(fish) == _category: + category_total += 1 + var available_now_resolver := Callable() + if _fishing_spot != null and is_instance_valid(_fishing_spot): + available_now_resolver = Callable( + _fishing_spot, + "is_species_available_now", + ) + species = LogbookCatalog.filtered_species( + _catalog.candidates, + _category, + _collection_log, + _inventory, + _index_options, + available_now_resolver, + _current_season(), + _current_is_night(), + ) for fish: FishDataType in species: - if LogbookCatalog.category_for(fish) != _category: - continue var discovered: bool = _collection_log.has_discovered(fish.id) var entry: Button = _make_entry(fish, discovered) var selection_key: StringName = ( @@ -572,7 +929,11 @@ func _refresh_catalog() -> void: ) ) _catalog_grid.add_child(entry) - _empty_state.text = LogbookCatalog.empty_state(_category) + _empty_state.text = ( + "No entries match this index." + if _index_has_active_criteria() + else LogbookCatalog.empty_state(_category) + ) _empty_state.visible = _entry_buttons.is_empty() _catalog_scroll.visible = not _entry_buttons.is_empty() if ( @@ -589,10 +950,269 @@ func _refresh_catalog() -> void: _selected_entry_key = StringName() _refresh_selection_styles() _refresh_details(false) + _update_index_controls(species.size(), category_total) set_interactive(_interactive) _refresh_catalog_scroll_indicators.call_deferred() +func _toggle_index_page() -> void: + _set_index_open(not _index_open) + + +func _set_index_open(open: bool, restore_focus: bool = true) -> void: + _index_open = open + if _index_tab != null: + _index_tab.set_selected(open) + _index_tab.tooltip_text = ( + "Return to catch record" if open else "Open catalog index" + ) + if _index_body != null: + _index_body.visible = open + if _detail_body != null: + _detail_body.visible = not open + if not open: + _refresh_details(false) + _update_index_controls() + if restore_focus and _active and _interactive: + if open: + _controller_zone = ControllerZone.INDEX + set_interactive(true) + _index_search.grab_focus() + elif _controller_zone == ControllerZone.INDEX: + _controller_zone = ControllerZone.TABS + set_interactive(true) + _index_tab.grab_focus() + + +func _on_index_search_changed(value: String) -> void: + if _index_update_in_progress: + return + _index_options["search"] = value + _refresh_catalog() + + +func _cycle_index_option(key: StringName, direction: int = 1) -> void: + if not _index_options.has(key): + return + if key == &"descending": + _index_options[key] = not bool(_index_options[key]) + elif key == &"group": + var groups: Array[String] = [""] + if _catalog != null: + groups.append_array(LogbookCatalog.available_groups( + _catalog.candidates, + _category, + _collection_log, + )) + var current_index := groups.find(str(_index_options[key])) + if current_index < 0: + current_index = 0 + _index_options[key] = groups[wrapi( + current_index + direction, + 0, + groups.size(), + )] + else: + var option_count := _index_option_count(key) + if option_count <= 0: + return + _index_options[key] = wrapi( + int(_index_options[key]) + direction, + 0, + option_count, + ) + _refresh_catalog() + + +func _on_index_option_gui_input( + event: InputEvent, + key: StringName, +) -> void: + var mouse_event := event as InputEventMouseButton + if mouse_event == null or not mouse_event.pressed: + return + if mouse_event.button_index in [MOUSE_BUTTON_RIGHT, MOUSE_BUTTON_WHEEL_UP]: + _cycle_index_option(key, -1) + get_viewport().set_input_as_handled() + elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: + _cycle_index_option(key, 1) + get_viewport().set_input_as_handled() + + +func _index_key_for_control(control: Control) -> StringName: + for key: StringName in _index_option_buttons: + if _index_option_buttons[key] == control: + return key + return StringName() + + +func _reset_index() -> void: + _index_options = { + "search": "", + "show": LogbookCatalog.DiscoveryFilter.ALL, + "sort": LogbookCatalog.SortMode.CATALOG_NUMBER, + "descending": false, + "availability": LogbookCatalog.AvailabilityFilter.ANY, + "season": LogbookCatalog.SeasonFilter.ANY, + "time": LogbookCatalog.TimeFilter.ANY, + "rarity": LogbookCatalog.RarityFilter.ANY, + "method": LogbookCatalog.MethodFilter.ANY, + "holding": LogbookCatalog.HoldingFilter.ANY, + "group": "", + } + _index_update_in_progress = true + _index_search.text = "" + _index_update_in_progress = false + _refresh_catalog() + + +func _update_index_controls( + visible_count: int = -1, + category_total: int = -1, +) -> void: + if visible_count >= 0: + _index_visible_count = visible_count + if category_total >= 0: + _index_category_total = category_total + if _index_summary != null: + _index_summary.text = "%d shown of %d" % [ + _index_visible_count, + _index_category_total, + ] + for key: StringName in _index_option_buttons: + var button := _index_option_buttons[key] + if button == null: + continue + button.text = _index_option_label(key) + button.accessibility_name = "%s: %s" % [ + String(key).replace("_", " "), + button.text, + ] + if _index_tab != null: + var marked := _index_has_active_criteria() + _index_tab.text = "index •" if marked else "index" + _index_tab.accessibility_name = ( + "Catalog index, filters active" if marked else "Catalog index" + ) + if _index_reset != null: + _index_reset.disabled = not _index_has_active_criteria() + + +func _index_option_label(key: StringName) -> String: + match key: + &"show": + return INDEX_SHOW_LABELS[int(_index_options[key])] + &"sort": + return INDEX_SORT_LABELS[int(_index_options[key])] + &"availability": + return INDEX_AVAILABILITY_LABELS[int(_index_options[key])] + &"season": + return INDEX_SEASON_LABELS[int(_index_options[key])] + &"time": + return INDEX_TIME_LABELS[int(_index_options[key])] + &"rarity": + return INDEX_RARITY_LABELS[int(_index_options[key])] + &"method": + return INDEX_METHOD_LABELS[int(_index_options[key])] + &"holding": + return INDEX_HOLDING_LABELS[int(_index_options[key])] + &"group": + var group := str(_index_options[key]) + return group if not group.is_empty() else "any group" + &"descending": + var descending := bool(_index_options[key]) + match int(_index_options["sort"]): + LogbookCatalog.SortMode.NAME: + return "z–a" if descending else "a–z" + LogbookCatalog.SortMode.CATALOG_NUMBER: + return "high–low" if descending else "low–high" + LogbookCatalog.SortMode.RARITY: + return ( + "rare–common" if descending else "common–rare" + ) + LogbookCatalog.SortMode.VALUE, LogbookCatalog.SortMode.WEIGHT: + return "high–low" if descending else "low–high" + _: + return "most first" if descending else "least first" + return "any" + + +func _index_option_count(key: StringName) -> int: + match key: + &"show": + return INDEX_SHOW_LABELS.size() + &"sort": + return INDEX_SORT_LABELS.size() + &"availability": + return INDEX_AVAILABILITY_LABELS.size() + &"season": + return INDEX_SEASON_LABELS.size() + &"time": + return INDEX_TIME_LABELS.size() + &"rarity": + return INDEX_RARITY_LABELS.size() + &"method": + return INDEX_METHOD_LABELS.size() + &"holding": + return INDEX_HOLDING_LABELS.size() + return 0 + + +func _index_has_active_criteria() -> bool: + return ( + not str(_index_options["search"]).is_empty() + or int(_index_options["show"]) + != LogbookCatalog.DiscoveryFilter.ALL + or int(_index_options["sort"]) + != LogbookCatalog.SortMode.CATALOG_NUMBER + or bool(_index_options["descending"]) + or int(_index_options["availability"]) + != LogbookCatalog.AvailabilityFilter.ANY + or int(_index_options["season"]) + != LogbookCatalog.SeasonFilter.ANY + or int(_index_options["time"]) + != LogbookCatalog.TimeFilter.ANY + or int(_index_options["rarity"]) + != LogbookCatalog.RarityFilter.ANY + or int(_index_options["method"]) + != LogbookCatalog.MethodFilter.ANY + or int(_index_options["holding"]) + != LogbookCatalog.HoldingFilter.ANY + or not str(_index_options["group"]).is_empty() + ) + + +func _current_season() -> int: + return ( + _world_time.get_season() + if _world_time != null + else CalendarSeason.UNKNOWN + ) + + +func _current_is_night() -> bool: + return _world_time != null and _world_time.is_night_period() + + +func notify_availability_context_changed() -> void: + _on_logbook_context_changed() + + +func _on_logbook_context_changed( + _first: Variant = null, + _second: Variant = null, +) -> void: + if ( + int(_index_options["availability"]) + == LogbookCatalog.AvailabilityFilter.AVAILABLE_NOW + or int(_index_options["season"]) + == LogbookCatalog.SeasonFilter.CURRENT + or int(_index_options["time"]) + == LogbookCatalog.TimeFilter.CURRENT + ): + _refresh_catalog() + + func _make_entry(fish: FishDataType, discovered: bool) -> Button: var entry := Button.new() entry.custom_minimum_size = CATALOG_ENTRY_SIZE @@ -663,8 +1283,11 @@ func _add_entry_content( func _select_category(category: LogbookCatalog.Category) -> void: if category == _category: + if _index_open: + _set_index_open(false) return _category = category + _set_index_open(false, false) _selected_id = StringName() _selected_entry_key = StringName() for index: int in _category_tabs.size(): @@ -683,6 +1306,11 @@ func _configure_category_focus() -> void: tab.focus_neighbor_right = tab.get_path_to( _category_tabs[mini(index + 1, _category_tabs.size() - 1)] ) + if not _category_tabs.is_empty() and _index_tab != null: + var final_tab: Button = _category_tabs.back() + final_tab.focus_neighbor_right = final_tab.get_path_to(_index_tab) + _index_tab.focus_neighbor_left = _index_tab.get_path_to(final_tab) + _index_tab.focus_neighbor_right = _index_tab.get_path_to(_index_tab) func _animate_tab_entry() -> void: @@ -690,11 +1318,15 @@ func _animate_tab_entry() -> void: (_category_tabs[index] as OrganizerTabType).animate_entrance( float(index) * 0.025 ) + if _index_tab != null: + _index_tab.animate_entrance(float(_category_tabs.size()) * 0.025) func _settle_tabs_for_close() -> void: for tab: OrganizerTabType in _category_tabs: tab.settle_for_close() + if _index_tab != null: + _index_tab.settle_for_close() func _begin_category_transition() -> void: @@ -750,9 +1382,12 @@ func _set_category_content_alpha(alpha: float) -> void: _empty_state.modulate.a = alpha if _detail_body != null: _detail_body.modulate.a = alpha + if _index_body != null: + _index_body.modulate.a = alpha func _select_entry(entry_key: StringName, fish_id: StringName) -> void: + _set_index_open(false, false) if fish_id.is_empty(): _selected_id = StringName() _selected_entry_key = entry_key @@ -1284,7 +1919,9 @@ func _on_collection_changed() -> void: func _on_inventory_changed() -> void: - _refresh_details(false) + # Ownership filters and favorite-backed views depend on the live cooler, + # while rebuilding the catalog also keeps the selected detail current. + _refresh_catalog() func _unknown_selection_key(fish: FishDataType) -> StringName: diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 3c5dc77..2f9f96f 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -105,6 +105,7 @@ signal controller_hotbar_management_requested(initial_slot: int) signal controller_hotbar_management_ended signal menu_exit_started signal shop_cooler_modal_changed(is_open: bool) +signal controller_text_entry_requested(control: Control) enum Section { COOLER, @@ -409,6 +410,9 @@ func _ready() -> void: _logbook_tab.pressed.connect( _show_section.bind(Section.LOGBOOK) ) + _catalog_logbook.controller_text_entry_requested.connect( + controller_text_entry_requested.emit + ) _the_net_tab.pressed.connect(_show_section.bind(Section.NET)) _mail_tab.pressed.connect(_show_section.bind(Section.MAIL)) _profile_tab.pressed.connect(_show_section.bind(Section.PROFILE)) @@ -556,6 +560,7 @@ func setup( discovery: DiscoveryClient, player_jobs: PlayerJobService, world_time: WorldTimeService, + world_weather: WorldWeatherService, world_environment: WorldEnvironment, world_sun: DirectionalLight3D, ) -> void: @@ -589,7 +594,14 @@ func setup( world_sun, ) _players_page.setup(network_player_list, discovery) - _catalog_logbook.setup(collection_log, inventory, catalog) + _catalog_logbook.setup( + collection_log, + inventory, + catalog, + fishing_spot, + world_time, + world_weather, + ) _the_net_page.setup(player_jobs, world_time) _network_mail_service.unread_count_changed.connect( _on_mail_unread_count_changed @@ -2779,6 +2791,7 @@ func _toggle_active_tackle() -> void: func _on_active_bait_changed(_item_id: StringName) -> void: _update_tackle_detail() + _catalog_logbook.notify_availability_context_changed() func _on_active_lure_changed(_item_id: StringName) -> void: @@ -3733,6 +3746,7 @@ func _refresh_all() -> void: func _on_bag_changed() -> void: _refresh_bag() _refresh_tackle_box() + _catalog_logbook.notify_availability_context_changed() func _on_hotbar_changed() -> void: From fe099f47dd4e5be4f18387c592c8a0cb5715ee1a Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 21:37:26 -0400 Subject: [PATCH 05/17] 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 From 983e4d0ec3cbd28b0bf26fdc62f9352d8e9d3da7 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 22:00:04 -0400 Subject: [PATCH 06/17] Update validation for portable home protocol --- tests/fish_hotbar_showcase_validation.gd | 4 ++-- tests/fish_quality_validation.gd | 4 ++-- tests/world_spawn_protocol_validation.gd | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/fish_hotbar_showcase_validation.gd b/tests/fish_hotbar_showcase_validation.gd index 9f4623a..af3644b 100644 --- a/tests/fish_hotbar_showcase_validation.gd +++ b/tests/fish_hotbar_showcase_validation.gd @@ -200,8 +200,8 @@ func _run() -> void: var invalid_state: Dictionary = valid_state.duplicate(true) invalid_state["display_scale"] = 1000.0 assert(not NetworkFishShowcaseProtocol.validate_state(invalid_state)) - assert(NetworkProtocol.PROTOCOL_VERSION == 11) - assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) + assert(NetworkProtocol.PROTOCOL_VERSION == 12) + assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18) assert( NetworkProtocol.FISH_QUALITY_CAPABILITY == "fish_quality_v1" diff --git a/tests/fish_quality_validation.gd b/tests/fish_quality_validation.gd index d8e482b..99a2169 100644 --- a/tests/fish_quality_validation.gd +++ b/tests/fish_quality_validation.gd @@ -26,8 +26,8 @@ func _run() -> void: _validate_collection_mastery() _validate_version_four_migration() _validate_fishing_protocol_v2() - assert(NetworkProtocol.PROTOCOL_VERSION == 11) - assert(NetworkProtocol.ENET_CHANNEL_COUNT == 17) + assert(NetworkProtocol.PROTOCOL_VERSION == 12) + assert(NetworkProtocol.ENET_CHANNEL_COUNT == 18) assert(NetworkProtocol.MOVEMENT_ANIMATION_CHANNEL == 11) assert( NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index d100260..0840f2d 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -20,7 +20,7 @@ func _initialize() -> void: func _run() -> void: - assert(NetworkProtocol.PROTOCOL_VERSION == 11) + assert(NetworkProtocol.PROTOCOL_VERSION == 12) assert( NetworkWorldSpawnProtocol.CAPABILITY == NetworkProtocol.WORLD_SPAWN_CAPABILITY From d2dce7f9009d7ed8bab748e2a24a76c9ff8ba28b Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 22:32:27 -0400 Subject: [PATCH 07/17] Stabilize runtime validation under dynamic UI load --- main/texture_sampling_policy.gd | 17 +++++++++++++++-- tests/economy_regression_validation.gd | 18 ++++++++++++++---- tests/gathering_showcase_validation.gd | 10 +++++++--- tests/job_system_validation.gd | 6 +++++- tests/surface_drawing_runtime_validation.gd | 6 +++++- 5 files changed, 46 insertions(+), 11 deletions(-) diff --git a/main/texture_sampling_policy.gd b/main/texture_sampling_policy.gd index 822299c..35a92a0 100644 --- a/main/texture_sampling_policy.gd +++ b/main/texture_sampling_policy.gd @@ -15,8 +15,12 @@ func _enter_tree() -> void: func _on_node_added(node: Node) -> void: enforce_node(node) # Some runtime presenters assign their mesh or material immediately after - # add_child(). Recheck once deferred so those resources are covered too. - call_deferred("_enforce_deferred_node", weakref(node)) + # add_child(). Recheck those 3D resource owners once deferred so the newly + # assigned resources are covered too. CanvasItems keep their filter setting + # when their texture changes, so deferring every dynamic UI node only floods + # the message queue during large inventory and catalog refreshes. + if _needs_deferred_resource_check(node): + call_deferred("_enforce_deferred_node", weakref(node)) func _enforce_existing_tree() -> void: @@ -29,6 +33,15 @@ func _enforce_deferred_node(node_ref: WeakRef) -> void: enforce_node(node) +static func _needs_deferred_resource_check(node: Node) -> bool: + return ( + node is GeometryInstance3D + or node is MultiMeshInstance3D + or node is GPUParticles3D + or node is CPUParticles3D + ) + + static func enforce_subtree(node: Node) -> void: enforce_node(node) for child: Node in node.get_children(): diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index ec80d1b..ff82495 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -31,9 +31,14 @@ func _run() -> void: for _frame: int in 8: await process_frame assert(bool(main.get("_application_initialized"))) + var session := main.get_node("%NetworkSession") as NetworkSession + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(session.set_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") for _frame: int in 8: await process_frame @@ -42,7 +47,6 @@ func _run() -> void: var catalog := main.get("fish_catalog") as FishPool var sale_service := main.get_node("%NetworkSaleService") as NetworkSaleService var shop_service := main.get_node("%NetworkShopService") as NetworkShopService - var session := main.get_node("%NetworkSession") as NetworkSession var reservations := ( main.get_node("%PlayerAssetReservationService") as PlayerAssetReservationService @@ -554,7 +558,10 @@ func _test_host_backpack_purchase( for expected_capacity: int in [18, 27, 36]: _shop_result.clear() assert(not shop_service.request_backpack_capacity_upgrade().is_empty()) - assert(not _shop_result.is_empty() and bool(_shop_result[1])) + assert( + not _shop_result.is_empty() and bool(_shop_result[1]), + "Backpack purchase failed: %s" % [_shop_result], + ) assert(layout.get_inventory_capacity() == expected_capacity) assert(player.wallet.get_balance() == balance_before - total_cost) assert(layout.get_next_backpack_cost() == -1) @@ -575,7 +582,10 @@ func _test_host_storage_purchase( for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]: _shop_result.clear() assert(not shop_service.request_cooler_capacity_upgrade().is_empty()) - assert(not _shop_result.is_empty() and bool(_shop_result[1])) + assert( + not _shop_result.is_empty() and bool(_shop_result[1]), + "Cooler purchase failed: %s" % [_shop_result], + ) assert(capacity.get_capacity() == expected_capacity) assert(player.wallet.get_balance() == balance_before - total_cost) assert(capacity.get_next_cost() == -1) diff --git a/tests/gathering_showcase_validation.gd b/tests/gathering_showcase_validation.gd index 7099f7c..51d6f38 100644 --- a/tests/gathering_showcase_validation.gd +++ b/tests/gathering_showcase_validation.gd @@ -21,9 +21,13 @@ func _run() -> void: assert(bool(main.get("_application_initialized"))) var session := main.get_node("%NetworkSession") as NetworkSession + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1)) assert(session.start_private_host(TEST_PORT)) 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") for _frame: int in 8: await process_frame @@ -38,7 +42,7 @@ func _run() -> void: and game_ui != null and catalog != null ) - fishing_spot.minimum_showcase_duration = 0.15 + fishing_spot.minimum_showcase_duration = 1.0 assert(player.bag.add_item(&"crab_net")) assert(player.hotbar.assign_item(0, &"crab_net")) assert(player.hotbar.select_slot(0)) @@ -81,7 +85,7 @@ func _run() -> void: await process_frame assert(fishing_spot.get("_pending_catch") == crab_catch) Input.action_release("fish_primary") - await create_timer(0.2).timeout + fishing_spot.set("_showcase_input_lock_remaining", 0.0) await process_frame assert(bool(fishing_spot.get("_put_away_press_armed"))) Input.parse_input_event(pocket_event) diff --git a/tests/job_system_validation.gd b/tests/job_system_validation.gd index 89e46f6..65b7164 100644 --- a/tests/job_system_validation.gd +++ b/tests/job_system_validation.gd @@ -21,13 +21,17 @@ func _run() -> void: assert(bool(main.get("_application_initialized"))) var session := main.get_node("%NetworkSession") as NetworkSession assert(session != null) + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1)) var occupied_port := PacketPeerUDP.new() assert(occupied_port.bind(18137, "127.0.0.1") == OK) assert(session.start_private_host(18137, 3)) assert(session.get_host_port() == 18138) occupied_port.close() 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 diff --git a/tests/surface_drawing_runtime_validation.gd b/tests/surface_drawing_runtime_validation.gd index e04afc2..85110cd 100644 --- a/tests/surface_drawing_runtime_validation.gd +++ b/tests/surface_drawing_runtime_validation.gd @@ -20,9 +20,13 @@ func _run() -> void: await process_frame assert(bool(main.get("_application_initialized"))) var session := main.get_node("%NetworkSession") as NetworkSession + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, 1, true + ))) + assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1)) assert(session.start_private_host(TEST_PORT)) 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 From b48cb73f3aa2cc9f1948d709e0b426ba0b518702 Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 23:52:25 -0400 Subject: [PATCH 08/17] Harden multiplayer validation for portable homes --- scripts/run_validations.sh | 1 + tests/fishing_multiplayer_validation.gd | 172 ++++++++++++++++-- tests/late_join_multiplayer_validation.gd | 34 +++- tests/movement_multiplayer_validation.gd | 149 ++++++++++++--- .../surface_drawing_multiplayer_validation.gd | 74 +++++++- 5 files changed, 381 insertions(+), 49 deletions(-) diff --git a/scripts/run_validations.sh b/scripts/run_validations.sh index 5d15b8e..ff108a8 100755 --- a/scripts/run_validations.sh +++ b/scripts/run_validations.sh @@ -87,6 +87,7 @@ readonly -a NETWORK_TESTS=( "tests/fish_showcase_multiplayer_validation.gd" "tests/friend_multiplayer_validation.gd" "tests/fishing_multiplayer_validation.gd" + "tests/home_spawn_multiplayer_validation.gd" "tests/job_multiplayer_validation.gd" "tests/movement_multiplayer_validation.gd" "tests/operator_multiplayer_validation.gd" diff --git a/tests/fishing_multiplayer_validation.gd b/tests/fishing_multiplayer_validation.gd index 73de1bd..9d7ef7f 100644 --- a/tests/fishing_multiplayer_validation.gd +++ b/tests/fishing_multiplayer_validation.gd @@ -63,12 +63,38 @@ func _run_host() -> void: var spawn_service := main.get_node( "%PlayerSpawnService" ) as PlayerSpawnService + var network_home := main.get_node( + "%NetworkHomeService" + ) as NetworkHomeService + var home_world := main.get_node("%HomeWorldService") as HomeWorldService var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id) assert(remote_avatar != null) assert(remote_avatar.bag.get_quantity(&"worms") == 0) - remote_avatar.global_position = Vector3(-0.5, 3.95, 2.1) - var remote_visuals := remote_avatar.get_node("Visuals") as Node3D - remote_visuals.rotation.y = PI * 0.5 + var remote_record := session.get_peer_record(remote_peer_id) + assert(remote_record != null) + assert(await _wait_for_home( + network_home, remote_record.identity_fingerprint + )) + var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform( + remote_record.identity_fingerprint + ) + var exterior_deadline: int = Time.get_ticks_msec() + 5000 + while ( + Time.get_ticks_msec() < exterior_deadline + and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25 + ): + await physics_frame + assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25) + # Give the client enough local frames to observe the initial RV-door spawn + # before this test publishes its separate fishing-position teleport. + for _frame: int in 30: + await physics_frame + var cast_setup: Dictionary = await _find_clear_fishing_setup( + main, remote_avatar + ) + assert(not cast_setup.is_empty(), "no clear fishing setup was found") + var expected_position: Vector3 = cast_setup["position"] + var expected_direction: Vector3 = cast_setup["direction"] session.publish_authoritative_teleport(remote_peer_id) var aiming_deadline: int = Time.get_ticks_msec() + 15000 while ( @@ -82,7 +108,7 @@ func _run_host() -> void: == Player.FishingVisualPhase.CASTING ) var remote_position_error := ( - remote_avatar.global_position - Vector3(-0.5, 3.95, 2.1) + remote_avatar.global_position - expected_position ) remote_position_error.y = 0.0 assert( @@ -90,7 +116,7 @@ func _run_host() -> void: "remote casting position drifted: %s" % remote_avatar.global_position ) assert( - remote_avatar.get_facing_direction().dot(Vector3.LEFT) > 0.99, + remote_avatar.get_facing_direction().dot(expected_direction) > 0.99, "remote casting facing drifted: %s" % remote_avatar.get_facing_direction() ) @@ -196,6 +222,23 @@ func _run_client() -> void: joined = true break assert(joined) + var network_home := main.get_node( + "%NetworkHomeService" + ) as NetworkHomeService + var home_world := main.get_node("%HomeWorldService") as HomeWorldService + var local_fingerprint: String = session.get_local_identity_fingerprint() + assert(await _wait_for_home(network_home, local_fingerprint)) + var player := main.get("_player") as Player + var exterior_spawn: Transform3D = ( + home_world.get_exterior_spawn_transform(local_fingerprint) + ) + var home_spawn_deadline: int = Time.get_ticks_msec() + 5000 + while ( + Time.get_ticks_msec() < home_spawn_deadline + and player.global_position.distance_to(exterior_spawn.origin) > 0.25 + ): + await physics_frame + assert(player.global_position.distance_to(exterior_spawn.origin) <= 0.25) var service := main.get_node( "%NetworkFishingService" @@ -206,7 +249,6 @@ func _run_client() -> void: cast_rejections.append(message) ) var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType - var player := main.get("_player") as Player var item_catalog := main.get("item_catalog") as ItemCatalog var worms: ItemData = item_catalog.get_item_by_id(&"worms") if player.bag.get_quantity(&"worms") == 0: @@ -217,15 +259,20 @@ func _run_client() -> void: "Visuals/CharacterRig/AnimationPlayer" ) as AnimationPlayer assert(character_animation_player.has_animation(&"retract_sit")) - var placement_deadline: int = Time.get_ticks_msec() + 5000 + # The host chooses and publishes a clear target after both RV manifests have + # settled. Waiting for that authoritative teleport also coordinates the + # animation-observation window between these two independent processes. + var placement_deadline: int = Time.get_ticks_msec() + 10000 while ( Time.get_ticks_msec() < placement_deadline - and player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) - > 0.25 + and player.global_position.distance_to(exterior_spawn.origin) <= 1.0 ): await physics_frame - assert(player.global_position.distance_to(Vector3(-0.5, 3.95, 2.1)) <= 0.25) - assert(player.get_facing_direction().dot(Vector3.LEFT) > 0.99) + assert(player.global_position.distance_to(exterior_spawn.origin) > 1.0) + for _frame: int in 2: + await physics_frame + var expected_position: Vector3 = player.global_position + var expected_direction: Vector3 = player.get_facing_direction() player.set_casting_visual() assert( @@ -241,7 +288,10 @@ func _run_client() -> void: assert(player.is_sitting()) for _frame: int in 15: await physics_frame + assert(fishing_spot.state == FishingSpotType.FishingState.READY) + assert(fishing_spot.get("_active_player") == null) fishing_spot.call("_begin_aiming", player) + assert(fishing_spot.state == FishingSpotType.FishingState.AIMING_CAST) assert(player.is_movement_enabled()) assert( int(player.get("_fishing_visual_phase")) @@ -250,6 +300,7 @@ func _run_client() -> void: var cast_charge: float = 0.0 var target: Vector3 = Vector3.ZERO var cast_origin: Vector3 = fishing_spot.get("_cast_origin_position") + var target_diagnostics: Array[String] = [] for charge_step: int in range(1, 21): var candidate_charge := float(charge_step) / 20.0 fishing_spot.set("_cast_charge", candidate_charge) @@ -258,14 +309,33 @@ func _run_client() -> void: var candidate_surface := fishing_spot.get( "_aim_surface_sample" ) as FishingSurfaceSample + target_diagnostics.append( + "charge=%.2f surface=%s path=%s target=%s" % [ + candidate_charge, + candidate_surface.is_fishable(), + fishing_spot.get("_cast_path_is_clear"), + candidate_target, + ] + ) if ( candidate_surface.is_fishable() - and fishing_spot.is_cast_path_clear(cast_origin, candidate_target) + and bool(fishing_spot.get("_cast_path_is_clear")) ): cast_charge = candidate_charge target = candidate_target break - assert(cast_charge > 0.0, "no clear fishable test target was found") + assert( + cast_charge > 0.0, + ( + "no clear fishable test target was found; position=%s facing=%s " + + "expected=%s diagnostics=%s" + ) % [ + player.global_position, + player.get_facing_direction(), + expected_direction, + target_diagnostics, + ], + ) var evidence: Dictionary = fishing_spot.call("_build_network_evidence") fishing_spot.set("state", FishingSpotType.FishingState.CASTING) player.set_movement_enabled(false) @@ -388,3 +458,79 @@ func _create_initialized_main() -> Node: 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 _find_clear_fishing_setup(main: Node, player: Player) -> Dictionary: + var world := main.get_node("TestWorld") as TestWorld + var fishing_spot := main.get_node("%FishingSpot") as FishingSpotType + var regions: Array[FishableWaterRegion] = world.get_fishable_water_regions() + for region: FishableWaterRegion in regions: + var body := region.get_parent() as WaterBodyAuthoring + if body == null or body.surface_size.x <= 0.0 or body.surface_size.y <= 0.0: + continue + var center: Vector3 = region.global_position + var half_size: Vector2 = body.surface_size * 0.5 + var candidate_offsets: Array[Vector3] = [ + Vector3(half_size.x + 0.8, 0.0, 0.0), + Vector3(-half_size.x - 0.8, 0.0, 0.0), + Vector3(0.0, 0.0, half_size.y + 0.8), + Vector3(0.0, 0.0, -half_size.y - 0.8), + ] + for offset: Vector3 in candidate_offsets: + var position := Vector3( + center.x + offset.x, + region.get_surface_height() + 1.44, + center.z + offset.z, + ) + var direction: Vector3 = center - position + direction.y = 0.0 + direction = direction.normalized() + player.global_position = position + var visuals := player.get_node("Visuals") as Node3D + visuals.global_rotation.y = atan2(-direction.x, -direction.z) + for _frame: int in 2: + await physics_frame + var cast_origin: Vector3 = player.get_cast_origin_position() + var launch_position: Vector3 = player.get_fishing_rod_tip().global_position + for charge_step: int in range(1, 21): + var cast_charge := float(charge_step) / 20.0 + var cast_distance := lerpf( + fishing_spot.minimum_cast_distance, + fishing_spot.maximum_cast_distance, + cast_charge, + ) + var query_position := cast_origin + direction * cast_distance + var sample: FishingSurfaceSample = ( + fishing_spot.resolve_fishing_surface( + query_position, cast_origin.y + ) + ) + var target: Vector3 = sample.get_bobber_position( + fishing_spot.solid_bobber_clearance + ) + if ( + sample.is_fishable() + and fishing_spot.is_cast_path_clear( + launch_position, target + ) + ): + return { + "position": player.global_position, + "direction": direction, + } + return {} diff --git a/tests/late_join_multiplayer_validation.gd b/tests/late_join_multiplayer_validation.gd index 3813486..6ea544f 100644 --- a/tests/late_join_multiplayer_validation.gd +++ b/tests/late_join_multiplayer_validation.gd @@ -90,11 +90,12 @@ func _run_first_client() -> void: player.toggle_sitting() assert(player.is_sitting()) assert(player.bag.add_item(&"crab_net")) - assert(player.hotbar.assign_item(0, &"crab_net")) - assert(player.hotbar.select_slot(0)) - for _frame: int in 2: - await process_frame - assert(bool(player.get("_active_item_is_net"))) + # Slot zero begins selected with the starter rod. Replacing that slot does + # not emit a selection change, so use the next slot to exercise the same + # equipped-item path a player uses in normal gameplay. + assert(player.hotbar.assign_item(1, &"crab_net")) + assert(player.hotbar.select_slot(1)) + assert(await _wait_for_local_net(main, player)) assert(player.begin_animation_action(ACTIVE_ACTION)) await create_timer(0.5).timeout assert(StringName(str(player.get("_animation_action_id"))) == ACTIVE_ACTION) @@ -185,6 +186,29 @@ func _wait_for_registry_size(session: NetworkSession, expected: int) -> bool: return false +func _wait_for_local_net(main: Node, player: Player) -> bool: + var deadline: int = Time.get_ticks_msec() + 10000 + while Time.get_ticks_msec() < deadline: + # A joined client can finish its world transition one frame after the + # hotbar selection signal. Refresh through the production path until the + # ready fishing state accepts the equipped item. + main.call("_refresh_active_hotbar_item") + await process_frame + if bool(player.get("_active_item_is_net")): + return true + var fishing_spot: Node = main.get_node("%FishingSpot") + print( + "net equip wait timed out: ", + { + "fishing_state": int(fishing_spot.get("state")), + "selected_slot": player.hotbar.get_selected_slot(), + "selected_item": String(player.hotbar.get_selected_item_id()), + "owns_net": player.bag.owns_item(&"crab_net"), + }, + ) + return false + + func _history_contains(service: NetworkChatService, body: String) -> bool: return service.get_history().any( func(message: Dictionary) -> bool: diff --git a/tests/movement_multiplayer_validation.gd b/tests/movement_multiplayer_validation.gd index 7073bea..05a668d 100644 --- a/tests/movement_multiplayer_validation.gd +++ b/tests/movement_multiplayer_validation.gd @@ -593,15 +593,22 @@ func _run_host() -> void: var main: Node = await _create_initialized_main() var session := main.get_node("%NetworkSession") as NetworkSession var save_manager := main.get("_save_manager") as PlayerSaveManager - assert(session.set_host_world( + assert(bool(main.call( + "_apply_world", WorldLayout.STARTER_ISLAND, NetworkProtocol.DEFAULT_WORLD_SEED, - )) - assert(session.start_private_host(TEST_PORT)) + true, + ))) + assert(bool(main.call( + "_prepare_host_world", + WorldLayout.STARTER_ISLAND, + NetworkProtocol.DEFAULT_WORLD_SEED, + ))) assert(save_manager.initialize_new_game( NetworkProtocol.DEFAULT_WORLD_SEED, WorldLayout.STARTER_ISLAND, )) + assert(session.start_private_host(TEST_PORT)) main.call("_enter_gameplay") for _frame: int in 4: await physics_frame @@ -613,31 +620,40 @@ func _run_host() -> void: # to settle before emitting the one-shot locomotion/action sequence. await create_timer(3.0).timeout var player := main.get("_player") as Player + var world := main.get_node("TestWorld") as TestWorld player.configure_network_remote(true) - player.apply_authoritative_network_input(_movement_input(1, true)) - await create_timer(2.5).timeout - player.apply_authoritative_network_input(_movement_input(2, false)) - await create_timer(2.5).timeout - player.apply_authoritative_network_input( - _movement_input(3, false, true, true) + await _reset_locomotion_test_position(player, session, world) + var input_sequence: int = 1 + input_sequence = await _sustain_locomotion_input( + player, input_sequence, 1.5, true ) - await create_timer(2.0).timeout - player.apply_authoritative_network_input( - _movement_input(4, false, false, true) + await _reset_locomotion_test_position(player, session, world) + input_sequence = await _sustain_locomotion_input( + player, input_sequence, 1.5, false ) - await create_timer(2.0).timeout - player.apply_authoritative_network_input( - _movement_input(5, false, false, true, &"draw", 1) + await _reset_locomotion_test_position(player, session, world) + input_sequence = await _sustain_locomotion_input( + player, input_sequence, 1.5, false, true, true ) + input_sequence = await _sustain_locomotion_input( + player, input_sequence, 1.5, false, false, true + ) + player.apply_authoritative_network_input( + _movement_input(input_sequence, false, false, true, &"draw", 1) + ) + input_sequence += 1 await create_timer(1.0).timeout player.apply_authoritative_network_input( - _movement_input(6, false, false, true, &"strike", 2, true) + _movement_input( + input_sequence, false, false, true, &"strike", 2, true + ) ) + input_sequence += 1 await create_timer(2.0).timeout player.apply_authoritative_network_input( - _movement_input(7, false, false, false, &"", 3) + _movement_input(input_sequence, false, false, false, &"", 3) ) - var disconnect_deadline: int = Time.get_ticks_msec() + 8000 + var disconnect_deadline: int = Time.get_ticks_msec() + 15000 while ( Time.get_ticks_msec() < disconnect_deadline and session.is_authenticated_peer(remote_peer_id) @@ -686,12 +702,27 @@ func _run_client() -> void: var saw_net_strike_paused: bool = false var saw_animation_advance: bool = false var saw_dust: bool = false + var observed_animations: Dictionary = {} + var observed_locomotion_states: Dictionary = {} + var maximum_sneak_speed: float = 0.0 var previous_animation: StringName = &"" var previous_animation_position: float = -1.0 - var observation_deadline: int = Time.get_ticks_msec() + 18000 + var observation_deadline: int = Time.get_ticks_msec() + 22000 while Time.get_ticks_msec() < observation_deadline: await process_frame var current_animation: StringName = animation_player.current_animation + observed_animations[current_animation] = true + var target_locomotion_state: int = int(host_avatar.get( + "_network_target_locomotion_state" + )) + observed_locomotion_states[target_locomotion_state] = true + if target_locomotion_state == Player.LocomotionState.SNEAKING: + maximum_sneak_speed = maxf( + maximum_sneak_speed, + Vector2( + host_avatar.velocity.x, host_avatar.velocity.z + ).length(), + ) saw_running = saw_running or current_animation.begins_with("running") saw_walking = saw_walking or current_animation.begins_with("walking") saw_sneaking = saw_sneaking or current_animation == &"sneaking" @@ -732,15 +763,44 @@ func _run_client() -> void: and saw_dust ): break - assert(saw_running) - assert(saw_walking) - assert(saw_sneaking) - assert(saw_idle_sneak) - assert(saw_net_draw) - assert(saw_net_strike) - assert(saw_net_strike_paused) - assert(saw_animation_advance) - assert(saw_dust) + var missing_observations: Array[String] = [] + if not saw_running: + missing_observations.append("running") + if not saw_walking: + missing_observations.append("walking") + if not saw_sneaking: + missing_observations.append("sneaking") + if not saw_idle_sneak: + missing_observations.append("idle_sneak") + if not saw_net_draw: + missing_observations.append("net_draw") + if not saw_net_strike: + missing_observations.append("net_strike") + if not saw_net_strike_paused: + missing_observations.append("net_strike_paused") + if not saw_animation_advance: + missing_observations.append("animation_advance") + if not saw_dust: + missing_observations.append("sprint_dust") + if not missing_observations.is_empty(): + push_error( + ( + "Missing movement observations %s; observed animations: %s; " + + "locomotion states: %s; maximum sneak speed: %.3f" + ) % [ + missing_observations, + observed_animations.keys(), + observed_locomotion_states.keys(), + maximum_sneak_speed, + ] + ) + session.disconnect_session("") + main.queue_free() + for _frame: int in 4: + await process_frame + await create_timer(0.1).timeout + quit(1) + return print("Movement multiplayer client validation: PASS") session.disconnect_session("") main.queue_free() @@ -778,6 +838,39 @@ func _movement_input( } +func _sustain_locomotion_input( + player: Player, + starting_sequence: int, + duration_seconds: float, + sprinting: bool, + moving: bool = true, + sneaking: bool = false, +) -> int: + var sequence: int = starting_sequence + var deadline: int = ( + Time.get_ticks_msec() + roundi(duration_seconds * 1000.0) + ) + while Time.get_ticks_msec() < deadline: + player.apply_authoritative_network_input( + _movement_input(sequence, sprinting, moving, sneaking) + ) + sequence += 1 + await create_timer(0.1).timeout + return sequence + + +func _reset_locomotion_test_position( + player: Player, + session: NetworkSession, + world: TestWorld, +) -> void: + player.velocity = Vector3.ZERO + player.global_transform = world.get_player_spawn_transform() + session.publish_authoritative_teleport(1) + for _frame: int in 4: + await physics_frame + + func _create_initialized_main() -> Node: root.size = Vector2i(1280, 720) var main: Node = MainScene.instantiate() diff --git a/tests/surface_drawing_multiplayer_validation.gd b/tests/surface_drawing_multiplayer_validation.gd index d0508b2..f79700e 100644 --- a/tests/surface_drawing_multiplayer_validation.gd +++ b/tests/surface_drawing_multiplayer_validation.gd @@ -27,10 +27,12 @@ func _run_host() -> void: assert(bool(main.call( "_apply_world", WorldLayout.STARTER_ISLAND, 1, true ))) - assert(session.set_host_world(WorldLayout.STARTER_ISLAND, 1)) - assert(session.start_private_host(TEST_PORT)) + assert(bool(main.call( + "_prepare_host_world", WorldLayout.STARTER_ISLAND, 1 + ))) 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)) + assert(session.start_private_host(TEST_PORT)) main.call("_enter_gameplay") await physics_frame await physics_frame @@ -62,6 +64,40 @@ func _run_host() -> void: remote_peer_id ) assert(remote_record != null) + 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 + assert(await _wait_for_home( + network_home, remote_record.identity_fingerprint + )) + var remote_avatar: Player = spawn_service.get_avatar(remote_peer_id) + assert(remote_avatar != null) + var exterior_spawn: Transform3D = home_world.get_exterior_spawn_transform( + remote_record.identity_fingerprint + ) + var exterior_deadline: int = Time.get_ticks_msec() + 5000 + while ( + Time.get_ticks_msec() < exterior_deadline + and remote_avatar.global_position.distance_to(exterior_spawn.origin) > 0.25 + ): + await physics_frame + assert(remote_avatar.global_position.distance_to(exterior_spawn.origin) <= 0.25) + var canvas_state: Dictionary = service.get_canvas_state(canvas_id) + var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector( + canvas_state["origin"] + ) + remote_avatar.global_position = Vector3( + canvas_origin.x + 1.0, + player.global_position.y, + canvas_origin.z, + ) + session.publish_authoritative_teleport(remote_peer_id) + for _frame: int in 4: + await physics_frame assert(session.peer_supports_capability( remote_peer_id, SurfaceDrawingProtocol.CAPABILITY )) @@ -152,6 +188,9 @@ func _run_host() -> void: ): await process_frame assert(service.get_canvas_ids().size() == 1) + # Keep the second stamp observable on the client before the ban removes all + # artwork carrying that participant fingerprint. + await create_timer(1.0).timeout var player_list := main.get_node( "%NetworkPlayerListService" ) as NetworkPlayerListService @@ -221,6 +260,20 @@ func _run_client() -> void: var canvas_id: String = service.get_canvas_ids()[0] var snapshot: Dictionary = service.get_canvas_state(canvas_id) assert(str(_find_cell(snapshot, 7, 7).get("color_id", "")) == "coral") + var canvas_origin: Vector3 = SurfaceDrawingProtocol.array_to_vector( + snapshot["origin"] + ) + var placement_deadline: int = Time.get_ticks_msec() + 10000 + while ( + Time.get_ticks_msec() < placement_deadline + and player.global_position.distance_to(canvas_origin) + > NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0 + ): + await physics_frame + assert( + player.global_position.distance_to(canvas_origin) + <= NetworkSurfaceDrawingService.MAX_DRAW_DISTANCE + 2.0 + ) assert(service.request_cell_edits(canvas_id, [{ "x": 7, "y": 7, @@ -381,3 +434,18 @@ func _create_initialized_main() -> Node: 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 From 826f62c3526e0f01d88a4de10a35a55042085b7c Mon Sep 17 00:00:00 2001 From: Voyager Date: Sun, 30 Aug 2026 23:53:07 -0400 Subject: [PATCH 09/17] Prepare private v0.20.1-alpha patron release --- docs/README-PLAYTEST.txt | 9 ++++++--- export_presets.cfg | 26 +++++++++++++------------- project.godot | 4 ++-- scripts/build_playtest.sh | 9 ++++++--- ui/title_screen.tscn | 2 +- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/README-PLAYTEST.txt b/docs/README-PLAYTEST.txt index 15e3698..d3f3432 100644 --- a/docs/README-PLAYTEST.txt +++ b/docs/README-PLAYTEST.txt @@ -1,8 +1,8 @@ straywild -v0.19.0-alpha -Alpha 0.19.0 +v0.20.1-alpha +Alpha 0.20.1 -Thank you for trying this early private playtest. +Thank you for supporting this private patron test release. straywild is an early multiplayer fishing game. This alpha includes local progression, player-hosted and dedicated multiplayer sessions, public room @@ -83,6 +83,8 @@ FEATURES TO TRY - Fishing, net gathering, and beach digging - Equipment and consumable use from the hotbar - Fishing Shop sell tray, wallet updates, and persistent upgrades +- Portable RV homes, multiplayer visits, RV storage, and the early decor + purchasing and placement prototype - Seeded generated worlds and the authored starter-island layout - Generated beaches, ponds, lakes, complete rivers, denser forests, varied elevation, and closed coastal cliffs @@ -117,5 +119,6 @@ KNOWN ALPHA LIMITATIONS networks may prevent some players from joining. - Multiplayer, dedicated-server deployment, UI, and balance are still alpha. - Some fish and character artwork remains temporary. +- RV exteriors and decor use temporary prototype geometry and presentation. - There is no installer or automatic updater. - Windows builds are unsigned. diff --git a/export_presets.cfg b/export_presets.cfg index 05b0918..ff588e2 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -9,7 +9,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.19.0-alpha/windows-x86_64/straywild.exe" +export_path="builds/v0.20.1-alpha/windows-x86_64/straywild.exe" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -35,11 +35,11 @@ application/modify_resources=true application/icon="res://art/exported/system_icons/straywild.ico" application/console_wrapper_icon="" application/icon_interpolation=0 -application/file_version="0.19.0.0" -application/product_version="0.19.0.0" +application/file_version="0.20.1.0" +application/product_version="0.20.1.0" application/company_name="Woofmeow" application/product_name="straywild" -application/file_description="straywild v0.19.0-alpha" +application/file_description="straywild v0.20.1-alpha" application/copyright="Copyright © 2026 Woofmeow" application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md" application/export_angle=0 @@ -62,7 +62,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.19.0-alpha/linux-arm64/straywild.arm64" +export_path="builds/v0.20.1-alpha/linux-arm64/straywild.arm64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -92,7 +92,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.19.0-alpha/macos/straywild.zip" +export_path="builds/v0.20.1-alpha/macos/straywild.zip" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -108,8 +108,8 @@ custom_template/release="" application/bundle_identifier="io.woofmeow.netfishing" application/icon="res://art/exported/system_icons/straywild_1024.png" application/icon_interpolation=0 -application/short_version="0.19.0" -application/version="0.19.0" +application/short_version="0.20.1" +application/version="0.20.1" application/architecture="universal" codesign/enable=false notarization/enable=false @@ -125,7 +125,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*,tests/*" -export_path="builds/v0.19.0-alpha/server-linux-x86_64/straywildServer.x86_64" +export_path="builds/v0.20.1-alpha/server-linux-x86_64/straywildServer.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -155,7 +155,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.19.0-alpha/android/straywild.apk" +export_path="builds/v0.20.1-alpha/android/straywild.apk" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -176,8 +176,8 @@ architectures/armeabi-v7a=false architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false -version/code=190000 -version/name="v0.19.0-alpha" +version/code=200100 +version/name="v0.20.1-alpha" package/unique_name="io.woofmeow.netfishing" package/name="straywild" package/signed=true @@ -214,7 +214,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.19.0-alpha/linux-x86_64/straywild.x86_64" +export_path="builds/v0.20.1-alpha/linux-x86_64/straywild.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/project.godot b/project.godot index d202817..23fdd1e 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="straywild" -config/version="0.19.0-alpha" +config/version="0.20.1-alpha" run/main_scene="res://main/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://art/exported/system_icons/straywild_256.png" @@ -260,7 +260,7 @@ alternate_fishing={ [network] -discovery/base_url="https://discovery.straywild.io" +discovery/base_url="https://discovery.straywild.io/patron-test" [physics] diff --git a/scripts/build_playtest.sh b/scripts/build_playtest.sh index 2347d3d..f1ff151 100755 --- a/scripts/build_playtest.sh +++ b/scripts/build_playtest.sh @@ -4,14 +4,14 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" -readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.19.0-alpha" +readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.1-alpha" readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64" readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64" readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt" readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)" readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild" -readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-windows-x86_64.zip" -readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.19.0-alpha-linux-x86_64.zip" +readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.1-alpha-windows-x86_64.zip" +readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.1-alpha-linux-x86_64.zip" readonly GODOT_BIN="${GODOT_BIN:-godot}" if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then @@ -77,6 +77,9 @@ straywild code is licensed under GPL-3.0-or-later. Source repository: ${SOURCE_URL} Source revision at build time: ${SOURCE_COMMIT} +This private patron test release is distributed with an exact corresponding +source archive named straywild-v0.20.1-alpha-source.tar.gz. + If this package includes uncommitted development changes, it must not be publicly redistributed until the exact corresponding source is also made available. diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index d1fc210..852fd71 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -213,7 +213,7 @@ unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 22 -text = "v0.19.0-alpha" +text = "v0.20.1-alpha" horizontal_alignment = 1 [node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] From 9b96a3bf53a8b66b47f01caf8825bfce5374c03d Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 00:31:02 -0400 Subject: [PATCH 10/17] Replace starter RV placeholder with donated motorcoach --- docs/ATTRIBUTION.md | 6 ++- homes/assets/starter_motorcoach.glb | Bin 0 -> 189284 bytes homes/assets/starter_motorcoach.glb.import | 45 ++++++++++++++++++ homes/home_world_service.gd | 51 +++++++++++++++------ 4 files changed, 85 insertions(+), 17 deletions(-) create mode 100644 homes/assets/starter_motorcoach.glb create mode 100644 homes/assets/starter_motorcoach.glb.import diff --git a/docs/ATTRIBUTION.md b/docs/ATTRIBUTION.md index c235601..d8676f0 100644 --- a/docs/ATTRIBUTION.md +++ b/docs/ATTRIBUTION.md @@ -142,8 +142,10 @@ environment and UI artwork, item artwork, finished fish and creature artwork, and character customization pattern and channel-map artwork. Voyager's incorporated original work includes 3D assets, the provisional fish placeholder artwork, the title and dusk music identified above, and the robot -animalese tones. Specific contributor exceptions are recorded below and in the -in-game credits. +animalese tones. The current starter motorcoach exterior was supplied by a +contributing artist who released its copyright to the project; the original +and cleaned source files are retained in the owners' source-work archive. +Specific contributor exceptions are recorded below and in the in-game credits. These credits intentionally describe work by creator and contribution family instead of maintaining a second file-by-file asset catalog. Runtime resources diff --git a/homes/assets/starter_motorcoach.glb b/homes/assets/starter_motorcoach.glb new file mode 100644 index 0000000000000000000000000000000000000000..5b0d0225da3af0dc3978f6b0bb19562b571ae8e3 GIT binary patch literal 189284 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 Y$FNH{19#B{>47_%9L|Qb!+zoa0dB5ZD*ylh literal 0 HcmV?d00001 diff --git a/homes/assets/starter_motorcoach.glb.import b/homes/assets/starter_motorcoach.glb.import new file mode 100644 index 0000000..5cba35b --- /dev/null +++ b/homes/assets/starter_motorcoach.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://bhph22caipcj6" +path="res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn" + +[deps] + +source_file="res://homes/assets/starter_motorcoach.glb" +dest_files=["res://.godot/imported/starter_motorcoach.glb-153681ef1e622a556fba4df576dc6f84.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd index 4e1b422..baec913 100644 --- a/homes/home_world_service.gd +++ b/homes/home_world_service.gd @@ -6,10 +6,13 @@ const NetworkHomeProtocolType = preload( ) const PlayerHomeStateType = preload("res://homes/player_home_state.gd") const DecorCatalogType = preload("res://homes/decor_catalog.gd") +const STARTER_MOTORCOACH_SCENE: PackedScene = preload( + "res://homes/assets/starter_motorcoach.glb" +) 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_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48) +const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 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) @@ -819,9 +822,18 @@ func _build_exterior( 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, + if not _dedicated_runtime: + var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D + if motorcoach != null: + motorcoach.name = "StarterMotorcoach" + root.add_child(motorcoach) + else: + push_error("The starter motorcoach exterior could not be instantiated.") + _add_box_collision( + root, + "Body", + EXTERIOR_BODY_SIZE, + Vector3(0.0, 1.35, 0.0), ) root.visible = not _dedicated_runtime return root @@ -986,19 +998,28 @@ func _add_box( 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 _add_box_collision(parent, node_name, size, position) return null +func _add_box_collision( + parent: Node3D, + node_name: String, + size: Vector3, + position: Vector3, +) -> StaticBody3D: + 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 + + func _spawn_decor_poof( owner_fingerprint: String, instance_id: String, From ee992d9f0c822856a0f8e619a521896110b5b301 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 00:52:31 -0400 Subject: [PATCH 11/17] Restore public discovery default after patron build --- project.godot | 2 +- scripts/build_portmaster.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project.godot b/project.godot index 23fdd1e..8e751aa 100644 --- a/project.godot +++ b/project.godot @@ -260,7 +260,7 @@ alternate_fishing={ [network] -discovery/base_url="https://discovery.straywild.io/patron-test" +discovery/base_url="https://discovery.straywild.io" [physics] diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index a40e0cd..2d9dcbb 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -236,7 +236,7 @@ unzip -tq "${ARCHIVE}" >/dev/null readonly TOP_LEVELS="$( unzip -Z1 "${ARCHIVE}" | cut -d/ -f1 | sort -u )" -readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' straywild.sh straywild)" +readonly EXPECTED_TOP_LEVELS="$(printf '%s\n' straywild straywild.sh)" if [[ "${TOP_LEVELS}" != "${EXPECTED_TOP_LEVELS}" ]]; then echo "Unexpected PortMaster archive roots:" >&2 printf '%s\n' "${TOP_LEVELS}" >&2 From 6578d3e8701fa38d0c91bdacf8543ca4e9cd1ec6 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 05:48:36 -0400 Subject: [PATCH 12/17] Add transactional RV decor synchronization --- homes/home_decor_controller.gd | 58 +++- homes/home_world_service.gd | 15 +- main/main.gd | 31 +- network/network_home_protocol.gd | 14 +- network/network_home_service.gd | 492 +++++++++++++++++++++++++++++-- network/network_session.gd | 70 +++++ player/player.gd | 31 ++ 7 files changed, 663 insertions(+), 48 deletions(-) diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd index 1a97a5d..8f9aa5f 100644 --- a/homes/home_decor_controller.gd +++ b/homes/home_decor_controller.gd @@ -47,6 +47,12 @@ func setup( _on_local_space_changed ): _network_home.local_space_changed.connect(_on_local_space_changed) + if not _network_home.local_manifest_rejected.is_connected( + _on_local_manifest_rejected + ): + _network_home.local_manifest_rejected.connect( + _on_local_manifest_rejected + ) 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( @@ -84,7 +90,13 @@ func _input(event: InputEvent) -> void: and pointer_button.button_index == MOUSE_BUTTON_LEFT and not pointer_button.pressed ): - _finish_drag() + # A gameplay menu can take ownership between the press and release. Do + # not let that release finalize decor behind the menu; preserve the + # original transform instead. + if _can_decorate(): + _finish_drag() + else: + _cancel_drag() get_viewport().set_input_as_handled() @@ -120,14 +132,17 @@ func _unhandled_input(event: InputEvent) -> void: func _process(delta: float) -> void: if _drag_instance_id.is_empty(): return + # Menus and other gameplay input handoffs must cancel an active drag before + # evaluating a held/released pointer. This prevents a same-frame TAB press + # followed by left-button release from committing a move behind the UI. + if not _can_decorate(): + _cancel_drag() + 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 ) @@ -146,6 +161,9 @@ func _process(delta: float) -> void: func _begin_drag(screen_position: Vector2) -> bool: + if _network_home.has_pending_local_decor_mutation(): + _toolbar.report_status("waiting for the previous decor update") + return false var owner_fingerprint: String = ( _network_home.get_local_home_owner_fingerprint() ) @@ -331,22 +349,34 @@ static func _placement_position(placement: Dictionary) -> Vector3: func _place_carried_decor(decor_id: StringName) -> void: + if _network_home.has_pending_local_decor_mutation(): + _toolbar.report_status("waiting for the previous decor update") + return 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() + # RV transitions keep the gameplay root axis-aligned so its transform can be + # synchronized independently from the presentation. Use the visual facing + # direction instead; otherwise carried decor is always placed along world -Z + # instead of in front of the character the player sees. + var forward: Vector3 = _player.get_facing_direction() 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 placement_yaw: float = atan2(-forward.x, -forward.z) var instance_id: String = _network_home.place_local_decor( - decor_id, local_position, _player.global_rotation.y, _mode + decor_id, local_position, placement_yaw, _mode ) if instance_id.is_empty(): _toolbar.report_status("make room in front of you to place this decor") return + # A local host can reject its own optimistic manifest synchronously. Do not + # claim success after that rollback or leave the player looking for decor that + # was never accepted into the room. + if _home_state.get_placement(instance_id).is_empty(): + _toolbar.report_status("decor placement was not accepted") + return _select_instance(instance_id) _toolbar.report_status("decor placed") @@ -366,6 +396,9 @@ func _select_instance(instance_id: String) -> void: func _rotate_selected() -> void: if not _can_decorate() or _selected_instance_id.is_empty(): return + if _network_home.has_pending_local_decor_mutation(): + _toolbar.report_status("waiting for the previous decor update") + return var placement: Dictionary = _home_state.get_placement( _selected_instance_id ) @@ -392,6 +425,9 @@ func _rotate_selected() -> void: func _store_selected() -> void: if _selected_instance_id.is_empty(): return + if _network_home.has_pending_local_decor_mutation(): + _toolbar.report_status("waiting for the previous decor update") + return if _network_home.remove_local_decor(_selected_instance_id): _clear_selection() _toolbar.report_status("decor packed into storage") @@ -418,6 +454,12 @@ func _on_local_space_changed( _refresh_toolbar() +func _on_local_manifest_rejected(message: String) -> void: + _clear_selection() + if _toolbar != null and _toolbar.visible: + _toolbar.report_status(message) + + func _on_home_changed() -> void: if ( not _selected_instance_id.is_empty() diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd index baec913..fd02d93 100644 --- a/homes/home_world_service.gd +++ b/homes/home_world_service.gd @@ -17,6 +17,7 @@ const EXTERIOR_INTERACTION_DISTANCE: float = 2.7 const INTERIOR_EXIT_DISTANCE: float = 2.1 const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0) const INTERIOR_SPACING: float = 14.0 +const INTERIOR_MAX_SPAWN_SLOTS: int = 64 const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5) const INTERIOR_WALL_HEIGHT: float = 2.7 const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0 @@ -461,13 +462,17 @@ func get_interior_spawn_transform( 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, + # `visitor_offset` is allocated by the host as a compact room-local slot. + # Keep it bounded as a last line of defense if a future caller accidentally + # supplies an engine peer ID instead of a slot. + var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS) + # Build the origin as a value rather than mutating Transform3D.origin. + var spawn_origin: Vector3 = anchor + Vector3( + -1.0 + float(posmod(visitor_slot, 4)) * 0.65, 0.25, - 0.4 + float(floori(float(visitor_offset) / 4.0)) * 0.55, + 0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55, ) - return result + return Transform3D(Basis.IDENTITY, spawn_origin) func get_interior_anchor(owner_fingerprint: String) -> Vector3: diff --git a/main/main.gd b/main/main.gd index 8335c69..00d2b40 100644 --- a/main/main.gd +++ b/main/main.gd @@ -1731,12 +1731,14 @@ func _process(delta: float) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void: if settings == null: return - var requested_window_mode: DisplayServer.WindowMode = ( - DisplayServer.WINDOW_MODE_FULLSCREEN - if settings.fullscreen_enabled - else DisplayServer.WINDOW_MODE_WINDOWED + var current_window_mode: DisplayServer.WindowMode = ( + DisplayServer.window_get_mode() ) - if DisplayServer.window_get_mode() != requested_window_mode: + var requested_window_mode: DisplayServer.WindowMode = resolve_window_mode( + settings.fullscreen_enabled, + current_window_mode, + ) + if current_window_mode != requested_window_mode: DisplayServer.window_set_mode(requested_window_mode) _apply_world_pixelation(settings.world_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size) @@ -1763,6 +1765,25 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void: ) +static func resolve_window_mode( + fullscreen_enabled: bool, + current_mode: DisplayServer.WindowMode, +) -> DisplayServer.WindowMode: + var is_fullscreen: bool = current_mode in [ + DisplayServer.WINDOW_MODE_FULLSCREEN, + DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN, + ] + if fullscreen_enabled: + return ( + current_mode + if is_fullscreen + else DisplayServer.WINDOW_MODE_FULLSCREEN + ) + if is_fullscreen: + return DisplayServer.WINDOW_MODE_WINDOWED + return current_mode + + func _apply_world_pixelation(pixel_size: int) -> void: var root_viewport: Viewport = get_viewport() root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST diff --git a/network/network_home_protocol.gd b/network/network_home_protocol.gd index 62b8488..96756e9 100644 --- a/network/network_home_protocol.gd +++ b/network/network_home_protocol.gd @@ -7,6 +7,7 @@ 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 MAX_TRANSITION_COORDINATE: float = 10000.0 const WORLD_SPACE: StringName = &"world" enum TransitionAction { @@ -143,7 +144,7 @@ static func transform_to_array(value: Transform3D) -> Array[float]: static func array_to_transform(value: Variant) -> Transform3D: - if not _valid_transform(value): + if not validate_transition_transform(value): return Transform3D() var fields: Array = value return Transform3D( @@ -156,6 +157,17 @@ static func array_to_transform(value: Variant) -> Transform3D: ) +static func validate_transition_transform(value: Variant) -> bool: + if not _valid_transform(value): + return false + var fields: Array = value + return ( + absf(float(fields[9])) <= MAX_TRANSITION_COORDINATE + and absf(float(fields[10])) <= MAX_TRANSITION_COORDINATE + and absf(float(fields[11])) <= MAX_TRANSITION_COORDINATE + ) + + static func _valid_transform(value: Variant) -> bool: if typeof(value) != TYPE_ARRAY or (value as Array).size() != 12: return false diff --git a/network/network_home_service.gd b/network/network_home_service.gd index df4fcde..33fa6ee 100644 --- a/network/network_home_service.gd +++ b/network/network_home_service.gd @@ -5,12 +5,15 @@ 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 OwnedItemType = preload("res://items/owned_item.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 +const MAX_PENDING_LOCAL_DECOR_TRANSACTIONS: int = 16 var _session: NetworkSession var _spawn_service: PlayerSpawnService @@ -24,9 +27,12 @@ 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 _interior_slots_by_space: Dictionary[StringName, Dictionary] = {} var _next_interior_index: int = 0 var _suppress_local_submission: bool = false var _publish_queued: bool = false +var _last_submitted_local_home_revision: int = -1 +var _pending_local_decor_transactions: Dictionary[int, Dictionary] = {} func setup( @@ -77,6 +83,10 @@ func can_local_decorate() -> bool: ) +func has_pending_local_decor_mutation() -> bool: + return not _pending_local_decor_transactions.is_empty() + + func get_authoritative_owned_count( peer_id: int, decor_id: StringName, @@ -101,6 +111,7 @@ func place_local_decor( ) -> String: if ( not can_local_decorate() + or has_pending_local_decor_mutation() or _bag == null or _bag.get_quantity(decor_id) <= 0 or _world_service == null @@ -123,6 +134,10 @@ func place_local_decor( ) if normalized.is_empty(): return "" + var transaction: Dictionary = _capture_local_decor_transaction() + if transaction.is_empty(): + return "" + _suppress_local_submission = true var instance_id: String = _local_home.place_decor( decor_id, normalized["position"], @@ -130,17 +145,22 @@ func place_local_decor( mode, ) if instance_id.is_empty(): + _suppress_local_submission = false return "" if not _bag.remove_item(decor_id, 1): _local_home.remove_placement(instance_id) + _suppress_local_submission = false + return "" + _suppress_local_submission = false + if not _commit_local_decor_transaction(transaction): return "" - _save_manager.save_if_dirty() return instance_id func remove_local_decor(instance_id: String) -> bool: if ( not can_local_decorate() + or has_pending_local_decor_mutation() or _bag == null or _inventory_layout == null or _hotbar == null @@ -149,6 +169,9 @@ func remove_local_decor(instance_id: String) -> bool: var placement: Dictionary = _local_home.get_placement(instance_id) if placement.is_empty(): return false + var transaction: Dictionary = _capture_local_decor_transaction() + if transaction.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: @@ -174,14 +197,20 @@ func remove_local_decor(instance_id: String) -> bool: _inventory_layout.cancel_prepared_item_placement(decor_id) returned_to_inventory = _bag.add_item_to_storage(decor_id, 1) if not returned_to_inventory: + if prepared_hotbar_placement: + _inventory_layout.cancel_prepared_item_placement(decor_id) return false if target_hotbar_slot >= 0: _hotbar.assign_item(target_hotbar_slot, decor_id) _hotbar.select_slot(target_hotbar_slot) + _suppress_local_submission = true if not _local_home.remove_placement(instance_id): - _bag.remove_item(decor_id, 1) + _suppress_local_submission = false + _restore_local_decor_transaction(transaction) + return false + _suppress_local_submission = false + if not _commit_local_decor_transaction(transaction): return false - _save_manager.save_if_dirty() return true @@ -191,19 +220,28 @@ func update_local_decor( yaw_radians: float, mode: PlayerHomeStateType.PlacementMode, ) -> bool: + if has_pending_local_decor_mutation(): + return false var normalized: Dictionary = get_valid_local_decor_transform( instance_id, position, yaw_radians, mode ) if normalized.is_empty(): return false + var transaction: Dictionary = _capture_local_decor_transaction() + if transaction.is_empty(): + return false + _suppress_local_submission = true if not _local_home.update_placement( instance_id, normalized["position"], float(normalized["yaw"]), mode, ): + _suppress_local_submission = false + return false + _suppress_local_submission = false + if not _commit_local_decor_transaction(transaction): return false - _save_manager.save_if_dirty() return true @@ -337,7 +375,9 @@ func refresh_world_presentations() -> void: ) -func publish_local_home() -> bool: +func publish_local_home( + allow_pending_decor_transaction: bool = false, +) -> bool: if ( _session == null or not _session.is_gameplay_session_active() @@ -345,11 +385,24 @@ func publish_local_home() -> bool: or _local_home == null ): return false + # A purchase can update the owned decor count while an optimistic placement + # is waiting on the host. Hold that follow-up manifest until the placement is + # accepted or rolled back; otherwise it can carry the still-unverified + # placement and turn one rejection into a second stale rejection. + if ( + not allow_pending_decor_transaction + and has_pending_local_decor_mutation() + ): + _publish_queued = true + return true 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 revision: int = int(home_data.get("revision", -1)) + if revision >= 0 and revision == _last_submitted_local_home_revision: + return true var request: Dictionary = { "request_id": _new_id("home"), "session_id": _session.get_session_id(), @@ -361,6 +414,7 @@ func publish_local_home() -> bool: "portable_home_manifest", NetworkHomeProtocolType.manifest_signature_fields(request), ) + _last_submitted_local_home_revision = revision if _session.is_host(): _process_manifest_request(_session.get_local_peer_id(), request) else: @@ -445,6 +499,15 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void: _manifests_by_fingerprint[fingerprint] = manifest _fingerprint_by_peer[peer_id] = fingerprint _consume_owned_authorizations(peer_id, home_data) + if peer_id == _session.get_local_peer_id(): + _discard_local_decor_transactions_through( + int(home_data.get("revision", -1)) + ) + if _last_submitted_local_home_revision == int( + home_data.get("revision", -1) + ): + _last_submitted_local_home_revision = -1 + _flush_queued_local_home_publish() _broadcast_manifest(manifest) if is_first_manifest: _spawn_peer_at_exterior(peer_id, fingerprint) @@ -588,6 +651,16 @@ func receive_home_manifest(data: Dictionary) -> void: var fingerprint: String = str(data["owner_fingerprint"]) _manifests_by_fingerprint[fingerprint] = data.duplicate(true) _fingerprint_by_peer[int(data["owner_peer_id"])] = fingerprint + if fingerprint == _session.get_local_identity_fingerprint(): + var home: Dictionary = data.get("home", {}) + _discard_local_decor_transactions_through( + int(home.get("revision", -1)) + ) + if _last_submitted_local_home_revision == int( + home.get("revision", -1) + ): + _last_submitted_local_home_revision = -1 + _flush_queued_local_home_publish() if _world_service != null and _world_service.is_world_ready(): _world_service.apply_manifest(data) @@ -613,20 +686,304 @@ func receive_home_manifest_rejection( func _apply_manifest_rejection(message: String, canonical: Dictionary) -> void: - if ( - canonical.is_empty() - or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical) - ): + _last_submitted_local_home_revision = -1 + var canonical_home: Dictionary = canonical.get("home", {}) + var canonical_revision: int = int(canonical_home.get("revision", -1)) + var transaction: Dictionary = _get_local_decor_rejection_transaction( + canonical_revision + ) + if canonical.is_empty() or not NetworkHomeProtocolType.validate_manifest_broadcast(canonical): + if not transaction.is_empty(): + _restore_local_decor_transaction(transaction) + _pending_local_decor_transactions.clear() + if _save_manager != null and not _save_manager.save_if_dirty(): + push_warning("Unable to save the restored RV decor state.") + _flush_queued_local_home_publish() local_manifest_rejected.emit(message) return + # A shop result can be accepted locally while its own home manifest is still + # in flight. A delayed rejection for an earlier placement must not erase that + # accepted purchase just because the host's canonical manifest predates it. + var locally_owned_decor: Dictionary[StringName, int] = ( + _capture_local_owned_decor_counts() + ) + # Only carry ownership forward across an actual pending decor-action + # rejection. A standalone rejected manifest must remain authoritative rather + # than repeatedly re-submitting an unverified owned-count increase. + var has_owned_decor_increase: bool = ( + not transaction.is_empty() + and _has_local_owned_decor_increase( + locally_owned_decor, + canonical_home, + ) + ) _suppress_local_submission = true - _local_home.restore_from_save_data(canonical["home"]) + var restored: bool = _local_home.restore_from_save_data(canonical_home) + var owned_decor_restored: bool = ( + restored + and ( + not has_owned_decor_increase + or _restore_local_owned_decor_increases(locally_owned_decor) + ) + ) + var reconciled: bool = ( + restored + and owned_decor_restored + and _reconcile_local_decor_inventory(transaction) + ) _suppress_local_submission = false - _save_manager.save_if_dirty() + _discard_local_decor_transactions_after(canonical_revision) + if not reconciled: + push_error("Unable to reconcile RV decor after a host rejection.") + elif _save_manager != null and not _save_manager.save_if_dirty(): + push_warning("Unable to save the restored RV decor state.") receive_home_manifest(canonical) + if reconciled and has_owned_decor_increase: + # Submit the preserved purchase as a fresh revision after applying the + # canonical placement rollback. This is deliberately separate from the + # rejected action, so the host can validate the already-authorized count. + if not publish_local_home(): + push_warning("Unable to publish the restored RV decor ownership.") local_manifest_rejected.emit(message) +func _capture_local_decor_transaction() -> Dictionary: + if ( + _local_home == null + or _bag == null + or _inventory_layout == null + or _hotbar == null + ): + return {} + var saved_items: Array[OwnedItemType] = [] + for item: OwnedItemType in _bag.get_all_items(): + saved_items.append(item.duplicate_record()) + return { + "home": _local_home.to_save_data(), + "bag_items": saved_items, + "layout": _inventory_layout.to_save_data(), + "hotbar_slots": _hotbar.get_slots(), + "hotbar_fish_slots": _hotbar.get_fish_slots(), + "selected_hotbar_slot": _hotbar.get_selected_slot(), + } + + +func _capture_local_owned_decor_counts() -> Dictionary[StringName, int]: + var counts: Dictionary[StringName, int] = {} + if _local_home == null: + return counts + for decor_id: StringName in DecorCatalogType.get_product_ids(): + counts[decor_id] = _local_home.get_owned_count(decor_id) + return counts + + +func _has_local_owned_decor_increase( + local_counts: Dictionary[StringName, int], + canonical_home: Dictionary, +) -> bool: + var canonical_owned: Dictionary = canonical_home.get("owned_decor", {}) + for decor_id: StringName in DecorCatalogType.get_product_ids(): + if int(local_counts.get(decor_id, 0)) > int( + canonical_owned.get(String(decor_id), 0) + ): + return true + return false + + +func _restore_local_owned_decor_increases( + local_counts: Dictionary[StringName, int], +) -> bool: + if _local_home == null: + return false + for decor_id: StringName in DecorCatalogType.get_product_ids(): + var local_count: int = int(local_counts.get(decor_id, 0)) + var canonical_count: int = _local_home.get_owned_count(decor_id) + while canonical_count < local_count: + if not _local_home.add_owned_decor(decor_id, 1): + return false + canonical_count += 1 + return true + + +func _commit_local_decor_transaction(transaction: Dictionary) -> bool: + if _save_manager == null or not _save_manager.save_if_dirty(): + _restore_local_decor_transaction(transaction) + return false + var revision: int = _local_home.get_revision() + _pending_local_decor_transactions[revision] = transaction.duplicate(true) + _trim_pending_local_decor_transactions() + if publish_local_home(true): + return true + _pending_local_decor_transactions.erase(revision) + _restore_local_decor_transaction(transaction) + _flush_queued_local_home_publish() + # The optimistic mutation was already written before we attempted to publish + # it. If the session ended or could not accept the manifest, immediately + # persist the restored state so a restart cannot resurrect the rejected + # placement or consume its decor item. + if _save_manager != null and not _save_manager.save_if_dirty(): + push_warning("Unable to save the restored RV decor state.") + return false + + +func _restore_local_decor_transaction(transaction: Dictionary) -> bool: + if ( + transaction.is_empty() + or _local_home == null + or _bag == null + or _inventory_layout == null + or _hotbar == null + ): + return false + var home_data: Variant = transaction.get("home", {}) + var layout_data: Variant = transaction.get("layout", {}) + var stored_items: Variant = transaction.get("bag_items", []) + var stored_slots: Variant = transaction.get("hotbar_slots", []) + var stored_fish_slots: Variant = transaction.get("hotbar_fish_slots", []) + if ( + typeof(home_data) != TYPE_DICTIONARY + or typeof(layout_data) != TYPE_DICTIONARY + or typeof(stored_items) != TYPE_ARRAY + or typeof(stored_slots) != TYPE_ARRAY + or typeof(stored_fish_slots) != TYPE_ARRAY + ): + return false + var items: Array[OwnedItemType] = [] + for value: Variant in stored_items as Array: + if not value is OwnedItemType: + return false + items.append((value as OwnedItemType).duplicate_record()) + var slots: Array[StringName] = [] + for value: Variant in stored_slots as Array: + slots.append(StringName(str(value))) + var fish_slots: Array[StringName] = [] + for value: Variant in stored_fish_slots as Array: + fish_slots.append(StringName(str(value))) + var was_suppressed: bool = _suppress_local_submission + _suppress_local_submission = true + var home_restored: bool = _local_home.restore_from_save_data( + home_data as Dictionary + ) + var bag_restored: bool = _bag.replace_all_items(items) + var layout_restored: bool = _inventory_layout.restore_from_save_data( + layout_data as Dictionary + ) + var hotbar_restored: bool = _hotbar.replace_state( + slots, + int(transaction.get("selected_hotbar_slot", 0)), + fish_slots, + false, + ) + _suppress_local_submission = was_suppressed + return home_restored and bag_restored and layout_restored and hotbar_restored + + +func _reconcile_local_decor_inventory(transaction: Dictionary) -> bool: + if _bag == null or _local_home == null: + return false + var preferred_slots: Dictionary[StringName, int] = ( + _decor_hotbar_slots_from_transaction(transaction) + ) + for decor_id: StringName in DecorCatalogType.get_product_ids(): + var target_quantity: int = _local_home.get_available_count(decor_id) + var current_quantity: int = _bag.get_quantity(decor_id) + if current_quantity > target_quantity: + if not _bag.remove_item( + decor_id, current_quantity - target_quantity + ): + return false + continue + while current_quantity < target_quantity: + var preferred_slot: int = int( + preferred_slots.get(decor_id, -1) + ) + if not _restore_decor_item_to_bag(decor_id, preferred_slot): + return false + current_quantity += 1 + return true + + +func _decor_hotbar_slots_from_transaction( + transaction: Dictionary, +) -> Dictionary[StringName, int]: + var result: Dictionary[StringName, int] = {} + var stored_slots: Variant = transaction.get("hotbar_slots", []) + if typeof(stored_slots) != TYPE_ARRAY: + return result + for slot_index: int in range((stored_slots as Array).size()): + var decor_id := StringName(str((stored_slots as Array)[slot_index])) + if DecorCatalogType.has_product(decor_id): + result[decor_id] = slot_index + return result + + +func _restore_decor_item_to_bag( + decor_id: StringName, + preferred_hotbar_slot: int, +) -> bool: + if _bag == null: + return false + var prepared_slot: int = -1 + if ( + _inventory_layout != null + and _hotbar != null + and _inventory_layout.get_container( + PlayerInventoryLayout.EntryKind.ITEM, decor_id + ) < 0 + ): + var candidate_slot: int = preferred_hotbar_slot + if candidate_slot < 0: + candidate_slot = _hotbar.find_first_empty_slot() + if candidate_slot >= 0 and _inventory_layout.prepare_new_item_placement( + decor_id, + PlayerInventoryLayout.InventoryContainer.HOTBAR, + candidate_slot, + ): + prepared_slot = candidate_slot + var restored: bool = _bag.add_item(decor_id, 1) + if not restored: + if prepared_slot >= 0: + _inventory_layout.cancel_prepared_item_placement(decor_id) + if _bag.get_owned_item(decor_id) == null: + restored = _bag.add_item_to_storage(decor_id, 1) + if restored and prepared_slot >= 0: + _hotbar.assign_item(prepared_slot, decor_id) + _hotbar.select_slot(prepared_slot) + return restored + + +func _get_local_decor_rejection_transaction( + canonical_revision: int, +) -> Dictionary: + var revisions: Array[int] = [] + revisions.assign(_pending_local_decor_transactions.keys()) + revisions.sort() + for revision: int in revisions: + if canonical_revision < 0 or revision > canonical_revision: + return _pending_local_decor_transactions[revision].duplicate(true) + return {} + + +func _discard_local_decor_transactions_through(revision: int) -> void: + for pending_revision: int in _pending_local_decor_transactions.keys(): + if pending_revision <= revision: + _pending_local_decor_transactions.erase(pending_revision) + + +func _discard_local_decor_transactions_after(revision: int) -> void: + for pending_revision: int in _pending_local_decor_transactions.keys(): + if pending_revision > revision: + _pending_local_decor_transactions.erase(pending_revision) + + +func _trim_pending_local_decor_transactions() -> void: + var revisions: Array[int] = [] + revisions.assign(_pending_local_decor_transactions.keys()) + revisions.sort() + while revisions.size() > MAX_PENDING_LOCAL_DECOR_TRANSACTIONS: + _pending_local_decor_transactions.erase(revisions.pop_front()) + + func _submit_transition(action: int, owner_fingerprint: String) -> bool: var request: Dictionary = { "request_id": _new_id("home_transition"), @@ -697,12 +1054,18 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: ): _send_transition_rejection(peer_id, "You cannot enter that RV.") return + var interior_space: StringName = ( + NetworkHomeProtocolType.make_space_id(owner_fingerprint) + ) + var visitor_slot: int = _reserve_interior_slot( + interior_space, peer_id + ) _apply_host_transition( peer_id, - NetworkHomeProtocolType.make_space_id(owner_fingerprint), + interior_space, owner_fingerprint, _world_service.get_interior_spawn_transform( - owner_fingerprint, peer_id + owner_fingerprint, visitor_slot ), ) return @@ -748,18 +1111,40 @@ func _apply_host_transition( owner_fingerprint: String, transform: Transform3D, ) -> void: + var avatar: Player = _spawn_service.get_avatar(peer_id) + var movement_sequence: int = 0 + if avatar != null: + movement_sequence = int( + avatar.make_network_snapshot(peer_id).get( + "acknowledged_input", 0 + ) + ) + var transform_data: Array[float] = ( + NetworkHomeProtocolType.transform_to_array(transform) + ) + if not NetworkHomeProtocolType.validate_transition_transform( + transform_data + ): + push_error("Refused an out-of-bounds RV transition transform.") + return + var previous_space: StringName = _session.get_peer_space(peer_id) + if ( + String(previous_space).begins_with("rv:") + and previous_space != space_id + ): + _release_interior_slot(previous_space, peer_id) var result: Dictionary = { "peer_id": peer_id, "space_id": String(space_id), "owner_fingerprint": owner_fingerprint, - "transform": NetworkHomeProtocolType.transform_to_array(transform), + "transform": transform_data, + "movement_sequence": movement_sequence, } _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( @@ -770,35 +1155,40 @@ 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 typeof(data.get("movement_sequence", 0)) != TYPE_INT + or int(data.get("movement_sequence", 0)) < 0 or not NetworkIdentityCrypto.valid_fingerprint( data.get("owner_fingerprint") ) + or not NetworkHomeProtocolType.validate_transition_transform( + data.get("transform") + ) ): 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 NetworkHomeProtocolType.validate_transition_transform( + data.get("transform") + ): + return + var target_transform: Transform3D = ( + NetworkHomeProtocolType.array_to_transform(data["transform"]) + ) 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 not _session.apply_authoritative_space_transition( + peer_id, + target_transform, + int(data.get("movement_sequence", 0)), + ): + return 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_space_changed.emit(space_id, str(data["owner_fingerprint"])) local_transition_finished.emit(true, "") _refresh_peer_visibility() @@ -840,12 +1230,20 @@ func _on_local_home_changed() -> void: func _publish_queued_home() -> void: + if has_pending_local_decor_mutation(): + return _publish_queued = false if _save_manager != null: _save_manager.save_if_dirty() publish_local_home() +func _flush_queued_local_home_publish() -> void: + if not _publish_queued or has_pending_local_decor_mutation(): + return + call_deferred("_publish_queued_home") + + func _on_join_authenticated() -> void: publish_local_home() @@ -880,6 +1278,7 @@ func _on_peer_authenticated(peer_id: int, _display_name: String) -> void: func _on_peer_removed(peer_id: int) -> void: + _release_peer_interior_slots(peer_id) var fingerprint: String = _fingerprint_by_peer.get(peer_id, "") _fingerprint_by_peer.erase(peer_id) _authorized_owned_counts.erase(peer_id) @@ -937,6 +1336,10 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _fingerprint_by_peer.clear() _authorized_owned_counts.clear() _authorization_result_owner.clear() + _publish_queued = false + _last_submitted_local_home_revision = -1 + _pending_local_decor_transactions.clear() + _interior_slots_by_space.clear() _next_interior_index = 0 _world_service.clear_presentations() _world_service.set_local_space(NetworkHomeProtocolType.WORLD_SPACE) @@ -950,6 +1353,37 @@ func _fingerprint_for_peer(peer_id: int) -> String: return record.identity_fingerprint if record != null else "" +func _reserve_interior_slot(space_id: StringName, peer_id: int) -> int: + var slots: Dictionary = _interior_slots_by_space.get(space_id, {}) + if slots.has(peer_id): + return int(slots[peer_id]) + var used_slots: Dictionary[int, bool] = {} + for value: Variant in slots.values(): + used_slots[int(value)] = true + var slot: int = 0 + while used_slots.has(slot): + slot += 1 + slots[peer_id] = slot + _interior_slots_by_space[space_id] = slots + return slot + + +func _release_interior_slot(space_id: StringName, peer_id: int) -> void: + var slots: Dictionary = _interior_slots_by_space.get(space_id, {}) + if slots.is_empty(): + return + slots.erase(peer_id) + if slots.is_empty(): + _interior_slots_by_space.erase(space_id) + else: + _interior_slots_by_space[space_id] = slots + + +func _release_peer_interior_slots(peer_id: int) -> void: + for space_id: StringName in _interior_slots_by_space.keys(): + _release_interior_slot(space_id, peer_id) + + func _new_id(prefix: String) -> String: return "%s:%s" % [ prefix, Crypto.new().generate_random_bytes(16).hex_encode(), diff --git a/network/network_session.gd b/network/network_session.gd index 8add90f..66d5101 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -28,6 +28,7 @@ const MAX_PENDING_MOVEMENT_INPUTS: int = 96 const ANIMATION_REFRESH_INTERVAL: float = 1.0 const MAX_MOVEMENT_INPUT_SEQUENCE: int = 2147483647 const MAX_MOVEMENT_ONE_WAY_TRANSIT_SECONDS: float = 0.25 +const SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE: float = 12.0 const MOVEMENT_FLAG_JUMP: int = 1 << 0 const MOVEMENT_FLAG_SPRINT: int = 1 << 1 @@ -124,6 +125,9 @@ var _pending_movement_inputs: Array[Dictionary] = [] var _snapshot_accumulator: float = 0.0 var _movement_snapshot_tick: int = 0 var _last_local_snapshot_received_msec: int = 0 +var _local_space_transition_guard_active: bool = false +var _local_space_transition_guard_position: Vector3 = Vector3.ZERO +var _local_space_transition_minimum_ack: int = 0 var _animation_refresh_accumulator: float = 0.0 var _last_animation_state_by_peer: Dictionary[int, Dictionary] = {} var _pending_animation_state_by_peer: Dictionary[int, Dictionary] = {} @@ -703,6 +707,40 @@ 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 apply_authoritative_space_transition( + peer_id: int, + target_transform: Transform3D, + acknowledged_input: int, +) -> bool: + if not target_transform.origin.is_finite(): + return false + var local_peer_id: int = get_local_peer_id() + var local_transition: bool = peer_id == local_peer_id + var avatar: Player = _spawn_service.get_avatar(peer_id) + # Remote home transitions can arrive before their player-spawn message. Keep + # the new space assignment so the later spawn inherits correct visibility; + # only a missing local avatar is a transition failure. + if avatar == null: + return not local_transition + avatar.apply_network_space_transition(target_transform, local_transition) + if ( + local_transition + and state == State.JOINED_CLIENT + ): + # Movement snapshots use another ENet channel and a pre-transition audit + # can therefore arrive after the reliable home transition. Clear local + # prediction and reject those old-world snapshots until the host publishes + # its first audit from the new space. + _pending_movement_inputs.clear() + _last_local_snapshot_received_msec = 0 + _last_input_state_hash = 0 + _idle_input_accumulator = IDLE_INPUT_INTERVAL + _local_space_transition_guard_active = true + _local_space_transition_guard_position = target_transform.origin + _local_space_transition_minimum_ack = maxi(acknowledged_input, 0) + return true + + func get_peer_rtt_ms(peer_id: int) -> int: if peer_id == 1: return 0 @@ -2569,6 +2607,8 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void: if avatar == null: continue if peer_id == local_peer_id: + if _reject_stale_space_transition_snapshot(snapshot): + continue _discard_acknowledged_movement_inputs( int(snapshot.get("acknowledged_input", 0)) ) @@ -2586,6 +2626,33 @@ func receive_movement_snapshots(encoded_snapshots: Array) -> void: ) +func _reject_stale_space_transition_snapshot(snapshot: Dictionary) -> bool: + if not _local_space_transition_guard_active: + return false + var position_values: Array = snapshot.get("position", []) + if position_values.size() != 3: + return true + var position := Vector3( + float(position_values[0]), + float(position_values[1]), + float(position_values[2]), + ) + var acknowledged_input: int = int( + snapshot.get("acknowledged_input", 0) + ) + if acknowledged_input < _local_space_transition_minimum_ack: + return true + if acknowledged_input > _local_space_transition_minimum_ack: + _local_space_transition_guard_active = false + return false + if position.distance_to(_local_space_transition_guard_position) > ( + SPACE_TRANSITION_SNAPSHOT_GUARD_DISTANCE + ): + return true + _local_space_transition_guard_active = false + return false + + func _discard_acknowledged_movement_inputs(acknowledged_sequence: int) -> void: while ( not _pending_movement_inputs.is_empty() @@ -2914,6 +2981,9 @@ func _teardown_peer() -> void: _snapshot_accumulator = 0.0 _movement_snapshot_tick = 0 _last_local_snapshot_received_msec = 0 + _local_space_transition_guard_active = false + _local_space_transition_guard_position = Vector3.ZERO + _local_space_transition_minimum_ack = 0 _animation_refresh_accumulator = 0.0 _last_animation_state_by_peer.clear() _pending_animation_state_by_peer.clear() diff --git a/player/player.gd b/player/player.gd index 30807be..e36f92b 100644 --- a/player/player.gd +++ b/player/player.gd @@ -2093,6 +2093,37 @@ func reset_network_movement_state() -> void: _local_prediction_largest_error = 0.0 +func apply_network_space_transition( + target_transform: Transform3D, + align_local_camera: bool, +) -> void: + if not target_transform.origin.is_finite(): + return + # A room transition is a discontinuity, not ordinary movement. Preserve the + # host's input acknowledgement while clearing every motion, animation, and + # reconciliation state that could pull the avatar toward the previous space. + var acknowledged_input: int = _last_network_input_sequence + reset_network_movement_state() + _last_network_input_sequence = acknowledged_input + _apply_network_casting(false) + velocity = Vector3.ZERO + global_transform = Transform3D(Basis.IDENTITY, target_transform.origin) + var target_yaw: float = target_transform.basis.get_euler().y + _visuals.rotation.y = target_yaw + _network_camera_yaw = target_yaw + _network_target_position = global_position + _network_target_velocity = Vector3.ZERO + _network_target_visual_yaw = target_yaw + _network_snapshot_age = 0.0 + _network_snapshot_ready = true + _authoritative_movement_history.clear() + if align_local_camera: + _set_camera_dragging(false) + if _free_camera_active: + _set_free_camera_active(false) + _camera_yaw.rotation.y = target_yaw + + func capture_network_input(sequence: int) -> Dictionary: if _sitting_intent_pending and _sitting_intent_sequence < 0: _sitting_intent_sequence = sequence From 3be79fa3fccbd8c1c373919c7e900af00c13ffac Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 05:48:47 -0400 Subject: [PATCH 13/17] Normalize cursor handoff across gameplay menus --- network/network_surface_drawing_service.gd | 26 ++++++++++++++++++++-- ui/decor_shop.gd | 10 ++++++++- ui/fishing_shop.gd | 15 ++++++++++--- ui/player_menu.gd | 19 +++++++++++++--- ui/player_storage.gd | 11 ++++++++- 5 files changed, 71 insertions(+), 10 deletions(-) diff --git a/network/network_surface_drawing_service.gd b/network/network_surface_drawing_service.gd index c322e5b..485dfaa 100644 --- a/network/network_surface_drawing_service.gd +++ b/network/network_surface_drawing_service.gd @@ -250,7 +250,11 @@ func activate( _pointer_screen_position = _clamped_pointer_position( initial_pointer_position ) - _prior_mouse_mode = Input.mouse_mode + # Art-kit activation can occur while the prior right-click camera drag is + # still held. Stop that drag before recording a return mode, then restore + # normal camera availability so right-click continues to work while drawing. + _stop_camera_drag_for_art_kit() + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _update_aim() _refresh_stencil_visibility() @@ -266,6 +270,10 @@ func deactivate() -> void: _eraser_mode = false _clear_armed_guide_action(false) _camera_look_active = false + # Do not leave a drag captured when drawing is closed by a menu, hotbar + # change, or session transition. The next camera drag must start on a fresh + # physical right-click press. + _stop_camera_drag_for_art_kit() _reset_stroke() _selected_canvas_id = "" _hovered_canvas_id = "" @@ -273,10 +281,24 @@ func deactivate() -> void: _aim_hit.clear() _hide_previews() _refresh_stencil_visibility() - Input.mouse_mode = _prior_mouse_mode + Input.mouse_mode = _get_restorable_mouse_mode(_prior_mouse_mode) _emit_hud_state("") +func _stop_camera_drag_for_art_kit() -> void: + if _local_player == null or not is_instance_valid(_local_player): + return + var camera_input_enabled: bool = _local_player.is_camera_input_enabled() + _local_player.set_camera_input_enabled(false) + _local_player.set_camera_input_enabled(camera_input_enabled) + + +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode + + func is_active() -> bool: return _active diff --git a/ui/decor_shop.gd b/ui/decor_shop.gd index 9292806..d05d9eb 100644 --- a/ui/decor_shop.gd +++ b/ui/decor_shop.gd @@ -82,9 +82,11 @@ func open_shop() -> bool: 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) + # Release any active right-click drag first. The shop must not remember the + # drag's transient captured mode because release may happen inside the UI. _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _transaction_pending = ( @@ -133,6 +135,12 @@ func close_for_session_end() -> void: close_shop(false) +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode + + func _unhandled_input(event: InputEvent) -> void: if not visible or not event.is_action_pressed("ui_cancel"): return diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index ad8247c..234a1c1 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -654,11 +654,14 @@ func open_shop() -> bool: ) _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) + # Releasing an active camera drag before the snapshot prevents the close + # path from restoring a captured pointer after its matching button release + # has been consumed by the shop. + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _snapshot_stored = true _mouse_snapshot_stored = true - _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 _set_feedback("") @@ -1897,3 +1900,9 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void: CloseReason.TEARDOWN: pass _mouse_snapshot_stored = false + + +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 2f9f96f..9b15913 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -1559,11 +1559,14 @@ func open_menu() -> void: _cancel_page_tween() _prior_movement_enabled = _player.is_movement_enabled() _prior_camera_input_enabled = _player.is_camera_input_enabled() - _prior_mouse_mode = Input.mouse_mode + _player.set_movement_enabled(false) + # Ending an active right-click camera drag restores its ordinary pointer + # mode. Snapshot after that handoff so this menu never restores the drag's + # transient captured mode when it closes. + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _control_snapshot_stored = true _mouse_snapshot_stored = true - _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 visible = true @@ -1775,6 +1778,16 @@ func _apply_mouse_close_policy(reason: CloseReason) -> void: _mouse_snapshot_stored = false +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + # Mouse capture belongs to an in-progress camera drag, never to the player + # menu itself. A held right button can be released while this menu owns + # input, so restoring CAPTURED here would leave the pointer hidden without + # an active drag to release it. + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode + + func _on_bite_activated() -> void: if visible: close_menu(CloseReason.BITE_STARTED) diff --git a/ui/player_storage.gd b/ui/player_storage.gd index 1f553ea..83817c5 100644 --- a/ui/player_storage.gd +++ b/ui/player_storage.gd @@ -109,9 +109,12 @@ func _open_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 _player.set_movement_enabled(false) + # End an in-progress right-click camera drag before taking the snapshot. + # A held button can be released while this page owns input, so captured mode + # is never a valid mouse mode to restore when the page closes. _player.set_camera_input_enabled(false) + _prior_mouse_mode = _get_restorable_mouse_mode(Input.mouse_mode) _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _feedback.text = "select an item to move it · drag to choose a slot" @@ -153,6 +156,12 @@ func close_for_session_end() -> void: close_storage(false) +func _get_restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + if mouse_mode == Input.MOUSE_MODE_CAPTURED: + return Input.MOUSE_MODE_VISIBLE + return mouse_mode + + func consume_escape() -> bool: if not visible: return false From 014c064fae7592a8fe0e4107d177a22729c0a0e2 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 06:06:09 -0400 Subject: [PATCH 14/17] Prepare private v0.20.2-alpha test build --- docs/README-PLAYTEST.txt | 6 +++--- export_presets.cfg | 26 +++++++++++++------------- project.godot | 4 ++-- scripts/build_playtest.sh | 10 +++++----- ui/title_screen.tscn | 2 +- 5 files changed, 24 insertions(+), 24 deletions(-) diff --git a/docs/README-PLAYTEST.txt b/docs/README-PLAYTEST.txt index d3f3432..f187182 100644 --- a/docs/README-PLAYTEST.txt +++ b/docs/README-PLAYTEST.txt @@ -1,8 +1,8 @@ straywild -v0.20.1-alpha -Alpha 0.20.1 +v0.20.2-alpha +Alpha 0.20.2 -Thank you for supporting this private patron test release. +Thank you for supporting this private testing release. straywild is an early multiplayer fishing game. This alpha includes local progression, player-hosted and dedicated multiplayer sessions, public room diff --git a/export_presets.cfg b/export_presets.cfg index ff588e2..a49f01f 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -9,7 +9,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.20.1-alpha/windows-x86_64/straywild.exe" +export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.exe" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -35,11 +35,11 @@ application/modify_resources=true application/icon="res://art/exported/system_icons/straywild.ico" application/console_wrapper_icon="" application/icon_interpolation=0 -application/file_version="0.20.1.0" -application/product_version="0.20.1.0" +application/file_version="0.20.2.0" +application/product_version="0.20.2.0" application/company_name="Woofmeow" application/product_name="straywild" -application/file_description="straywild v0.20.1-alpha" +application/file_description="straywild v0.20.2-alpha" application/copyright="Copyright © 2026 Woofmeow" application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md" application/export_angle=0 @@ -62,7 +62,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.20.1-alpha/linux-arm64/straywild.arm64" +export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -92,7 +92,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.20.1-alpha/macos/straywild.zip" +export_path="builds/v0.20.2-alpha/macos/straywild.zip" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -108,8 +108,8 @@ custom_template/release="" application/bundle_identifier="io.woofmeow.netfishing" application/icon="res://art/exported/system_icons/straywild_1024.png" application/icon_interpolation=0 -application/short_version="0.20.1" -application/version="0.20.1" +application/short_version="0.20.2" +application/version="0.20.2" application/architecture="universal" codesign/enable=false notarization/enable=false @@ -125,7 +125,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*,tests/*" -export_path="builds/v0.20.1-alpha/server-linux-x86_64/straywildServer.x86_64" +export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -155,7 +155,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.20.1-alpha/android/straywild.apk" +export_path="builds/v0.20.2-alpha/android/straywild.apk" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -176,8 +176,8 @@ architectures/armeabi-v7a=false architectures/arm64-v8a=true architectures/x86=false architectures/x86_64=false -version/code=200100 -version/name="v0.20.1-alpha" +version/code=200200 +version/name="v0.20.2-alpha" package/unique_name="io.woofmeow.netfishing" package/name="straywild" package/signed=true @@ -214,7 +214,7 @@ custom_features="" export_filter="all_resources" include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" exclude_filter="builds/*,scripts/*" -export_path="builds/v0.20.1-alpha/linux-x86_64/straywild.x86_64" +export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/project.godot b/project.godot index 8e751aa..5bcbb4d 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="straywild" -config/version="0.20.1-alpha" +config/version="0.20.2-alpha" run/main_scene="res://main/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://art/exported/system_icons/straywild_256.png" @@ -260,7 +260,7 @@ alternate_fishing={ [network] -discovery/base_url="https://discovery.straywild.io" +discovery/base_url="https://discovery.straywild.io/patron-test" [physics] diff --git a/scripts/build_playtest.sh b/scripts/build_playtest.sh index f1ff151..d54ba70 100755 --- a/scripts/build_playtest.sh +++ b/scripts/build_playtest.sh @@ -4,14 +4,14 @@ set -euo pipefail readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)" -readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.1-alpha" +readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.2-alpha" readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64" readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64" readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt" readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)" readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild" -readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.1-alpha-windows-x86_64.zip" -readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.1-alpha-linux-x86_64.zip" +readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-windows-x86_64.zip" +readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-linux-x86_64.zip" readonly GODOT_BIN="${GODOT_BIN:-godot}" if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then @@ -77,8 +77,8 @@ straywild code is licensed under GPL-3.0-or-later. Source repository: ${SOURCE_URL} Source revision at build time: ${SOURCE_COMMIT} -This private patron test release is distributed with an exact corresponding -source archive named straywild-v0.20.1-alpha-source.tar.gz. +This private testing release is distributed with an exact corresponding +source archive named straywild-v0.20.2-alpha-source.tar.gz. If this package includes uncommitted development changes, it must not be publicly redistributed until the exact corresponding source is also made diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index 852fd71..22f00f2 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -213,7 +213,7 @@ unique_name_in_owner = true layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 22 -text = "v0.20.1-alpha" +text = "v0.20.2-alpha" horizontal_alignment = 1 [node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"] From eed361681a50ca94be043624f218980873ba7046 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 16:03:21 -0400 Subject: [PATCH 15/17] Import completed creature artwork --- art/exported/creatures/2502.png | Bin 0 -> 5970 bytes art/exported/creatures/2502.png.import | 42 ++++++++++++++++++ art/exported/creatures/2504.png | Bin 0 -> 1683 bytes art/exported/creatures/2504.png.import | 42 ++++++++++++++++++ art/exported/creatures/2505.png | Bin 0 -> 1473 bytes art/exported/creatures/2505.png.import | 42 ++++++++++++++++++ art/exported/creatures/2508.png | Bin 0 -> 1622 bytes art/exported/creatures/2508.png.import | 42 ++++++++++++++++++ art/exported/creatures/2601.png | Bin 0 -> 2345 bytes art/exported/creatures/2601.png.import | 42 ++++++++++++++++++ art/exported/creatures/2801.png | Bin 0 -> 1607 bytes art/exported/creatures/2801.png.import | 42 ++++++++++++++++++ art/exported/creatures/2802.png | Bin 0 -> 1768 bytes art/exported/creatures/2802.png.import | 42 ++++++++++++++++++ art/exported/creatures/2803.png | Bin 0 -> 1629 bytes art/exported/creatures/2803.png.import | 42 ++++++++++++++++++ art/exported/creatures/2804.png | Bin 0 -> 2307 bytes art/exported/creatures/2804.png.import | 42 ++++++++++++++++++ art/exported/creatures/2806.png | Bin 0 -> 1992 bytes art/exported/creatures/2806.png.import | 42 ++++++++++++++++++ art/exported/creatures/2807.png | Bin 0 -> 1549 bytes art/exported/creatures/2807.png.import | 42 ++++++++++++++++++ art/exported/creatures/3002.png | Bin 0 -> 3594 bytes art/exported/creatures/3002.png.import | 42 ++++++++++++++++++ art/exported/creatures/3102.png | Bin 0 -> 3066 bytes art/exported/creatures/3102.png.import | 42 ++++++++++++++++++ art/exported/creatures/3103.png | Bin 0 -> 3237 bytes art/exported/creatures/3103.png.import | 42 ++++++++++++++++++ art/exported/creatures/3104.png | Bin 0 -> 3156 bytes art/exported/creatures/3104.png.import | 42 ++++++++++++++++++ art/exported/creatures/3107.png | Bin 0 -> 3375 bytes art/exported/creatures/3107.png.import | 42 ++++++++++++++++++ art/exported/creatures/3201.png | Bin 0 -> 2361 bytes art/exported/creatures/3201.png.import | 42 ++++++++++++++++++ art/exported/creatures/3401.png | Bin 0 -> 2252 bytes art/exported/creatures/3401.png.import | 42 ++++++++++++++++++ art/exported/creatures/3902.png | Bin 0 -> 4205 bytes art/exported/creatures/3902.png.import | 42 ++++++++++++++++++ art/exported/creatures/4101.png | Bin 0 -> 1328 bytes art/exported/creatures/4101.png.import | 42 ++++++++++++++++++ art/exported/creatures/5001.png | Bin 0 -> 3151 bytes art/exported/creatures/5001.png.import | 42 ++++++++++++++++++ art/exported/creatures/5902.png | Bin 0 -> 2897 bytes art/exported/creatures/5902.png.import | 42 ++++++++++++++++++ art/exported/creatures/5903.png | Bin 0 -> 2317 bytes art/exported/creatures/5903.png.import | 42 ++++++++++++++++++ art/exported/creatures/5904.png | Bin 0 -> 2722 bytes art/exported/creatures/5904.png.import | 42 ++++++++++++++++++ art/exported/creatures/6001.png | Bin 0 -> 2757 bytes art/exported/creatures/6001.png.import | 42 ++++++++++++++++++ art/exported/creatures/6406.png | Bin 0 -> 4084 bytes art/exported/creatures/6406.png.import | 42 ++++++++++++++++++ art/exported/creatures/6601.png | Bin 0 -> 1129 bytes art/exported/creatures/6601.png.import | 42 ++++++++++++++++++ art/exported/creatures/6801.png | Bin 0 -> 2238 bytes art/exported/creatures/6801.png.import | 42 ++++++++++++++++++ art/exported/creatures/6807.png | Bin 0 -> 2061 bytes art/exported/creatures/6807.png.import | 42 ++++++++++++++++++ art/exported/creatures/6901.png | Bin 0 -> 7209 bytes art/exported/creatures/6901.png.import | 42 ++++++++++++++++++ art/exported/creatures/6902.png | Bin 0 -> 5800 bytes art/exported/creatures/6902.png.import | 42 ++++++++++++++++++ art/exported/creatures/6904.png | Bin 0 -> 2693 bytes art/exported/creatures/6904.png.import | 42 ++++++++++++++++++ fish/species/bluegill/bluegill.tres | 2 +- fish/species/bream_common/bream_common.tres | 2 +- fish/species/chub_creek/chub_creek.tres | 2 +- fish/species/chub_european/chub_european.tres | 2 +- fish/species/chub_lake/chub_lake.tres | 2 +- fish/species/clam_manila/clam_manila.tres | 2 +- fish/species/crab_blue/crab_blue.tres | 8 +++- fish/species/crappie_white/crappie_white.tres | 2 +- .../crayfish_red_swamp.tres | 2 +- .../croaker_atlantic/croaker_atlantic.tres | 2 +- .../croaker_yellowfin/croaker_yellowfin.tres | 2 +- .../damselfish_sergeant_major.tres | 2 +- fish/species/goby_round/goby_round.tres | 2 +- fish/species/goldfish/goldfish.tres | 2 +- fish/species/green_sunfish/green_sunfish.tres | 2 +- .../grunt_bluestriped/grunt_bluestriped.tres | 2 +- fish/species/grunt_french/grunt_french.tres | 2 +- .../halfbeak_ballyhoo/halfbeak_ballyhoo.tres | 2 +- .../minnow_fathead/minnow_fathead.tres | 2 +- fish/species/porgy_scup/porgy_scup.tres | 2 +- fish/species/pumpkinseed/pumpkinseed.tres | 2 +- fish/species/roach_common/roach_common.tres | 2 +- .../rockfish_black/rockfish_black.tres | 2 +- fish/species/rudd/rudd.tres | 2 +- .../sea_bass_black/sea_bass_black.tres | 2 +- fish/species/shiner_common/shiner_common.tres | 2 +- .../shiner_emerald/shiner_emerald.tres | 2 +- fish/species/shiner_golden/shiner_golden.tres | 2 +- fish/species/smelt_rainbow/smelt_rainbow.tres | 2 +- fish/species/snapper_lane/snapper_lane.tres | 2 +- fish/species/sunfish/sunfish.tres | 2 +- fish/species/yellow_perch/yellow_perch.tres | 2 +- 96 files changed, 1381 insertions(+), 33 deletions(-) create mode 100644 art/exported/creatures/2502.png create mode 100644 art/exported/creatures/2502.png.import create mode 100644 art/exported/creatures/2504.png create mode 100644 art/exported/creatures/2504.png.import create mode 100644 art/exported/creatures/2505.png create mode 100644 art/exported/creatures/2505.png.import create mode 100644 art/exported/creatures/2508.png create mode 100644 art/exported/creatures/2508.png.import create mode 100644 art/exported/creatures/2601.png create mode 100644 art/exported/creatures/2601.png.import create mode 100644 art/exported/creatures/2801.png create mode 100644 art/exported/creatures/2801.png.import create mode 100644 art/exported/creatures/2802.png create mode 100644 art/exported/creatures/2802.png.import create mode 100644 art/exported/creatures/2803.png create mode 100644 art/exported/creatures/2803.png.import create mode 100644 art/exported/creatures/2804.png create mode 100644 art/exported/creatures/2804.png.import create mode 100644 art/exported/creatures/2806.png create mode 100644 art/exported/creatures/2806.png.import create mode 100644 art/exported/creatures/2807.png create mode 100644 art/exported/creatures/2807.png.import create mode 100644 art/exported/creatures/3002.png create mode 100644 art/exported/creatures/3002.png.import create mode 100644 art/exported/creatures/3102.png create mode 100644 art/exported/creatures/3102.png.import create mode 100644 art/exported/creatures/3103.png create mode 100644 art/exported/creatures/3103.png.import create mode 100644 art/exported/creatures/3104.png create mode 100644 art/exported/creatures/3104.png.import create mode 100644 art/exported/creatures/3107.png create mode 100644 art/exported/creatures/3107.png.import create mode 100644 art/exported/creatures/3201.png create mode 100644 art/exported/creatures/3201.png.import create mode 100644 art/exported/creatures/3401.png create mode 100644 art/exported/creatures/3401.png.import create mode 100644 art/exported/creatures/3902.png create mode 100644 art/exported/creatures/3902.png.import create mode 100644 art/exported/creatures/4101.png create mode 100644 art/exported/creatures/4101.png.import create mode 100644 art/exported/creatures/5001.png create mode 100644 art/exported/creatures/5001.png.import create mode 100644 art/exported/creatures/5902.png create mode 100644 art/exported/creatures/5902.png.import create mode 100644 art/exported/creatures/5903.png create mode 100644 art/exported/creatures/5903.png.import create mode 100644 art/exported/creatures/5904.png create mode 100644 art/exported/creatures/5904.png.import create mode 100644 art/exported/creatures/6001.png create mode 100644 art/exported/creatures/6001.png.import create mode 100644 art/exported/creatures/6406.png create mode 100644 art/exported/creatures/6406.png.import create mode 100644 art/exported/creatures/6601.png create mode 100644 art/exported/creatures/6601.png.import create mode 100644 art/exported/creatures/6801.png create mode 100644 art/exported/creatures/6801.png.import create mode 100644 art/exported/creatures/6807.png create mode 100644 art/exported/creatures/6807.png.import create mode 100644 art/exported/creatures/6901.png create mode 100644 art/exported/creatures/6901.png.import create mode 100644 art/exported/creatures/6902.png create mode 100644 art/exported/creatures/6902.png.import create mode 100644 art/exported/creatures/6904.png create mode 100644 art/exported/creatures/6904.png.import diff --git a/art/exported/creatures/2502.png b/art/exported/creatures/2502.png new file mode 100644 index 0000000000000000000000000000000000000000..467431b449e65f3034dfc091fbcf0534f427dee8 GIT binary patch literal 5970 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 TT$uRB$^mLhnu;~@mf`;ccK=x5 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2502.png.import b/art/exported/creatures/2502.png.import new file mode 100644 index 0000000..17bcad9 --- /dev/null +++ b/art/exported/creatures/2502.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://busj0feq2dncu" +path.s3tc="res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.s3tc.ctex" +path.etc2="res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2502.png" +dest_files=["res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.s3tc.ctex", "res://.godot/imported/2502.png-3922e306611b91541a86520cf32cbdbd.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/2504.png b/art/exported/creatures/2504.png new file mode 100644 index 0000000000000000000000000000000000000000..d0279d826c0ff9a749d3d912a6eefcfb20b274bf GIT binary patch literal 1683 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 P00000NkvXXu0mjf0=tY! literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2601.png.import b/art/exported/creatures/2601.png.import new file mode 100644 index 0000000..7444643 --- /dev/null +++ b/art/exported/creatures/2601.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d14jvhurcfryb" +path.s3tc="res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.s3tc.ctex" +path.etc2="res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2601.png" +dest_files=["res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.s3tc.ctex", "res://.godot/imported/2601.png-912942ea4c6b2c66982e089ee46fde12.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/2801.png b/art/exported/creatures/2801.png new file mode 100644 index 0000000000000000000000000000000000000000..ad8294db2ae8f27040de1a27e0fbb41580e78a20 GIT binary patch literal 1607 zcmV-N2Dtf&P)+iT&ZCckc4laq!A@zRg+AEUg6&omD29g8*6pe!_zJihwctKNVpcaK{zI~e z#;kvV(HI^kpc_}T5>R%1Y*rT*LEEA&P+Nr7LQ9cShCZfqXU^u_c2*wKLV?s|J)h)e z&Ybt}dw=J5eisx9g+ifFC=?2XLZMJ76bgkxq4+6q17S_m`uQ1gA0T{FCX1@F5;3p7w8R@=I*}}Qz zpIY zs>({RS*;|w6ZxXHfx8#a$D&*0&>eurIHBF(*swINx*~hjj$a+^j#}S!zMtb==Lriz zd|WK^Z=cQnuf8$P_4p$mb2C^1?7qlW?M4L9q|vglsxoKHI~hrc2m`?o^(VSyaUDD3kO;{@%LQ7 z<R{#ThsWdvcDtR#L?>NcrvBiUmcid-_qc{^f1E2^yhM4SMF0UtQH`DUx9r7$JiTem~7E-_qIHfnk`2 zT)}^ioF=#6m!`pc4;~^rBOSLZ!ED>U4d{g+t!&NkX{3=a;Cs>`tIH#Bc4jpb8lK{V6wiriT z%$N)`j1ZcxVbS#w+b6l5oa&TE#K3N|;YoIPc6Oa@2Yh1@psu;TwqQ<{i%lzYxvi*} zyqrwt6&Dbfl*;uYMf~H6yRUn^)oSJlQHjmClfTwH#luU>$;wFM+heWNH8gQ=`CV8n zddl`4ABvHVyERH({{H3;_Ig4+rwB8&5t+f|=G|&8+Sm3a3+7~# z;q#C@eD+4eHhBh?grVRnF}mB(<6Y=>2O)na!LDW^*FaDKpZ&TFVFf2SK9%gPzvBlcJt4FA7tU3g?!g~QpSkQz++bv zKq4By_!P^8B%+a(o5#_{uUY%tGX#1%NYH!P_Gt?%7G(}kDa>nX^FNx*{C2CoWnr`Gyt3=?6&;fjVp&q?V`7}m6tFK~ zurl)$v`af06_JV6zS6*#X z`9Hu8)LbUmxP1;Q6bgkxp-?Ck3WY+UP$(1%g@T_E{{=jGAIqaMHNF4<002ovPDHLk FV1jS@AI$&& literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2801.png.import b/art/exported/creatures/2801.png.import new file mode 100644 index 0000000..512f812 --- /dev/null +++ b/art/exported/creatures/2801.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cc7el34sfad78" +path.s3tc="res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.s3tc.ctex" +path.etc2="res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2801.png" +dest_files=["res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.s3tc.ctex", "res://.godot/imported/2801.png-d1c5df9faa2d1eb1cc3453c8e18d4049.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/2802.png b/art/exported/creatures/2802.png new file mode 100644 index 0000000000000000000000000000000000000000..419061017872f8b48bafd6365007c1abfb9ebe35 GIT binary patch literal 1768 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 bg5dBIZ?g@-{Qj^A00000NkvXXu0mjfvK~AU literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2803.png.import b/art/exported/creatures/2803.png.import new file mode 100644 index 0000000..5096057 --- /dev/null +++ b/art/exported/creatures/2803.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjwm07240ehr2" +path.s3tc="res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.s3tc.ctex" +path.etc2="res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2803.png" +dest_files=["res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.s3tc.ctex", "res://.godot/imported/2803.png-c2e31a8adbab3bf9899bc74b3feb4abb.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/2804.png b/art/exported/creatures/2804.png new file mode 100644 index 0000000000000000000000000000000000000000..90952fdca6a853fc0796933cf9a3a4813a9a5222 GIT binary patch literal 2307 zcmV+e3H2fF>y(w5Jdr%B?u#+FzmC>yFKqgA{x*kVzuOb{sAuc{oe2U zyZ3(YeHRcA5D*X$5D*X$5D*X$5D<7cC;)7i0*UyR!3;@^BvZzu5VOHR-ulffGBVTB z4nTr|3?pKWMutc5f|Kkv5^c7Sy|2$CHnW-R&zr%w@6Ve>0L;YzBtOP%AsNZBBr~N` z{GKf{h|Ods`?h|E?0bDC(QCDQ5%z4I!4-jekjc>f94ivQ8~}d>umix)0Av6Vcl8l( zd&xAerK24w@nLW_2l3MUB&^v{fGsPAW6|1ttl3creS`=dt^mXgpWZ<{K&bnCUTxIN zYwH@^$~c=8k>NDxZV_1mxZEtR-wdGL#e!u!+chdJlyU`FmW510Y{2&>4aTe6Pf-0?fG24h|Hw&~_xV9W zfJ%oSbqxW~G>yn`C1g?!Bn;pW0(A`ye0#V?g{2FIz+zD0ndR??oTpOGEXz#N<}G}B z1d6XzAu}x=MOSWOSSRBEKvr$bF7O0Tc?$xrPaGBz+!cm zzw{)6d-2OUjcC)_1qRf)!jKqm=fxLZyb3}nSWN~j+4!#|U7plqfV+_?W~hQ;sWNaH zMIstQ609h?+Ju~R&hJ7)O-qp=yLbPafp$22Enq?553p$K@WWB>!ly+IXm0cI%ZHmO zL6$%vr7=E339p|;b32O(qwKdQo)(X5jvM&nflH{YY3)@f)$2bfT+d3DnWU1=E~7JV zy}+bs#Al^$42rh$N!>|Syd7XDK-@$48Xw^8bw=KXQ+y$$ewV9#kZO#Gkh{?Ak@Gp3 z)vefh$c@s9#=8zO)0_Ax9m9)%cD(k>bnrz>1Xz}!)}=$by#XqP6Z+^8JqG6LQeM@H z>e>z{6%0JyAnF?2C@gO5v1mOLn)xr7M8go~xv@0MZ3^eni%4%B;eSoD1z6zmP`I^si& zxt;fj{=jh~qR-Fx0|Zhrg_tmB_h#-8_Qt)QrM}ic!JU&XmsQvMp%hEd>ZVZJ(1G=H zvSBx9@Miue7@K{|5At?jLQDLD3VC8n57 zeDl%sRXBdJ8Vzl3e*3i;)hNH#v>U()0NfOu20+I649VkiI&Kp$qG<$K7Om|rjLizi zbFHIyM%|HhcuTJsh!SDPqh@+;Dm^_3>1^b*LEce{{9C ze3Q9x8%XG?)QrnMWyc8SBgM@ z1!t0L=0|oKpC7nZEytY4#jqI~F>gf){)0;PD1H@ssY7YY719sR9R16*$%bSiIu-RGqWA4Q$t*%jG)% zyFG3W!N`c# zm_?=Bn;tFh{Ynr4N~(X_Jd{bS-H)C`+uxeMN;DQ z(5fZacjyXN(U4CNT@lyxstYCK+IfQsG+7b$%Q2_&N|3 z&L9{h$V`yA3$J<}8AGG{JK%}4)hUDxA|gxB&|| z#Nw~+4Ue2~K&!s(F!oTP9%S$&yN;~+$!Pu@e!09FWmQeE8dXTN+pzGJBh)}N!Q0^O z;|fda*{l>Jm7HKhjDdzc%nH9hv^&9p1O;FMfOG(xCXb2z_EkVYKtMo1KtMo1KtMo1 dKmd;l{{>!SBn$KX*Y5xT002ovPDHLkV1f`2Xm$Vq literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2804.png.import b/art/exported/creatures/2804.png.import new file mode 100644 index 0000000..e6147c2 --- /dev/null +++ b/art/exported/creatures/2804.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dgcrfxo1k4juj" +path.s3tc="res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.s3tc.ctex" +path.etc2="res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2804.png" +dest_files=["res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.s3tc.ctex", "res://.godot/imported/2804.png-f86c52a01f04302b057cf526a498e87a.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/2806.png b/art/exported/creatures/2806.png new file mode 100644 index 0000000000000000000000000000000000000000..acb930e82266c70893c62d08beac6e04f939ce21 GIT binary patch literal 1992 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 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/2807.png.import b/art/exported/creatures/2807.png.import new file mode 100644 index 0000000..af51ff5 --- /dev/null +++ b/art/exported/creatures/2807.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bdus4qo0ebs3e" +path.s3tc="res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.s3tc.ctex" +path.etc2="res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/2807.png" +dest_files=["res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.s3tc.ctex", "res://.godot/imported/2807.png-aceaf0f586f975d29e318a9513fc3125.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/3002.png b/art/exported/creatures/3002.png new file mode 100644 index 0000000000000000000000000000000000000000..2ae7e7339d52b9fe8ccc1765cb2fba17b99c59d6 GIT binary patch literal 3594 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 QC;$Ke07*qoM6N<$f?Ew300000 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/3002.png.import b/art/exported/creatures/3002.png.import new file mode 100644 index 0000000..e193cfd --- /dev/null +++ b/art/exported/creatures/3002.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b68vlgtclvkfg" +path.s3tc="res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.s3tc.ctex" +path.etc2="res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3002.png" +dest_files=["res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.s3tc.ctex", "res://.godot/imported/3002.png-a87d71158798b43b6bf702a6e1664b4d.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/3102.png b/art/exported/creatures/3102.png new file mode 100644 index 0000000000000000000000000000000000000000..fe4d8fb4e88cf2fea72caac4f28d2cbd6a67567f GIT binary patch literal 3066 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 IM6N<$f}L)_q5uE@ literal 0 HcmV?d00001 diff --git a/art/exported/creatures/3102.png.import b/art/exported/creatures/3102.png.import new file mode 100644 index 0000000..47e9c80 --- /dev/null +++ b/art/exported/creatures/3102.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c4d3f28rf86bb" +path.s3tc="res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.s3tc.ctex" +path.etc2="res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3102.png" +dest_files=["res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.s3tc.ctex", "res://.godot/imported/3102.png-951ca90331737e3057749e1bdbd88c44.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/3103.png b/art/exported/creatures/3103.png new file mode 100644 index 0000000000000000000000000000000000000000..3d3d615899c08b9d5d5fd2c95ec2c437b2305e61 GIT binary patch literal 3237 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 Xz98;3y5|{k00000NkvXXu0mjfh66BL literal 0 HcmV?d00001 diff --git a/art/exported/creatures/3103.png.import b/art/exported/creatures/3103.png.import new file mode 100644 index 0000000..9743e89 --- /dev/null +++ b/art/exported/creatures/3103.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cjtncgnb0hcsj" +path.s3tc="res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.s3tc.ctex" +path.etc2="res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3103.png" +dest_files=["res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.s3tc.ctex", "res://.godot/imported/3103.png-f3b4b5bb43b7611f7c448dbdfff36375.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/3104.png b/art/exported/creatures/3104.png new file mode 100644 index 0000000000000000000000000000000000000000..425932e656cb59e81061b73bc709a3b7bade1871 GIT binary patch literal 3156 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 D{7Ein literal 0 HcmV?d00001 diff --git a/art/exported/creatures/3902.png.import b/art/exported/creatures/3902.png.import new file mode 100644 index 0000000..a8e5440 --- /dev/null +++ b/art/exported/creatures/3902.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ddc556gcby50v" +path.s3tc="res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.s3tc.ctex" +path.etc2="res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/3902.png" +dest_files=["res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.s3tc.ctex", "res://.godot/imported/3902.png-485bc4f9f1a505225bdc037111e32499.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/4101.png b/art/exported/creatures/4101.png new file mode 100644 index 0000000000000000000000000000000000000000..74a068baa7949253fd50bda29cc2d620a4255518 GIT binary patch literal 1328 zcmV-01<(44P)ab~&) zCYzaa(?GH~V_Y(q8E3L=naWg_A#T8!fRl%W4RzAeho$X3%e@txLa{@pe|SDgY0vFB z=brPs=lo6q7=~dOhG7_nVHk#C7=~dOhGFhKv&m@i;{jAKJvN4ZzjY}+wBvPpuWv5k zKH>1PKrTc&Ldd7?civPDP&{>ZOk0Ubw)+% zG07AKd9EB6=!WNcgoSCL3)evu^-ycnARq{~u_eDB9Ubj(IvnT^nylC9LQ{Jul_*^f zU>`=$bO67PBmf4EBi)t>W86L z|CJmVr_+g!jt)2+j)6eQu3T}UvGMXP^=s?u@mo_fyuQGxsnu%Xa=9V^I0zw9oY_R= zEho#FmkEo-h@#T+A?HhijOppL#P~zx#<$(QHeCC=7YVTwFlT-WrcI53(P)IlV!?{F z+p*_^&B{|f-{OYX7kHY=-~IBQl~Re}5T--A-L zx3{6!ZpT%dUCB8Gg;r=ZBk1?I^FrX9tenl0ct-HdlG~`+bmi(!O2mv3%8gRFY`OD?nyhoR`_WC^rLI zQJxUNk)z*WVP5XQB;%%~VSi_+d9K2pHRI1^Zxy$8( zAP7*a)q}n}{@oYw_IU=%e_g(YEn6#4yeuDO8#m+hsXA0tR9yP@hvO6f-Hs4KEfdU$ zjL_lbl7-5(qSA6XN`n#<8FqAm24AmPRh+l7q)?fRoS@{0zx)0kl)bS+sWdfVCKg$r zgmrELW-oXS=T4u)o9~pPqw^A?qsQZ7Yb!jSJ|#iQZJ&5N71XCf=B!zmk(`X5&YZ@= zCG#Os3WHvR!C-*RX2aHP<@oZeqnI)!76&VL;6m$lP!Oc1B_KX2V<&)hcNqUfLP&dB z;xr`?MWy8-C&5t7lXDV)i*jLRY62}TuwriRQYBFGgeO~4Z@-T!#yqaC>claeIaP=0 z@sp5}F=x1OkiEMXrBWpQVxNQhwqSA`h7A_2K(R+!Mnv48t%C m!!QiPFbu;m48t%CM#n$tjk!olS=gEY00001pYE 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 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/5001.png.import b/art/exported/creatures/5001.png.import new file mode 100644 index 0000000..c87c852 --- /dev/null +++ b/art/exported/creatures/5001.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://byil5mr6w3j1q" +path.s3tc="res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.s3tc.ctex" +path.etc2="res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5001.png" +dest_files=["res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.s3tc.ctex", "res://.godot/imported/5001.png-ebe37fb8bd4263bb83e36bb5890b5641.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/5902.png b/art/exported/creatures/5902.png new file mode 100644 index 0000000000000000000000000000000000000000..809057d320c15837b754b821c64965b0d8e380a5 GIT binary patch literal 2897 zcmV-X3$FBuP)lj;^rba{GvV`y!k7I+b&ma{B_8fh?eb z$z(_tLK>kc$_Kw^5C#Kceq% z{mP-#t{GxDJ#Vaq2Rekab>~qIo%E6LpkIh)8TJGhjVO23y*F)VQf29u1%Y~-Qp+erEGK@2F242>tO$Du4C zM8Gc`OVqQE$!q4k7{SJrG`e5rMCn=$zgBHRLPF6yuYJFwvDmtF)cK+G#&;qjSp`z$Ox6UVZpaTP0;H>xpwTkdUWTr+oK6SnS5Bf#H`!BD7K z2olPANBXq}P+crIP(D5I+0V6`@ej}D)4fkKEOH?;)+JyHEFjlx*Jj>kbYeDos8=DK z>+V1X)m^N;6SqH+7C{F{&#upeh*vQgv>4UM&Q<92f|0g8M-mWRjdSaxSu&*(6+0Yo z3L)%e4Ql4-yTz#fA#B{1hQ$o`w{NJq|G3xPoV)BRQ>W=yiWehBEn)VP^MUx{!i=oD znYMNsA@wdYyuo|LE&fBR${EyIUw*mpFq#B2uoOJdW4`s_b+mPg0^#D@8mSfADi>$PNt&UBHN zCu-fQZ|N6^j}H&EP2Kfz1c~dW;AcEZQ~ycYgW+_Geqr5Y<4SupibN}8kPSx_|e~$q|`adx%qgwR{ydF0u z*+RR3J{->*O=dwyt%7>L-L{%8)i%>14|Q`vTvCV>a3=68WQ<<;fCY}6#$v*ud3xPn7h zLV2bO^ca)QYZGen$+aYW!oZ2;J=nCzds`2e$|`@P`vhOZ@XlPla*A*FUEos2Vw$uX z$Kl=c85X?=BQ9D8JYD=kifxT1ZvK(+jBPwm3xoE~LC83?=kKiXFubRX@x~(Rl0w*H zU7)#d5gpq!z+}4h$~^Pi5wvoRDeiOA;+b?E9D`kCkn-U_+461!&HWQUF5tj1<+cVe zl~!mpnl$k$cLmqR>pg}$&%x!>=b6wii^c(cN(c||9ig+c7h8A2HCqtdcK$$TdXbqW zcz>;j@NSc-`B*qc!!x*~O6<12uhlOSuVCXV+cLI)FSOjDq8jr`er4V~S}0aR;U3o{ z+}0qi&(^Nzv^M-A6!P#gAb1M%j^(rC>vXOD11*iJ0d}cTdMg7+rId*mCay1O5OO%a zXCQs=mpPEXQY&j)u`d_p9#r?P<%~RlW1jBRmA6pGvsjDS0r5`853GWt(L#(Sv-VE7 zXFip^J4&>zicy6WamqA^^KvgbIG@5qKHF`{2uxJzKA4Y{_*Aml$!MR=e&hW z2`S#*w#n|rGl@ax)IrH1Wh|eVaf0o@9m42Kes2zi83DS zytO=wb#FK0^hGzJ)huK=6q3?raOTKfhK9Y$1-Un;$yl)eYTlpiGi5V+^&RR6~_9i9Ez zW&4<&?>&hpxCiZ{Mo>xF!{}BUFiHv^toVT!-gMnErdu~QiHT3i+W)jmLHu(-Wm0yf z(Q8tEsV72-Uxshkh>s+H|DT6nA*+((=`pr&O;<5cfWUx+7n?7WR_-x3iB;38x;wFo zYqW6<*UArCrjT6_#Cr+Di63I7UiJSv=YmxQI9xKD-)%&N_WSPxzPZhK#pko_bZ*xW zcY#maIBnj3R85NN4$}2o3{ZrhB;n}N^?4dcJXD+%i`Z^ki4X=gM)>kyvpDhHcCCx} zxk)s27apF}jK16YE?(^TB&)V!ES()q?oa2jSk1W&Yv4C$Y#`xvLy+adB%70`XkU>k z_!;Rv@uDYnUuNO5atkDh)U2H?ikH9}TVw4JcZhbfZ1kATcJauNc=6y9ycQ6Uj4dLXr>`i>yw%b{UO^(}4QPtJQ7x>J?iz^;7ItNysKiSj`6UO9iB) zbkZ`G+IMzp3vE1h%gx(ji917Nc4~_qnb$oWJbXg!6rT^;-`4<})xh4vxg0*>AGR{gfklzFFD`tEI5{m_1X}AN51V|0T`; v>H+BJ=;-L^=;-L^=;-L^=;-L^P|SY-Dr###yCjbl00000NkvXXu0mjfo@2x| literal 0 HcmV?d00001 diff --git a/art/exported/creatures/5902.png.import b/art/exported/creatures/5902.png.import new file mode 100644 index 0000000..2c8a5fb --- /dev/null +++ b/art/exported/creatures/5902.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://0k4qnt7wusam" +path.s3tc="res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.s3tc.ctex" +path.etc2="res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5902.png" +dest_files=["res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.s3tc.ctex", "res://.godot/imported/5902.png-e3a13a1e827488fa86f34c2aad883127.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/5903.png b/art/exported/creatures/5903.png new file mode 100644 index 0000000000000000000000000000000000000000..74a8af64d12aa97e4540b8aad40520b0923d5c50 GIT binary patch literal 2317 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 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/5904.png.import b/art/exported/creatures/5904.png.import new file mode 100644 index 0000000..23458ae --- /dev/null +++ b/art/exported/creatures/5904.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dtapwjgdbcgr7" +path.s3tc="res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.s3tc.ctex" +path.etc2="res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/5904.png" +dest_files=["res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.s3tc.ctex", "res://.godot/imported/5904.png-e43ba3245eadac7081a6a23e64ccc940.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/6001.png b/art/exported/creatures/6001.png new file mode 100644 index 0000000000000000000000000000000000000000..921959712933c9f994c4b6b89f4cc35e18778993 GIT binary patch literal 2757 zcmV;$3OeKQiJ3WB%&0NOj2X8Rjk9DC z872x50t7{kilc-C6b%wUje-UgWeL0RmhPIL}d9SK#x%bwsTelkE z;o;%o;o;%o;o;%o;o;%o`Tv6YjpnHm31O)(qXa@B@Jky3LjdZ|?CcLiKmgDHngMjP zRRFF6r~|MBK*xWT0rYh#x?4pA3}1DXhBDdmJtqChtrj8NmMP&k%^wn}4ytGx-abM1HUS|3Lu()uiE;DRb?C|Up`$YiMW;_; z;dVQmDJ_Cbs*72!2XUm(1E22Ojlk)FcyYmEI8JoHB;P5BoazIyV0ey80cstwSma}E z#g!RWfcp?VqT(hLMb(FgJj)e!EjkDwANiG5__D~MpEzvLW0hwaJ<5#xb+xL)-(O^7 zUMT<_EZ?R?z=~3gxA6c)QSh4VhVjAtV*LI4cl1$`sB>DNE zrK<}xLpXOQk82BvMPd>h7{w{QG9?@|^WdQ*A{t{ywxYdT3WqTb2z0E*2)!oEDmCW2Irl2~ z@xKc2riU|@966B*Am))hAR;t|u!^rp2390TKwqZBrnqCn>f6w!i?x|gYZ)xoqY?oQ z)ptCoj}^($P}CfUtG_!+@=s%Gm=E6Y^TwLIZ~E;4ilVqZW~NZU)6$~H8e$Qg13AwmueVZC`p2Y^bNz z-#;x|-PGMJ=_enChciV{XM@}gaO_Mw-dXvk*2{R0k02D#ShPunfu0F_#$HNZlT9S{jLy=4_Gfkdd?W;k>z^^t!$0`=`< zxbja84%`zG5{CBnc4+8DIQDHmHrn+ZuXmZ5m4}9|t_LnOHOQP|7sqj`(dJG#csK*4 z@o{*kpa@@Ixr#MOZ)-JV4M8&^2n8B0F3flP7y`pEFk&kZfdPnkENF=XKn=V-v;?AC zAZ|;As3QXub2q?p8TNqfmB#C#k%wVlW*W>}Z^5Lk5#x3G=5b0%A29e0Kr@_Dx*ZG4d#Og6;K&2 zh1zflz;tj11l5x~4Ms}ZKQLyLDd^fNnCQyz!8ET2W`FlW4OAK67eUUHoPZD#1bF(v z-oc(T!V6y8d!zWhC%|M!OStL3iaG0*=i&u`Mn>vTuMp9i1z+Qr+a|~^ zdmg3+H3)auuch=|?Cl*eRj~w}mc_X7tvhVZhY?pPF}h6mff4s4EQ8@5=n17t35KD; z9_s>%%@w$tC!(hb@%n5rmf*63Sq&Gp7ngHyEINzg&-y-i zur#$1uB=i6bxj1D)75x=juTlh`;#Xe4 z_j7yhCHFFU4;R26>KLs3>z2QmZP>B_wKuNgU2hM}vbBaz;#z3b1E1MjcWuL}6@4C3 zaj_B(cJ_UfPz3D95t^pC?ZiGTi(XDZPI}foBlQG4wyl|+1>Rk3@Wa~A+IkZE2Oty( zh>)W2L1Y+L{(bfVlq{GH(VaO|Gy}KPFR!rn9MS+rhDOSQgD3liYc|tbzc>~nH$#eRUtULOCWmOyc@GdQY;x^PzMw)QP@RMFrJYvB1rxH!G}a?|gPvX6{#b zf{1e7mmaCuEhD-*AY}!mr1a!D^5eJX$;y`!No-UM(bv;w0~zbrTDKiL3+jZif+7ZT zid;#Rk*j-U#L-H?{n}XwNO@ry_jo%BHVjjiT-Feqqaw(bnKQ`NnKQ`w_&7GWXs3x! z2#!#IF6uP5*g`D#2b7+&4INJ({R3aTbqZyd< ztm7^7_T&r!K#JdrH4DY0GFe7GTq@xdIeqjbE5LidayZ3f5UJn(f8vNmLQ=Lrg1j?d zOsc++u?86WGVni2_{9{?06aWAJUl!+JUl!+JUl!+JUl!+_>b~$2qFk=H*``D00000 LNkvXXu0mjfKlUK` literal 0 HcmV?d00001 diff --git a/art/exported/creatures/6001.png.import b/art/exported/creatures/6001.png.import new file mode 100644 index 0000000..a63cf85 --- /dev/null +++ b/art/exported/creatures/6001.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c0164i40101lq" +path.s3tc="res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.s3tc.ctex" +path.etc2="res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6001.png" +dest_files=["res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.s3tc.ctex", "res://.godot/imported/6001.png-6fd8c3ef0885c466baece687cd24d38d.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/art/exported/creatures/6406.png b/art/exported/creatures/6406.png new file mode 100644 index 0000000000000000000000000000000000000000..15c3b8311737e8b6ae585c0df9fc5e6babedb33d GIT binary patch literal 4084 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 vDE5vhiqSg?2qAbFdG$f`@5;a4O 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 literal 0 HcmV?d00001 diff --git a/art/exported/creatures/6904.png.import b/art/exported/creatures/6904.png.import new file mode 100644 index 0000000..75051e3 --- /dev/null +++ b/art/exported/creatures/6904.png.import @@ -0,0 +1,42 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bqh7d4v8jrbpr" +path.s3tc="res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.s3tc.ctex" +path.etc2="res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.etc2.ctex" +metadata={ +"imported_formats": ["s3tc_bptc", "etc2_astc"], +"vram_texture": true +} + +[deps] + +source_file="res://art/exported/creatures/6904.png" +dest_files=["res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.s3tc.ctex", "res://.godot/imported/6904.png-38739e4e48fbb83e3f65074a93d681bb.etc2.ctex"] + +[params] + +compress/mode=2 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=0 diff --git a/fish/species/bluegill/bluegill.tres b/fish/species/bluegill/bluegill.tres index 1ad5656..181b969 100644 --- a/fish/species/bluegill/bluegill.tres +++ b/fish/species/bluegill/bluegill.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3102.png" id="4_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/bream_common/bream_common.tres b/fish/species/bream_common/bream_common.tres index 1c7398c..cc44fef 100644 --- a/fish/species/bream_common/bream_common.tres +++ b/fish/species/bream_common/bream_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2502.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/chub_creek/chub_creek.tres b/fish/species/chub_creek/chub_creek.tres index 1739712..778e9da 100644 --- a/fish/species/chub_creek/chub_creek.tres +++ b/fish/species/chub_creek/chub_creek.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2802.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/chub_european/chub_european.tres b/fish/species/chub_european/chub_european.tres index a218aec..16f43a1 100644 --- a/fish/species/chub_european/chub_european.tres +++ b/fish/species/chub_european/chub_european.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2505.png" id="4_texture"] [sub_resource type="Resource" id="ChubEuropeanAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/chub_lake/chub_lake.tres b/fish/species/chub_lake/chub_lake.tres index 6da6b43..1dc2d9a 100644 --- a/fish/species/chub_lake/chub_lake.tres +++ b/fish/species/chub_lake/chub_lake.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2807.png" id="4_texture"] [sub_resource type="Resource" id="ChubLakeAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/clam_manila/clam_manila.tres b/fish/species/clam_manila/clam_manila.tres index 20e3f8a..2b3fe73 100644 --- a/fish/species/clam_manila/clam_manila.tres +++ b/fish/species/clam_manila/clam_manila.tres @@ -2,7 +2,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] -[ext_resource type="Texture2D" path="res://fish/species/clam_manila/clam_manila.png" id="3_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6406.png" id="3_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/crab_blue/crab_blue.tres b/fish/species/crab_blue/crab_blue.tres index 2ee3279..b715522 100644 --- a/fish/species/crab_blue/crab_blue.tres +++ b/fish/species/crab_blue/crab_blue.tres @@ -1,9 +1,11 @@ -[gd_resource type="Resource" script_class="FishData" load_steps=4 format=3] +[gd_resource type="Resource" script_class="FishData" load_steps=5 format=3] [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3902.png" id="99_creature_texture"] + [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") allow_night = false @@ -12,7 +14,7 @@ allow_night = false script = ExtResource("1_fish_data") id = &"crab_blue" display_name = "blue crab" -active = false +active = true catalog_number = 3902 collection_group = &"Marine crabs" logbook_fact = "blue crabs swim with flattened rear legs shaped like paddles. bright blue claws and an olive shell conceal a quick, defensive temperament." @@ -30,3 +32,5 @@ weight_max_lb = 1.1 sell_value_min = 3 sell_value_max = 8 sell_value_curve = 0.95 + +display_texture = ExtResource("99_creature_texture") diff --git a/fish/species/crappie_white/crappie_white.tres b/fish/species/crappie_white/crappie_white.tres index 7806c37..6766f1e 100644 --- a/fish/species/crappie_white/crappie_white.tres +++ b/fish/species/crappie_white/crappie_white.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3107.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres b/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres index b53448d..6c055d2 100644 --- a/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres +++ b/fish/species/crayfish_red_swamp/crayfish_red_swamp.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2601.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/croaker_atlantic/croaker_atlantic.tres b/fish/species/croaker_atlantic/croaker_atlantic.tres index c35e7de..d0fb147 100644 --- a/fish/species/croaker_atlantic/croaker_atlantic.tres +++ b/fish/species/croaker_atlantic/croaker_atlantic.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6801.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/croaker_yellowfin/croaker_yellowfin.tres b/fish/species/croaker_yellowfin/croaker_yellowfin.tres index 84885f9..1e282da 100644 --- a/fish/species/croaker_yellowfin/croaker_yellowfin.tres +++ b/fish/species/croaker_yellowfin/croaker_yellowfin.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6807.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres b/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres index 483565c..b758404 100644 --- a/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres +++ b/fish/species/damselfish_sergeant_major/damselfish_sergeant_major.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5904.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/goby_round/goby_round.tres b/fish/species/goby_round/goby_round.tres index d8edf5e..3a8ffee 100644 --- a/fish/species/goby_round/goby_round.tres +++ b/fish/species/goby_round/goby_round.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3401.png" id="4_texture"] [sub_resource type="Resource" id="RoundGobyAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/goldfish/goldfish.tres b/fish/species/goldfish/goldfish.tres index 444741b..58c7934 100644 --- a/fish/species/goldfish/goldfish.tres +++ b/fish/species/goldfish/goldfish.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3002.png" id="4_texture"] [sub_resource type="Resource" id="GoldfishAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/green_sunfish/green_sunfish.tres b/fish/species/green_sunfish/green_sunfish.tres index fdd1ddd..1733035 100644 --- a/fish/species/green_sunfish/green_sunfish.tres +++ b/fish/species/green_sunfish/green_sunfish.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3103.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/grunt_bluestriped/grunt_bluestriped.tres b/fish/species/grunt_bluestriped/grunt_bluestriped.tres index 87e9215..5f247a8 100644 --- a/fish/species/grunt_bluestriped/grunt_bluestriped.tres +++ b/fish/species/grunt_bluestriped/grunt_bluestriped.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5902.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/grunt_french/grunt_french.tres b/fish/species/grunt_french/grunt_french.tres index da0bd90..8e8161e 100644 --- a/fish/species/grunt_french/grunt_french.tres +++ b/fish/species/grunt_french/grunt_french.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5903.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres b/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres index 9512381..6d7fe32 100644 --- a/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres +++ b/fish/species/halfbeak_ballyhoo/halfbeak_ballyhoo.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6601.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/minnow_fathead/minnow_fathead.tres b/fish/species/minnow_fathead/minnow_fathead.tres index cf00159..c4a3aff 100644 --- a/fish/species/minnow_fathead/minnow_fathead.tres +++ b/fish/species/minnow_fathead/minnow_fathead.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2804.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/porgy_scup/porgy_scup.tres b/fish/species/porgy_scup/porgy_scup.tres index 59dd833..ad96831 100644 --- a/fish/species/porgy_scup/porgy_scup.tres +++ b/fish/species/porgy_scup/porgy_scup.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6904.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/pumpkinseed/pumpkinseed.tres b/fish/species/pumpkinseed/pumpkinseed.tres index 7928e4f..7a4f090 100644 --- a/fish/species/pumpkinseed/pumpkinseed.tres +++ b/fish/species/pumpkinseed/pumpkinseed.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3104.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/roach_common/roach_common.tres b/fish/species/roach_common/roach_common.tres index 8814f1c..c12d380 100644 --- a/fish/species/roach_common/roach_common.tres +++ b/fish/species/roach_common/roach_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2504.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/rockfish_black/rockfish_black.tres b/fish/species/rockfish_black/rockfish_black.tres index f26a41c..c37b427 100644 --- a/fish/species/rockfish_black/rockfish_black.tres +++ b/fish/species/rockfish_black/rockfish_black.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6901.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/rudd/rudd.tres b/fish/species/rudd/rudd.tres index ae031ac..5de8d57 100644 --- a/fish/species/rudd/rudd.tres +++ b/fish/species/rudd/rudd.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2508.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/sea_bass_black/sea_bass_black.tres b/fish/species/sea_bass_black/sea_bass_black.tres index 1c0eabd..7a5b567 100644 --- a/fish/species/sea_bass_black/sea_bass_black.tres +++ b/fish/species/sea_bass_black/sea_bass_black.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6902.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_common/shiner_common.tres b/fish/species/shiner_common/shiner_common.tres index 562daf6..5a8a2f3 100644 --- a/fish/species/shiner_common/shiner_common.tres +++ b/fish/species/shiner_common/shiner_common.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2801.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_emerald/shiner_emerald.tres b/fish/species/shiner_emerald/shiner_emerald.tres index eeb3653..8a39e5c 100644 --- a/fish/species/shiner_emerald/shiner_emerald.tres +++ b/fish/species/shiner_emerald/shiner_emerald.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2803.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/shiner_golden/shiner_golden.tres b/fish/species/shiner_golden/shiner_golden.tres index 5ab924f..af57441 100644 --- a/fish/species/shiner_golden/shiner_golden.tres +++ b/fish/species/shiner_golden/shiner_golden.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/2806.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/smelt_rainbow/smelt_rainbow.tres b/fish/species/smelt_rainbow/smelt_rainbow.tres index 6a0d08a..34691f9 100644 --- a/fish/species/smelt_rainbow/smelt_rainbow.tres +++ b/fish/species/smelt_rainbow/smelt_rainbow.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/4101.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") diff --git a/fish/species/snapper_lane/snapper_lane.tres b/fish/species/snapper_lane/snapper_lane.tres index 0288e2d..d91444f 100644 --- a/fish/species/snapper_lane/snapper_lane.tres +++ b/fish/species/snapper_lane/snapper_lane.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" uid="uid://h7hg45b7wmst" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" uid="uid://bs7cqi88csolc" path="res://fish/fish_availability.gd" id="3_availability_script"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/6001.png" id="4_texture"] [sub_resource type="Resource" id="SnapperLaneAvailability"] script = ExtResource("3_availability_script") diff --git a/fish/species/sunfish/sunfish.tres b/fish/species/sunfish/sunfish.tres index a8e2290..5efd181 100644 --- a/fish/species/sunfish/sunfish.tres +++ b/fish/species/sunfish/sunfish.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Resource" path="res://fish/day_availability.tres" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="4_texture"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/5001.png" id="4_texture"] [resource] script = ExtResource("1_fish_data") diff --git a/fish/species/yellow_perch/yellow_perch.tres b/fish/species/yellow_perch/yellow_perch.tres index 5098a1e..3e0501f 100644 --- a/fish/species/yellow_perch/yellow_perch.tres +++ b/fish/species/yellow_perch/yellow_perch.tres @@ -3,7 +3,7 @@ [ext_resource type="Script" path="res://fish/fish_data.gd" id="1_fish_data"] [ext_resource type="Resource" path="res://fishing/common_catch_profile.tres" id="2_profile"] [ext_resource type="Script" path="res://fish/fish_availability.gd" id="3_availability"] -[ext_resource type="Texture2D" path="res://art/exported/creatures/fish_placeholder.png" id="99_fish_placeholder"] +[ext_resource type="Texture2D" path="res://art/exported/creatures/3201.png" id="99_fish_placeholder"] [sub_resource type="Resource" id="Availability"] script = ExtResource("3_availability") From d8bf59bca068842e1046ee3960f8662ab72bc4a8 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 16:03:55 -0400 Subject: [PATCH 16/17] Add starter RV progression and fishing feedback --- fishing/fishing_spot.gd | 4 + homes/assets/starter_motorcoach.glb | Bin 189284 -> 156012 bytes homes/assets/starter_rv_interior.glb | Bin 0 -> 15564 bytes homes/assets/starter_rv_interior.glb.import | 45 ++ homes/assets/tent_interior.glb | Bin 0 -> 9880 bytes homes/assets/tent_interior.glb.import | 45 ++ homes/home_decor_controller.gd | 15 +- homes/home_world_service.gd | 503 ++++++++++++++---- homes/player_home_state.gd | 81 ++- main/main.gd | 411 ++++++++++++-- network/network_home_service.gd | 80 ++- network/network_protocol.gd | 3 + network/network_session.gd | 3 + network/network_shop_protocol.gd | 3 +- network/network_shop_service.gd | 145 +++-- player/player.gd | 83 ++- progression/player_cooler_capacity.gd | 75 ++- save/player_save_manager.gd | 28 +- tests/economy_regression_validation.gd | 58 +- tests/home_spawn_multiplayer_validation.gd | 23 + tests/logbook_runtime_validation.gd | 8 +- ui/fishing_danger_vignette.gdshader | 10 + ui/fishing_danger_vignette.gdshader.uid | 1 + ui/fishing_shop.gd | 76 --- ui/fishing_shop.tscn | 29 - ui/game_ui.gd | 153 +++++- ui/game_ui.tscn | 27 +- ui/home_decor_toolbar.gd | 142 +++-- ui/player_menu.gd | 5 +- ui/player_storage.gd | 6 +- ui/rv_upgrade_shop.gd | 330 ++++++++++++ ui/rv_upgrade_shop.gd.uid | 1 + ui/rv_upgrade_shop.tscn | 14 + world/decor_shop_interaction.gd | 8 + world/fishing_shop_interaction.gd | 8 + world/generation/generated_world_region.gd | 156 +++++- world/generation/generated_world_region.tscn | 6 +- world/generation/terrain_chunk_generator.gd | 13 + world/interactables/decor_shop_world.tscn | 14 +- world/interactables/fishing_shop_world.tscn | 2 +- .../interactables/rv_upgrade_shop_world.tscn | 36 ++ world/regions/starter_island_region.gd | 20 +- world/regions/starter_island_region.tscn | 5 + world/regions/world_region.gd | 18 + world/rv_upgrade_interaction.gd | 15 + world/rv_upgrade_interaction.gd.uid | 1 + world/test_world.gd | 26 + 47 files changed, 2268 insertions(+), 467 deletions(-) create mode 100644 homes/assets/starter_rv_interior.glb create mode 100644 homes/assets/starter_rv_interior.glb.import create mode 100644 homes/assets/tent_interior.glb create mode 100644 homes/assets/tent_interior.glb.import create mode 100644 ui/fishing_danger_vignette.gdshader create mode 100644 ui/fishing_danger_vignette.gdshader.uid create mode 100644 ui/rv_upgrade_shop.gd create mode 100644 ui/rv_upgrade_shop.gd.uid create mode 100644 ui/rv_upgrade_shop.tscn create mode 100644 world/interactables/rv_upgrade_shop_world.tscn create mode 100644 world/rv_upgrade_interaction.gd create mode 100644 world/rv_upgrade_interaction.gd.uid diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 8160eec..fdfad53 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -684,6 +684,10 @@ func _is_reel_input_pressed() -> bool: ) +func is_reel_input_active() -> bool: + return state == FishingState.FIGHTING and _is_reel_input_pressed() + + func _begin_aiming(player: PlayerType) -> void: if state != FishingState.READY or _active_player != null: return diff --git a/homes/assets/starter_motorcoach.glb b/homes/assets/starter_motorcoach.glb index 5b0d0225da3af0dc3978f6b0bb19562b571ae8e3..becd54edcc889a1754cfb1e1d6fdffb43e5bf8b0 100644 GIT binary patch literal 156012 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 literal 189284 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 Y$FNH{19#B{>47_%9L|Qb!+zoa0dB5ZD*ylh diff --git a/homes/assets/starter_rv_interior.glb b/homes/assets/starter_rv_interior.glb new file mode 100644 index 0000000000000000000000000000000000000000..21608ea0c68307d10e8e8d75d59d17e658abe789 GIT binary patch literal 15564 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 literal 0 HcmV?d00001 diff --git a/homes/assets/tent_interior.glb.import b/homes/assets/tent_interior.glb.import new file mode 100644 index 0000000..fc01ba7 --- /dev/null +++ b/homes/assets/tent_interior.glb.import @@ -0,0 +1,45 @@ +[remap] + +importer="scene" +importer_version=1 +type="PackedScene" +uid="uid://dkfqs051shmxe" +path="res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.scn" + +[deps] + +source_file="res://homes/assets/tent_interior.glb" +dest_files=["res://.godot/imported/tent_interior.glb-576cc47c2c633f7ae635651598663518.scn"] + +[params] + +nodes/root_type="" +nodes/root_name="" +nodes/root_script=null +mesh_library/use_node_names_as_mesh_names=false +array_mesh/deduplicate_surfaces=true +nodes/apply_root_scale=true +nodes/root_scale=1.0 +nodes/import_as_skeleton_bones=false +nodes/use_name_suffixes=true +nodes/use_node_type_suffixes=true +meshes/ensure_tangents=true +meshes/generate_lods=true +meshes/create_shadow_meshes=true +meshes/light_baking=1 +meshes/lightmap_texel_size=0.2 +meshes/force_disable_compression=false +skins/use_named_skins=true +animation/import=true +animation/fps=30 +animation/trimming=false +animation/remove_immutable_tracks=true +animation/import_rest_as_RESET=false +import_script/path="" +materials/extract=0 +materials/extract_format=0 +materials/extract_path="" +_subresources={} +gltf/naming_version=2 +gltf/embedded_image_handling=1 +gltf/texture_map_mode=1 diff --git a/homes/home_decor_controller.gd b/homes/home_decor_controller.gd index 8f9aa5f..f67207f 100644 --- a/homes/home_decor_controller.gd +++ b/homes/home_decor_controller.gd @@ -130,6 +130,8 @@ func _unhandled_input(event: InputEvent) -> void: func _process(delta: float) -> void: + if _toolbar != null and _toolbar.visible: + _toolbar.set_interactive(_can_decorate()) if _drag_instance_id.is_empty(): return # Menus and other gameplay input handoffs must cancel an active drag before @@ -472,9 +474,20 @@ func _on_home_changed() -> void: func _refresh_toolbar() -> void: if _toolbar == null: return - _toolbar.set_active(_can_decorate()) + var inside_home: bool = _is_inside_home_space() + _toolbar.set_active(inside_home) + _toolbar.set_interactive(_can_decorate()) if not _toolbar.visible: _clear_selection() + elif not _can_decorate(): + _toolbar.report_status("decor tools pause while another menu is open") + + +func _is_inside_home_space() -> bool: + return ( + _network_home != null + and String(_network_home.get_local_space()).begins_with("rv:") + ) func _can_decorate() -> bool: diff --git a/homes/home_world_service.gd b/homes/home_world_service.gd index fd02d93..4ec0645 100644 --- a/homes/home_world_service.gd +++ b/homes/home_world_service.gd @@ -9,23 +9,42 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd") const STARTER_MOTORCOACH_SCENE: PackedScene = preload( "res://homes/assets/starter_motorcoach.glb" ) +const TENT_INTERIOR_SCENE: PackedScene = preload( + "res://homes/assets/tent_interior.glb" +) +const STARTER_RV_INTERIOR_SCENE: PackedScene = preload( + "res://homes/assets/starter_rv_interior.glb" +) +const HOME_PROMPT_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf") -const EXTERIOR_BODY_SIZE := Vector3(5.6, 2.5, 2.35) -const EXTERIOR_DOOR_LOCAL := Vector3(-0.2, 0.85, 1.48) -const EXTERIOR_SPAWN_LOCAL := Vector3(-0.2, 0.25, 2.45) +const RV_EXTERIOR_BODY_SIZE := Vector3(11.2, 5.0, 4.7) +const RV_EXTERIOR_BODY_CENTER_Y: float = 2.7 +const RV_EXTERIOR_DOOR_LOCAL := Vector3(-0.4, 1.7, 2.96) +const RV_EXTERIOR_SPAWN_LOCAL := Vector3(-0.4, 0.25, 3.95) +const TENT_EXTERIOR_BODY_SIZE := Vector3(4.6, 3.0, 4.6) +const TENT_EXTERIOR_BODY_CENTER_Y: float = 1.5 +const TENT_EXTERIOR_DOOR_LOCAL := Vector3(0.0, 1.0, 2.3) +const TENT_EXTERIOR_SPAWN_LOCAL := Vector3(0.0, 0.25, 3.3) +const RV_EXTERIOR_PROMPT_LOCAL := Vector3(-0.4, 3.72, 3.02) +const TENT_EXTERIOR_PROMPT_LOCAL := Vector3(0.0, 2.48, 2.36) +const HOME_PROMPT_SIZE := Vector2(0.95, 0.28) +const HOME_PROMPT_CORNER_RADIUS: float = 0.08 +const HOME_PROMPT_TEXT_PIXEL_SIZE: float = 0.0045 +const HOME_PROMPT_TEXT_COLOR := Color("c3dfe6") +const HOME_PROMPT_BACKGROUND_COLOR := Color("0a2d3a") const EXTERIOR_INTERACTION_DISTANCE: float = 2.7 const INTERIOR_EXIT_DISTANCE: float = 2.1 const INTERIOR_ORIGIN := Vector3(1000.0, 300.0, 1000.0) const INTERIOR_SPACING: float = 14.0 const INTERIOR_MAX_SPAWN_SLOTS: int = 64 -const INTERIOR_FLOOR_SIZE := Vector3(8.0, 0.18, 4.5) -const INTERIOR_WALL_HEIGHT: float = 2.7 +const TENT_INTERIOR_SIZE := Vector2(4.0, 4.0) +const STARTER_RV_INTERIOR_SIZE := Vector2(4.0, 6.0) const MIN_EXTERIOR_TRIANGLE_AREA: float = 1.0 -const MIN_EXTERIOR_SEPARATION: float = 9.0 -const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(3.85, 2.55) +const MIN_EXTERIOR_SEPARATION: float = 14.6 +const EXTERIOR_FOOTPRINT_HALF_EXTENTS := Vector2(6.65, 3.73) const EXTERIOR_SURFACE_INDEX_CELL_SIZE: float = 5.0 const EXTERIOR_MAX_SURFACE_HEIGHT_DELTA: float = 0.24 -const EXTERIOR_CLEARANCE_HEIGHT: float = 2.7 +const EXTERIOR_CLEARANCE_HEIGHT: float = 5.5 const WORLD_GEOMETRY_COLLISION_LAYER: int = 1 const DECOR_SELECTION_COLLISION_LAYER: int = 1 << 8 # Includes the player's 0.45 m capsule radius plus enough movement margin for @@ -113,7 +132,9 @@ func apply_manifest(data: Dictionary) -> bool: var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data( data["home"] ) - var exterior := _build_exterior(fingerprint, exterior_transform) + var exterior := _build_exterior( + fingerprint, exterior_transform, home_data + ) var interior := _build_interior(fingerprint, interior_index, home_data) var presentation: Dictionary = { "owner_peer_id": int(data["owner_peer_id"]), @@ -191,19 +212,27 @@ func choose_exterior_transform( ) if dry_result != Transform3D.IDENTITY: push_warning( - "RV exterior used a dry fallback with obstructed clearance." + "Home exterior used a dry fallback with obstructed clearance." ) return dry_result - push_error("No dry, level RV exterior footprint was available.") - var fallback: Transform3D = spawn_transform - var column: int = posmod(owner_peer_id - 1, 4) - var row: int = maxi(floori(float(owner_peer_id - 1) / 4.0), 0) - fallback.origin += Vector3( - -12.0 + float(column) * 8.0, - 0.0, - 8.0 + float(row) * 7.0, - ) - return fallback + var emergency_result: Transform3D = _find_exterior_transform( + owner_peer_id, + triangles, + surface_index, + spawn_transform, + [], + false, + false, + ) + if emergency_result != Transform3D.IDENTITY: + push_warning( + "Home exterior used an accessible emergency placement." + ) + return emergency_result + push_error("No dry, level home exterior footprint was available.") + # The authored spawn is the only universally reachable point. Never strand + # a home on an unchecked elevation when malformed terrain offers no site. + return spawn_transform func _find_exterior_transform( @@ -229,7 +258,7 @@ func _find_exterior_transform( ) / 3.0 if ( require_spawn_clearance - and position.distance_to(spawn_transform.origin) < 6.0 + and position.distance_to(spawn_transform.origin) < 9.0 ): continue if _too_close_to_reserved(position, reserved_transforms): @@ -240,6 +269,13 @@ func _find_exterior_transform( ) if not grounded_position.is_finite(): continue + if ( + _world != null + and not _world.is_home_exterior_position_accessible( + grounded_position + ) + ): + continue if ( require_obstacle_clearance and not _exterior_volume_is_clear(grounded_position, yaw) @@ -444,16 +480,48 @@ func get_exterior_transform(owner_fingerprint: String) -> Transform3D: func get_exterior_prompt_position(owner_fingerprint: String) -> Vector3: - return get_exterior_transform(owner_fingerprint) * EXTERIOR_DOOR_LOCAL + return get_exterior_transform(owner_fingerprint) * ( + _exterior_prompt_local(owner_fingerprint) + ) + + +func set_exterior_prompt_visible( + owner_fingerprint: String, + requested_visible: bool, + action_text: String = "enter home", +) -> void: + for fingerprint: String in _presentations_by_fingerprint: + var presentation: Dictionary = _presentations_by_fingerprint[fingerprint] + var exterior: Node3D = presentation.get("exterior") + if not is_instance_valid(exterior): + continue + var prompt := exterior.get_node_or_null("DoorPrompt") as Node3D + if prompt == null: + continue + prompt.visible = ( + requested_visible + and fingerprint == owner_fingerprint + and not _dedicated_runtime + ) + if prompt.visible: + var label := prompt.get_node_or_null("Message") as Label3D + if label != null: + label.text = action_text func get_exterior_spawn_transform(owner_fingerprint: String) -> Transform3D: var exterior_transform: Transform3D = get_exterior_transform( owner_fingerprint ) + # Exterior local +Z leads away from the doorway. Player visuals face local + # -Z, so turn the authoritative spawn basis around to start the day facing + # outward instead of looking back into the home. + var outward_spawn_basis: Basis = ( + exterior_transform.basis * Basis(Vector3.UP, PI) + ) return Transform3D( - exterior_transform.basis, - exterior_transform * EXTERIOR_SPAWN_LOCAL, + outward_spawn_basis, + exterior_transform * _exterior_spawn_local(owner_fingerprint), ) @@ -461,17 +529,26 @@ func get_interior_spawn_transform( owner_fingerprint: String, visitor_offset: int = 0, ) -> Transform3D: - var anchor: Vector3 = get_interior_anchor(owner_fingerprint) # `visitor_offset` is allocated by the host as a compact room-local slot. # Keep it bounded as a last line of defense if a future caller accidentally # supplies an engine peer ID instead of a slot. var visitor_slot: int = posmod(visitor_offset, INTERIOR_MAX_SPAWN_SLOTS) - # Build the origin as a value rather than mutating Transform3D.origin. - var spawn_origin: Vector3 = anchor + Vector3( - -1.0 + float(posmod(visitor_slot, 4)) * 0.65, - 0.25, - 0.4 + float(floori(float(visitor_slot) / 4.0)) * 0.55, + var exit_direction: Vector3 = get_interior_exit_direction(owner_fingerprint) + var lane_offset: float = ( + float(posmod(visitor_slot, 4)) - 1.5 + ) * 0.28 + var row_offset: float = ( + float(floori(float(visitor_slot) / 4.0)) * 0.28 ) + # Spawn just through the authored doorway. Local presentation then walks the + # avatar farther into the room before returning movement control. + var spawn_origin: Vector3 = ( + get_interior_exit_position(owner_fingerprint) + + exit_direction * 0.42 + - exit_direction * row_offset + + Vector3(0.0, 0.0, lane_offset) + ) + spawn_origin.y = get_interior_anchor(owner_fingerprint).y return Transform3D(Basis.IDENTITY, spawn_origin) @@ -487,7 +564,9 @@ func is_avatar_near_exterior(avatar: Player, owner_fingerprint: String) -> bool: if avatar == null or not _presentations_by_fingerprint.has(owner_fingerprint): return false var transform: Transform3D = get_exterior_transform(owner_fingerprint) - var door_position: Vector3 = transform * EXTERIOR_DOOR_LOCAL + var door_position: Vector3 = ( + transform * _exterior_door_local(owner_fingerprint) + ) return avatar.global_position.distance_to(door_position) <= ( EXTERIOR_INTERACTION_DISTANCE ) @@ -504,11 +583,46 @@ func is_avatar_near_interior_exit( ) <= INTERIOR_EXIT_DISTANCE +func has_avatar_crossed_interior_exit_threshold( + avatar: Player, + owner_fingerprint: String, +) -> bool: + if avatar == null: + return false + var local_position: Vector3 = interior_global_to_local( + owner_fingerprint, + avatar.global_position, + ) + var threshold_position: Vector3 = interior_global_to_local( + owner_fingerprint, + get_interior_exit_position(owner_fingerprint), + ) + var exit_direction: Vector3 = get_interior_exit_direction( + owner_fingerprint + ) + return ( + (local_position - threshold_position).dot(exit_direction) >= 0.0 + and absf(local_position.z - threshold_position.z) <= 0.85 + and local_position.y >= -0.25 + and local_position.y <= 2.25 + ) + + +func get_interior_exit_direction(owner_fingerprint: String) -> Vector3: + return ( + Vector3.LEFT + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else Vector3.RIGHT + ) + + func get_interior_exit_position(owner_fingerprint: String) -> Vector3: - return get_interior_anchor(owner_fingerprint) + Vector3( - -INTERIOR_FLOOR_SIZE.x * 0.5 + 0.6, - 0.35, - 0.0, + var tier: int = _home_tier(owner_fingerprint) + return get_interior_anchor(owner_fingerprint) + ( + Vector3(-STARTER_RV_INTERIOR_SIZE.x * 0.5 + 0.6, 0.35, 0.0) + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else Vector3(TENT_INTERIOR_SIZE.x * 0.5 - 0.55, 0.35, 0.0) ) @@ -705,7 +819,7 @@ func find_nearby_exterior_owner(avatar: Player) -> String: for fingerprint: String in _presentations_by_fingerprint: var transform: Transform3D = get_exterior_transform(fingerprint) var distance: float = avatar.global_position.distance_to( - transform * EXTERIOR_DOOR_LOCAL + transform * _exterior_door_local(fingerprint) ) if distance <= closest_distance: closest = fingerprint @@ -713,6 +827,41 @@ func find_nearby_exterior_owner(avatar: Player) -> String: return closest +func _home_tier(owner_fingerprint: String) -> int: + var presentation: Dictionary = _presentations_by_fingerprint.get( + owner_fingerprint, {} + ) + var home_data: Dictionary = presentation.get("home", {}) + return int(home_data.get("tier", PlayerHomeStateType.STARTER_TIER)) + + +func _exterior_door_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_DOOR_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_DOOR_LOCAL + ) + + +func _exterior_spawn_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_SPAWN_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_SPAWN_LOCAL + ) + + +func _exterior_prompt_local(owner_fingerprint: String) -> Vector3: + return ( + RV_EXTERIOR_PROMPT_LOCAL + if _home_tier(owner_fingerprint) + >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_PROMPT_LOCAL + ) + + func set_local_space(space_id: StringName) -> void: _local_space = space_id _refresh_local_visibility() @@ -822,24 +971,44 @@ func _ensure_roots() -> void: func _build_exterior( fingerprint: String, world_transform: Transform3D, + home_data: Dictionary, ) -> Node3D: var root := Node3D.new() - root.name = "RV_%s" % fingerprint.left(8) + root.name = "Home_%s" % fingerprint.left(8) _exteriors_root.add_child(root) root.global_transform = world_transform + var tier: int = int(home_data.get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + if tier >= PlayerHomeStateType.STARTER_RV_TIER: + if not _dedicated_runtime: + var motorcoach: Node3D = ( + STARTER_MOTORCOACH_SCENE.instantiate() as Node3D + ) + if motorcoach != null: + motorcoach.name = "StarterMotorcoach" + root.add_child(motorcoach) + else: + push_error( + "The starter motorcoach exterior could not be instantiated." + ) + _add_box_collision( + root, + "Body", + RV_EXTERIOR_BODY_SIZE, + Vector3(0.0, RV_EXTERIOR_BODY_CENTER_Y, 0.0), + ) + else: + if not _dedicated_runtime: + _add_tent_exterior(root) + _add_box_collision( + root, + "Body", + TENT_EXTERIOR_BODY_SIZE, + Vector3(0.0, TENT_EXTERIOR_BODY_CENTER_Y, 0.0), + ) if not _dedicated_runtime: - var motorcoach: Node3D = STARTER_MOTORCOACH_SCENE.instantiate() as Node3D - if motorcoach != null: - motorcoach.name = "StarterMotorcoach" - root.add_child(motorcoach) - else: - push_error("The starter motorcoach exterior could not be instantiated.") - _add_box_collision( - root, - "Body", - EXTERIOR_BODY_SIZE, - Vector3(0.0, 1.35, 0.0), - ) + _add_exterior_prompt(root, tier) root.visible = not _dedicated_runtime return root @@ -850,59 +1019,26 @@ func _build_interior( home_data: Dictionary, ) -> Node3D: var root := Node3D.new() - root.name = "RVInterior_%s" % fingerprint.left(8) + root.name = "HomeInterior_%s" % fingerprint.left(8) root.position = INTERIOR_ORIGIN + Vector3( float(interior_index) * INTERIOR_SPACING, 0.0, 0.0 ) - var half_width: float = INTERIOR_FLOOR_SIZE.x * 0.5 - var half_depth: float = INTERIOR_FLOOR_SIZE.z * 0.5 - var wall_center_y: float = INTERIOR_WALL_HEIGHT * 0.5 _interiors_root.add_child(root) - _add_box( - root, "Floor", INTERIOR_FLOOR_SIZE, Vector3.ZERO, - Color("a6754e"), true, - ) - _add_wall( - root, "WallNorth", - Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18), - Vector3(0.0, wall_center_y, -half_depth), - ) - _add_wall( - root, "WallSouth", - Vector3(INTERIOR_FLOOR_SIZE.x, INTERIOR_WALL_HEIGHT, 0.18), - Vector3(0.0, wall_center_y, half_depth), - ) - _add_wall( - root, "WallWest", - Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z), - Vector3(-half_width, wall_center_y, 0.0), - ) - _add_wall( - root, "WallEast", - Vector3(0.18, INTERIOR_WALL_HEIGHT, INTERIOR_FLOOR_SIZE.z), - Vector3(half_width, wall_center_y, 0.0), - ) - for x_position: float in [-1.75, 1.75]: - _add_box( - root, "WallNorthWindow%s" % ("Left" if x_position < 0.0 else "Right"), - Vector3(1.65, 0.95, 0.04), - Vector3(x_position, 1.55, -half_depth + 0.11), - Color("153640"), false, - ) - _add_box( - root, "WallSouthWindow%s" % ("Left" if x_position < 0.0 else "Right"), - Vector3(1.65, 0.95, 0.04), - Vector3(x_position, 1.55, half_depth - 0.11), - Color("153640"), false, - ) - _add_box( - root, "WallEastWindow", Vector3(0.04, 0.95, 1.55), - Vector3(half_width - 0.11, 1.55, 0.0), Color("153640"), false, - ) - _add_box( - root, "WallWestDoor", Vector3(0.04, 1.9, 1.25), - Vector3(-half_width + 0.11, 1.0, 0.0), Color("2897a8"), false, + var tier: int = int(home_data.get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + var scene: PackedScene = ( + STARTER_RV_INTERIOR_SCENE + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_INTERIOR_SCENE ) + var shell: Node3D = scene.instantiate() as Node3D + if shell == null: + push_error("The home interior shell could not be instantiated.") + else: + shell.name = "InteriorShell" + root.add_child(shell) + _prepare_imported_interior(shell) var decor := Node3D.new() decor.name = "Decor" root.add_child(decor) @@ -913,6 +1049,153 @@ func _build_interior( return root +func _add_tent_exterior(parent: Node3D) -> void: + var tent := MeshInstance3D.new() + tent.name = "StarterTent" + var mesh := CylinderMesh.new() + mesh.top_radius = 0.0 + mesh.bottom_radius = 3.05 + mesh.height = 3.0 + mesh.radial_segments = 4 + mesh.rings = 1 + mesh.cap_top = false + mesh.material = _material(Color("9a7049")) + tent.mesh = mesh + tent.position.y = 1.5 + tent.rotation.y = PI * 0.25 + parent.add_child(tent) + _add_box( + parent, + "TentDoor", + Vector3(1.25, 1.85, 0.04), + Vector3(0.0, 0.95, 2.18), + Color("173b32"), + false, + ) + + +func _add_exterior_prompt(parent: Node3D, tier: int) -> void: + var prompt := Node3D.new() + prompt.name = "DoorPrompt" + prompt.position = ( + RV_EXTERIOR_PROMPT_LOCAL + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_EXTERIOR_PROMPT_LOCAL + ) + prompt.visible = false + parent.add_child(prompt) + + var background := MeshInstance3D.new() + background.name = "Background" + background.mesh = _rounded_rectangle_mesh( + HOME_PROMPT_SIZE, + HOME_PROMPT_CORNER_RADIUS, + ) + background.material_override = _prompt_material( + HOME_PROMPT_BACKGROUND_COLOR + ) + prompt.add_child(background) + + var label := Label3D.new() + label.name = "Message" + label.text = "enter home" + label.position.z = 0.012 + label.font = HOME_PROMPT_FONT + label.font_size = 28 + label.pixel_size = HOME_PROMPT_TEXT_PIXEL_SIZE + label.modulate = HOME_PROMPT_TEXT_COLOR + label.outline_size = 0 + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + label.width = HOME_PROMPT_SIZE.x / HOME_PROMPT_TEXT_PIXEL_SIZE + label.double_sided = true + prompt.add_child(label) + + +func _rounded_rectangle_mesh(size: Vector2, radius: float) -> ArrayMesh: + var half_size: Vector2 = size * 0.5 + var clamped_radius: float = minf( + radius, + minf(half_size.x, half_size.y), + ) + var corners: Array[Vector2] = [ + Vector2(half_size.x - clamped_radius, half_size.y - clamped_radius), + Vector2(-half_size.x + clamped_radius, half_size.y - clamped_radius), + Vector2(-half_size.x + clamped_radius, -half_size.y + clamped_radius), + Vector2(half_size.x - clamped_radius, -half_size.y + clamped_radius), + ] + var start_angles: Array[float] = [0.0, PI * 0.5, PI, PI * 1.5] + var perimeter: Array[Vector2] = [] + const CORNER_SEGMENTS: int = 4 + for corner_index: int in corners.size(): + for segment_index: int in range(CORNER_SEGMENTS + 1): + var angle: float = start_angles[corner_index] + ( + PI * 0.5 * float(segment_index) / float(CORNER_SEGMENTS) + ) + perimeter.append( + corners[corner_index] + + Vector2(cos(angle), sin(angle)) * clamped_radius + ) + var surface := SurfaceTool.new() + surface.begin(Mesh.PRIMITIVE_TRIANGLES) + for index: int in perimeter.size(): + var first: Vector2 = perimeter[index] + var second: Vector2 = perimeter[(index + 1) % perimeter.size()] + for point: Vector2 in [Vector2.ZERO, first, second]: + surface.set_normal(Vector3.FORWARD) + surface.add_vertex(Vector3(point.x, point.y, 0.0)) + return surface.commit() + + +func _prompt_material(color: Color) -> StandardMaterial3D: + var material := StandardMaterial3D.new() + material.albedo_color = color + material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + material.roughness = 1.0 + material.metallic = 0.0 + material.cull_mode = BaseMaterial3D.CULL_DISABLED + return material + + +func _prepare_imported_interior(shell: Node3D) -> void: + for value: Node in shell.find_children("*", "MeshInstance3D", true, false): + var mesh_instance := value as MeshInstance3D + if mesh_instance == null or mesh_instance.mesh == null: + continue + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var source_material: Material = mesh_instance.get_active_material( + surface_index + ) + var material := source_material as StandardMaterial3D + if material == null: + continue + var flat_material := material.duplicate() as StandardMaterial3D + flat_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED + flat_material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_NEAREST + flat_material.roughness = 1.0 + flat_material.metallic = 0.0 + mesh_instance.set_surface_override_material( + surface_index, flat_material + ) + _add_imported_mesh_collision(mesh_instance) + + +func _add_imported_mesh_collision(mesh_instance: MeshInstance3D) -> void: + var parent: Node = mesh_instance.get_parent() + if parent == null or mesh_instance.mesh == null: + return + var shape: ConcavePolygonShape3D = mesh_instance.mesh.create_trimesh_shape() + if shape == null: + return + var body := StaticBody3D.new() + body.name = "%sCollision" % mesh_instance.name + body.transform = mesh_instance.transform + var collision := CollisionShape3D.new() + collision.shape = shape + body.add_child(collision) + parent.add_child(body) + + func _rebuild_decor( parent: Node3D, home_data: Dictionary, @@ -1115,9 +1398,9 @@ func _refresh_local_visibility() -> void: for wall_name: String in [ "WallNorth", "WallSouth", "WallWest", "WallEast", ]: - var body: StaticBody3D = active_interior.get_node_or_null( - "%sCollision" % wall_name - ) + var body := active_interior.find_child( + "%sCollision" % wall_name, true, false + ) as StaticBody3D if body != null: excluded_wall_rids.append(body.get_rid()) local_player.set_rv_interior_camera_mode( @@ -1145,12 +1428,20 @@ func _refresh_nearest_wall() -> void: ).normalized() var horizontal_view_weight: float = absf(local_view_direction.x) var depth_view_weight: float = absf(local_view_direction.z) + var tier: int = int((presentation.get("home", {}) as Dictionary).get( + "tier", PlayerHomeStateType.STARTER_TIER + )) + var interior_size: Vector2 = ( + STARTER_RV_INTERIOR_SIZE + if tier >= PlayerHomeStateType.STARTER_RV_TIER + else TENT_INTERIOR_SIZE + ) var hidden_walls: Dictionary[String, bool] = {} var outside_horizontal_wall: bool = ( - absf(local_camera.x) >= INTERIOR_FLOOR_SIZE.x * 0.5 - 0.05 + absf(local_camera.x) >= interior_size.x * 0.5 - 0.05 ) var outside_depth_wall: bool = ( - absf(local_camera.z) >= INTERIOR_FLOOR_SIZE.z * 0.5 - 0.05 + absf(local_camera.z) >= interior_size.y * 0.5 - 0.05 ) # At a diagonal, both near walls occupy the view. Use the camera's viewing # direction to hide both instead of choosing only the dominant position @@ -1181,10 +1472,14 @@ func _refresh_nearest_wall() -> void: for wall_name: String in [ "WallNorth", "WallSouth", "WallWest", "WallEast", ]: - var wall: MeshInstance3D = interior.get_node_or_null(wall_name) + var wall := interior.find_child( + wall_name, true, false + ) as MeshInstance3D if wall != null: wall.visible = not hidden_walls.has(wall_name) - for child: Node in interior.get_children(): + for child: Node in interior.find_children( + "%s*" % wall_name, "GeometryInstance3D", true, false + ): if ( child is GeometryInstance3D and String(child.name).begins_with(wall_name) diff --git a/homes/player_home_state.gd b/homes/player_home_state.gd index decd34a..a89d6a3 100644 --- a/homes/player_home_state.gd +++ b/homes/player_home_state.gd @@ -7,8 +7,12 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd") const HOME_SCHEMA_VERSION: int = 1 const STARTER_TIER: int = 0 -const STARTER_EXTERIOR_ID: StringName = &"starter_motorcoach" -const MAX_TIER: int = 8 +const STARTER_EXTERIOR_ID: StringName = &"starter_tent" +const STARTER_RV_TIER: int = 1 +const STARTER_RV_EXTERIOR_ID: StringName = &"starter_motorcoach" +const MAX_TIER: int = STARTER_RV_TIER +const HOME_UPGRADE_COSTS: Array[int] = [50] +const STORAGE_CAPACITIES: Array[int] = [9, 18] const MAX_PLACEMENTS: int = 128 const MAX_INSTANCE_ID_LENGTH: int = 96 const MAX_REVISION: int = 2147483647 @@ -16,11 +20,8 @@ const GRID_SIZE: float = 0.5 const FREE_PLACEMENT_SEARCH_STEP: float = 0.1 const FREE_PLACEMENT_SEARCH_ANGLES: int = 24 const NEAREST_PLACEMENT_SEARCH_DISTANCE: float = 2.0 -# The starter RV is intentionally cramped. Later RV tiers can replace these -# bounds with larger layouts when tiered interiors are introduced. -# The starter interior is 8.0 m by 4.5 m with 0.18 m walls. These bounds -# follow the walls' inner faces, with a small visual gap to avoid z-fighting. -const INTERIOR_HALF_EXTENTS := Vector2(3.91, 2.16) +const TENT_INTERIOR_HALF_EXTENTS := Vector2(1.91, 1.91) +const STARTER_RV_INTERIOR_HALF_EXTENTS := Vector2(1.91, 2.91) const PLACEMENT_EDGE_PADDING: float = 0.02 enum Privacy { @@ -50,6 +51,51 @@ func get_exterior_id() -> StringName: return _exterior_id +func get_storage_capacity() -> int: + return storage_capacity_for_tier(_tier) + + +func get_next_upgrade_cost() -> int: + if _tier >= MAX_TIER: + return -1 + return HOME_UPGRADE_COSTS[_tier] + + +func get_next_storage_capacity() -> int: + if _tier >= MAX_TIER: + return -1 + return storage_capacity_for_tier(_tier + 1) + + +func upgrade_to_next_tier() -> bool: + if _tier >= MAX_TIER: + return false + _tier += 1 + _exterior_id = exterior_id_for_tier(_tier) + _touch() + return true + + +static func exterior_id_for_tier(tier: int) -> StringName: + return ( + STARTER_RV_EXTERIOR_ID + if tier >= STARTER_RV_TIER + else STARTER_EXTERIOR_ID + ) + + +static func storage_capacity_for_tier(tier: int) -> int: + return STORAGE_CAPACITIES[clampi(tier, STARTER_TIER, MAX_TIER)] + + +static func interior_half_extents_for_tier(tier: int) -> Vector2: + return ( + STARTER_RV_INTERIOR_HALF_EXTENTS + if tier >= STARTER_RV_TIER + else TENT_INTERIOR_HALF_EXTENTS + ) + + func get_privacy() -> Privacy: return _privacy @@ -248,7 +294,7 @@ func get_nearest_valid_placement_transform( candidate_validator: Callable = Callable(), ) -> Dictionary: var normalized: Dictionary = normalized_placement_transform( - decor_id, position, yaw_radians, mode + decor_id, position, yaw_radians, mode, _tier ) if normalized.is_empty(): return {} @@ -322,7 +368,7 @@ func restore_from_save_data(data: Dictionary) -> bool: if sanitized.is_empty(): return false _tier = int(sanitized["tier"]) - _exterior_id = StringName(str(sanitized["exterior_id"])) + _exterior_id = exterior_id_for_tier(_tier) _privacy = int(sanitized["privacy"]) _owned_decor.clear() for key: Variant in (sanitized["owned_decor"] as Dictionary): @@ -384,6 +430,7 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary: or next_sequence > MAX_REVISION ): return {} + exterior_id = String(exterior_id_for_tier(tier)) var sanitized_owned: Dictionary = {} var owned: Dictionary = data["owned_decor"] for key: Variant in owned: @@ -408,7 +455,7 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary: for value: Variant in values: if typeof(value) != TYPE_DICTIONARY: return {} - var sanitized: Dictionary = sanitize_placement(value) + var sanitized: Dictionary = sanitize_placement(value, tier) if sanitized.is_empty(): return {} var instance_id: String = str(sanitized["instance_id"]) @@ -440,7 +487,10 @@ static func sanitize_save_data(data: Dictionary) -> Dictionary: } -static func sanitize_placement(value: Dictionary) -> Dictionary: +static func sanitize_placement( + value: Dictionary, + home_tier: int = STARTER_TIER, +) -> Dictionary: for key: String in [ "instance_id", "decor_id", "mode", "position", "yaw", ]: @@ -483,6 +533,7 @@ static func sanitize_placement(value: Dictionary) -> Dictionary: position, yaw, mode as PlacementMode, + home_tier, ) if normalized.is_empty(): return {} @@ -509,6 +560,7 @@ static func normalized_placement_transform( position: Vector3, yaw_radians: float, mode: PlacementMode, + home_tier: int = STARTER_TIER, ) -> Dictionary: if ( not DecorCatalogType.has_product(decor_id) @@ -530,11 +582,12 @@ static func normalized_placement_transform( (footprint.x * cosine + footprint.y * sine) * 0.5, (footprint.x * sine + footprint.y * cosine) * 0.5, ) + var interior_half_extents := interior_half_extents_for_tier(home_tier) var maximum_x: float = ( - INTERIOR_HALF_EXTENTS.x - half_size.x - PLACEMENT_EDGE_PADDING + interior_half_extents.x - half_size.x - PLACEMENT_EDGE_PADDING ) var maximum_z: float = ( - INTERIOR_HALF_EXTENTS.y - half_size.y - PLACEMENT_EDGE_PADDING + interior_half_extents.y - half_size.y - PLACEMENT_EDGE_PADDING ) if maximum_x < 0.0 or maximum_z < 0.0: return {} @@ -580,6 +633,7 @@ func _find_nearest_grid_transform( ), yaw, PlacementMode.GRID, + _tier, ) if candidate.is_empty() or not _mark_candidate_unseen( candidate, seen @@ -630,6 +684,7 @@ func _find_nearest_free_transform( ), yaw, PlacementMode.FREE, + _tier, ) if candidate.is_empty() or not _mark_candidate_unseen( candidate, seen diff --git a/main/main.gd b/main/main.gd index 00d2b40..324c268 100644 --- a/main/main.gd +++ b/main/main.gd @@ -40,6 +40,9 @@ const FishingShopInteractionType = preload( const DecorShopInteractionType = preload( "res://world/decor_shop_interaction.gd" ) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PlayerStorageInteractionType = preload( "res://world/player_storage_interaction.gd" ) @@ -148,6 +151,13 @@ const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0) const SHOP_PATTERN_SCALE: float = 1.75 const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10 const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 +const HOME_ENTRY_INPUT_OWNER: StringName = &"home_entry_transition" +const HOME_EXIT_INPUT_OWNER: StringName = &"home_exit_transition" +const HOME_EXIT_FADE_GENERATION: int = -104729 +const HOME_ENTRY_WALK_DISTANCE: float = 1.05 +const HOME_ENTRY_WALK_DURATION: float = 0.55 +const HOME_EXIT_WALK_DISTANCE: float = 1.25 +const HOME_EXIT_WALK_DURATION: float = 0.62 @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType @@ -260,6 +270,7 @@ var _gameplay_started: bool = false var _save_slots := PlayerSaveSlotCatalogType.new() var _shop_interaction: FishingShopInteractionType var _decor_shop_interaction: DecorShopInteractionType +var _rv_upgrade_shop_interaction: RVUpgradeInteractionType var _storage_interaction: PlayerStorageInteractionType var _title_music_tween: Tween var _title_music_transition_generation: int = 0 @@ -290,6 +301,12 @@ var _pending_existing_root_path := "" var _local_recovery_attempt_id: String = "" var _dedicated_runtime: bool = false var _dedicated_metrics_accumulator: float = 0.0 +var _home_exit_transition_active: bool = false +var _home_exit_waiting_for_network: bool = false +var _home_exit_start_position: Vector3 = Vector3.ZERO +var _home_exit_walk_tween: Tween +var _home_entry_transition_active: bool = false +var _home_entry_walk_tween: Tween @onready var _shoreline_ambience: ShorelineAmbience = %ShorelineAmbience var _rain_ambience: RainAmbienceType @@ -743,6 +760,7 @@ func _initialize_application(dedicated: bool) -> void: _player.wallet, _asset_reservations ) + _player.cooler_capacity.bind_home_state(_home_state) _player.bag.setup(item_catalog) _player.inventory_layout.setup( _player.bag, @@ -760,19 +778,24 @@ func _initialize_application(dedicated: bool) -> void: ) _shop_interaction = _test_world.get_fishing_shop() _decor_shop_interaction = _test_world.get_decor_shop() + _rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_shop() _storage_interaction = _test_world.get_player_storage() if not dedicated: + if not _home_state.changed.is_connected(_on_home_state_changed): + _home_state.changed.connect(_on_home_state_changed) _shop_interaction.setup_local_player(_player) _shop_interaction.local_player_range_changed.connect( _on_shop_range_changed ) - _game_ui.set_shop_npc_player_in_range( - _shop_interaction.is_local_player_in_range() - ) + _refresh_shop_npc_speech_state() _decor_shop_interaction.setup_local_player(_player) _decor_shop_interaction.local_player_range_changed.connect( _on_decor_shop_range_changed ) + _rv_upgrade_shop_interaction.setup_local_player(_player) + _rv_upgrade_shop_interaction.local_player_range_changed.connect( + _on_rv_upgrade_shop_range_changed + ) _storage_interaction.setup_local_player(_player) _storage_interaction.local_player_range_changed.connect( _on_storage_range_changed @@ -947,6 +970,7 @@ func _initialize_application(dedicated: bool) -> void: _home_state, _network_home, _decor_shop_interaction, + _rv_upgrade_shop_interaction, ) _fishing_spot.setup( _player, @@ -987,6 +1011,7 @@ func _initialize_application(dedicated: bool) -> void: _shop_interaction, _storage_interaction, _decor_shop_interaction, + _rv_upgrade_shop_interaction, _player.item_effects, _player.cooler_capacity, _network_session, @@ -1024,6 +1049,12 @@ func _initialize_application(dedicated: bool) -> void: _home_decor_controller.begin_decorating ) _network_home.local_space_changed.connect(_on_local_home_space_changed) + _network_home.local_transition_finished.connect( + _on_local_home_transition_finished + ) + _game_ui.get_screen_fade().transition_completed.connect( + _on_home_exit_fade_transition_completed + ) _game_ui.rv_storage_requested.connect(_on_rv_storage_requested) _game_ui.setup_data_and_identity( _data_root, @@ -1539,6 +1570,9 @@ func _input(event: InputEvent) -> void: if _game_ui.get_decor_shop().consume_escape(): get_viewport().set_input_as_handled() return + if _game_ui.get_rv_upgrade_shop().consume_escape(): + get_viewport().set_input_as_handled() + return if fishing_shop.consume_escape(): get_viewport().set_input_as_handled() return @@ -1653,17 +1687,12 @@ func _unhandled_input(event: InputEvent) -> void: 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) + _refresh_home_door_prompt({}) get_viewport().set_input_as_handled() return var nearby_home_owner: String = _home_world.find_nearby_exterior_owner( @@ -1671,9 +1700,18 @@ func _unhandled_input(event: InputEvent) -> void: ) if not nearby_home_owner.is_empty(): if _network_home.request_enter(nearby_home_owner): - _game_ui.set_storage_prompt_visible(false) + _refresh_home_door_prompt({}) get_viewport().set_input_as_handled() return + if ( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + and _fishing_spot.can_open_fishing_shop() + and _game_ui.get_rv_upgrade_shop().open_shop() + ): + _game_ui.set_shop_prompt_visible(false) + get_viewport().set_input_as_handled() + return if ( _decor_shop_interaction != null and _decor_shop_interaction.is_local_player_in_range() @@ -1703,14 +1741,24 @@ func _process(delta: float) -> void: ) _print_dedicated_metrics() return + _maybe_begin_home_exit_transition() + var show_rv_upgrade_prompt := _can_show_rv_upgrade_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 show_shop_prompt := ( + show_rv_upgrade_prompt + or show_decor_shop_prompt + or _can_show_shop_prompt() + ) var shop_prompt_anchor := ( - _decor_shop_interaction.get_prompt_anchor_position() - if show_decor_shop_prompt + _rv_upgrade_shop_interaction.get_prompt_anchor_position() + if show_rv_upgrade_prompt else ( - _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( @@ -1718,11 +1766,7 @@ func _process(delta: float) -> void: shop_prompt_anchor, ) var home_prompt := _get_home_prompt() - _game_ui.set_storage_prompt_visible( - not home_prompt.is_empty(), - home_prompt.get("anchor", Vector3.ZERO), - str(home_prompt.get("label", "enter RV")), - ) + _refresh_home_door_prompt(home_prompt) _game_ui.set_rv_storage_button_visible( _can_show_rv_storage_button() ) @@ -2577,12 +2621,23 @@ func _refresh_active_world_bindings() -> void: _decor_shop_interaction.local_player_range_changed.disconnect( _on_decor_shop_range_changed ) + if is_instance_valid(_rv_upgrade_shop_interaction): + if _rv_upgrade_shop_interaction.local_player_range_changed.is_connected( + _on_rv_upgrade_shop_range_changed + ): + _rv_upgrade_shop_interaction.local_player_range_changed.disconnect( + _on_rv_upgrade_shop_range_changed + ) _shop_interaction = _test_world.get_fishing_shop() _decor_shop_interaction = _test_world.get_decor_shop() + _rv_upgrade_shop_interaction = _test_world.get_rv_upgrade_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) + _network_shop.set_rv_upgrade_shop_interaction( + _rv_upgrade_shop_interaction + ) if _dedicated_runtime: return if _shop_interaction != null: @@ -2600,10 +2655,16 @@ func _refresh_active_world_bindings() -> void: _decor_shop_interaction.local_player_range_changed.connect( _on_decor_shop_range_changed ) + if _rv_upgrade_shop_interaction != null: + _rv_upgrade_shop_interaction.setup_local_player(_player) + _rv_upgrade_shop_interaction.local_player_range_changed.connect( + _on_rv_upgrade_shop_range_changed + ) _game_ui.set_world_interactions( _shop_interaction, _storage_interaction, _decor_shop_interaction, + _rv_upgrade_shop_interaction, ) _on_shop_range_changed( _shop_interaction != null @@ -2617,6 +2678,10 @@ func _refresh_active_world_bindings() -> void: _decor_shop_interaction != null and _decor_shop_interaction.is_local_player_in_range() ) + _on_rv_upgrade_shop_range_changed( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ) func _on_water_recovery_starting() -> void: @@ -2631,6 +2696,7 @@ func _on_water_recovery_starting() -> void: _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_rv_upgrade_shop().close_for_water_recovery() _game_ui.get_player_storage().close_for_water_recovery() @@ -2679,6 +2745,7 @@ func _on_active_hotbar_item_changed( and item != null and item.is_available() and _player.bag.owns_item(item_id) + and not String(_network_home.get_local_space()).begins_with("rv:") ) var active_is_catching_net: bool = ( item_id == FishingShopStockType.CRAB_NET_ID @@ -2828,6 +2895,8 @@ func _finish_quit() -> void: func _exit_tree() -> void: + _cancel_home_entry_transition() + _cancel_home_exit_transition(true) if _dedicated_runtime and _network_session != null: _network_session.disconnect_session("Dedicated server stopping.") if _title_music_tween != null: @@ -2845,52 +2914,46 @@ func _exit_tree() -> void: func _on_shop_range_changed(in_range: bool) -> void: - _game_ui.set_shop_npc_player_in_range( - in_range - or ( - _decor_shop_interaction != null - and _decor_shop_interaction.is_local_player_in_range() - ) - ) + _refresh_shop_npc_speech_state() if not in_range: _game_ui.get_fishing_shop().close_for_range_exit() - _game_ui.set_shop_prompt_visible(_can_show_shop_prompt()) + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) func _on_storage_range_changed(in_range: bool) -> void: if not in_range: _game_ui.get_player_storage().close_for_range_exit() 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")), - ) + _refresh_home_door_prompt(prompt) _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() - ) - ) + _refresh_shop_npc_speech_state() 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() - ) + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) + + +func _on_rv_upgrade_shop_range_changed(in_range: bool) -> void: + _refresh_shop_npc_speech_state() + if not in_range: + _game_ui.get_rv_upgrade_shop().close_for_range_exit() + _game_ui.set_shop_prompt_visible(_can_show_any_shop_prompt()) + + +func _on_home_state_changed() -> void: + _refresh_shop_npc_speech_state() func _on_local_home_space_changed( space_id: StringName, - _owner_fingerprint: String, + owner_fingerprint: String, ) -> void: var inside_rv: bool = String(space_id).begins_with("rv:") + _refresh_active_hotbar_item() _fishing_spot.set_fishing_environment_enabled(not inside_rv) _game_ui.set_rv_interior_camera_mode( inside_rv @@ -2898,6 +2961,171 @@ func _on_local_home_space_changed( _game_ui.set_rv_storage_button_visible( _can_show_rv_storage_button() ) + if inside_rv and not _home_exit_transition_active: + _begin_home_entry_transition(owner_fingerprint) + elif not inside_rv and _home_entry_transition_active: + _cancel_home_entry_transition() + + +func _begin_home_entry_transition(owner_fingerprint: String) -> void: + if owner_fingerprint.is_empty() or _home_entry_transition_active: + return + _home_entry_transition_active = true + _refresh_home_door_prompt({}) + _player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, true) + _player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, true) + var inward_direction: Vector3 = ( + -_home_world.get_interior_exit_direction(owner_fingerprint) + ) + _player.face_world_direction(inward_direction) + _player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION) + _home_entry_walk_tween = create_tween() + _home_entry_walk_tween.set_trans(Tween.TRANS_SINE) + _home_entry_walk_tween.set_ease(Tween.EASE_IN_OUT) + _home_entry_walk_tween.tween_property( + _player, + "global_position", + _player.global_position + inward_direction * HOME_ENTRY_WALK_DISTANCE, + HOME_ENTRY_WALK_DURATION, + ) + _home_entry_walk_tween.finished.connect( + _cancel_home_entry_transition, + CONNECT_ONE_SHOT, + ) + + +func _cancel_home_entry_transition() -> void: + if _home_entry_walk_tween != null and _home_entry_walk_tween.is_valid(): + _home_entry_walk_tween.kill() + _home_entry_walk_tween = null + if is_instance_valid(_player): + _player.end_animation_action() + _player.set_local_input_suppressed(HOME_ENTRY_INPUT_OWNER, false) + _player.set_camera_input_suppressed(HOME_ENTRY_INPUT_OWNER, false) + _home_entry_transition_active = false + + +func _maybe_begin_home_exit_transition() -> void: + if ( + _home_exit_transition_active + or _home_entry_transition_active + or not _gameplay_started + or _water_recovery.is_recovery_active() + or not _fishing_spot.can_open_fishing_shop() + or _game_ui.get_player_storage().visible + or _game_ui.get_fishing_shop().visible + or _game_ui.get_decor_shop().visible + or _game_ui.get_rv_upgrade_shop().visible + ): + return + var owner_fingerprint: String = ( + _network_home.get_local_home_owner_fingerprint() + ) + if ( + owner_fingerprint.is_empty() + or not _home_world.has_avatar_crossed_interior_exit_threshold( + _player, + owner_fingerprint, + ) + ): + return + _begin_home_exit_transition(owner_fingerprint) + + +func _begin_home_exit_transition(owner_fingerprint: String) -> void: + _home_exit_transition_active = true + _home_exit_waiting_for_network = false + _home_exit_start_position = _player.global_position + _refresh_home_door_prompt({}) + _player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, true) + _player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, true) + var exit_direction: Vector3 = _home_world.get_interior_exit_direction( + owner_fingerprint + ) + _player.face_world_direction(exit_direction) + _player.begin_animation_action(PlayerType.CHARACTER_WALKING_ANIMATION) + _home_exit_walk_tween = create_tween() + _home_exit_walk_tween.set_trans(Tween.TRANS_SINE) + _home_exit_walk_tween.set_ease(Tween.EASE_IN_OUT) + _home_exit_walk_tween.tween_property( + _player, + "global_position", + _player.global_position + exit_direction * HOME_EXIT_WALK_DISTANCE, + HOME_EXIT_WALK_DURATION, + ) + _home_exit_walk_tween.finished.connect( + _on_home_exit_walk_finished, + CONNECT_ONE_SHOT, + ) + + +func _on_home_exit_walk_finished() -> void: + _home_exit_walk_tween = null + if not _home_exit_transition_active: + return + # End the walking action on the same frame translation ends. The fade and + # authoritative relocation may take longer, but a stationary avatar must + # never keep cycling its walk animation during that wait. + _player.end_animation_action() + _game_ui.get_screen_fade().fade_to_black(HOME_EXIT_FADE_GENERATION) + + +func _on_home_exit_fade_transition_completed( + generation: int, + faded_to_black: bool, +) -> void: + if ( + generation != HOME_EXIT_FADE_GENERATION + or not _home_exit_transition_active + ): + return + if faded_to_black: + # A local host can apply the authoritative transition synchronously from + # request_exit(). Mark this first so its completion signal is not lost. + _home_exit_waiting_for_network = true + var request_started: bool = _network_home.request_exit() + if not request_started: + _home_exit_waiting_for_network = false + _player.global_position = _home_exit_start_position + _game_ui.get_screen_fade().fade_from_black( + HOME_EXIT_FADE_GENERATION + ) + return + _cancel_home_exit_transition(false) + + +func _on_local_home_transition_finished( + accepted: bool, + _message: String, +) -> void: + if ( + not _home_exit_transition_active + or not _home_exit_waiting_for_network + ): + return + _home_exit_waiting_for_network = false + if not accepted: + _player.global_position = _home_exit_start_position + _game_ui.get_screen_fade().fade_from_black(HOME_EXIT_FADE_GENERATION) + + +func _cancel_home_exit_transition(reset_fade: bool) -> void: + if _home_exit_walk_tween != null and _home_exit_walk_tween.is_valid(): + _home_exit_walk_tween.kill() + _home_exit_walk_tween = null + if is_instance_valid(_player): + _player.end_animation_action() + _player.set_local_input_suppressed(HOME_EXIT_INPUT_OWNER, false) + _player.set_camera_input_suppressed(HOME_EXIT_INPUT_OWNER, false) + if ( + reset_fade + and is_instance_valid(_game_ui) + and is_instance_valid(_game_ui.get_screen_fade()) + ): + _game_ui.get_screen_fade().reset_immediately() + _home_exit_transition_active = false + _home_exit_waiting_for_network = false + _home_exit_start_position = Vector3.ZERO func _on_rv_storage_requested() -> void: @@ -2913,6 +3141,7 @@ func _can_show_shop_prompt() -> bool: 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 _game_ui.get_rv_upgrade_shop().visible and not _water_recovery.is_recovery_active() and _fishing_spot.can_open_fishing_shop() ) @@ -2925,15 +3154,96 @@ func _can_show_decor_shop_prompt() -> bool: 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 _game_ui.get_rv_upgrade_shop().visible and not _water_recovery.is_recovery_active() and _fishing_spot.can_open_fishing_shop() ) +func _can_show_rv_upgrade_shop_prompt() -> bool: + return ( + _gameplay_started + and _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + and not _game_ui.get_rv_upgrade_shop().visible + 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_any_shop_prompt() -> bool: + return ( + _can_show_shop_prompt() + or _can_show_decor_shop_prompt() + or _can_show_rv_upgrade_shop_prompt() + ) + + +func _is_any_shop_npc_in_range() -> bool: + return ( + ( + _shop_interaction != null + and _shop_interaction.is_local_player_in_range() + ) + or ( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ) + or ( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ) + ) + + +func _refresh_shop_npc_speech_state() -> void: + var interaction := _get_active_shop_npc_interaction() + var dialogue_text: String = "" + if interaction != null: + dialogue_text = interaction.get_dialogue_text(_home_state.get_tier()) + _game_ui.set_shop_npc_player_in_range( + interaction != null, + dialogue_text, + ) + + +func _get_active_shop_npc_interaction() -> FishingShopInteractionType: + # Match the interaction and prompt priority used above: RV, decor, fishing. + if ( + _rv_upgrade_shop_interaction != null + and _rv_upgrade_shop_interaction.is_local_player_in_range() + ): + return _rv_upgrade_shop_interaction + if ( + _decor_shop_interaction != null + and _decor_shop_interaction.is_local_player_in_range() + ): + return _decor_shop_interaction + if ( + _shop_interaction != null + and _shop_interaction.is_local_player_in_range() + ): + return _shop_interaction + return null + + func _can_show_storage_prompt() -> bool: return not _get_home_prompt().is_empty() +func _refresh_home_door_prompt(prompt: Dictionary) -> void: + # Home entry belongs to the doorway in world space. Keep the retired HUD + # prompt hidden so it cannot overlap the compact door-mounted sign. + _game_ui.set_storage_prompt_visible(false) + _home_world.set_exterior_prompt_visible( + str(prompt.get("owner_fingerprint", "")), + not prompt.is_empty(), + str(prompt.get("label", "enter home")), + ) + + func _can_show_rv_storage_button() -> bool: return ( _gameplay_started @@ -2950,6 +3260,7 @@ func _get_home_prompt() -> Dictionary: or _game_ui.get_player_storage().visible or _game_ui.get_fishing_shop().visible or _game_ui.get_decor_shop().visible + or _game_ui.get_rv_upgrade_shop().visible or _water_recovery.is_recovery_active() or not _fishing_spot.can_open_fishing_shop() ): @@ -2958,20 +3269,12 @@ func _get_home_prompt() -> Dictionary: _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", + "label": "enter home", "anchor": _home_world.get_exterior_prompt_position(nearby_owner), + "owner_fingerprint": nearby_owner, } diff --git a/network/network_home_service.gd b/network/network_home_service.gd index 33fa6ee..4885961 100644 --- a/network/network_home_service.gd +++ b/network/network_home_service.gd @@ -27,6 +27,8 @@ 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 _authorized_home_tiers: Dictionary[int, int] = {} +var _home_upgrade_result_owner: Dictionary[String, int] = {} var _interior_slots_by_space: Dictionary[StringName, Dictionary] = {} var _next_interior_index: int = 0 var _suppress_local_submission: bool = false @@ -103,6 +105,17 @@ func get_authoritative_owned_count( return int(owned.get(String(decor_id), 0)) +func get_authoritative_home_tier(peer_id: int) -> int: + if _authorized_home_tiers.has(peer_id): + return int(_authorized_home_tiers[peer_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", {}) + return int(home.get("tier", -1)) + + func place_local_decor( decor_id: StringName, position: Vector3, @@ -337,6 +350,35 @@ func finalize_decor_purchase( _authorized_owned_counts.erase(peer_id) +func authorize_home_upgrade( + peer_id: int, + resulting_tier: int, + result_id: String, +) -> void: + if ( + not _session.is_host() + or peer_id <= 0 + or result_id.is_empty() + or resulting_tier < PlayerHomeStateType.STARTER_TIER + or resulting_tier > PlayerHomeStateType.MAX_TIER + ): + return + _authorized_home_tiers[peer_id] = resulting_tier + _home_upgrade_result_owner[result_id] = peer_id + + +func finalize_home_upgrade( + peer_id: int, + result_id: String, + applied: bool, +) -> void: + if _home_upgrade_result_owner.get(result_id, 0) != peer_id: + return + _home_upgrade_result_owner.erase(result_id) + if not applied: + _authorized_home_tiers.erase(peer_id) + + func refresh_world_presentations() -> void: if _world_service == null: return @@ -442,7 +484,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void: not NetworkHomeProtocolType.validate_manifest_request(data) or str(data["session_id"]) != _session.get_session_id() ): - _reject_manifest(peer_id, "The RV update was invalid.") + _reject_manifest(peer_id, "The home update was invalid.") return var record := _session.get_peer_record(peer_id) var fingerprint: String = str(data["owner_fingerprint"]) @@ -456,7 +498,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void: data["sender_signature"], ) ): - _reject_manifest(peer_id, "The RV owner could not be verified.") + _reject_manifest(peer_id, "The home owner could not be verified.") return var home_data: Dictionary = PlayerHomeStateType.sanitize_save_data( data["home"] @@ -464,7 +506,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void: 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.") + _reject_manifest(peer_id, "The home 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.") @@ -472,7 +514,7 @@ func _process_manifest_request(peer_id: int, data: Dictionary) -> void: var manifest: Dictionary if prior.is_empty(): if _manifests_by_fingerprint.size() >= MAX_MANIFESTS: - _reject_manifest(peer_id, "This room cannot place another RV.") + _reject_manifest(peer_id, "This room cannot place another home.") return var reserved: Array[Transform3D] = [] for existing: Dictionary in _manifests_by_fingerprint.values(): @@ -523,10 +565,17 @@ func _valid_manifest_revision( # no prior room ledger with which to compare its first manifest. return true var prior_home: Dictionary = prior_manifest["home"] + var prior_tier: int = int(prior_home["tier"]) + var next_tier: int = int(home_data["tier"]) + var tier_is_authorized: bool = ( + next_tier != prior_tier + and int(_authorized_home_tiers.get(peer_id, -1)) == next_tier + ) 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"]) + or (next_tier != prior_tier and not tier_is_authorized) + or str(home_data["exterior_id"]) + != str(PlayerHomeStateType.exterior_id_for_tier(next_tier)) ): return false var prior_owned: Dictionary = prior_home["owned_decor"] @@ -619,6 +668,11 @@ static func _placement_position(placement: Dictionary) -> Vector3: func _consume_owned_authorizations(peer_id: int, home_data: Dictionary) -> void: + if ( + _authorized_home_tiers.has(peer_id) + and int(home_data["tier"]) == int(_authorized_home_tiers[peer_id]) + ): + _authorized_home_tiers.erase(peer_id) var authorized: Dictionary = _authorized_owned_counts.get(peer_id, {}) if authorized.is_empty(): return @@ -1027,7 +1081,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: data["sender_signature"], ) ): - _send_transition_rejection(peer_id, "The RV transition was invalid.") + _send_transition_rejection(peer_id, "The home transition was invalid.") return var owner_fingerprint: String = str(data["owner_fingerprint"]) var action: int = int(data["action"]) @@ -1036,7 +1090,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: owner_fingerprint, {} ) if avatar == null or manifest.is_empty(): - _send_transition_rejection(peer_id, "That RV is unavailable.") + _send_transition_rejection(peer_id, "That home is unavailable.") return if action == NetworkHomeProtocolType.TransitionAction.ENTER: var record := _session.get_peer_record(peer_id) @@ -1052,7 +1106,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: avatar, owner_fingerprint ) ): - _send_transition_rejection(peer_id, "You cannot enter that RV.") + _send_transition_rejection(peer_id, "You cannot enter that home.") return var interior_space: StringName = ( NetworkHomeProtocolType.make_space_id(owner_fingerprint) @@ -1076,7 +1130,7 @@ func _process_transition_request(peer_id: int, data: Dictionary) -> void: avatar, owner_fingerprint ) ): - _send_transition_rejection(peer_id, "Move closer to the RV door.") + _send_transition_rejection(peer_id, "Move closer to the home door.") return _apply_host_transition( peer_id, @@ -1282,6 +1336,10 @@ 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) + _authorized_home_tiers.erase(peer_id) + for result_id: String in _home_upgrade_result_owner.keys(): + if int(_home_upgrade_result_owner[result_id]) == peer_id: + _home_upgrade_result_owner.erase(result_id) if fingerprint.is_empty(): return if _session.is_host(): @@ -1336,6 +1394,8 @@ func _on_session_state_changed(state: NetworkSession.State) -> void: _fingerprint_by_peer.clear() _authorized_owned_counts.clear() _authorization_result_owner.clear() + _authorized_home_tiers.clear() + _home_upgrade_result_owner.clear() _publish_queued = false _last_submitted_local_home_revision = -1 _pending_local_decor_transactions.clear() diff --git a/network/network_protocol.gd b/network/network_protocol.gd index 49aca95..5b3ca89 100644 --- a/network/network_protocol.gd +++ b/network/network_protocol.gd @@ -47,6 +47,7 @@ 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 HOME_UPGRADE_CAPABILITY: String = "home_upgrade_v1" const DEFAULT_WORLD_SEED: int = 13001 const MAX_WORLD_SEED: int = 2147483646 @@ -205,6 +206,7 @@ static func make_client_hello( MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, HOME_CAPABILITY, + HOME_UPGRADE_CAPABILITY, ]), "cosmetic_snapshot": cosmetic_snapshot, "identity_fingerprint": identity_fingerprint, @@ -346,6 +348,7 @@ static func make_server_hello( MOVEMENT_RECONCILIATION_CAPABILITY, FISHING_REPLICATION_CAPABILITY, HOME_CAPABILITY, + HOME_UPGRADE_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", diff --git a/network/network_session.gd b/network/network_session.gd index 66d5101..7c259e0 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -340,6 +340,7 @@ func _register_player_host() -> void: NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, ]), ) _registry.update_appearance(1, _local_appearance_snapshot) @@ -668,6 +669,7 @@ func supports_server_capability(capability: StringName) -> bool: NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, "chat_v1", "mail_v1", "profile_v1", @@ -1638,6 +1640,7 @@ func receive_server_hello(data: Dictionary) -> void: NetworkProtocol.MOVEMENT_RECONCILIATION_CAPABILITY, NetworkProtocol.FISHING_REPLICATION_CAPABILITY, NetworkProtocol.HOME_CAPABILITY, + NetworkProtocol.HOME_UPGRADE_CAPABILITY, ]), ) _registry.update_appearance(local_peer_id, _local_appearance_snapshot) diff --git a/network/network_shop_protocol.gd b/network/network_shop_protocol.gd index da9419a..e58556e 100644 --- a/network/network_shop_protocol.gd +++ b/network/network_shop_protocol.gd @@ -5,6 +5,7 @@ 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 HOME_UPGRADE_CAPABILITY: StringName = &"home_upgrade_v1" const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL const MAX_ID_LENGTH: int = 96 const MAX_MESSAGE_LENGTH: int = 160 @@ -15,7 +16,7 @@ enum ProductCategory { ROD, REEL_SPEED_UPGRADE, BARRIER_POWER_UPGRADE, - COOLER_CAPACITY_UPGRADE, + HOME_UPGRADE, ART_KIT, ART_UPGRADE, BACKPACK_CAPACITY_UPGRADE, diff --git a/network/network_shop_service.gd b/network/network_shop_service.gd index 8294b53..e4d6f9c 100644 --- a/network/network_shop_service.gd +++ b/network/network_shop_service.gd @@ -12,9 +12,10 @@ const DecorCatalogType = preload("res://homes/decor_catalog.gd") const FISHING_SHOP_ID: StringName = &"main_fishing_shop" const DECOR_SHOP_ID: StringName = &"rv_decor_shop" +const RV_UPGRADE_SHOP_ID: StringName = &"rv_upgrade_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" +const HOME_UPGRADE_PRODUCT_ID: StringName = &"starter_rv_upgrade" const BACKPACK_PRODUCT_ID: StringName = &"backpack_capacity_upgrade" const MAX_LEDGER_ENTRIES_PER_PEER: int = 64 @@ -34,6 +35,7 @@ var _spawn_service: PlayerSpawnService var _network_fishing: NetworkFishingService var _interaction: FishingShopInteraction var _decor_interaction: DecorShopInteraction +var _rv_upgrade_interaction: FishingShopInteraction var _wallet: PlayerWallet var _bag: PlayerBag var _item_catalog: ItemCatalog @@ -71,6 +73,7 @@ func setup( home_state: PlayerHomeState = null, network_home: NetworkHomeService = null, decor_interaction: DecorShopInteraction = null, + rv_upgrade_interaction: FishingShopInteraction = null, ) -> void: _session = session _spawn_service = spawn_service @@ -88,6 +91,7 @@ func setup( _home_state = home_state _network_home = network_home _decor_interaction = decor_interaction + _rv_upgrade_interaction = rv_upgrade_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): @@ -104,6 +108,12 @@ func set_decor_shop_interaction( _decor_interaction = interaction +func set_rv_upgrade_shop_interaction( + interaction: FishingShopInteraction, +) -> void: + _rv_upgrade_interaction = interaction + + func can_request_purchase() -> bool: return ( _session != null @@ -154,6 +164,19 @@ func can_request_decor_purchase() -> bool: ) +func can_request_home_upgrade() -> bool: + return ( + can_request_purchase() + and _home_state != null + and ( + _session.is_host() + or _session.supports_server_capability( + NetworkShopProtocol.HOME_UPGRADE_CAPABILITY + ) + ) + ) + + func is_local_purchase_pending() -> bool: return not _pending_local_request.is_empty() @@ -204,12 +227,12 @@ func request_barrier_power_upgrade() -> String: ) -func request_cooler_capacity_upgrade() -> String: +func request_home_upgrade() -> String: return _request_purchase( - COOLER_PRODUCT_ID, - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE, + HOME_UPGRADE_PRODUCT_ID, + NetworkShopProtocol.ProductCategory.HOME_UPGRADE, 1, - _cooler_capacity.get_level() if _cooler_capacity != null else 0 + _home_state.get_tier() if _home_state != null else 0 ) @@ -308,6 +331,15 @@ func _request_purchase( product_id, category, 0, 0, ) return "" + if ( + category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE + and not can_request_home_upgrade() + ): + local_purchase_finished.emit( + "", false, "RV upgrades require a newer server.", + product_id, category, 0, 0, + ) + return "" if ( category == NetworkShopProtocol.ProductCategory.DECOR and not can_request_decor_purchase() @@ -318,11 +350,7 @@ func _request_purchase( ) 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 shop_id: StringName = _shop_id_for_category(category) var request: Dictionary = { "request_id": request_id, "session_id": _session.get_session_id(), @@ -402,9 +430,7 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void: 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 + _interaction_for_shop(requested_shop_id) ) var message: String = ( "The shop is unavailable." @@ -416,16 +442,22 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void: 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"]), - ) + match int(result.get("category", -1)): + NetworkShopProtocol.ProductCategory.DECOR: + _network_home.authorize_decor_purchase( + peer_id, + StringName(str(result["product_id"])), + int(result["resulting_state"]), + str(result["result_id"]), + ) + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + _network_home.authorize_home_upgrade( + peer_id, + int(result["resulting_state"]), + str(result["result_id"]), + ) _pending_by_peer[peer_id] = request_id _record_and_send(peer_id, result) @@ -448,14 +480,19 @@ func _build_authoritative_result( ) if authoritative_count >= 0: current_state = authoritative_count + elif ( + category == NetworkShopProtocol.ProductCategory.HOME_UPGRADE + and _network_home != null + ): + var authoritative_tier: int = ( + _network_home.get_authoritative_home_tier(peer_id) + ) + if authoritative_tier >= 0: + current_state = authoritative_tier var cost: int = -1 var resulting_state: int = current_state var rejection: String = "" - var expected_shop_id: StringName = ( - DECOR_SHOP_ID - if category == NetworkShopProtocol.ProductCategory.DECOR - else FISHING_SHOP_ID - ) + var expected_shop_id: StringName = _shop_id_for_category(category) if StringName(str(request["shop_id"])) != expected_shop_id: rejection = "The shop is unavailable." elif quantity < 1: @@ -569,18 +606,14 @@ func _build_authoritative_result( ] ) resulting_state = current_state + 1 - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE: + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: if ( - product_id != COOLER_PRODUCT_ID - or current_state >= PlayerCoolerCapacity.MAX_LEVEL + product_id != HOME_UPGRADE_PRODUCT_ID + or current_state >= PlayerHomeState.MAX_TIER ): rejection = "Upgrade is already at maximum." else: - cost = ( - PlayerCoolerCapacity.EXPANSION_COSTS[ - current_state - ] - ) + cost = PlayerHomeState.HOME_UPGRADE_COSTS[current_state] resulting_state = current_state + 1 NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: if ( @@ -874,10 +907,12 @@ func _validate_local_result(data: Dictionary) -> String: return "Purchase could not be completed." if _upgrades.get_next_barrier_power_cost() != cost: return "Purchase could not be completed." - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE: - if _cooler_capacity.get_level() != expected_state: - return "Purchase could not be completed." - if _cooler_capacity.get_next_cost() != cost: + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + if ( + product_id != HOME_UPGRADE_PRODUCT_ID + or _home_state.get_tier() != expected_state + or _home_state.get_next_upgrade_cost() != cost + ): return "Purchase could not be completed." NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: if ( @@ -947,8 +982,11 @@ func _apply_local_product(data: Dictionary) -> bool: return _upgrades.purchase_reel_speed(_wallet) NetworkShopProtocol.ProductCategory.BARRIER_POWER_UPGRADE: return _upgrades.purchase_barrier_power(_wallet) - NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE: - return _cooler_capacity.purchase(_wallet) + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + return ( + _wallet.debit(int(data["total_cost"])) + and _home_state.upgrade_to_next_tier() + ) NetworkShopProtocol.ProductCategory.BACKPACK_CAPACITY_UPGRADE: return ( _inventory_layout != null @@ -1061,6 +1099,9 @@ func _handle_acknowledgement( _network_home.finalize_decor_purchase( peer_id, result_id, applied ) + _network_home.finalize_home_upgrade( + peer_id, result_id, applied + ) _acknowledged_results[result_id] = true _bound_dictionary(_acknowledged_results) if _pending_by_peer.get(peer_id, "") == request_id: @@ -1071,11 +1112,7 @@ 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 - ) + var interaction: FishingShopInteraction = _interaction_for_shop(shop_id) if ( _session == null or not _session.is_host() @@ -1097,6 +1134,24 @@ func _is_shop_available_for_peer( ) +func _shop_id_for_category(category: int) -> StringName: + match category: + NetworkShopProtocol.ProductCategory.DECOR: + return DECOR_SHOP_ID + NetworkShopProtocol.ProductCategory.HOME_UPGRADE: + return RV_UPGRADE_SHOP_ID + return FISHING_SHOP_ID + + +func _interaction_for_shop(shop_id: StringName) -> FishingShopInteraction: + match shop_id: + DECOR_SHOP_ID: + return _decor_interaction + RV_UPGRADE_SHOP_ID: + return _rv_upgrade_interaction + return _interaction + + func _on_peer_removed(peer_id: int) -> void: _request_ledgers.erase(peer_id) _pending_by_peer.erase(peer_id) diff --git a/player/player.gd b/player/player.gd index e36f92b..78dac3d 100644 --- a/player/player.gd +++ b/player/player.gd @@ -90,6 +90,7 @@ const CATCH_SHOWCASE_INPUT_OWNER: StringName = &"catch_showcase" # safe IDs so newer clients can extend it, but Player only presents actions # explicitly approved by this catalog. const NETWORK_ANIMATION_ACTION_IDS: Array[StringName] = [ + CHARACTER_WALKING_ANIMATION, CHARACTER_CASTING_ANIMATION, CHARACTER_NET_DRAW_ANIMATION, CHARACTER_NET_STRIKE_ANIMATION, @@ -436,6 +437,11 @@ var _rv_previous_camera_environment: Environment @export var maximum_zoom: float = 8.0 @export var zoom_step: float = 0.75 @export var zoom_smoothing: float = 12.0 +@export_range(0.0, 1.0, 0.01) var fishing_reel_zoom_amount: float = 0.22 +@export_range(0.0, 1.5, 0.01) var fishing_barrier_break_zoom: float = 0.38 +@export_range(0.05, 1.0, 0.01) var fishing_break_zoom_decay: float = 0.34 +@export_range(0.0, 0.2, 0.005) var fishing_barrier_hit_shake: float = 0.018 +@export_range(0.0, 0.3, 0.005) var fishing_barrier_break_shake: float = 0.045 @export_range(0.1, 12.0, 0.1) var controller_zoom_speed: float = 4.0 @export_range(0.0, 1.0, 0.01) var controller_trigger_threshold: float = 0.55 @export_range(1.0, 40.0, 0.5) var free_camera_speed: float = 10.0 @@ -497,6 +503,12 @@ var _remote_recovery_presentation_active: bool = false var _remote_recovery_visual_origin: Vector3 var _remote_recovery_elapsed: float = 0.0 var _target_zoom: float = 5.0 +var _fishing_reel_camera_active: bool = false +var _fishing_break_zoom_offset: float = 0.0 +var _fishing_camera_shake_remaining: float = 0.0 +var _fishing_camera_shake_duration: float = 0.0 +var _fishing_camera_shake_strength: float = 0.0 +var _fishing_camera_shake_phase: float = 0.0 var _showcase_rod_visibility: bool = true var _showcase_net_visibility: bool = false var _showcase_shovel_visibility: bool = false @@ -1162,6 +1174,7 @@ func _process(delta: float) -> void: ) if not local_control_enabled: return + _update_fishing_camera_effects(delta) if _free_camera_active: if _is_camera_input_enabled(): _rotate_free_camera( @@ -1190,13 +1203,68 @@ func _process(delta: float) -> void: ) var zoom_weight: float = 1.0 - exp(-zoom_smoothing * delta) + var fishing_zoom_offset: float = ( + fishing_reel_zoom_amount + if _fishing_reel_camera_active + else 0.0 + ) + _fishing_break_zoom_offset + var effective_target_zoom: float = clampf( + _target_zoom - fishing_zoom_offset, + 1.25 if _rv_interior_camera_mode else minimum_zoom, + 16.0 if _rv_interior_camera_mode else maximum_zoom, + ) _spring_arm.spring_length = lerpf( _spring_arm.spring_length, - _target_zoom, + effective_target_zoom, zoom_weight ) +func set_fishing_reel_camera_active(active: bool) -> void: + _fishing_reel_camera_active = active + + +func play_fishing_barrier_camera_feedback(barrier_broken: bool) -> void: + if not local_control_enabled or _free_camera_active: + return + _fishing_camera_shake_strength = ( + fishing_barrier_break_shake + if barrier_broken + else fishing_barrier_hit_shake + ) + _fishing_camera_shake_duration = 0.20 if barrier_broken else 0.10 + _fishing_camera_shake_remaining = _fishing_camera_shake_duration + if barrier_broken: + _fishing_break_zoom_offset = fishing_barrier_break_zoom + + +func _update_fishing_camera_effects(delta: float) -> void: + _fishing_break_zoom_offset = move_toward( + _fishing_break_zoom_offset, + 0.0, + ( + fishing_barrier_break_zoom + / maxf(fishing_break_zoom_decay, 0.05) + ) * delta, + ) + if _fishing_camera_shake_remaining <= 0.0: + _camera.h_offset = 0.0 + _camera.v_offset = 0.0 + return + _fishing_camera_shake_remaining = maxf( + _fishing_camera_shake_remaining - delta, + 0.0, + ) + _fishing_camera_shake_phase += delta * 92.0 + var decay: float = ( + _fishing_camera_shake_remaining + / maxf(_fishing_camera_shake_duration, 0.001) + ) + var strength: float = _fishing_camera_shake_strength * decay + _camera.h_offset = sin(_fishing_camera_shake_phase) * strength + _camera.v_offset = cos(_fishing_camera_shake_phase * 1.31) * strength * 0.7 + + func _update_blink(delta: float) -> void: if _fighting_visual_active: return @@ -3052,6 +3120,19 @@ func set_camera_active(active: bool) -> void: _camera.current = active +func face_world_direction(direction: Vector3) -> void: + var horizontal_direction := Vector3(direction.x, 0.0, direction.z) + if horizontal_direction.is_zero_approx() or _visuals == null: + return + horizontal_direction = horizontal_direction.normalized() + var target_world_yaw: float = atan2( + -horizontal_direction.x, + -horizontal_direction.z, + ) + _visuals.rotation.y = target_world_yaw - global_rotation.y + _network_target_visual_yaw = _visuals.rotation.y + + func apply_camera_settings( new_mouse_sensitivity: float, new_controller_sensitivity: float, diff --git a/progression/player_cooler_capacity.gd b/progression/player_cooler_capacity.gd index b8c62be..bbd0b87 100644 --- a/progression/player_cooler_capacity.gd +++ b/progression/player_cooler_capacity.gd @@ -2,62 +2,77 @@ class_name PlayerCoolerCapacity extends Node const PlayerWalletType = preload("res://economy/player_wallet.gd") +const PlayerHomeStateType = preload("res://homes/player_home_state.gd") signal capacity_changed(level: int, capacity: int) -## Kept under the historical class name so existing saves and network product -## identifiers migrate without losing their purchased level. This progression -## now controls the player's private storage box rather than carried catches. +## Compatibility adapter for saves and systems that still refer to the old +## cooler-capacity object. Storage is now derived exclusively from the player's +## home tier; this object is not independently purchasable. const CAPACITIES: Array[int] = [9, 18, 27, 36, 45, 54, 63, 72] const EXPANSION_COSTS: Array[int] = [75, 175, 400, 850, 1500, 2500, 4000] const MAX_LEVEL: int = 7 const MAX_CAPACITY: int = 72 -var _capacity_level: int = 0 +var _home_state: PlayerHomeStateType +var _legacy_capacity_level: int = 0 + + +func bind_home_state(home_state: PlayerHomeStateType) -> void: + if _home_state == home_state: + return + if ( + _home_state != null + and _home_state.changed.is_connected(_on_home_changed) + ): + _home_state.changed.disconnect(_on_home_changed) + _home_state = home_state + if ( + _home_state != null + and not _home_state.changed.is_connected(_on_home_changed) + ): + _home_state.changed.connect(_on_home_changed) + _on_home_changed() func get_level() -> int: - return _capacity_level + return _legacy_capacity_level func get_capacity() -> int: - return CAPACITIES[_capacity_level] + if _home_state != null: + # A new home upgrade grants its authored capacity. Older saves retain any + # separately purchased storage level so reducing the first RV tier cannot + # hide or discard items that were already placed in later slots. + return maxi( + _home_state.get_storage_capacity(), + CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)], + ) + return CAPACITIES[clampi(_legacy_capacity_level, 0, MAX_LEVEL)] func get_next_capacity() -> int: - if _capacity_level >= MAX_LEVEL: - return -1 - return CAPACITIES[_capacity_level + 1] + return -1 func get_next_cost() -> int: - if _capacity_level >= MAX_LEVEL: - return -1 - return EXPANSION_COSTS[_capacity_level] + return -1 -func can_purchase(wallet: PlayerWalletType) -> bool: - var cost: int = get_next_cost() - return wallet != null and cost >= 0 and wallet.can_afford(cost) +func can_purchase(_wallet: PlayerWalletType) -> bool: + return false -func purchase(wallet: PlayerWalletType) -> bool: - if not can_purchase(wallet): - return false - var cost: int = get_next_cost() - if not wallet.debit(cost): - return false - _capacity_level += 1 - capacity_changed.emit(_capacity_level, get_capacity()) - return true +func purchase(_wallet: PlayerWalletType) -> bool: + return false func restore_level(level: int) -> bool: var validated: int = clampi(level, 0, MAX_LEVEL) - var changed: bool = _capacity_level != validated - _capacity_level = validated + var changed: bool = _legacy_capacity_level != validated + _legacy_capacity_level = validated if changed: - capacity_changed.emit(_capacity_level, get_capacity()) + capacity_changed.emit(get_level(), get_capacity()) return true @@ -66,4 +81,8 @@ func reset_to_defaults() -> void: func to_save_data() -> Dictionary: - return {"capacity_level": _capacity_level} + return {"capacity_level": get_level()} + + +func _on_home_changed() -> void: + capacity_changed.emit(get_level(), get_capacity()) diff --git a/save/player_save_manager.gd b/save/player_save_manager.gd index 56fd49c..c41f861 100644 --- a/save/player_save_manager.gd +++ b/save/player_save_manager.gd @@ -338,6 +338,11 @@ func load_player_data() -> bool: snapshot.reel_speed_level, snapshot.barrier_power_level ) + # Home tier is now the authority for private-storage capacity, so restore it + # before the compatibility capacity adapter and inventory layout. + var home_restored: bool = _home_state.restore_from_save_data( + snapshot.home_data + ) var cooler_restored: bool = _cooler_capacity.restore_level( snapshot.cooler_capacity_level ) @@ -368,9 +373,6 @@ 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 @@ -1324,6 +1326,26 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot: PlayerCoolerCapacityType.MAX_LEVEL, "cooler.capacity_level" ) + # Before homes had upgrade tiers, storage was purchased separately. Preserve + # those saves by granting the starter RV to anybody who owned at least one + # legacy expansion. PlayerCoolerCapacity also retains the purchased legacy + # capacity so entries beyond the new RV's first 18 slots remain accessible. + if ( + snapshot.cooler_capacity_level > 0 + and int(snapshot.home_data.get("tier", 0)) + == PlayerHomeStateType.STARTER_TIER + ): + var migrated_home: Dictionary = home_data.duplicate(true) + migrated_home["tier"] = PlayerHomeStateType.STARTER_RV_TIER + migrated_home["exterior_id"] = String( + PlayerHomeStateType.STARTER_RV_EXTERIOR_ID + ) + var sanitized_migrated_home := ( + PlayerHomeStateType.sanitize_save_data(migrated_home) + ) + if sanitized_migrated_home.is_empty(): + return null + snapshot.home_data = sanitized_migrated_home snapshot.art_unlock_mask = _read_integer( art_data.get("unlock_mask"), 0, diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index ff82495..0728d82 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -53,7 +53,7 @@ func _run() -> void: ) assert(player != null) assert(catalog != null and catalog.candidates.size() == 316) - assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 313) + assert(LogbookCatalog.ordered_species(catalog.candidates).size() == 314) assert(sale_service != null) assert(shop_service != null) assert(session != null and session.is_host()) @@ -82,7 +82,7 @@ func _run() -> void: main, player, catalog, sale_service, reservations ) _test_host_backpack_purchase(player, shop_service) - _test_host_storage_purchase(player, shop_service) + await _test_host_home_upgrade(main, player, shop_service) assert(not sale_service.is_local_sale_pending()) assert(not shop_service.is_local_purchase_pending()) @@ -175,7 +175,6 @@ func _run_multiplayer_client() -> void: assert(player.inventory_layout.restore_backpack_level( PlayerInventoryLayout.MAX_BACKPACK_LEVEL )) - assert(player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL)) player.inventory.set_inventory_layout(null) var catch_ids: Array[StringName] = [] for fish: FishData in catalog.candidates: @@ -202,7 +201,6 @@ func _run_multiplayer_client() -> void: assert(not player.inventory.contains_catch_id(catch_id)) assert(not sale_service.is_local_sale_pending()) assert(player.inventory_layout.restore_backpack_level(0)) - assert(player.cooler_capacity.restore_level(0)) var interaction := main.get("_shop_interaction") as FishingShopInteraction player.global_position = interaction.global_position @@ -567,27 +565,49 @@ func _test_host_backpack_purchase( assert(layout.get_next_backpack_cost() == -1) -func _test_host_storage_purchase( +func _test_host_home_upgrade( + main: Node, player: Player, shop_service: NetworkShopService, ) -> void: + var interaction := main.get( + "_rv_upgrade_shop_interaction" + ) as RVUpgradeInteraction + var home_state := main.get("_home_state") as PlayerHomeState var capacity := player.cooler_capacity + assert(interaction != null and home_state != null) + player.global_position = interaction.global_position + for _frame: int in 4: + await physics_frame + assert(interaction.is_avatar_in_range(player)) + assert(home_state.get_tier() == PlayerHomeState.STARTER_TIER) + assert( + interaction.get_dialogue_text(home_state.get_tier()) + == RVUpgradeInteraction.TENT_DIALOGUE_TEXT + ) assert(capacity.get_capacity() == 9) - var total_cost: int = 0 - for cost: int in PlayerCoolerCapacity.EXPANSION_COSTS: - total_cost += cost - if player.wallet.get_balance() < total_cost: - assert(player.wallet.credit(total_cost - player.wallet.get_balance())) + var upgrade_cost: int = home_state.get_next_upgrade_cost() + assert(upgrade_cost == PlayerHomeState.HOME_UPGRADE_COSTS.front()) + assert(upgrade_cost == 50) + if player.wallet.get_balance() < upgrade_cost: + assert(player.wallet.credit( + upgrade_cost - player.wallet.get_balance() + )) var balance_before := player.wallet.get_balance() - for expected_capacity: int in [18, 27, 36, 45, 54, 63, 72]: - _shop_result.clear() - assert(not shop_service.request_cooler_capacity_upgrade().is_empty()) - assert( - not _shop_result.is_empty() and bool(_shop_result[1]), - "Cooler purchase failed: %s" % [_shop_result], - ) - assert(capacity.get_capacity() == expected_capacity) - assert(player.wallet.get_balance() == balance_before - total_cost) + _shop_result.clear() + assert(not shop_service.request_home_upgrade().is_empty()) + assert( + not _shop_result.is_empty() and bool(_shop_result[1]), + "Home purchase failed: %s" % [_shop_result], + ) + assert(home_state.get_tier() == PlayerHomeState.STARTER_RV_TIER) + assert( + interaction.get_dialogue_text(home_state.get_tier()) + == RVUpgradeInteraction.RV_DIALOGUE_TEXT + ) + assert(capacity.get_capacity() == 18) + assert(player.wallet.get_balance() == balance_before - upgrade_cost) + assert(home_state.get_next_upgrade_cost() == -1) assert(capacity.get_next_cost() == -1) diff --git a/tests/home_spawn_multiplayer_validation.gd b/tests/home_spawn_multiplayer_validation.gd index 1f16147..2855eb2 100644 --- a/tests/home_spawn_multiplayer_validation.gd +++ b/tests/home_spawn_multiplayer_validation.gd @@ -49,6 +49,7 @@ func _run_host() -> void: host_fingerprint ) assert(_same_door_position(host_avatar.global_position, host_spawn.origin)) + assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint)) assert(network_home.request_enter(host_fingerprint)) await physics_frame host_avatar.global_position = home_world.get_interior_exit_position( @@ -57,6 +58,7 @@ func _run_host() -> void: assert(network_home.request_exit()) await physics_frame assert(_same_door_position(host_avatar.global_position, host_spawn.origin)) + assert(_faces_away_from_home(host_avatar, home_world, host_fingerprint)) var remote_peer_id: int = await _wait_for_remote_peer(session) assert(remote_peer_id > 1) @@ -70,6 +72,9 @@ func _run_host() -> void: assert(_same_door_position( remote_avatar.global_position, remote_spawn.origin )) + assert(_faces_away_from_home( + remote_avatar, home_world, remote_fingerprint + )) var disconnect_deadline: int = Time.get_ticks_msec() + 12000 while ( @@ -118,6 +123,7 @@ func _run_client() -> void: assert(_same_door_position( player.global_position, exterior_spawn.origin )) + assert(_faces_away_from_home(player, home_world, fingerprint)) print("Home spawn multiplayer client validation: PASS") await _cleanup(main, session) @@ -151,6 +157,23 @@ func _wait_for_home( return false +func _faces_away_from_home( + avatar: Player, + home_world: HomeWorldService, + fingerprint: String, +) -> bool: + var exterior_basis: Basis = home_world.get_exterior_transform( + fingerprint + ).basis + var outward_direction := exterior_basis.z + outward_direction.y = 0.0 + if outward_direction.is_zero_approx(): + return false + return avatar.get_facing_direction().dot( + outward_direction.normalized() + ) >= 0.99 + + func _wait_for_remote_peer(session: NetworkSession) -> int: var deadline: int = Time.get_ticks_msec() + 20000 while Time.get_ticks_msec() < deadline: diff --git a/tests/logbook_runtime_validation.gd b/tests/logbook_runtime_validation.gd index 47e795d..e33e551 100644 --- a/tests/logbook_runtime_validation.gd +++ b/tests/logbook_runtime_validation.gd @@ -143,7 +143,7 @@ func _validate_save_round_trip( assert(catalog != null) assert(catalog.candidates.size() == 316) var active_species := LogbookCatalog.ordered_species(catalog.candidates) - assert(active_species.size() == 313) + assert(active_species.size() == 314) # Keep this runtime save fixture within maximum player capacity while the # focused catalog validation exercises every active species individually. var round_trip_species: Array[FishData] = [] @@ -164,12 +164,14 @@ func _validate_save_round_trip( player.inventory_layout.restore_backpack_level( PlayerInventoryLayout.MAX_BACKPACK_LEVEL ) - player.cooler_capacity.restore_level(PlayerCoolerCapacity.MAX_LEVEL) + var home_state := main.get("_home_state") as PlayerHomeState + assert(home_state != null) + assert(home_state.upgrade_to_next_tier()) assert( player.inventory_layout.get_backpack_level() == PlayerInventoryLayout.MAX_BACKPACK_LEVEL ) - assert(player.cooler_capacity.get_level() == PlayerCoolerCapacity.MAX_LEVEL) + assert(player.cooler_capacity.get_capacity() == 18) player.inventory.set_inventory_layout(null) for index: int in 4: _add_test_catch(player, round_trip_species[index]) diff --git a/ui/fishing_danger_vignette.gdshader b/ui/fishing_danger_vignette.gdshader new file mode 100644 index 0000000..8eee3df --- /dev/null +++ b/ui/fishing_danger_vignette.gdshader @@ -0,0 +1,10 @@ +shader_type canvas_item; + +uniform float intensity : hint_range(0.0, 1.0) = 0.0; + +void fragment() { + vec2 centered = UV * 2.0 - 1.0; + centered.x *= 0.56; + float edge = smoothstep(0.42, 1.02, length(centered)); + COLOR = vec4(0.16, 0.018, 0.025, edge * intensity * 0.72); +} diff --git a/ui/fishing_danger_vignette.gdshader.uid b/ui/fishing_danger_vignette.gdshader.uid new file mode 100644 index 0000000..578ec8b --- /dev/null +++ b/ui/fishing_danger_vignette.gdshader.uid @@ -0,0 +1 @@ +uid://c1qcj3m4b10x4 diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 234a1c1..7cc8fa8 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -15,9 +15,6 @@ const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" ) const PlayerType = preload("res://player/player.gd") -const PlayerCoolerCapacityType = preload( - "res://progression/player_cooler_capacity.gd" -) const PlayerArtUnlocksType = preload( "res://progression/player_art_unlocks.gd" ) @@ -154,9 +151,6 @@ const ROD_PRICE_ICON_SIZE: float = 22.0 @onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble @onready var _barrier_purchase: Button = %BarrierPurchase @onready var _supplies_list: VBoxContainer = %SuppliesList -@onready var _cooler_cost: CurrencyAmount = %CoolerCost -@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble -@onready var _cooler_purchase: Button = %CoolerPurchase @onready var _backpack_cost: CurrencyAmount = %BackpackCost @onready var _backpack_price_bubble: PanelContainer = %BackpackPriceBubble @onready var _backpack_purchase: Button = %BackpackPurchase @@ -172,7 +166,6 @@ var _inventory: FishInventory var _hotbar: PlayerHotbar var _inventory_layout: PlayerInventoryLayout var _item_catalog: ItemCatalogType -var _cooler_capacity: PlayerCoolerCapacityType var _art_unlocks: PlayerArtUnlocksType var _network_shop: NetworkShopService var _network_sale: NetworkSaleService @@ -202,7 +195,6 @@ func _ready() -> void: %CloseButton.pressed.connect(close_shop) _reel_purchase.pressed.connect(_purchase_reel_speed) _barrier_purchase.pressed.connect(_purchase_barrier_power) - _cooler_purchase.pressed.connect(_purchase_cooler_capacity) _backpack_purchase.pressed.connect(_purchase_backpack_capacity) @@ -353,7 +345,6 @@ func _focus_shop_section() -> void: for upgrade_button: Button in [ _reel_purchase, _barrier_purchase, - _cooler_purchase, _backpack_purchase, ]: if not upgrade_button.disabled: @@ -509,7 +500,6 @@ func _apply_shop_styles() -> void: for panel: PanelContainer in [ _reel_price_bubble, _barrier_price_bubble, - _cooler_price_bubble, _backpack_price_bubble, ]: var price_style := UtilityPageStyleType.rounded_style( @@ -526,7 +516,6 @@ func _apply_shop_styles() -> void: for price: CurrencyAmount in [ _reel_cost, _barrier_cost, - _cooler_cost, _backpack_cost, ]: var amount_label := price.get_amount_label() @@ -538,7 +527,6 @@ func _apply_shop_styles() -> void: %CloseButton, _reel_purchase, _barrier_purchase, - _cooler_purchase, _backpack_purchase, ]: UtilityPageStyleType.apply_ocean_button(button) @@ -559,7 +547,6 @@ func setup( hotbar: PlayerHotbar, inventory_layout: PlayerInventoryLayout, item_catalog: ItemCatalogType, - cooler_capacity: PlayerCoolerCapacityType, art_unlocks: PlayerArtUnlocksType, network_shop: NetworkShopService, network_sale: NetworkSaleService, @@ -576,7 +563,6 @@ func setup( _hotbar = hotbar _inventory_layout = inventory_layout _item_catalog = item_catalog - _cooler_capacity = cooler_capacity _art_unlocks = art_unlocks _network_shop = network_shop _network_sale = network_sale @@ -612,10 +598,6 @@ func setup( _upgrades.upgrades_changed.connect(_on_upgrades_changed) if not _bag.contents_changed.is_connected(_on_bag_changed): _bag.contents_changed.connect(_on_bag_changed) - if not _cooler_capacity.capacity_changed.is_connected( - _on_cooler_capacity_changed - ): - _cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed) if not _inventory_layout.backpack_capacity_changed.is_connected( _on_backpack_capacity_changed ): @@ -836,7 +818,6 @@ func _refresh_all() -> void: _refresh_wallet() _refresh_upgrades() _refresh_supplies() - _refresh_cooler_capacity() _refresh_backpack_capacity() @@ -1587,41 +1568,6 @@ func _add_supply_icon_tile( return tile_host -func _refresh_cooler_capacity() -> void: - if _cooler_capacity == null: - return - var level: int = _cooler_capacity.get_level() - var cost: int = _cooler_capacity.get_next_cost() - var next_capacity: int = _cooler_capacity.get_next_capacity() - _cooler_purchase.disabled = ( - cost < 0 - or _transaction_in_progress - or _closing - or _network_shop == null - or not _network_shop.can_request_purchase() - or not _cooler_capacity.can_purchase(_wallet) - ) - var cooler_effect: String - if cost < 0: - cooler_effect = "%d slots · maximum level" % ( - _cooler_capacity.get_capacity() - ) - _cooler_price_bubble.hide() - else: - _cooler_price_bubble.show() - cooler_effect = "%d → %d slots" % [ - _cooler_capacity.get_capacity(), - next_capacity, - ] - _cooler_cost.set_amount(cost) - _cooler_purchase.tooltip_text = _upgrade_tooltip( - "storage capacity", - level, - cooler_effect, - ) - _cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text - - func _refresh_backpack_capacity() -> void: if _inventory_layout == null: return @@ -1723,23 +1669,6 @@ func _purchase_art_upgrade(product_id: StringName) -> void: _network_shop.request_art_upgrade(product_id) -func _purchase_cooler_capacity() -> void: - if _transaction_in_progress or not _is_transaction_context_valid(): - _set_feedback("unable to complete purchase.") - return - var cost: int = _cooler_capacity.get_next_cost() - if cost < 0: - _set_feedback("Upgrade is already at maximum.") - return - if not _wallet.can_afford(cost): - _set_feedback("Insufficient funds.") - return - if _network_shop == null: - _set_feedback("Purchase could not be completed.") - return - _network_shop.request_cooler_capacity_upgrade() - - func _purchase_backpack_capacity() -> void: if _transaction_in_progress or not _is_transaction_context_valid(): _set_feedback("unable to complete purchase.") @@ -1860,7 +1789,6 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void: _refresh_wallet() _refresh_upgrades() _refresh_supplies() - _refresh_cooler_capacity() _refresh_backpack_capacity() @@ -1875,10 +1803,6 @@ func _on_bag_changed() -> void: _refresh_supplies() -func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: - _refresh_cooler_capacity() - - func _on_backpack_capacity_changed(_level: int, _capacity: int) -> void: _refresh_backpack_capacity() diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 70ccec1..66fcbd9 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -257,35 +257,6 @@ layout_mode = 2 amount = 100 icon_size = 24.0 -[node name="CoolerTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"] -custom_minimum_size = Vector2(144, 168) -layout_mode = 2 -mouse_filter = 2 - -[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"] -unique_name_in_owner = true -offset_right = 144.0 -offset_bottom = 144.0 -tooltip_text = "storage capacity" -icon = ExtResource("7_close") -expand_icon = true -icon_max_width = 92 - -[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"] -unique_name_in_owner = true -z_index = 2 -offset_left = 22.0 -offset_top = 128.0 -offset_right = 122.0 -offset_bottom = 160.0 -mouse_filter = 2 - -[node name="CoolerCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile/CoolerPriceBubble" instance=ExtResource("8_currency")] -unique_name_in_owner = true -layout_mode = 2 -amount = 75 -icon_size = 24.0 - [node name="BackpackTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"] custom_minimum_size = Vector2(144, 168) layout_mode = 2 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 585efc9..55a07eb 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -21,6 +21,7 @@ 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 RVUpgradeShopType = preload("res://ui/rv_upgrade_shop.gd") const SettingsPanelType = preload("res://ui/settings_panel.gd") const PlayerFishingUpgradesType = preload( "res://progression/player_fishing_upgrades.gd" @@ -31,6 +32,9 @@ const ShopInteractionType = preload( const DecorShopInteractionType = preload( "res://world/decor_shop_interaction.gd" ) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PlayerStorageType = preload("res://ui/player_storage.gd") const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd") const PlayerStorageInteractionType = preload( @@ -129,6 +133,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0 @onready var _barrier_health: Label = %BarrierHealth @onready var _showcase_details: Label = %ShowcaseDetails @onready var _fishing_panel: PanelContainer = %FishingPanel +@onready var _fishing_danger_vignette: ColorRect = %FishingDangerVignette @onready var _experience_panel: PanelContainer = %ExperienceProgressPanel @onready var _experience_level_label: Label = %ExperienceLevelLabel @onready var _experience_award_label: Label = %ExperienceAwardLabel @@ -144,6 +149,8 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural" const SHOP_ANIMALESE_BASE_PITCH: float = 1.08 const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0 const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 +const FISHING_DANGER_START_GAP: float = 0.15 +const FISHING_DANGER_FULL_GAP: float = 0.025 @onready var _canonical_stage: Control = %CanonicalStage @@ -155,6 +162,7 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000 @onready var _hotbar_ui: HotbarUIType = %Hotbar @onready var _fishing_shop: FishingShopType = %FishingShop @onready var _decor_shop: DecorShopType = %DecorShop +@onready var _rv_upgrade_shop: RVUpgradeShopType = %RVUpgradeShop @onready var _player_storage: PlayerStorageType = %PlayerStorage @onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar @onready var _rv_storage_button: Button = %RVStorageButton @@ -208,6 +216,7 @@ var _player_menu_hotbar_visible: bool = false var _main_shop_buyer: FishBuyerProfileType var _shop_interaction: ShopInteractionType var _decor_shop_interaction: DecorShopInteractionType +var _rv_upgrade_shop_interaction: RVUpgradeInteractionType var _storage_interaction: PlayerStorageInteractionType var _surface_drawing: NetworkSurfaceDrawingService var _surface_drawing_hotbar_selected: bool = false @@ -235,6 +244,12 @@ 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 +var _previous_barrier_health := PackedInt32Array() +var _barrier_feedback_initialized: bool = false +var _fishing_danger_target: float = 0.0 +var _fishing_danger_intensity: float = 0.0 +var _fishing_danger_pulse_time: float = 0.0 +var _catch_track_shake_tween: Tween func _ready() -> void: @@ -363,6 +378,7 @@ func setup( shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, decor_shop_interaction: DecorShopInteractionType, + rv_upgrade_shop_interaction: RVUpgradeInteractionType, item_effects: PlayerItemEffectsType, cooler_capacity: PlayerCoolerCapacityType, network_session: NetworkSessionType, @@ -506,7 +522,6 @@ func setup( hotbar, inventory_layout, item_catalog, - cooler_capacity, art_unlocks, network_shop_service, network_sale_service, @@ -521,6 +536,14 @@ func setup( home_state, network_shop_service, ) + _rv_upgrade_shop.setup( + player, + wallet, + fishing_spot, + rv_upgrade_shop_interaction, + home_state, + network_shop_service, + ) _player_storage.setup( player, fishing_spot, @@ -538,6 +561,8 @@ func setup( _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) + _rv_upgrade_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _rv_upgrade_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 @@ -548,6 +573,7 @@ func setup( _main_shop_buyer = main_shop_buyer _shop_interaction = shop_interaction _decor_shop_interaction = decor_shop_interaction + _rv_upgrade_shop_interaction = rv_upgrade_shop_interaction _storage_interaction = storage_interaction _surface_drawing = surface_drawing if ( @@ -571,13 +597,16 @@ func set_world_interactions( shop_interaction: ShopInteractionType, storage_interaction: PlayerStorageInteractionType, decor_shop_interaction: DecorShopInteractionType, + rv_upgrade_shop_interaction: RVUpgradeInteractionType, ) -> void: _shop_interaction = shop_interaction _storage_interaction = storage_interaction _decor_shop_interaction = decor_shop_interaction + _rv_upgrade_shop_interaction = rv_upgrade_shop_interaction _fishing_shop.set_world_interaction(shop_interaction) _player_storage.set_world_interaction(storage_interaction) _decor_shop.set_world_interaction(decor_shop_interaction) + _rv_upgrade_shop.set_world_interaction(rv_upgrade_shop_interaction) func _prioritize_surface_drawing_pointer_input() -> void: @@ -1539,6 +1568,13 @@ func _process(delta: float) -> void: _update_virtual_mouse(delta) _update_controller_menu_scroll(delta) _update_experience_bubble_position() + _update_fishing_danger_vignette(delta) + _try_start_shop_npc_speech() + if _player != null: + _player.set_fishing_reel_camera_active( + _fishing_spot != null + and _fishing_spot.is_reel_input_active() + ) func _update_controller_menu_scroll(delta: float) -> void: @@ -1793,6 +1829,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void: close_player_menu_for_session_end() _fishing_shop.close_for_session_end() _decor_shop.close_for_session_end() + _rv_upgrade_shop.close_for_session_end() _player_storage.close_for_session_end() _fishing_panel.visible = false _shop_prompt.hide() @@ -1933,6 +1970,10 @@ func get_decor_shop() -> DecorShopType: return _decor_shop +func get_rv_upgrade_shop() -> RVUpgradeShopType: + return _rv_upgrade_shop + + func get_player_storage() -> PlayerStorageType: return _player_storage @@ -2027,8 +2068,19 @@ func set_shop_prompt_visible( _position_shop_prompt(world_anchor) -func set_shop_npc_player_in_range(in_range: bool) -> void: - if _shop_npc_player_in_range == in_range: +func set_shop_npc_player_in_range( + in_range: bool, + dialogue_text: String = "", +) -> void: + var dialogue_changed: bool = ( + not dialogue_text.is_empty() + and _shop_prompt_message.text != dialogue_text + ) + if dialogue_changed: + _shop_prompt_message.text = dialogue_text + _shop_prompt_message.visible_characters = -1 + _shop_npc_spoken_for_current_visit = false + if _shop_npc_player_in_range == in_range and not dialogue_changed: return _shop_npc_player_in_range = in_range if not in_range: @@ -2044,10 +2096,10 @@ func _try_start_shop_npc_speech() -> void: or not _gameplay_ui_enabled ): return - _shop_npc_spoken_for_current_visit = true var now_msec: int = Time.get_ticks_msec() if now_msec < _shop_npc_next_speech_msec: return + _shop_npc_spoken_for_current_visit = true _shop_npc_next_speech_msec = ( now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS ) @@ -2310,6 +2362,12 @@ func _on_catch_display_changed( and _fishing_spot != null and _fishing_spot.is_fighting() ) + _update_fishing_feedback( + progress, + chase_progress, + barrier_health, + encounter_visible, + ) _green_catch_progress.value = progress * 100.0 _red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0 _catch_track.visible = encounter_visible @@ -2346,6 +2404,93 @@ func _on_catch_display_changed( _refresh_fishing_panel_visibility() +func _update_fishing_feedback( + progress: float, + chase_progress: float, + barrier_health: PackedInt32Array, + encounter_visible: bool, +) -> void: + if not encounter_visible: + _previous_barrier_health.clear() + _barrier_feedback_initialized = false + _fishing_danger_target = 0.0 + return + if _barrier_feedback_initialized: + var comparison_count: int = mini( + _previous_barrier_health.size(), + barrier_health.size(), + ) + var barrier_hit: bool = false + var barrier_broken: bool = false + for barrier_index: int in range(comparison_count): + if ( + barrier_health[barrier_index] + >= _previous_barrier_health[barrier_index] + ): + continue + barrier_hit = true + barrier_broken = ( + barrier_broken + or barrier_health[barrier_index] <= 0 + ) + if barrier_hit: + _play_barrier_feedback(barrier_broken) + _previous_barrier_health = barrier_health.duplicate() + _barrier_feedback_initialized = true + var chase_gap: float = progress - maxf(chase_progress, 0.0) + _fishing_danger_target = clampf( + inverse_lerp( + FISHING_DANGER_START_GAP, + FISHING_DANGER_FULL_GAP, + chase_gap, + ), + 0.0, + 1.0, + ) + + +func _play_barrier_feedback(barrier_broken: bool) -> void: + if _player != null: + _player.play_fishing_barrier_camera_feedback(barrier_broken) + if _catch_track_shake_tween != null: + _catch_track_shake_tween.kill() + _catch_track.rotation = 0.0 + var tilt: float = 0.018 if barrier_broken else 0.008 + var beat: float = 0.038 if barrier_broken else 0.028 + _catch_track_shake_tween = create_tween() + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", -tilt, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", tilt, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", -tilt * 0.45, beat + ) + _catch_track_shake_tween.tween_property( + _catch_track, "rotation", 0.0, beat + ) + + +func _update_fishing_danger_vignette(delta: float) -> void: + _fishing_danger_pulse_time += delta + _fishing_danger_intensity = lerpf( + _fishing_danger_intensity, + _fishing_danger_target, + 1.0 - exp(-8.0 * delta), + ) + var pulse: float = ( + 0.88 + sin(_fishing_danger_pulse_time * 5.5) * 0.12 + ) + var shown_intensity: float = _fishing_danger_intensity * pulse + _fishing_danger_vignette.visible = shown_intensity > 0.01 + var vignette_material := ( + _fishing_danger_vignette.material as ShaderMaterial + ) + if vignette_material != null: + vignette_material.set_shader_parameter("intensity", shown_intensity) + + func _update_barrier_markers( positions: PackedFloat32Array, health: PackedInt32Array, diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index c219c84..c5dea31 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=24 format=3] +[gd_scene load_steps=27 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"] @@ -17,6 +17,11 @@ [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"] +[ext_resource type="PackedScene" path="res://ui/rv_upgrade_shop.tscn" id="18_rv_upgrade_shop"] +[ext_resource type="Shader" path="res://ui/fishing_danger_vignette.gdshader" id="19_fishing_vignette"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_fishing_vignette"] +shader = ExtResource("19_fishing_vignette") [sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"] bg_color = Color(0.032, 0.118, 0.15, 1) @@ -86,6 +91,20 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 +[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] +unique_name_in_owner = true +visible = false +z_index = 50 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +material = SubResource("ShaderMaterial_fishing_vignette") +color = Color(1, 1, 1, 1) + [node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true z_index = 54 @@ -312,6 +331,7 @@ offset_right = 420.0 offset_bottom = -124.0 grow_horizontal = 2 grow_vertical = 0 +pivot_offset = Vector2(420, 18) mouse_filter = 2 [node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"] @@ -440,7 +460,7 @@ theme_override_constants/margin_bottom = 8 unique_name_in_owner = true layout_mode = 2 theme_override_font_sizes/font_size = 18 -text = "Hey, if you've got any fish I've got cash! OwO" +text = "Listen, you may suck at fishing but I've got some things that might help." horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 2 @@ -477,6 +497,9 @@ unique_name_in_owner = true [node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")] unique_name_in_owner = true +[node name="RVUpgradeShop" parent="UIRoot/CanonicalStage" instance=ExtResource("18_rv_upgrade_shop")] +unique_name_in_owner = true + [node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")] unique_name_in_owner = true diff --git a/ui/home_decor_toolbar.gd b/ui/home_decor_toolbar.gd index 6f73219..50ae444 100644 --- a/ui/home_decor_toolbar.gd +++ b/ui/home_decor_toolbar.gd @@ -10,7 +10,10 @@ var _mode_button: Button var _selection_label: Label var _rotate_button: Button var _store_button: Button +var _clear_button: Button var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID +var _interactive: bool = true +var _selection_active: bool = false func _ready() -> void: @@ -22,31 +25,33 @@ 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_interactive(interactive: bool) -> void: + _interactive = interactive + _refresh_button_interactivity() func set_mode(mode: PlayerHomeState.PlacementMode) -> void: _mode = mode if _mode_button != null: _mode_button.text = ( - "placement: grid" + "grid" if mode == PlayerHomeState.PlacementMode.GRID - else "placement: free" + else "free" ) + _mode_button.tooltip_text = "placement mode: %s" % _mode_button.text func set_selection(instance_id: String, display_name: String = "") -> void: var selected: bool = not instance_id.is_empty() + _selection_active = selected if _selection_label != null: _selection_label.text = ( "selected: %s" % display_name - if selected else "click decor to select; drag to move" + if selected else "use held decor to place; click or drag to edit" ) - if _rotate_button != null: - _rotate_button.disabled = not selected - if _store_button != null: - _store_button.disabled = not selected + _refresh_button_interactivity() func report_status(message: String) -> void: @@ -55,65 +60,92 @@ func report_status(message: String) -> void: func _build_ui() -> void: - custom_minimum_size = Vector2(330.0, 0.0) - position = Vector2(925.0, 540.0) - z_index = 58 + set_anchors_preset(Control.PRESET_TOP_RIGHT) + offset_left = -500.0 + offset_top = 0.0 + offset_right = 0.0 + offset_bottom = 106.0 + custom_minimum_size = Vector2(500.0, 0.0) + z_index = 85 mouse_filter = Control.MOUSE_FILTER_STOP - add_theme_stylebox_override( - "panel", - UtilityPageStyle.rounded_style( - Color(UtilityPageStyle.OCEAN_PANEL_DEEP, 0.96), 16 - ), - ) + var panel_style: StyleBoxFlat = UtilityPageStyle.panel_style() + panel_style.set_corner_radius_all(10) + panel_style.content_margin_left = 6.0 + panel_style.content_margin_top = 6.0 + panel_style.content_margin_right = 6.0 + panel_style.content_margin_bottom = 6.0 + add_theme_stylebox_override("panel", panel_style) var margin := MarginContainer.new() for side: StringName in [ &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", ]: - margin.add_theme_constant_override(side, 12) + margin.add_theme_constant_override(side, 6) add_child(margin) var layout := VBoxContainer.new() - layout.add_theme_constant_override("separation", 8) + layout.add_theme_constant_override("separation", 5) 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) + row.add_theme_constant_override("separation", 5) 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) + var title := Label.new() + title.text = "decor" + title.custom_minimum_size = Vector2(70.0, 46.0) + title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + title.add_theme_font_size_override("font_size", 19) + row.add_child(title) + _mode_button = _add_tool_button(row, "grid", "switch placement mode", 82.0) _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 = _add_tool_button(row, "rotate", "rotate selected decor", 92.0) _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 = _add_tool_button(row, "pack", "pack selected decor", 82.0) _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) + _clear_button = _add_tool_button(row, "clear", "clear selection", 82.0) + _clear_button.pressed.connect(cancel_requested.emit) + _selection_label = Label.new() + _selection_label.text = "use held decor to place; click or drag to edit" + _selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + _selection_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _selection_label.add_theme_font_size_override("font_size", 15) + layout.add_child(_selection_label) + _refresh_button_interactivity() + + +func _add_tool_button( + parent: Container, + button_text: String, + button_tooltip: String, + minimum_width: float, +) -> Button: + var button := Button.new() + button.text = button_text + button.tooltip_text = button_tooltip + button.accessibility_name = button_tooltip + button.custom_minimum_size = Vector2(minimum_width, 46.0) + button.size_flags_horizontal = Control.SIZE_EXPAND_FILL + UtilityPageStyle.apply_ocean_button(button) + for style_name: StringName in [ + &"normal", &"hover", &"pressed", &"focus", &"disabled", + ]: + var existing: StyleBox = button.get_theme_stylebox(style_name) + var rounded := existing.duplicate() as StyleBoxFlat + if rounded == null: + continue + rounded.set_corner_radius_all(20) + button.add_theme_stylebox_override(style_name, rounded) + parent.add_child(button) + return button + + +func _refresh_button_interactivity() -> void: + if _mode_button != null: + _mode_button.disabled = not _interactive + if _rotate_button != null: + _rotate_button.disabled = not _interactive or not _selection_active + if _store_button != null: + _store_button.disabled = not _interactive or not _selection_active + if _clear_button != null: + _clear_button.disabled = not _interactive or not _selection_active func _toggle_mode() -> void: diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 9b15913..95263f5 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -2575,7 +2575,10 @@ func _on_tackle_button_gui_input( call_deferred("_release_pointer_focus", button) if ( mouse_event == null - or mouse_event.button_index != MOUSE_BUTTON_RIGHT + or mouse_event.button_index not in [ + MOUSE_BUTTON_LEFT, + MOUSE_BUTTON_RIGHT, + ] or not mouse_event.pressed ): return diff --git a/ui/player_storage.gd b/ui/player_storage.gd index 83817c5..e5369a4 100644 --- a/ui/player_storage.gd +++ b/ui/player_storage.gd @@ -105,7 +105,7 @@ func _open_storage( ): return false _home_access = home_access - _title.text = "RV storage" if home_access else "private storage" + _title.text = "home 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() @@ -290,8 +290,8 @@ func _build_ui() -> void: _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.text = "decorate home" + _decorate_button.tooltip_text = "Place decor inside your home" _decorate_button.custom_minimum_size = Vector2(160.0, 48.0) UtilityPageStyle.apply_ocean_button(_decorate_button) _decorate_button.hide() diff --git a/ui/rv_upgrade_shop.gd b/ui/rv_upgrade_shop.gd new file mode 100644 index 0000000..11c0a4d --- /dev/null +++ b/ui/rv_upgrade_shop.gd @@ -0,0 +1,330 @@ +class_name RVUpgradeShop +extends Control + +const INPUT_OWNER: StringName = &"rv_upgrade_shop" +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") +const CurrencyPresentationType = preload("res://ui/currency_presentation.gd") + +signal menu_visibility_changed(is_open: bool) +signal menu_exit_started + +var _player: Player +var _wallet: PlayerWallet +var _fishing_spot: FishingSpot +var _interaction: RVUpgradeInteraction +var _home_state: PlayerHomeState +var _network_shop: NetworkShopService +var _wallet_amount: CurrencyAmount +var _price_amount: CurrencyAmount +var _home_label: Label +var _storage_label: Label +var _feedback: Label +var _upgrade_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: RVUpgradeInteraction, + home_state: PlayerHomeState, + network_shop: NetworkShopService, +) -> void: + _player = player + _wallet = wallet + _fishing_spot = fishing_spot + _interaction = interaction + _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 _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: RVUpgradeInteraction) -> 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() + _player.set_movement_enabled(false) + _player.set_camera_input_enabled(false) + _prior_mouse_mode = _restorable_mouse_mode(Input.mouse_mode) + _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() + _upgrade_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_upgrade() -> void: + if _network_shop == null or _transaction_pending: + return + var request_id: String = _network_shop.request_home_upgrade() + if request_id.is_empty(): + _refresh() + + +func _on_purchase_pending(_request_id: String) -> void: + _transaction_pending = true + _feedback.text = "upgrading…" + _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.HOME_UPGRADE + or product_id != NetworkShopService.HOME_UPGRADE_PRODUCT_ID + ): + return + _transaction_pending = false + _feedback.text = "starter RV unlocked" if accepted else message.to_lower() + _refresh() + + +func _on_wallet_changed(_balance: int, _delta: int) -> void: + _refresh() + + +func _refresh() -> void: + if _wallet_amount == null: + return + var balance: int = _wallet.get_balance() if _wallet != null else 0 + var tier: int = _home_state.get_tier() if _home_state != null else 0 + var current_capacity: int = ( + _home_state.get_storage_capacity() if _home_state != null else 9 + ) + var next_capacity: int = ( + _home_state.get_next_storage_capacity() if _home_state != null else -1 + ) + var cost: int = ( + _home_state.get_next_upgrade_cost() if _home_state != null else -1 + ) + _wallet_amount.set_amount(balance) + _price_amount.set_amount(maxi(cost, 0)) + if tier >= PlayerHomeState.MAX_TIER: + _home_label.text = "starter RV" + _storage_label.text = "storage · %d slots" % current_capacity + _upgrade_button.text = "unlocked" + _upgrade_button.disabled = true + _price_amount.visible = false + else: + _home_label.text = "tent → starter RV" + _storage_label.text = "storage · %d → %d slots" % [ + current_capacity, next_capacity, + ] + _upgrade_button.text = "upgrade" + _upgrade_button.disabled = ( + _transaction_pending + or _wallet == null + or not _wallet.can_afford(cost) + or _network_shop == null + or not _network_shop.can_request_home_upgrade() + ) + _price_amount.visible = true + + +func _restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode: + return ( + Input.MOUSE_MODE_VISIBLE + if mouse_mode == Input.MOUSE_MODE_CAPTURED + else mouse_mode + ) + + +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(-390.0, -225.0) + panel.size = Vector2(780.0, 450.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 = "home upgrades" + title.add_theme_font_size_override("font_size", 38) + title.size_flags_horizontal = Control.SIZE_EXPAND_FILL + header.add_child(title) + _wallet_amount = CurrencyPresentationType.instantiate_amount(0, 24.0) + header.add_child(_wallet_amount) + + var offer := PanelContainer.new() + offer.size_flags_vertical = Control.SIZE_EXPAND_FILL + offer.add_theme_stylebox_override( + "panel", + UtilityPageStyleType.rounded_style( + UtilityPageStyleType.OCEAN_FIELD, 18 + ), + ) + layout.add_child(offer) + var offer_margin := MarginContainer.new() + for side: StringName in [ + &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", + ]: + offer_margin.add_theme_constant_override(side, 28) + offer.add_child(offer_margin) + var details := VBoxContainer.new() + details.alignment = BoxContainer.ALIGNMENT_CENTER + details.add_theme_constant_override("separation", 14) + offer_margin.add_child(details) + _home_label = Label.new() + _home_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _home_label.add_theme_font_size_override("font_size", 34) + details.add_child(_home_label) + var description := Label.new() + description.text = ( + "A weatherproof home with a larger interior and built-in storage." + ) + description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART + description.add_theme_font_size_override("font_size", 20) + details.add_child(description) + _storage_label = Label.new() + _storage_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _storage_label.add_theme_font_size_override("font_size", 22) + details.add_child(_storage_label) + var purchase_row := HBoxContainer.new() + purchase_row.alignment = BoxContainer.ALIGNMENT_CENTER + purchase_row.add_theme_constant_override("separation", 10) + details.add_child(purchase_row) + _upgrade_button = Button.new() + _upgrade_button.custom_minimum_size = Vector2(220.0, 60.0) + UtilityPageStyleType.apply_ocean_button(_upgrade_button) + _upgrade_button.pressed.connect(_purchase_upgrade) + purchase_row.add_child(_upgrade_button) + _price_amount = CurrencyPresentationType.instantiate_amount(0, 24.0) + purchase_row.add_child(_price_amount) + + 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) + _upgrade_button.focus_neighbor_bottom = _upgrade_button.get_path_to( + _close_button + ) + _close_button.focus_neighbor_top = _close_button.get_path_to( + _upgrade_button + ) diff --git a/ui/rv_upgrade_shop.gd.uid b/ui/rv_upgrade_shop.gd.uid new file mode 100644 index 0000000..ddd043e --- /dev/null +++ b/ui/rv_upgrade_shop.gd.uid @@ -0,0 +1 @@ +uid://b5pnsuph7fxrd diff --git a/ui/rv_upgrade_shop.tscn b/ui/rv_upgrade_shop.tscn new file mode 100644 index 0000000..22abf58 --- /dev/null +++ b/ui/rv_upgrade_shop.tscn @@ -0,0 +1,14 @@ +[gd_scene load_steps=2 format=3] + +[ext_resource type="Script" path="res://ui/rv_upgrade_shop.gd" id="1_script"] + +[node name="RVUpgradeShop" 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/world/decor_shop_interaction.gd b/world/decor_shop_interaction.gd index 049bfbe..a325134 100644 --- a/world/decor_shop_interaction.gd +++ b/world/decor_shop_interaction.gd @@ -1,2 +1,10 @@ class_name DecorShopInteraction extends FishingShopInteraction + +const DECOR_DIALOGUE_TEXT: String = ( + "You gonna decorate that hot box or what? I've got some new decor in, take a look." +) + + +func get_dialogue_text(_home_tier: int = -1) -> String: + return DECOR_DIALOGUE_TEXT diff --git a/world/fishing_shop_interaction.gd b/world/fishing_shop_interaction.gd index d912eca..9628ee6 100644 --- a/world/fishing_shop_interaction.gd +++ b/world/fishing_shop_interaction.gd @@ -3,6 +3,10 @@ extends Area3D signal local_player_range_changed(in_range: bool) +const DIALOGUE_TEXT: String = ( + "Listen, you may suck at fishing but I've got some things that might help." +) + @export_node_path("Node3D") var look_character_path: NodePath = ^"../Shopkeeper" var _local_player: Player @@ -37,6 +41,10 @@ func is_avatar_in_range(avatar: Player) -> bool: ) +func get_dialogue_text(_home_tier: int = -1) -> String: + return DIALOGUE_TEXT + + func get_prompt_anchor_position() -> Vector3: if _look_character != null: return _look_character.get_head_anchor_position() diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 4e15251..61484fd 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -12,6 +12,9 @@ const PlayerStorageInteractionType = preload( const DecorShopInteractionType = preload( "res://world/decor_shop_interaction.gd" ) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) @@ -52,6 +55,8 @@ const WATER_RECOVERY_FOOTPRINT_RADIUS := 0.6 const WATER_RECOVERY_INSET_STEP := 0.25 const WATER_RECOVERY_MAXIMUM_INSET := 1.5 const WATER_RECOVERY_SEARCH_EXPANSIONS: Array[float] = [1.5, 4.0, 12.0] +const HOME_GROUND_HEIGHT_TOLERANCE := 0.5 +const HOME_ELEVATION_HEIGHT_TOLERANCE := 0.6 const PROP_SURFACE_SAMPLE_DIRECTIONS: Array[Vector2] = [ Vector2(1.0, 0.0), Vector2(0.70710678, 0.70710678), @@ -86,9 +91,13 @@ const PROCEDURAL_PROP_GROUPS: Array[StringName] = [ @onready var _decor_shop: DecorShopInteractionType = ( $Interactables/DecorShopWorld/InteractionArea ) +@onready var _rv_upgrade_shop: RVUpgradeInteractionType = ( + $Interactables/RVUpgradeShopWorld/InteractionArea +) @onready var _shop_root: Node3D = %FishingShopWorld @onready var _storage_root: Node3D = %PlayerStorageBox @onready var _decor_shop_root: Node3D = %DecorShopWorld +@onready var _rv_upgrade_shop_root: Node3D = %RVUpgradeShopWorld @onready var _decorations: Node3D = %Decorations @onready var _tree_anchors: GatherableAnchorSet3D = %ReachableTreeTrunks @onready var _diggable_beach: DiggableArea3D = %DiggableBeach @@ -104,6 +113,7 @@ var _placed_prop_groups: Array[StringName] = [] var _placed_group_coordinates: Dictionary[StringName, Array] = {} var _biome_assignments: Dictionary[Vector2i, StringName] = {} var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {} +var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {} func _ready() -> void: @@ -159,6 +169,10 @@ func get_decor_shop() -> DecorShopInteractionType: return _decor_shop +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return _rv_upgrade_shop + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return _shoreline_reference @@ -184,6 +198,7 @@ func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] if material_names.is_empty(): @@ -204,10 +219,27 @@ func get_spawn_surface_triangles( mesh.surface_get_arrays(surface_index), minimum_global_y, minimum_up_dot, + maximum_global_y, ) return triangles +func is_home_exterior_position_accessible(position: Vector3) -> bool: + var local_position := _generator.to_local(position) + if local_position.y <= HOME_GROUND_HEIGHT_TOLERANCE: + return true + var coordinate := _generator.coordinate_for_local_position(local_position) + for value: Variant in _home_accessible_elevations_by_coordinate.get( + coordinate, + [], + ): + if absf(local_position.y - float(value)) <= ( + HOME_ELEVATION_HEIGHT_TOLERANCE + ): + return true + return false + + func get_water_recovery_position( entry_position: Vector3, fallback_position: Vector3, @@ -242,6 +274,7 @@ func get_water_recovery_position( func _on_generation_completed(summary: Dictionary) -> void: _diggable_beach.invalidate_surface_cache() var records: Array[Dictionary] = _generator.placement_records() + _cache_home_accessible_elevations(records) _assign_biomes(records, summary) var spawn_position := Vector3.ZERO _clear_children(_decorations) @@ -388,6 +421,97 @@ func _on_generation_completed(summary: Dictionary) -> void: world_generated.emit(_current_seed, summary) +func _cache_home_accessible_elevations( + records: Array[Dictionary], +) -> void: + _home_accessible_elevations_by_coordinate.clear() + var records_by_coordinate: Dictionary[Vector2i, Dictionary] = {} + var elevation_tiers: Dictionary[int, bool] = {} + for record: Dictionary in records: + var coordinate: Vector2i = record.get( + "coordinate", + Vector2i(-1, -1), + ) + records_by_coordinate[coordinate] = record + for tier: int in _elevation_tiers_for_tags( + record.get("tags", PackedStringArray()) + ): + elevation_tiers[tier] = true + var visited: Dictionary[String, bool] = {} + for tier: int in elevation_tiers: + var elevation_tag := "elevation_%d" % tier + for coordinate: Vector2i in records_by_coordinate: + var visit_key := _home_elevation_visit_key(coordinate, tier) + if visited.has(visit_key): + continue + var record: Dictionary = records_by_coordinate[coordinate] + var tags: PackedStringArray = record.get( + "tags", + PackedStringArray(), + ) + if elevation_tag not in tags: + continue + var component: Array[Vector2i] = [] + var pending: Array[Vector2i] = [coordinate] + var has_ramp := false + while not pending.is_empty(): + var current: Vector2i = pending.pop_back() + var current_key := _home_elevation_visit_key(current, tier) + if visited.has(current_key): + continue + var current_record: Dictionary = records_by_coordinate.get( + current, + {}, + ) + var current_tags: PackedStringArray = current_record.get( + "tags", + PackedStringArray(), + ) + if elevation_tag not in current_tags: + continue + visited[current_key] = true + component.append(current) + has_ramp = has_ramp or "ramp" in current_tags + for offset: Vector2i in [ + Vector2i.LEFT, + Vector2i.RIGHT, + Vector2i.UP, + Vector2i.DOWN, + ]: + pending.append(current + offset) + if not has_ramp: + continue + var surface_height := ( + float(tier - 1) * _generator.elevated_cliff_level_height + ) + for component_coordinate: Vector2i in component: + var heights: Array = ( + _home_accessible_elevations_by_coordinate.get( + component_coordinate, + [], + ) + ) + heights.append(surface_height) + _home_accessible_elevations_by_coordinate[ + component_coordinate + ] = heights + + +func _elevation_tiers_for_tags(tags: PackedStringArray) -> Array[int]: + var tiers: Array[int] = [] + for tag_value: String in tags: + if not tag_value.begins_with("elevation_"): + continue + var suffix := tag_value.trim_prefix("elevation_") + if suffix.is_valid_int(): + tiers.append(suffix.to_int()) + return tiers + + +func _home_elevation_visit_key(coordinate: Vector2i, tier: int) -> String: + return "%d:%d:%d" % [tier, coordinate.x, coordinate.y] + + func _mark_precipitation_occluders() -> void: for prop: Node in _decorations.get_children(): var prop_group := prop.get_meta(&"terrain_prop_group", &"") as StringName @@ -441,6 +565,8 @@ func _place_spawn_amenities(center: Vector3) -> void: _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 + _rv_upgrade_shop_root.position = center + Vector3(-2.35, 0.0, -1.35) + _rv_upgrade_shop_root.rotation.y = PI func _configure_static_water() -> void: @@ -1304,6 +1430,7 @@ func _terrain_surface_triangles_by_coordinate( mesh_instance.mesh.surface_get_arrays(surface_index), -INF, 0.35, + 100.0, ) if triangles.is_empty(): continue @@ -1651,6 +1778,7 @@ func _append_surface_triangles( arrays: Array, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: if arrays.size() <= Mesh.ARRAY_INDEX: return @@ -1660,10 +1788,28 @@ func _append_surface_triangles( return if indices.is_empty(): for index: int in range(0, vertices.size() - 2, 3): - _append_triangle(result, mesh_instance, vertices[index], vertices[index + 1], vertices[index + 2], minimum_global_y, minimum_up_dot) + _append_triangle( + result, + mesh_instance, + vertices[index], + vertices[index + 1], + vertices[index + 2], + minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) return for index: int in range(0, indices.size() - 2, 3): - _append_triangle(result, mesh_instance, vertices[indices[index]], vertices[indices[index + 1]], vertices[indices[index + 2]], minimum_global_y, minimum_up_dot) + _append_triangle( + result, + mesh_instance, + vertices[indices[index]], + vertices[indices[index + 1]], + vertices[indices[index + 2]], + minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) func _append_triangle( @@ -1674,11 +1820,15 @@ func _append_triangle( local_c: Vector3, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: var a := mesh_instance.to_global(local_a) var b := mesh_instance.to_global(local_b) var c := mesh_instance.to_global(local_c) - if minf(a.y, minf(b.y, c.y)) <= minimum_global_y: + if ( + maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y + or minf(a.y, minf(b.y, c.y)) >= maximum_global_y + ): return var cross := (b - a).cross(c - a) if cross.length_squared() <= 0.0000001: diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index 04602e1..c996b88 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=15 format=3] +[gd_scene load_steps=16 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"] @@ -14,6 +14,7 @@ [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"] +[ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="15_rv_upgrade"] [node name="GeneratedWorldRegion" type="Node3D"] script = ExtResource("1_region") @@ -102,6 +103,9 @@ unique_name_in_owner = true [node name="DecorShopWorld" parent="Interactables" instance=ExtResource("14_decor_shop")] unique_name_in_owner = true +[node name="RVUpgradeShopWorld" parent="Interactables" instance=ExtResource("15_rv_upgrade")] +unique_name_in_owner = true + [node name="Decorations" type="Node3D" parent="."] unique_name_in_owner = true diff --git a/world/generation/terrain_chunk_generator.gd b/world/generation/terrain_chunk_generator.gd index 49bf693..64a8bd2 100644 --- a/world/generation/terrain_chunk_generator.gd +++ b/world/generation/terrain_chunk_generator.gd @@ -4414,6 +4414,19 @@ func chunk_position(coordinate: Vector2i) -> Vector3: ) +func coordinate_for_local_position(position: Vector3) -> Vector2i: + if catalog == null or catalog.chunk_size <= 0.0: + return Vector2i(-1, -1) + var half_grid := Vector2( + float(grid_size.x - 1) * 0.5, + float(grid_size.y - 1) * 0.5, + ) + return Vector2i( + roundi(position.x / catalog.chunk_size + half_grid.x), + roundi(position.z / catalog.chunk_size + half_grid.y), + ) + + func get_generated_chunks_root() -> Node3D: return _generated_chunks diff --git a/world/interactables/decor_shop_world.tscn b/world/interactables/decor_shop_world.tscn index 6b735e2..7f94ec4 100644 --- a/world/interactables/decor_shop_world.tscn +++ b/world/interactables/decor_shop_world.tscn @@ -6,19 +6,19 @@ [sub_resource type="SphereShape3D" id="InteractionShape"] resource_local_to_scene = true -radius = 3.0 +radius = 2.1 [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" +fur_color_id = "brown" +ears_id = "bear" +tail_id = "bear" +eyes_id = "tired" +nose_id = "triangle_pink" +mouth_id = "simple_smile" head_look_enabled = true [node name="AnimationPlayer" parent="Shopkeeper"] diff --git a/world/interactables/fishing_shop_world.tscn b/world/interactables/fishing_shop_world.tscn index 760f3b6..040b558 100644 --- a/world/interactables/fishing_shop_world.tscn +++ b/world/interactables/fishing_shop_world.tscn @@ -6,7 +6,7 @@ [sub_resource type="SphereShape3D" id="InteractionShape"] resource_local_to_scene = true -radius = 3.0 +radius = 2.1 [node name="FishingShopWorld" type="Node3D"] diff --git a/world/interactables/rv_upgrade_shop_world.tscn b/world/interactables/rv_upgrade_shop_world.tscn new file mode 100644 index 0000000..9fd0ed2 --- /dev/null +++ b/world/interactables/rv_upgrade_shop_world.tscn @@ -0,0 +1,36 @@ +[gd_scene load_steps=5 format=3] + +[ext_resource type="Script" path="res://world/rv_upgrade_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 = 2.1 + +[node name="RVUpgradeShopWorld" type="Node3D"] + +[node name="Shopkeeper" parent="." instance=ExtResource("2_character")] +script = ExtResource("3_display") +species_id = "pointy" +fur_color_id = "teal" +ears_id = "pointy_long" +tail_id = "fox" +eyes_id = "interested" +nose_id = "triangle_small" +mouth_id = "cheeky_smile" +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") +vertical_icon_alignment = 1 diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index b418292..c690e4b 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -11,6 +11,9 @@ const PlayerStorageInteractionType = preload( const DecorShopInteractionType = preload( "res://world/decor_shop_interaction.gd" ) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const FOLIAGE_WIND_SHADER: Shader = preload( "res://world/materials/foliage_wind.gdshader" ) @@ -56,6 +59,10 @@ var player_storage_path: NodePath = ( var decor_shop_path: NodePath = ( ^"Interactables/DecorShopWorld/InteractionArea" ) +@export_node_path("Area3D") +var rv_upgrade_shop_path: NodePath = ( + ^"Interactables/RVUpgradeShopWorld/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. @@ -95,6 +102,10 @@ func get_decor_shop() -> DecorShopInteractionType: return get_node_or_null(decor_shop_path) as DecorShopInteractionType +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return get_node_or_null(rv_upgrade_shop_path) as RVUpgradeInteractionType + + func get_saltwater_shoreline_mesh() -> MeshInstance3D: return get_node_or_null(^"ShorelineRibbons/Ocean") as MeshInstance3D @@ -150,6 +161,7 @@ func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: var triangles: Array[PackedVector3Array] = [] if material_names.is_empty(): @@ -188,6 +200,7 @@ func get_spawn_surface_triangles( vertices[vertex_index + 2], minimum_global_y, minimum_up_dot, + maximum_global_y, ) else: for index_offset: int in range(0, indices.size() - 2, 3): @@ -199,6 +212,7 @@ func get_spawn_surface_triangles( vertices[indices[index_offset + 2]], minimum_global_y, minimum_up_dot, + maximum_global_y, ) return triangles @@ -211,14 +225,14 @@ func _append_spawn_triangle( local_c: Vector3, minimum_global_y: float, minimum_up_dot: float, + maximum_global_y: float, ) -> void: var a: Vector3 = mesh_instance.to_global(local_a) var b: Vector3 = mesh_instance.to_global(local_b) var c: Vector3 = mesh_instance.to_global(local_c) if ( - a.y <= minimum_global_y - or b.y <= minimum_global_y - or c.y <= minimum_global_y + maxf(a.y, maxf(b.y, c.y)) <= minimum_global_y + or minf(a.y, minf(b.y, c.y)) >= maximum_global_y ): return var cross: Vector3 = (b - a).cross(c - a) diff --git a/world/regions/starter_island_region.tscn b/world/regions/starter_island_region.tscn index 9b95dfa..7682491 100644 --- a/world/regions/starter_island_region.tscn +++ b/world/regions/starter_island_region.tscn @@ -19,6 +19,7 @@ [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"] +[ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="26_rv_upgrade"] [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) @@ -293,3 +294,7 @@ 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) + +[node name="RVUpgradeShopWorld" parent="Interactables" instance=ExtResource("26_rv_upgrade")] +position = Vector3(-6.25, 3.42, 9) +rotation = Vector3(0, 1.5707964, 0) diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index 85f6e1b..d054938 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -22,6 +22,10 @@ func get_decor_shop() -> DecorShopInteraction: return null +func get_rv_upgrade_shop() -> RVUpgradeInteraction: + return null + + func get_player_storage() -> PlayerStorageInteraction: return null @@ -42,10 +46,24 @@ func get_spawn_surface_triangles( _material_names: Array[StringName], _minimum_global_y: float, _minimum_up_dot: float = 0.6, + _maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: return [] +func get_saltwater_surface_height() -> float: + for water_region: FishableWaterRegion in get_fishable_water_regions(): + if water_region.water_type == WaterType.Type.SALT_WATER: + return water_region.get_surface_height() + return NAN + + +## Authored regions are assumed to expose only deliberately reachable home +## sites. Procedural regions override this to reject isolated elevations. +func is_home_exterior_position_accessible(_position: Vector3) -> bool: + return true + + func get_fishable_water_regions() -> Array[FishableWaterRegion]: var regions: Array[FishableWaterRegion] = [] var root: Node = get_node_or_null(fishable_water_root) diff --git a/world/rv_upgrade_interaction.gd b/world/rv_upgrade_interaction.gd new file mode 100644 index 0000000..fb24246 --- /dev/null +++ b/world/rv_upgrade_interaction.gd @@ -0,0 +1,15 @@ +class_name RVUpgradeInteraction +extends FishingShopInteraction + +const TENT_DIALOGUE_TEXT: String = ( + "That tent is looking awful cramped. Can I talk you into an upgrade?" +) +const RV_DIALOGUE_TEXT: String = "How's the new RV treating you?" + + +func get_dialogue_text(home_tier: int = -1) -> String: + return ( + RV_DIALOGUE_TEXT + if home_tier >= PlayerHomeState.STARTER_RV_TIER + else TENT_DIALOGUE_TEXT + ) diff --git a/world/rv_upgrade_interaction.gd.uid b/world/rv_upgrade_interaction.gd.uid new file mode 100644 index 0000000..cb57804 --- /dev/null +++ b/world/rv_upgrade_interaction.gd.uid @@ -0,0 +1 @@ +uid://c0h08goorv2of diff --git a/world/test_world.gd b/world/test_world.gd index 1b0a813..2e25206 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -10,6 +10,9 @@ const PlayerStorageInteractionType = preload( const DecorShopInteractionType = preload( "res://world/decor_shop_interaction.gd" ) +const RVUpgradeInteractionType = preload( + "res://world/rv_upgrade_interaction.gd" +) const WorldLayoutType = preload("res://world/world_layout.gd") const GENERATED_REGION_SCENE: PackedScene = preload( "res://world/generation/generated_world_region.tscn" @@ -75,6 +78,10 @@ func get_decor_shop() -> DecorShopInteractionType: return _active_region.get_decor_shop() +func get_rv_upgrade_shop() -> RVUpgradeInteractionType: + return _active_region.get_rv_upgrade_shop() + + func get_player_spawn_transform() -> Transform3D: return _active_region.get_player_spawn_transform() @@ -170,10 +177,29 @@ func get_saltwater_shoreline_mesh() -> MeshInstance3D: func get_spawn_surface_triangles( material_names: Array[StringName], minimum_global_y: float, + minimum_up_dot: float = 0.6, + maximum_global_y: float = 100.0, ) -> Array[PackedVector3Array]: return _active_region.get_spawn_surface_triangles( material_names, minimum_global_y, + minimum_up_dot, + maximum_global_y, + ) + + +func get_saltwater_surface_height() -> float: + return ( + _active_region.get_saltwater_surface_height() + if _active_region != null + else NAN + ) + + +func is_home_exterior_position_accessible(position: Vector3) -> bool: + return ( + _active_region != null + and _active_region.is_home_exterior_position_accessible(position) ) From 0158b0215f63a409de7ec532bad581f2ae82e5b7 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 31 Aug 2026 16:04:34 -0400 Subject: [PATCH 17/17] Scale coastal crab spawning by habitat --- gathering/catalog/crab_blue.tres | 3 + gathering/catalog/crab_brown.tres | 3 + gathering/gatherable_data.gd | 37 ++++++++++ network/network_world_spawn_service.gd | 61 ++++++++++++++-- tests/fish_catalog_content_validation.gd | 61 ++++++---------- tests/generated_world_runtime_validation.gd | 80 ++++++++++++++++++++- tests/logbook_validation.gd | 7 +- tests/world_spawn_multiplayer_validation.gd | 27 +++++-- tests/world_spawn_protocol_validation.gd | 41 +++++++++-- 9 files changed, 261 insertions(+), 59 deletions(-) diff --git a/gathering/catalog/crab_blue.tres b/gathering/catalog/crab_blue.tres index 1ccabe8..705948e 100644 --- a/gathering/catalog/crab_blue.tres +++ b/gathering/catalog/crab_blue.tres @@ -10,5 +10,8 @@ catch_data = ExtResource("2_catch") required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.8 +surface_water_relation = 2 population = 4 +surface_population_per_100_square_meters = 0.4 +maximum_surface_population = 16 sprite_pixel_size = 0.0005 diff --git a/gathering/catalog/crab_brown.tres b/gathering/catalog/crab_brown.tres index 83d20c8..b27bb7e 100644 --- a/gathering/catalog/crab_brown.tres +++ b/gathering/catalog/crab_brown.tres @@ -10,7 +10,10 @@ catch_data = ExtResource("2_catch") required_tool_id = &"crab_net" surface_materials = Array[StringName]([&"sand"]) minimum_surface_y = -0.44 +surface_water_relation = 1 population = 2 +surface_population_per_100_square_meters = 0.15 +maximum_surface_population = 24 movement_speed = 0.35 roam_radius = 3.5 scare_radius = 2.8 diff --git a/gathering/gatherable_data.gd b/gathering/gatherable_data.gd index d60dcc9..41de7fe 100644 --- a/gathering/gatherable_data.gd +++ b/gathering/gatherable_data.gd @@ -10,6 +10,12 @@ enum PresentationMode { WATER_SPURT, } +enum SurfaceWaterRelation { + ANY, + ABOVE_SALT_WATER, + BELOW_SALT_WATER, +} + @export var type_id: StringName @export var catch_data: FishDataType @export var required_tool_id: StringName @@ -17,12 +23,20 @@ enum PresentationMode { @export var diggable_area_id: StringName @export var spawn_anchor_set_id: StringName @export_range(-100.0, 100.0, 0.01) var minimum_surface_y: float = 0.08 +@export_range(-100.0, 100.0, 0.01) var maximum_surface_y: float = 100.0 +@export var surface_water_relation := SurfaceWaterRelation.ANY +@export_range(0.0, 2.0, 0.01) var waterline_margin: float = 0.01 @export_range(0, 64, 1) var population: int = 0 ## Anchored gatherables can scale with authored/generated attachment geometry. ## Zero preserves the fixed population above. @export_range(0.0, 1.0, 0.01) var spawn_anchor_occupancy_ratio := 0.0 ## Zero leaves the anchor count as the only ceiling. @export_range(0, 256, 1) var maximum_anchor_population := 0 +## Optional density scaling for large generated surfaces. The authored +## population remains the minimum while the maximum keeps large maps bounded. +@export_range(0.0, 8.0, 0.01) +var surface_population_per_100_square_meters := 0.0 +@export_range(0, 256, 1) var maximum_surface_population := 0 @export var presentation_mode: PresentationMode = PresentationMode.VISIBLE_CREATURE @export var requires_sneaking: bool = true @export_range(0.0, 120.0, 0.1) var active_lifetime_seconds: float = 0.0 @@ -77,6 +91,7 @@ func is_valid() -> bool: or uses_spawn_anchors or not surface_materials.is_empty() ) + and maximum_surface_y >= minimum_surface_y and ( not uses_diggable_area or catch_data.collection_method @@ -87,6 +102,10 @@ func is_valid() -> bool: maximum_anchor_population == 0 or maximum_anchor_population >= population ) + and ( + maximum_surface_population == 0 + or maximum_surface_population >= population + ) and movement_parameters_valid and _quality_multipliers_are_valid( quality_movement_speed_multipliers @@ -126,6 +145,24 @@ func target_population_for_anchor_count(anchor_count: int) -> int: return target +func target_population_for_surface_area(surface_area: float) -> int: + if surface_population_per_100_square_meters <= 0.0: + return population + if surface_area <= 0.0: + return 0 + var target := maxi( + population, + ceili( + surface_area + * surface_population_per_100_square_meters + / 100.0 + ), + ) + if maximum_surface_population > 0: + target = mini(target, maximum_surface_population) + return target + + func can_be_scared() -> bool: return not is_stationary_spawn() and scare_radius > 0.0 diff --git a/network/network_world_spawn_service.gd b/network/network_world_spawn_service.gd index a45ad37..8981237 100644 --- a/network/network_world_spawn_service.gd +++ b/network/network_world_spawn_service.gd @@ -43,6 +43,7 @@ var _entity_revisions: Dictionary = {} var _surface_triangles: Dictionary = {} var _surface_areas: Dictionary = {} var _surface_total_areas: Dictionary = {} +var _surface_height_ranges: Dictionary = {} var _spawn_anchor_positions: Dictionary = {} var _respawns: Array[Dictionary] = [] var _next_respawn_by_type: Dictionary[StringName, float] = {} @@ -322,10 +323,14 @@ func _cache_spawn_surface(entry: GatherableDataType) -> void: if not entry.diggable_area_id.is_empty(): triangles = _world.get_diggable_area_triangles(entry.diggable_area_id) else: + var height_range := _resolved_surface_height_range(entry) triangles = _world.get_spawn_surface_triangles( entry.surface_materials, - entry.minimum_surface_y, + height_range.x, + 0.6, + height_range.y, ) + _surface_height_ranges[entry.type_id] = height_range var areas := PackedFloat32Array() var total_area: float = 0.0 for triangle: PackedVector3Array in triangles: @@ -519,9 +524,18 @@ func _sample_surface_position( if triangles.is_empty() or total_area <= 0.0: return Vector3(INF, INF, INF) var validates_diggable_surface := not entry.diggable_area_id.is_empty() + var height_range: Vector2 = _surface_height_ranges.get( + entry.type_id, + Vector2(entry.minimum_surface_y, entry.maximum_surface_y), + ) + var validates_height_range := _surface_height_ranges.has(entry.type_id) var attempts: int = ( 48 - if validates_diggable_surface or is_finite(maximum_distance) + if ( + validates_diggable_surface + or validates_height_range + or is_finite(maximum_distance) + ) else 1 ) var fallback: Vector3 = Vector3(INF, INF, INF) @@ -551,9 +565,20 @@ func _sample_surface_position( point, ) ) - if within_requested_distance and valid_diggable_surface: + var valid_height := ( + not validates_height_range + or ( + point.y > height_range.x + and point.y < height_range.y + ) + ) + if ( + within_requested_distance + and valid_diggable_surface + and valid_height + ): return point - if validates_diggable_surface: + if validates_diggable_surface or validates_height_range: return Vector3(INF, INF, INF) if origin.is_finite() and is_finite(maximum_distance): return origin @@ -1405,8 +1430,12 @@ func _reconcile_seasonal_population() -> void: func _target_population(entry: GatherableDataType) -> int: - if entry == null or entry.spawn_anchor_set_id.is_empty(): - return entry.population if entry != null else 0 + if entry == null: + return 0 + if entry.spawn_anchor_set_id.is_empty(): + return entry.target_population_for_surface_area( + float(_surface_total_areas.get(entry.type_id, 0.0)) + ) var anchors: PackedVector3Array = _spawn_anchor_positions.get( entry.type_id, PackedVector3Array(), @@ -1414,6 +1443,25 @@ func _target_population(entry: GatherableDataType) -> int: return entry.target_population_for_anchor_count(anchors.size()) +func _resolved_surface_height_range(entry: GatherableDataType) -> Vector2: + var minimum_y := entry.minimum_surface_y + var maximum_y := entry.maximum_surface_y + var saltwater_height := _world.get_saltwater_surface_height() + if is_finite(saltwater_height): + match entry.surface_water_relation: + GatherableDataType.SurfaceWaterRelation.ABOVE_SALT_WATER: + minimum_y = maxf( + minimum_y, + saltwater_height + entry.waterline_margin, + ) + GatherableDataType.SurfaceWaterRelation.BELOW_SALT_WATER: + maximum_y = minf( + maximum_y, + saltwater_height - entry.waterline_margin, + ) + return Vector2(minimum_y, maximum_y) + + func _population_for_type(type_id: StringName) -> int: var count: int = 0 for state: Dictionary in _entities.values(): @@ -1445,6 +1493,7 @@ func _clear_world() -> void: _surface_triangles.clear() _surface_areas.clear() _surface_total_areas.clear() + _surface_height_ranges.clear() _spawn_anchor_positions.clear() _respawns.clear() _next_respawn_by_type.clear() diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd index 3b31005..82f5604 100644 --- a/tests/fish_catalog_content_validation.gd +++ b/tests/fish_catalog_content_validation.gd @@ -40,39 +40,17 @@ const StarterRegionScene = preload( const SharedFishPlaceholder: Texture2D = preload( "res://art/exported/creatures/fish_placeholder.png" ) -const FINAL_CREATURE_TEXTURE_PATHS: Dictionary[StringName, String] = { - &"bowfin": "res://art/exported/creatures/101.png", - &"sturgeon_lake": "res://art/exported/creatures/102.png", - &"gar_longnose": "res://art/exported/creatures/103.png", - &"paddlefish": "res://art/exported/creatures/104.png", - &"sturgeon_shovelnose": "res://art/exported/creatures/105.png", - &"gar_spotted": "res://art/exported/creatures/106.png", - &"lungfish_west_african": "res://art/exported/creatures/107.png", - &"mudskipper_atlantic": "res://art/exported/creatures/201.png", - &"flounder_winter": "res://art/exported/creatures/403.png", - &"mackerel_atlantic": "res://art/exported/creatures/701.png", - &"mackerel_chub": "res://art/exported/creatures/704.png", - &"sand_lance_american": "res://art/exported/creatures/801.png", - &"menhaden_atlantic": "res://art/exported/creatures/802.png", - &"anchovy_european": "res://art/exported/creatures/805.png", - &"sardine_european": "res://art/exported/creatures/806.png", - &"anchovy_northern": "res://art/exported/creatures/808.png", - &"crab_brown": "res://art/exported/creatures/3906.png", - &"salmon_atlantic": "res://art/exported/creatures/4401.png", - &"mahi_mahi": "res://art/exported/creatures/4701.png", - &"clownfish_ocellaris": "res://art/exported/creatures/5702.png", - &"chromis_blue_green": "res://art/exported/creatures/5901.png", - &"hogfish": "res://art/exported/creatures/6103.png", - &"mullet_striped": "res://art/exported/creatures/6804.png", - &"sheepshead": "res://art/exported/creatures/6905.png", - &"milkfish": "res://art/exported/creatures/7101.png", - &"ladyfish": "res://art/exported/creatures/7403.png", - &"tilapia_nile": "res://art/exported/creatures/7506.png", - &"betta_siamese": "res://art/exported/creatures/7508.png", -} +const FINAL_CREATURE_CATALOG_NUMBERS: Array[int] = [ + 101, 102, 103, 104, 105, 106, 107, 201, 403, 701, 704, 801, + 802, 805, 806, 808, 2502, 2504, 2505, 2508, 2601, 2801, 2802, + 2803, 2804, 2806, 2807, 3002, 3102, 3103, 3104, 3107, 3201, + 3401, 3902, 3906, 4101, 4401, 4701, 5001, 5702, 5901, 5902, + 5903, 5904, 6001, 6103, 6406, 6601, 6801, 6804, 6807, 6901, + 6902, 6904, 6905, 7101, 7403, 7506, 7508, +] -const ACTIVE_CATALOG_COUNT: int = 313 -const INACTIVE_CATALOG_COUNT: int = 3 +const ACTIVE_CATALOG_COUNT: int = 314 +const INACTIVE_CATALOG_COUNT: int = 2 const FISHING_SPECIES_COUNT: int = 310 const GENERATED_POND_COUNT: int = 33 const GENERATED_LAKE_COUNT: int = 20 @@ -235,10 +213,15 @@ func _validate_catalog_and_pools() -> void: else: inactive_count += 1 assert(not fish.is_selectable()) - assert(fish.display_texture == null) - if FINAL_CREATURE_TEXTURE_PATHS.has(fish.id): + if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS: + assert(fish.display_texture == null) + if fish.catalog_number in FINAL_CREATURE_CATALOG_NUMBERS: + var expected_path := ( + "res://art/exported/creatures/%d.png" + % fish.catalog_number + ) var expected_texture: Texture2D = load( - FINAL_CREATURE_TEXTURE_PATHS[fish.id] + expected_path ) as Texture2D assert(expected_texture != null) assert(fish.display_texture == expected_texture) @@ -246,14 +229,14 @@ func _validate_catalog_and_pools() -> void: if fish.collection_method == FishDataType.CollectionMethod.FISHING: fishing_count += 1 assert(fish.active) - if not FINAL_CREATURE_TEXTURE_PATHS.has(fish.id): + if fish.catalog_number not in FINAL_CREATURE_CATALOG_NUMBERS: assert(fish.display_texture == SharedFishPlaceholder) placeholder_fishing_count += 1 assert(active_count == ACTIVE_CATALOG_COUNT) assert(inactive_count == INACTIVE_CATALOG_COUNT) assert(fishing_count == FISHING_SPECIES_COUNT) - assert(final_art_count == FINAL_CREATURE_TEXTURE_PATHS.size()) - assert(placeholder_fishing_count == 283) + assert(final_art_count == FINAL_CREATURE_CATALOG_NUMBERS.size()) + assert(placeholder_fishing_count == 253) var chum: FishDataType = Catalog.get_fish_by_id(&"salmon_chum") assert(chum != null) assert(chum.get_season_text() == "fall") @@ -291,7 +274,7 @@ func _validate_catalog_and_pools() -> void: ) == chum ) seasonal_collection.free() - var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_blue") + var inactive_fish: FishDataType = Catalog.get_fish_by_id(&"crab_dungeness") assert(inactive_fish != null and not inactive_fish.active) var inactive_pool := FishPoolType.new() inactive_pool.candidates = [inactive_fish] diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 6a3f6c8..076f1c9 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -12,6 +12,9 @@ const GeneratedLakePool: FishPool = preload( const GeneratedRiverPool: FishPool = preload( "res://fish/pools/generated_river_pool.tres" ) +const Gatherables: GatherableCatalog = preload( + "res://gathering/catalog/gatherable_catalog.tres" +) const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) @@ -402,7 +405,7 @@ func _validate_generated_region( + generator.stacked_elevated_placement_keys().size() ) ) - _validate_elevated_cliff_feature(generator) + _validate_elevated_cliff_feature(region, generator) var shop := region.get_node("Interactables/FishingShopWorld") as Node3D var storage := region.get_node("Interactables/PlayerStorageBox") as Node3D @@ -536,6 +539,7 @@ func _validate_generated_region( -INF, 0.35, ) + _validate_crab_spawn_surfaces(region) _validate_water_recovery_positions( region, grass_prop_surface_triangles, @@ -691,6 +695,7 @@ func _validate_generated_region( func _validate_elevated_cliff_feature( + region: GeneratedWorldRegion, generator: TerrainChunkGenerator, ) -> void: var elevated_coordinates: Dictionary[Vector2i, StringName] = {} @@ -824,6 +829,27 @@ func _validate_elevated_cliff_feature( or generator.placement_keys()[low_index].begins_with("chunk_spawn@") ) assert(found_primary_ramp == not primary_is_third_tier) + var primary_surface_height := ( + generator.elevated_cliff_level_height + * (2.0 if primary_is_third_tier else 1.0) + ) + var primary_surface_position := generator.to_global( + generator.chunk_position(feature_center) + + Vector3.UP * primary_surface_height + ) + assert( + region.is_home_exterior_position_accessible(primary_surface_position) + == not primary_is_third_tier + ) + assert(region.is_home_exterior_position_accessible(generator.to_global( + generator.chunk_position(feature_center) + ))) + # A higher surface sharing a ramp-accessible base cell is still invalid + # unless that elevation has its own authored ramp connection. + assert(not region.is_home_exterior_position_accessible(generator.to_global( + generator.chunk_position(feature_center) + + Vector3.UP * (generator.elevated_cliff_level_height * 2.0) + ))) var generated := generator.get_generated_chunks_root() assert(generated != null) @@ -1251,6 +1277,58 @@ func _validate_decoration_transform( ) +func _validate_crab_spawn_surfaces(region: GeneratedWorldRegion) -> void: + var brown := Gatherables.get_entry(&"crab_brown") + var blue := Gatherables.get_entry(&"crab_blue") + assert(brown != null and blue != null) + assert(is_equal_approx( + region.get_saltwater_surface_height(), + GeneratedWorldRegion.WATER_HEIGHT, + )) + var brown_minimum := maxf( + brown.minimum_surface_y, + GeneratedWorldRegion.WATER_HEIGHT + brown.waterline_margin, + ) + var blue_maximum := minf( + blue.maximum_surface_y, + GeneratedWorldRegion.WATER_HEIGHT - blue.waterline_margin, + ) + assert(brown_minimum > blue_maximum) + var brown_triangles := region.get_spawn_surface_triangles( + brown.surface_materials, + brown_minimum, + 0.6, + brown.maximum_surface_y, + ) + var blue_triangles := region.get_spawn_surface_triangles( + blue.surface_materials, + blue.minimum_surface_y, + 0.6, + blue_maximum, + ) + assert(not brown_triangles.is_empty()) + assert(not blue_triangles.is_empty()) + assert( + brown.target_population_for_surface_area( + _surface_triangle_area(brown_triangles) + ) > brown.population + ) + assert( + blue.target_population_for_surface_area( + _surface_triangle_area(blue_triangles) + ) > blue.population + ) + + +func _surface_triangle_area(triangles: Array[PackedVector3Array]) -> float: + var area := 0.0 + for triangle: PackedVector3Array in triangles: + area += (triangle[1] - triangle[0]).cross( + triangle[2] - triangle[0] + ).length() * 0.5 + return area + + func _triangle_surface_height_at( position: Vector3, triangles: Array[PackedVector3Array], diff --git a/tests/logbook_validation.gd b/tests/logbook_validation.gd index 431ed2e..67747a5 100644 --- a/tests/logbook_validation.gd +++ b/tests/logbook_validation.gd @@ -46,7 +46,7 @@ func _run() -> void: func _validate_catalog() -> void: assert(CatalogResource.candidates.size() == 316) var ordered := LogbookCatalog.ordered_species(CatalogResource.candidates) - assert(ordered.size() == 313) + assert(ordered.size() == 314) var previous_number: int = 0 var catalog_numbers: Dictionary[int, bool] = {} for fish: FishDataType in CatalogResource.candidates: @@ -640,7 +640,10 @@ func _validate_page() -> void: page.call("_select_category", LogbookCatalog.Category.SHELLFISH) await create_timer(0.25).timeout - assert((page.get("_entry_buttons") as Dictionary).size() == 2) + assert((page.get("_entry_buttons") as Dictionary).size() == 3) + assert( + (page.get("_entry_buttons") as Dictionary).has(&"unknown_3902") + ) assert( (page.get("_entry_buttons") as Dictionary).has(&"unknown_3906") ) diff --git a/tests/world_spawn_multiplayer_validation.gd b/tests/world_spawn_multiplayer_validation.gd index 45be081..f5ffd2e 100644 --- a/tests/world_spawn_multiplayer_validation.gd +++ b/tests/world_spawn_multiplayer_validation.gd @@ -2,9 +2,10 @@ extends SceneTree const MainScene: PackedScene = preload("res://main/main.tscn") const TEST_PORT: int = 18170 -const EXPECTED_POPULATION: int = 8 +const EXPECTED_POPULATION: int = 12 const EXPECTED_BY_TYPE: Dictionary = { &"crab_brown": 2, + &"crab_blue": 4, &"clam_manila": 3, &"beetle_stag_common": 3, } @@ -161,6 +162,10 @@ func _validate_population( (position as Vector3).y >= entry.minimum_surface_y - 0.001 ) + if entry.type_id == &"crab_brown": + assert((position as Vector3).y > -0.45) + elif entry.type_id == &"crab_blue": + assert((position as Vector3).y < -0.45) if expect_authoritative_state: var quality: int = int(state.get("quality", -1)) assert(FishQuality.is_valid(quality)) @@ -196,7 +201,10 @@ func _validate_respawn_budget(service: Node) -> void: false, true, ) - assert((service.get("_entities") as Dictionary).size() == 6) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION - crab_ids.size() + ) var now: float = Time.get_ticks_msec() / 1000.0 var respawns: Array = service.get("_respawns") @@ -209,18 +217,27 @@ func _validate_respawn_budget(service: Node) -> void: respawns[index] = respawn service.call("_update_respawns") - assert((service.get("_entities") as Dictionary).size() == 7) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION - 1 + ) assert((service.get("_respawns") as Array).size() == 1) var next_by_type: Dictionary = service.get("_next_respawn_by_type") var next_allowed: float = float(next_by_type.get(&"crab_brown", 0.0)) assert(next_allowed - now >= 179.0) service.call("_update_respawns") - assert((service.get("_entities") as Dictionary).size() == 7) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION - 1 + ) assert((service.get("_respawns") as Array).size() == 1) next_by_type[&"crab_brown"] = now - 1.0 service.call("_update_respawns") - assert((service.get("_entities") as Dictionary).size() == 8) + assert( + (service.get("_entities") as Dictionary).size() + == EXPECTED_POPULATION + ) assert((service.get("_respawns") as Array).is_empty()) _validate_population(service, true) diff --git a/tests/world_spawn_protocol_validation.gd b/tests/world_spawn_protocol_validation.gd index 0840f2d..e300808 100644 --- a/tests/world_spawn_protocol_validation.gd +++ b/tests/world_spawn_protocol_validation.gd @@ -55,6 +55,17 @@ func _validate_catalog_statuses() -> void: assert(is_equal_approx(brown.minimum_respawn_spacing_seconds, 180.0)) assert(brown.required_tool_id == &"crab_net") assert(is_equal_approx(brown.minimum_surface_y, -0.44)) + assert( + brown.surface_water_relation + == GatherableData.SurfaceWaterRelation.ABOVE_SALT_WATER + ) + assert(is_equal_approx( + brown.surface_population_per_100_square_meters, + 0.15, + )) + assert(brown.maximum_surface_population == 24) + assert(brown.target_population_for_surface_area(100.0) == 2) + assert(brown.target_population_for_surface_area(7500.0) == 12) assert(FishCatalog.get_fish_by_id(&"crab_brown") == brown.catch_data) assert(not brown.catch_data.is_fishable()) var clam: GatherableData = Gatherables.get_entry(&"clam_manila") @@ -86,11 +97,27 @@ func _validate_catalog_statuses() -> void: assert(FishCatalog.get_fish_by_id(&"beetle_stag_common") == beetle.catch_data) assert(not beetle.catch_data.is_fishable()) - for type_id: StringName in [ - &"crab_ghost", - &"crab_blue", - &"crab_dungeness", - ]: + var blue: GatherableData = Gatherables.get_entry(&"crab_blue") + assert(blue != null and blue.is_available()) + assert(blue.catch_data.active) + assert(blue.is_available(CalendarSeasonType.Season.SUMMER)) + assert(blue.is_available(CalendarSeasonType.Season.FALL)) + assert(not blue.is_available(CalendarSeasonType.Season.WINTER)) + assert( + blue.surface_water_relation + == GatherableData.SurfaceWaterRelation.BELOW_SALT_WATER + ) + assert(is_equal_approx( + blue.surface_population_per_100_square_meters, + 0.4, + )) + assert(blue.maximum_surface_population == 16) + assert(blue.target_population_for_surface_area(100.0) == 4) + assert(blue.target_population_for_surface_area(2250.0) == 9) + assert(FishCatalog.get_fish_by_id(&"crab_blue") == blue.catch_data) + assert(not blue.catch_data.is_fishable()) + + for type_id: StringName in [&"crab_ghost", &"crab_dungeness"]: var entry: GatherableData = Gatherables.get_entry(type_id) assert(entry != null) assert(entry.is_valid()) @@ -109,8 +136,9 @@ func _validate_catalog_statuses() -> void: ) var available: Array[GatherableData] = Gatherables.get_available_entries() - assert(available.size() == 3) + assert(available.size() == 4) assert(available.has(brown)) + assert(available.has(blue)) assert(available.has(clam)) assert(available.has(beetle)) var winter_available: Array[GatherableData] = ( @@ -118,6 +146,7 @@ func _validate_catalog_statuses() -> void: ) assert(winter_available.has(brown)) assert(winter_available.has(clam)) + assert(not winter_available.has(blue)) assert(not winter_available.has(beetle)) var rng := RandomNumberGenerator.new() rng.seed = 24680