From 4463e524aaa6bef1b28b9df1c26e1d2482125ade Mon Sep 17 00:00:00 2001 From: Rijad Zuzo Date: Sun, 5 Jul 2026 23:06:23 +0200 Subject: [PATCH] style: apply gdformat formatting across the entire project Auto-format all GDScript files using gdformat from gdtoolkit. This is a baseline formatting pass to ensure consistent style: - Normalizes indentation and spacing - Wraps long lines to 100 characters - Removes trailing whitespace - Standardizes blank lines between functions 68 files reformatted, 8 files left unchanged (3rd-party addon files with parse errors excluded). --- .../terrain_3d/extras/3rd_party/import_sgt.gd | 28 +- .../extras/3rd_party/project_on_terrain3d.gd | 192 ++--- .../particle_example/terrain_3D_particles.gd | 69 +- .../menu/channel_packer_dragdrop.gd | 2 + addons/terrain_3d/menu/directory_setup.gd | 12 +- addons/terrain_3d/menu/terrain_menu.gd | 11 +- addons/terrain_3d/src/asset_dock.gd | 219 +++--- addons/terrain_3d/src/asset_dock_45.gd | 181 ++--- addons/terrain_3d/src/double_slider.gd | 42 +- addons/terrain_3d/src/editor_plugin.gd | 199 +++-- .../src/gradient_operation_builder.gd | 19 +- addons/terrain_3d/src/multi_picker.gd | 13 +- addons/terrain_3d/src/operation_builder.gd | 6 +- addons/terrain_3d/src/region_gizmo.gd | 40 +- addons/terrain_3d/src/tool_settings.gd | 694 +++++++++++++----- addons/terrain_3d/src/toolbar.gd | 184 +++-- addons/terrain_3d/tools/importer.gd | 40 +- addons/terrain_3d/tools/region_mover.gd | 28 +- addons/terrain_3d/utils/terrain_3d_objects.gd | 61 +- demo/src/CameraManager.gd | 4 +- demo/src/CaveEntrance.gd | 2 +- demo/src/CodeGenerated.gd | 26 +- demo/src/DemoScene.gd | 25 +- demo/src/Enemy.gd | 8 +- demo/src/Player.gd | 38 +- demo/src/RuntimeNavigationBaker.gd | 47 +- demo/src/UI.gd | 17 +- player/camera_rig.gd | 4 +- player/npc/NpcVisual.gd | 22 +- player/player.gd | 23 +- simulation/SimNPC.gd | 22 +- simulation/SimVillage.gd | 55 +- simulation/SimulationClock.gd | 3 + simulation/SimulationManager.gd | 306 ++++---- simulation/actions/ActionExecutionSystem.gd | 2 + simulation/actions/ActionSelectionResult.gd | 6 +- simulation/actions/ActionSelectionSystem.gd | 69 +- simulation/actions/ActionTargetResolver.gd | 27 +- simulation/definitions/ActionDefinition.gd | 15 +- .../definitions/ProfessionDefinition.gd | 1 + .../definitions/SimulationDefinitions.gd | 22 +- simulation/persistence/SaveSlotController.gd | 1 + simulation/persistence/SaveSlotStore.gd | 16 +- simulation/state/EconomicEventRecord.gd | 44 +- simulation/state/NPCStateRecord.gd | 136 ++-- simulation/state/ResourceStateRecord.gd | 81 +- simulation/state/SimulationStateRecord.gd | 44 +- simulation/state/StorageStateRecord.gd | 34 +- simulation/state/VillageStateRecord.gd | 21 +- tests/action_system_boundaries_test.gd | 57 +- tests/deterministic_simulation_test.gd | 4 + tests/food_storage_loop_test.gd | 98 ++- tests/jajce_runtime_integration_test.gd | 54 +- tests/jajce_world_scaffold_test.gd | 53 +- tests/npc_visual_lifecycle_test.gd | 34 +- tests/resource_node_player_parity_test.gd | 37 +- tests/save_slot_persistence_test.gd | 10 +- tests/simulation_definitions_test.gd | 51 +- tests/simulation_state_serialization_test.gd | 87 +-- tools/generate_jajce_terrain.gd | 84 +-- tools/generate_jajce_terrain_seed.gd | 14 +- tools/generate_terrain_textures.gd | 3 + world/active_world_adapter.gd | 22 +- world/jajce/StylizedTree.gd | 1 + world/jajce/beauty_camera.gd | 2 + world/jajce/jajce_world.gd | 4 +- world/resource_nodes/ResourceNode.gd | 26 +- world/ui/ui.gd | 27 +- world/world_view_manager.gd | 71 +- 69 files changed, 2253 insertions(+), 1647 deletions(-) diff --git a/addons/terrain_3d/extras/3rd_party/import_sgt.gd b/addons/terrain_3d/extras/3rd_party/import_sgt.gd index 98b1be4..2708361 100644 --- a/addons/terrain_3d/extras/3rd_party/import_sgt.gd +++ b/addons/terrain_3d/extras/3rd_party/import_sgt.gd @@ -1,6 +1,6 @@ # Copyright © 2023-2026 Cory Petkovsek, Roope Palmroos, and Contributors. # Import From SimpleGrassTextured -# +# # This script demonstrates how to import transforms from SimpleGrassTextured. To use it: # # 1. Setup the mesh asset you wish to use in the asset dock. @@ -9,14 +9,14 @@ # 1. At the very top, assign your SimpleGrassTextured node. # 1. Input the desired mesh asset ID. # 1. Click import. The output window and console will report when finished. -# 1. Clear the script from your Terrain3D node, and save your scene. +# 1. Clear the script from your Terrain3D node, and save your scene. # # The instance transforms are now stored in your region files. # # Use clear_instances to erase all instances that match the assign_mesh_id. # -# The add_transforms function (called by add_multimesh) applies the height_offset specified in the -# Terrain3DMeshAsset. +# The add_transforms function (called by add_multimesh) applies the height_offset specified in the +# Terrain3DMeshAsset. # Once the transforms are imported, you can reassign any mesh you like into this mesh slot. @tool @@ -24,8 +24,10 @@ extends Terrain3D @export var simple_grass_textured: MultiMeshInstance3D @export var assign_mesh_id: int -@export var import: bool = false : set = import_sgt -@export var clear_instances: bool = false : set = clear_multimeshes +@export var import: bool = false: + set = import_sgt +@export var clear_instances: bool = false: + set = clear_multimeshes func clear_multimeshes(value: bool) -> void: @@ -34,9 +36,13 @@ func clear_multimeshes(value: bool) -> void: func import_sgt(value: bool) -> void: var sgt_mm: MultiMesh = simple_grass_textured.multimesh - var global_xform: Transform3D = simple_grass_textured.global_transform - print("Starting to import %d instances from SimpleGrassTextured using mesh id %d" % [ sgt_mm.instance_count, assign_mesh_id]) + var global_xform: Transform3D = simple_grass_textured.global_transform + print( + ( + "Starting to import %d instances from SimpleGrassTextured using mesh id %d" + % [sgt_mm.instance_count, assign_mesh_id] + ) + ) var time: int = Time.get_ticks_msec() - get_instancer().add_multimesh(assign_mesh_id, sgt_mm, simple_grass_textured.global_transform) - print("Import complete in %.2f seconds" % [ float(Time.get_ticks_msec() - time)/1000. ]) - + get_instancer().add_multimesh(assign_mesh_id, sgt_mm, simple_grass_textured.global_transform) + print("Import complete in %.2f seconds" % [float(Time.get_ticks_msec() - time) / 1000.]) diff --git a/addons/terrain_3d/extras/3rd_party/project_on_terrain3d.gd b/addons/terrain_3d/extras/3rd_party/project_on_terrain3d.gd index ef482de..4e4159d 100644 --- a/addons/terrain_3d/extras/3rd_party/project_on_terrain3d.gd +++ b/addons/terrain_3d/extras/3rd_party/project_on_terrain3d.gd @@ -1,6 +1,6 @@ # Copyright © 2023-2026 Cory Petkovsek, Roope Palmroos, and Contributors. # This script is an addon for HungryProton's Scatter https://github.com/HungryProton/scatter -# It provides a `Project on Terrain3D` modifier, which allows Scatter +# It provides a `Project on Terrain3D` modifier, which allows Scatter # to detect the terrain height from Terrain3D without using collision. # # Copy this file into /addons/proton_scatter/src/modifiers @@ -26,112 +26,112 @@ # # #func _init() -> void: - #display_name = "Project On Terrain3D" - #category = "Edit" - #can_restrict_height = false - #global_reference_frame_available = true - #local_reference_frame_available = true - #individual_instances_reference_frame_available = true - #use_global_space_by_default() +#display_name = "Project On Terrain3D" +#category = "Edit" +#can_restrict_height = false +#global_reference_frame_available = true +#local_reference_frame_available = true +#individual_instances_reference_frame_available = true +#use_global_space_by_default() # - #documentation.add_paragraph( - #"This is a modified version of `Project on Colliders` that queries Terrain3D - #for heights without using collision. It constrains placement by slope or texture. +#documentation.add_paragraph( +#"This is a modified version of `Project on Colliders` that queries Terrain3D +#for heights without using collision. It constrains placement by slope or texture. # - #This modifier must have terrain_node set to a Terrain3D node.") +#This modifier must have terrain_node set to a Terrain3D node.") # - #var p := documentation.add_parameter("Terrain Node") - #p.set_type("NodePath") - #p.set_description("Set your Terrain3D node.") - # - #p = documentation.add_parameter("Align with collision normal") - #p.set_type("bool") - #p.set_description( - #"Rotate the transform to align it with the collision normal in case - #the ray cast hit a collider.") - # - #p = documentation.add_parameter("Enable Texture Filtering") - #p.set_type("bool") - #p.set_description( - #"If enabled, objects will only be placed based on the ground texture specified.") - # - #p = documentation.add_parameter("Target Texture ID") - #p.set_type("int") - #p.set_description( - #"The ID of the texture to place objects on (0-31). Objects will only be placed on this texture.") - # - #p = documentation.add_parameter("Not Target Texture") - #p.set_type("bool") - #p.set_description( - #"If true, objects will be placed on all textures EXCEPT the target texture.") - # - #p = documentation.add_parameter("Texture Threshold") - #p.set_type("float") - #p.set_description("The blend value required for placement on the texture.") +#var p := documentation.add_parameter("Terrain Node") +#p.set_type("NodePath") +#p.set_description("Set your Terrain3D node.") +# +#p = documentation.add_parameter("Align with collision normal") +#p.set_type("bool") +#p.set_description( +#"Rotate the transform to align it with the collision normal in case +#the ray cast hit a collider.") +# +#p = documentation.add_parameter("Enable Texture Filtering") +#p.set_type("bool") +#p.set_description( +#"If enabled, objects will only be placed based on the ground texture specified.") +# +#p = documentation.add_parameter("Target Texture ID") +#p.set_type("int") +#p.set_description( +#"The ID of the texture to place objects on (0-31). Objects will only be placed on this texture.") +# +#p = documentation.add_parameter("Not Target Texture") +#p.set_type("bool") +#p.set_description( +#"If true, objects will be placed on all textures EXCEPT the target texture.") +# +#p = documentation.add_parameter("Texture Threshold") +#p.set_type("float") +#p.set_description("The blend value required for placement on the texture.") # # #func _process_transforms(transforms, domain, _seed) -> void: - #if transforms.is_empty(): - #return +#if transforms.is_empty(): +#return # - #if terrain_node: - #_terrain = domain.get_root().get_node_or_null(terrain_node) +#if terrain_node: +#_terrain = domain.get_root().get_node_or_null(terrain_node) # - #if not _terrain: - #warning += """No Terrain3D node found""" - #return +#if not _terrain: +#warning += """No Terrain3D node found""" +#return # - #if not _terrain.data: - #warning += """Terrain3DData is not initialized""" - #return +#if not _terrain.data: +#warning += """Terrain3DData is not initialized""" +#return # - ## Review transforms - #var gt: Transform3D = domain.get_global_transform() - #var gt_inverse := gt.affine_inverse() - #var new_transforms_array: Array[Transform3D] = [] - #var remapped_max_slope: float = remap(max_slope, 0.0, 90.0, 0.0, 1.0) - #for i in transforms.list.size(): - #var t: Transform3D = transforms.list[i] - # - #var location: Vector3 = (gt * t).origin - #var height: float = _terrain.data.get_height(location) - #if is_nan(height): - #continue - # - #var normal: Vector3 = _terrain.data.get_normal(location) - #if not abs(Vector3.UP.dot(normal)) >= (1.0 - remapped_max_slope): - #continue - # - #if enable_texture_filtering: - #var texture_info: Vector3 = _terrain.data.get_texture_id(location) - #var base_id: int = int(texture_info.x) - #var overlay_id: int = int(texture_info.y) - #var blend_value: float = texture_info.z - ## Skip if overlay or blend != target texture, unless inverted - #if ((overlay_id != target_texture_id or blend_value < texture_threshold) and \ - #(base_id != target_texture_id or blend_value >= texture_threshold)) != not_target_texture: - #continue +## Review transforms +#var gt: Transform3D = domain.get_global_transform() +#var gt_inverse := gt.affine_inverse() +#var new_transforms_array: Array[Transform3D] = [] +#var remapped_max_slope: float = remap(max_slope, 0.0, 90.0, 0.0, 1.0) +#for i in transforms.list.size(): +#var t: Transform3D = transforms.list[i] # - #if align_with_collision_normal and not is_nan(normal.x): - #t.basis.y = normal - #t.basis.x = -t.basis.z.cross(normal) - #t.basis = t.basis.orthonormalized() +#var location: Vector3 = (gt * t).origin +#var height: float = _terrain.data.get_height(location) +#if is_nan(height): +#continue # - #t.origin.y = height - gt.origin.y - #new_transforms_array.push_back(t) +#var normal: Vector3 = _terrain.data.get_normal(location) +#if not abs(Vector3.UP.dot(normal)) >= (1.0 - remapped_max_slope): +#continue # - #transforms.list.clear() - #transforms.list.append_array(new_transforms_array) +#if enable_texture_filtering: +#var texture_info: Vector3 = _terrain.data.get_texture_id(location) +#var base_id: int = int(texture_info.x) +#var overlay_id: int = int(texture_info.y) +#var blend_value: float = texture_info.z +## Skip if overlay or blend != target texture, unless inverted +#if ((overlay_id != target_texture_id or blend_value < texture_threshold) and \ +#(base_id != target_texture_id or blend_value >= texture_threshold)) != not_target_texture: +#continue # - #if transforms.is_empty(): - #warning += """All transforms have been removed. Possible reasons include: \n""" - #if enable_texture_filtering: - #warning += """+ No matching texture found at any position. - #+ Texture threshold may be too high. - #""" - #warning += """+ No collider is close enough to the shapes. - #+ Ray length is too short. - #+ Ray direction is incorrect. - #+ Collision mask is not set properly. - #+ Max slope is too low. - #""" +#if align_with_collision_normal and not is_nan(normal.x): +#t.basis.y = normal +#t.basis.x = -t.basis.z.cross(normal) +#t.basis = t.basis.orthonormalized() +# +#t.origin.y = height - gt.origin.y +#new_transforms_array.push_back(t) +# +#transforms.list.clear() +#transforms.list.append_array(new_transforms_array) +# +#if transforms.is_empty(): +#warning += """All transforms have been removed. Possible reasons include: \n""" +#if enable_texture_filtering: +#warning += """+ No matching texture found at any position. +#+ Texture threshold may be too high. +#""" +#warning += """+ No collider is close enough to the shapes. +#+ Ray length is too short. +#+ Ray direction is incorrect. +#+ Collision mask is not set properly. +#+ Max slope is too low. +#""" diff --git a/addons/terrain_3d/extras/particle_example/terrain_3D_particles.gd b/addons/terrain_3d/extras/particle_example/terrain_3D_particles.gd index 2864f09..2f4b8c6 100644 --- a/addons/terrain_3d/extras/particle_example/terrain_3D_particles.gd +++ b/addons/terrain_3d/extras/particle_example/terrain_3D_particles.gd @@ -7,7 +7,6 @@ @tool extends Node3D - #region settings ## Auto set if attached as a child of a Terrain3D node @export var terrain: Terrain3D: @@ -15,7 +14,6 @@ extends Node3D terrain = value _create_grid() - ## Distance between instances @export_range(0.125, 2.0, 0.015625) var instance_spacing: float = 0.5: set(value): @@ -24,7 +22,6 @@ extends Node3D amount = rows * rows _set_offsets() - ## Width of an individual cell of the grid @export_range(8.0, 256.0, 1.0) var cell_width: float = 32.0: set(value): @@ -44,8 +41,7 @@ extends Node3D p.custom_aabb = aabb _set_offsets() - -## Grid width. Must be odd. +## Grid width. Must be odd. ## Higher values cull slightly better, draw further out. @export_range(1, 15, 2) var grid_width: int = 9: set(value): @@ -54,7 +50,6 @@ extends Node3D min_draw_distance = 1.0 _create_grid() - @export_storage var rows: int = 1 @export_storage var amount: int = 1: @@ -65,7 +60,6 @@ extends Node3D for p in particle_nodes: p.amount = amount - @export_range(1, 256, 1) var process_fixed_fps: int = 30: set(value): process_fixed_fps = maxi(value, 1) @@ -73,7 +67,6 @@ extends Node3D p.fixed_fps = process_fixed_fps p.preprocess = 1.0 / float(process_fixed_fps) - ## Access to process material parameters @export var process_material: ShaderMaterial @@ -81,23 +74,21 @@ extends Node3D @export var mesh: Mesh @export var shadow_mode: GeometryInstance3D.ShadowCastingSetting = ( - GeometryInstance3D.ShadowCastingSetting.SHADOW_CASTING_SETTING_ON): + GeometryInstance3D.ShadowCastingSetting.SHADOW_CASTING_SETTING_ON +): set(value): shadow_mode = value for p in particle_nodes: p.cast_shadow = value - ## Override material for the particle mesh -@export_custom( - PROPERTY_HINT_RESOURCE_TYPE, - "BaseMaterial3D,ShaderMaterial") var mesh_material_override: Material: +@export_custom(PROPERTY_HINT_RESOURCE_TYPE, "BaseMaterial3D,ShaderMaterial") +var mesh_material_override: Material: set(value): mesh_material_override = value for p in particle_nodes: p.material_override = mesh_material_override - @export_group("Info") ## The minimum distance that particles will be drawn upto ## If using fade out effects like pixel alpha this is the limit to use. @@ -105,7 +96,6 @@ extends Node3D set(value): min_draw_distance = float(cell_width * grid_width) * 0.5 - ## Displays current total particle count based on Cell Width and Instance Spacing @export var particle_count: int = 1: set(value): @@ -113,7 +103,6 @@ extends Node3D #endregion - var offsets: Array[Vector3] var last_pos: Vector3 = Vector3.ZERO var particle_nodes: Array[GPUParticles3D] @@ -139,7 +128,9 @@ func _physics_process(delta: float) -> void: if last_pos.distance_squared_to(camera.global_position) > 1.0: var pos: Vector3 = camera.global_position.snapped(Vector3.ONE) _position_grid(pos) - RenderingServer.material_set_param(process_material.get_rid(), "camera_position", pos ) + RenderingServer.material_set_param( + process_material.get_rid(), "camera_position", pos + ) last_pos = camera.global_position _update_process_parameters() else: @@ -180,7 +171,7 @@ func _create_grid() -> void: if mesh_material_override: particle_node.material_override = mesh_material_override particle_node.use_fixed_seed = true - if (x > -half_grid and z > -half_grid): # Use the same seed across all nodes + if x > -half_grid and z > -half_grid: # Use the same seed across all nodes particle_node.seed = particle_nodes[0].seed self.add_child(particle_node) particle_node.emitting = true @@ -194,9 +185,7 @@ func _set_offsets() -> void: for x in range(-half_grid, half_grid + 1): for z in range(-half_grid, half_grid + 1): var offset := Vector3( - float(x * rows) * instance_spacing, - 0.0, - float(z * rows) * instance_spacing + float(x * rows) * instance_spacing, 0.0, float(z * rows) * instance_spacing ) offsets.append(offset) @@ -214,24 +203,42 @@ func _position_grid(pos: Vector3) -> void: var snap = Vector3(pos.x, 0, pos.z).snapped(Vector3.ONE) + offsets[i] node.global_position = (snap / instance_spacing).round() * instance_spacing node.reset_physics_interpolation() - node.restart(true) # keep the same seed. + node.restart(true) # keep the same seed. func _update_process_parameters() -> void: if process_material: var process_rid: RID = process_material.get_rid() if terrain and process_rid.is_valid(): - RenderingServer.material_set_param(process_rid, "_background_mode", terrain.material.world_background) - RenderingServer.material_set_param(process_rid, "_vertex_spacing", terrain.vertex_spacing) - RenderingServer.material_set_param(process_rid, "_vertex_density", 1.0 / terrain.vertex_spacing) + RenderingServer.material_set_param( + process_rid, "_background_mode", terrain.material.world_background + ) + RenderingServer.material_set_param( + process_rid, "_vertex_spacing", terrain.vertex_spacing + ) + RenderingServer.material_set_param( + process_rid, "_vertex_density", 1.0 / terrain.vertex_spacing + ) RenderingServer.material_set_param(process_rid, "_region_size", terrain.region_size) - RenderingServer.material_set_param(process_rid, "_region_texel_size", 1.0 / terrain.region_size) + RenderingServer.material_set_param( + process_rid, "_region_texel_size", 1.0 / terrain.region_size + ) RenderingServer.material_set_param(process_rid, "_region_map_size", 32) - RenderingServer.material_set_param(process_rid, "_region_map", terrain.data.get_region_map()) - RenderingServer.material_set_param(process_rid, "_region_locations", terrain.data.get_region_locations()) - RenderingServer.material_set_param(process_rid, "_height_maps", terrain.data.get_height_maps_rid()) - RenderingServer.material_set_param(process_rid, "_control_maps", terrain.data.get_control_maps_rid()) - RenderingServer.material_set_param(process_rid, "_color_maps", terrain.data.get_color_maps_rid()) + RenderingServer.material_set_param( + process_rid, "_region_map", terrain.data.get_region_map() + ) + RenderingServer.material_set_param( + process_rid, "_region_locations", terrain.data.get_region_locations() + ) + RenderingServer.material_set_param( + process_rid, "_height_maps", terrain.data.get_height_maps_rid() + ) + RenderingServer.material_set_param( + process_rid, "_control_maps", terrain.data.get_control_maps_rid() + ) + RenderingServer.material_set_param( + process_rid, "_color_maps", terrain.data.get_color_maps_rid() + ) RenderingServer.material_set_param(process_rid, "instance_spacing", instance_spacing) RenderingServer.material_set_param(process_rid, "instance_rows", rows) RenderingServer.material_set_param(process_rid, "max_dist", min_draw_distance) diff --git a/addons/terrain_3d/menu/channel_packer_dragdrop.gd b/addons/terrain_3d/menu/channel_packer_dragdrop.gd index f2fc633..cda709c 100644 --- a/addons/terrain_3d/menu/channel_packer_dragdrop.gd +++ b/addons/terrain_3d/menu/channel_packer_dragdrop.gd @@ -5,6 +5,7 @@ extends Button signal dropped + func _can_drop_data(p_position, p_data) -> bool: if typeof(p_data) == TYPE_DICTIONARY: if p_data.files.size() == 1: @@ -13,5 +14,6 @@ func _can_drop_data(p_position, p_data) -> bool: return true return false + func _drop_data(p_position, p_data) -> void: dropped.emit(p_data.files[0]) diff --git a/addons/terrain_3d/menu/directory_setup.gd b/addons/terrain_3d/menu/directory_setup.gd index 791a325..4a2a8eb 100644 --- a/addons/terrain_3d/menu/directory_setup.gd +++ b/addons/terrain_3d/menu/directory_setup.gd @@ -29,19 +29,21 @@ func _init() -> void: func directory_setup_popup() -> void: dialog = load(DIRECTORY_SETUP).instantiate() dialog.hide() - + # Nodes select_dir_btn = dialog.get_node("Margin/VBox/DirHBox/SelectDir") selected_dir_le = dialog.get_node("Margin/VBox/DirHBox/LineEdit") if plugin.terrain.data_directory: selected_dir_le.text = plugin.terrain.data_directory - + # Icons plugin.ui.set_button_editor_icon(select_dir_btn, "Folder") - + #Signals - select_dir_btn.pressed.connect(_on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR)) + select_dir_btn.pressed.connect( + _on_select_file_pressed.bind(EditorFileDialog.FILE_MODE_OPEN_DIR) + ) dialog.confirmed.connect(_on_close_requested) dialog.canceled.connect(_on_close_requested) dialog.get_ok_button().pressed.connect(_on_ok_pressed) @@ -74,7 +76,7 @@ func _on_ok_pressed() -> void: if not DirAccess.dir_exists_absolute(selected_dir_le.text): push_error("Directory doesn't exist: ", selected_dir_le.text) return - # Check if directory empty of terrain files + # Check if directory empty of terrain files var data_found: bool = false var files: Array = DirAccess.get_files_at(selected_dir_le.text) for file in files: diff --git a/addons/terrain_3d/menu/terrain_menu.gd b/addons/terrain_3d/menu/terrain_menu.gd index dc2e7b7..cfaf408 100644 --- a/addons/terrain_3d/menu/terrain_menu.gd +++ b/addons/terrain_3d/menu/terrain_menu.gd @@ -2,7 +2,6 @@ # Menu for Terrain3D extends HBoxContainer - const DirectoryWizard: Script = preload("res://addons/terrain_3d/menu/directory_setup.gd") const Packer: Script = preload("res://addons/terrain_3d/menu/channel_packer.gd") const Baker: Script = preload("res://addons/terrain_3d/menu/baker.gd") @@ -32,17 +31,17 @@ func _enter_tree() -> void: baker.plugin = plugin add_child(directory_setup) add_child(baker) - + menu_button.text = "Terrain3D" - menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP) - menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES) + menu_button.get_popup().add_item("Directory Setup...", MENU_DIRECTORY_SETUP) + menu_button.get_popup().add_item("Pack Textures...", MENU_PACK_TEXTURES) menu_button.get_popup().add_separator("", MENU_SEPARATOR) menu_button.get_popup().add_item("Bake ArrayMesh...", MENU_BAKE_ARRAY_MESH) menu_button.get_popup().add_item("Bake Occluder3D...", MENU_BAKE_OCCLUDER) menu_button.get_popup().add_separator("", MENU_SEPARATOR2) menu_button.get_popup().add_item("Set up Navigation...", MENU_SET_UP_NAVIGATION) menu_button.get_popup().add_item("Bake NavMesh...", MENU_BAKE_NAV_MESH) - + menu_button.get_popup().id_pressed.connect(_on_menu_pressed) menu_button.about_to_popup.connect(_on_menu_about_to_popup) add_child(menu_button) @@ -53,7 +52,7 @@ func _on_menu_pressed(p_id: int) -> void: MENU_DIRECTORY_SETUP: directory_setup.directory_setup_popup() MENU_PACK_TEXTURES: - packer.pack_textures_popup() + packer.pack_textures_popup() MENU_BAKE_ARRAY_MESH: baker.bake_mesh_popup() MENU_BAKE_OCCLUDER: diff --git a/addons/terrain_3d/src/asset_dock.gd b/addons/terrain_3d/src/asset_dock.gd index 4e500a2..f45e2c5 100644 --- a/addons/terrain_3d/src/asset_dock.gd +++ b/addons/terrain_3d/src/asset_dock.gd @@ -30,11 +30,11 @@ var search_button: Button #DEPRECATED 4.5 #class EdDock extends EditorDock: - #func _update_layout(layout: int) -> void: - #layout 1 vertical, 2 horizontal, 4 window - #print("Terrain3DAssetDock: _update_layout called with: ", layout) +#func _update_layout(layout: int) -> void: +#layout 1 vertical, 2 horizontal, 4 window +#print("Terrain3DAssetDock: _update_layout called with: ", layout) -var _dock: MarginContainer #DEPRECATED 4.5 - Use EdDock +var _dock: MarginContainer #DEPRECATED 4.5 - Use EdDock var _initialized: bool = false var plugin: EditorPlugin var window: Window @@ -44,18 +44,18 @@ func _notification(what: int) -> void: if what == NOTIFICATION_ENTER_TREE: await get_tree().process_frame update_layout() - - + + func initialize(p_plugin: EditorPlugin) -> void: if p_plugin: plugin = p_plugin - _dock = ClassDB.instantiate("EditorDock") #DEPRECATED 4.5 - EdDock.new() + _dock = ClassDB.instantiate("EditorDock") #DEPRECATED 4.5 - EdDock.new() _dock.title = "Terrain3D" _dock.dock_icon = preload("../icons/terrain3d.svg") - _dock.default_slot = 8 #DEPRECATED 4.5 - EditorDock.DockSlot.DOCK_SLOT_BOTTOM + _dock.default_slot = 8 #DEPRECATED 4.5 - EditorDock.DockSlot.DOCK_SLOT_BOTTOM _dock.closable = false - _dock.available_layouts = 0x7 #DEPRECATED 4.5 - EditorDock.DOCK_LAYOUT_ALL + _dock.available_layouts = 0x7 #DEPRECATED 4.5 - EditorDock.DOCK_LAYOUT_ALL _dock.add_child(self) plugin.add_dock(_dock) _dock.open() @@ -75,7 +75,7 @@ func initialize(p_plugin: EditorPlugin) -> void: var editor_scale: float = EditorInterface.get_editor_scale() search_box.custom_minimum_size = Vector2(100. * editor_scale, 30. * editor_scale) search_button = $Box/Buttons/SearchBox/SearchButton - + texture_list = ListContainer.new() texture_list.name = "TextureList" texture_list.plugin = plugin @@ -100,21 +100,31 @@ func initialize(p_plugin: EditorPlugin) -> void: size_slider.value_changed.connect(_on_slider_changed) plugin.ui.toolbar.tool_changed.connect(_on_tool_changed) - meshes_btn.add_theme_font_size_override("font_size", int(16. * EditorInterface.get_editor_scale())) - textures_btn.add_theme_font_size_override("font_size", int(16. * EditorInterface.get_editor_scale())) + meshes_btn.add_theme_font_size_override( + "font_size", int(16. * EditorInterface.get_editor_scale()) + ) + textures_btn.add_theme_font_size_override( + "font_size", int(16. * EditorInterface.get_editor_scale()) + ) search_box.text_changed.connect(_on_search_text_changed) search_button.pressed.connect(_on_search_button_pressed) - + confirm_dialog = ConfirmationDialog.new() add_child(confirm_dialog, true) confirm_dialog.hide() - confirm_dialog.confirmed.connect(func(): _confirmed = true; \ - confirmation_closed.emit(); \ - confirmation_confirmed.emit() ) - confirm_dialog.canceled.connect(func(): _confirmed = false; \ - confirmation_closed.emit(); \ - confirmation_canceled.emit() ) + confirm_dialog.confirmed.connect( + func(): + _confirmed = true + confirmation_closed.emit() + confirmation_confirmed.emit() + ) + confirm_dialog.canceled.connect( + func(): + _confirmed = false + confirmation_closed.emit() + confirmation_canceled.emit() + ) # Setup styles set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) @@ -155,7 +165,7 @@ func update_layout() -> void: if not _initialized: return if plugin.debug > 1: - print("Terrain3DAssetDock: update_layout") + print("Terrain3DAssetDock: update_layout") ## Detect if we have a new window from `Make floating` and grab it if not window: @@ -163,12 +173,12 @@ func update_layout() -> void: window = get_parent().get_parent().get_parent() _on_pin_changed(pinned_btn.button_pressed) plugin.godot_editor_window.mouse_entered.connect(_on_godot_window_entered) - return # Displaying will call this function again + return # Displaying will call this function again # Check if window was just freed. Freed objects have a different hash than null elif hash(window) != hash(null): window = null plugin.godot_editor_window.mouse_entered.disconnect(_on_godot_window_entered) - return # Will call this function again upon display + return # Will call this function again upon display ## Vertical layout: buttons on top if size.x < 700: @@ -180,7 +190,7 @@ func update_layout() -> void: box.move_child(size_slider, 2) pinned_btn.reparent(buttons) else: - # Wide layout: buttons on left + # Wide layout: buttons on left box.vertical = false buttons.vertical = true search_box.reparent(buttons) @@ -205,8 +215,8 @@ func set_selected_by_asset_id(p_id: int) -> void: search_box.text = "" _on_search_text_changed() current_list.set_selected_id(p_id) - - + + func _on_search_text_changed() -> void: if plugin.debug: print("Terrain3DAssetDock: _on_search_text_changed: ", search_box.text) @@ -217,7 +227,7 @@ func _on_search_text_changed() -> void: search_button.icon = get_theme_icon("Close", "EditorIcons") else: search_button.icon = get_theme_icon("Search", "EditorIcons") - + mesh_list.search_text = search_box.text texture_list.search_text = search_box.text current_list.update_asset_list() @@ -298,7 +308,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor print("Terrain3DAssetDock: _on_tool_changed: ", p_tool, ", ", p_operation) if p_tool == Terrain3DEditor.INSTANCER: _on_meshes_pressed() - elif p_tool in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + elif p_tool in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS]: _on_textures_pressed() @@ -307,10 +317,12 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor func update_assets() -> void: if plugin.debug: - print("Terrain3DAssetDock: update_assets: ", plugin.terrain.assets if plugin.terrain else "") + print( + "Terrain3DAssetDock: update_assets: ", plugin.terrain.assets if plugin.terrain else "" + ) if not _initialized: return - + # Verify signals to individual lists if plugin.is_terrain_valid() and plugin.terrain.assets: if not plugin.terrain.assets.textures_changed.is_connected(texture_list.update_asset_list): @@ -327,7 +339,7 @@ func update_assets() -> void: func load_editor_settings() -> void: # Remove old editor settings const ES_DOCK: String = "terrain3d/dock/" - for setting in [ "slot", "floating", "window_position", "window_size" ]: + for setting in ["slot", "floating", "window_position", "window_size"]: plugin.erase_setting(ES_DOCK + setting) pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true) size_slider.value = plugin.get_setting(ES_DOCK_TILE_SIZE, 90) @@ -350,8 +362,9 @@ func save_editor_settings() -> void: ## class ListContainer ############################################################## - -class ListContainer extends Container: + +class ListContainer: + extends Container var plugin: EditorPlugin var type := Terrain3DAssets.TYPE_TEXTURE var entries: Array[ListEntry] @@ -362,7 +375,6 @@ class ListContainer extends Container: var _clearing_resource: bool = false var search_text: String = "" - func _ready() -> void: set_v_size_flags(SIZE_EXPAND_FILL) set_h_size_flags(SIZE_EXPAND_FILL) @@ -371,24 +383,22 @@ class ListContainer extends Container: add_theme_constant_override("shadow_offset_x", 1) add_theme_constant_override("shadow_offset_y", 1) - func clear() -> void: for e in entries: e.get_parent().remove_child(e) e.queue_free() entries.clear() - func update_asset_list() -> void: if plugin.debug: print("Terrain3DListContainer ", name, ": update_asset_list") clear() - + # Grab terrain var t: Terrain3D = plugin.get_terrain() if not (t and t.assets): return - + if type == Terrain3DAssets.TYPE_TEXTURE: var texture_count: int = t.assets.get_texture_count() for i in texture_count: @@ -407,7 +417,6 @@ class ListContainer extends Container: add_item() set_selected_id(selected_id) - func add_item(p_resource: Resource = null) -> void: var entry: ListEntry = ListEntry.new() entry.focus_style = focus_style @@ -423,58 +432,79 @@ class ListContainer extends Container: entry.type = type add_child(entry, true) entries.push_back(entry) - + if p_resource: if not p_resource.id_changed.is_connected(set_selected_after_swap): p_resource.id_changed.connect(set_selected_after_swap) - func _on_resource_hovered(p_id: int): if type == Terrain3DAssets.TYPE_MESH: if plugin.terrain: plugin.terrain.assets.create_mesh_thumbnails(p_id) - - func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void: + func set_selected_after_swap( + p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int + ) -> void: EditorInterface.mark_scene_as_unsaved() set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) - func clicked_id(p_id: int) -> void: # Select Tool if clicking an asset plugin.select_terrain() - if type == Terrain3DAssets.TYPE_TEXTURE and \ - not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + if ( + type == Terrain3DAssets.TYPE_TEXTURE + and not ( + plugin.editor.get_tool() + in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS] + ) + ): var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintTexture") if paint_btn: paint_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE) - elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: + elif ( + type == Terrain3DAssets.TYPE_MESH + and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER + ): var instancer_btn: Button = plugin.ui.toolbar.get_node_or_null("InstanceMeshes") if instancer_btn: instancer_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD) set_selected_id(p_id) - func set_selected_id(p_id: int) -> void: # "Add new" is the final entry only when search box is blank var max_id: int = max(0, entries.size() - (1 if search_text else 2)) if plugin.debug: - print("Terrain3DListContainer ", name, ": set_selected_id: ", selected_id, " to ", clamp(p_id, 0, max_id)) + print( + "Terrain3DListContainer ", + name, + ": set_selected_id: ", + selected_id, + " to ", + clamp(p_id, 0, max_id) + ) selected_id = clamp(p_id, 0, max_id) for i in entries.size(): var entry: ListEntry = entries[i] entry.set_selected(i == selected_id) plugin.ui._on_setting_changed() - func get_selected_asset_id() -> int: # "Add new" is the final entry only when search box is blank var max_id: int = max(0, entries.size() - (1 if search_text else 2)) var id: int = clamp(selected_id, 0, max_id) if plugin.debug: - print("Terrain3DListContainer ", name, ": get_selected_asset_id: selected_id: ", selected_id, ", clamped: ", id, ", entries: ", entries.size()) + print( + "Terrain3DListContainer ", + name, + ": get_selected_asset_id: selected_id: ", + selected_id, + ", clamped: ", + id, + ", entries: ", + entries.size() + ) if id >= entries.size(): return 0 var res: Resource = entries[id].resource @@ -485,18 +515,21 @@ class ListContainer extends Container: else: return (res as Terrain3DTextureAsset).id - func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().process_frame EditorInterface.edit_resource(p_resource) - - + func _on_resource_changed(p_resource: Resource, p_id: int) -> void: if not p_resource and _clearing_resource: return if not p_resource: if plugin.debug: - print("Terrain3DListContainer ", name, ": _on_resource_changed: removing asset ID: ", p_id) + print( + "Terrain3DListContainer ", + name, + ": _on_resource_changed: removing asset ID: ", + p_id + ) _clearing_resource = true var asset_dock: Control = get_parent().get_parent().get_parent() if type == Terrain3DAssets.TYPE_TEXTURE: @@ -509,7 +542,7 @@ class ListContainer extends Container: update_asset_list() _clearing_resource = false return - + if not plugin.is_terrain_valid(): plugin.select_terrain() await get_tree().process_frame @@ -522,19 +555,16 @@ class ListContainer extends Container: # If removing an entry, clear inspector if not p_resource: - EditorInterface.inspect_object(null) + EditorInterface.inspect_object(null) _clearing_resource = false - func set_entry_width(value: float) -> void: var min_width: float = 90.0 * max(1.0, EditorInterface.get_editor_scale()) width = clamp(value, min_width, 512.0) redraw() - func get_entry_width() -> float: return width - func redraw() -> void: height = 0 @@ -543,22 +573,24 @@ class ListContainer extends Container: var columns: int = 3 columns = clamp(size.x / width, 1, 100) var tile_size: Vector2 = Vector2(width, width) - Vector2(separation, separation) - var name_font_size := int(clamp(tile_size.x/12., 12., 16.) * EditorInterface.get_editor_scale()) + var name_font_size := int( + clamp(tile_size.x / 12., 12., 16.) * EditorInterface.get_editor_scale() + ) for c in get_children(): if is_instance_valid(c): c.size = tile_size - c.position = Vector2(id % columns, id / columns) * width + \ - Vector2(separation / columns, separation / columns) + c.position = ( + Vector2(id % columns, id / columns) * width + + Vector2(separation / columns, separation / columns) + ) height = max(height, c.position.y + width) id += 1 c.name_label.add_theme_font_size_override("font_size", name_font_size) - # Needed to enable ScrollContainer scroll bar func _get_minimum_size() -> Vector2: return Vector2(0, height) - func _notification(p_what) -> void: if p_what == NOTIFICATION_SORT_CHILDREN: redraw() @@ -569,24 +601,25 @@ class ListContainer extends Container: ############################################################## -class ListEntry extends MarginContainer: - signal hovered() - signal clicked() +class ListEntry: + extends MarginContainer + signal hovered + signal clicked signal changed(resource: Resource) signal inspected(resource: Resource) - + var resource: Resource var type := Terrain3DAssets.TYPE_TEXTURE var _thumbnail: Texture2D var drop_data: bool = false var is_hovered: bool = false var is_selected: bool = false - + var name_label: Label var button_row: FlowContainer var button_enabled: TextureButton var button_edit: TextureButton - var spacer: Control + var spacer: Control var button_clear: TextureButton @onready var focus_style: StyleBox = get_theme_stylebox("focus", "Button").duplicate() @@ -597,7 +630,6 @@ class ListEntry extends MarginContainer: @onready var disabled_icon: Texture2D = get_theme_icon("GuiVisibilityHidden", "EditorIcons") @onready var add_icon: Texture2D = get_theme_icon("Add", "EditorIcons") - func _ready() -> void: name = "ListEntry" custom_minimum_size = Vector2i(86., 86.) @@ -611,18 +643,17 @@ class ListEntry extends MarginContainer: focus_style.set_border_width_all(2) focus_style.set_border_color(Color(1, 1, 1, .67)) - func setup_buttons() -> void: destroy_buttons() - + button_row = FlowContainer.new() - button_enabled = TextureButton.new() - button_edit = TextureButton.new() + button_enabled = TextureButton.new() + button_edit = TextureButton.new() spacer = Control.new() button_clear = TextureButton.new() - + var icon_size: Vector2 = Vector2(12, 12) - + button_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL button_row.alignment = FlowContainer.ALIGNMENT_CENTER button_row.mouse_filter = Control.MOUSE_FILTER_PASS @@ -640,7 +671,7 @@ class ListEntry extends MarginContainer: button_enabled.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button_enabled.pressed.connect(_on_enable) button_row.add_child(button_enabled, true) - + button_edit.set_texture_normal(edit_icon) button_edit.set_custom_minimum_size(icon_size) button_edit.set_h_size_flags(Control.SIZE_SHRINK_END) @@ -654,7 +685,7 @@ class ListEntry extends MarginContainer: spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL spacer.mouse_filter = Control.MOUSE_FILTER_PASS button_row.add_child(spacer, true) - + button_clear.set_texture_normal(clear_icon) button_clear.set_custom_minimum_size(icon_size) button_clear.set_h_size_flags(Control.SIZE_SHRINK_END) @@ -664,7 +695,6 @@ class ListEntry extends MarginContainer: button_clear.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND button_clear.pressed.connect(_on_clear) button_row.add_child(button_clear, true) - func destroy_buttons() -> void: if button_row: @@ -683,7 +713,6 @@ class ListEntry extends MarginContainer: button_clear.free() button_clear = null - func get_resource_name() -> StringName: if resource: if resource is Terrain3DMeshAsset: @@ -692,7 +721,6 @@ class ListEntry extends MarginContainer: return (resource as Terrain3DTextureAsset).get_name() return "" - func setup_label() -> void: name_label = Label.new() name_label.name = "MeshLabel" @@ -700,17 +728,18 @@ class ListEntry extends MarginContainer: name_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER name_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL name_label.size_flags_vertical = Control.SIZE_EXPAND_FILL - name_label.add_theme_font_size_override("font_size", int(14. * EditorInterface.get_editor_scale())) + name_label.add_theme_font_size_override( + "font_size", int(14. * EditorInterface.get_editor_scale()) + ) name_label.add_theme_color_override("font_color", Color.WHITE) name_label.add_theme_color_override("font_shadow_color", Color.BLACK) name_label.add_theme_constant_override("shadow_offset_x", 1) name_label.add_theme_constant_override("shadow_offset_y", 1) name_label.visible = false name_label.autowrap_mode = TextServer.AUTOWRAP_OFF - name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS + name_label.text_overrun_behavior = TextServer.OVERRUN_TRIM_ELLIPSIS add_child(name_label, true) - func _notification(p_what) -> void: match p_what: NOTIFICATION_PREDELETE: @@ -758,7 +787,6 @@ class ListEntry extends MarginContainer: drop_data = false queue_redraw() - func _gui_input(p_event: InputEvent) -> void: if p_event is InputEventMouseButton: if p_event.is_pressed(): @@ -780,7 +808,6 @@ class ListEntry extends MarginContainer: if resource: _on_clear() - func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool: drop_data = false if typeof(p_data) == TYPE_DICTIONARY: @@ -789,7 +816,6 @@ class ListEntry extends MarginContainer: drop_data = true return drop_data - func _drop_data(p_at_position: Vector2, p_data: Variant) -> void: if typeof(p_data) == TYPE_DICTIONARY: var res: Resource = load(p_data.files[0]) @@ -806,7 +832,7 @@ class ListEntry extends MarginContainer: set_edited_resource(res, false) elif res is PackedScene and type == Terrain3DAssets.TYPE_MESH: if not resource: - resource = Terrain3DMeshAsset.new() + resource = Terrain3DMeshAsset.new() set_edited_resource(resource, false) resource.set_scene_file(res) elif res is Terrain3DMeshAsset and type == Terrain3DAssets.TYPE_MESH: @@ -816,7 +842,6 @@ class ListEntry extends MarginContainer: emit_signal("clicked") emit_signal("inspected", resource) - func set_edited_resource(p_res: Resource, p_no_signal: bool = true) -> void: resource = p_res if resource: @@ -828,20 +853,18 @@ class ListEntry extends MarginContainer: elif resource is Terrain3DMeshAsset: if not resource.instancer_setting_changed.is_connected(_on_resource_changed): resource.instancer_setting_changed.connect(_on_resource_changed) - + if button_clear: button_clear.set_visible(resource != null) - + queue_redraw() if not p_no_signal: emit_signal("changed", resource) - func _on_resource_changed(_value: int = 0) -> void: queue_redraw() emit_signal("changed", resource) - func set_selected(value: bool) -> void: if not is_inside_tree(): #push_error("not in tree") @@ -851,26 +874,24 @@ class ListEntry extends MarginContainer: # Handle scrolling to show the selected item await get_tree().process_frame if is_inside_tree(): - get_parent().get_parent().get_v_scroll_bar().ratio = position.y / get_parent().size.y + get_parent().get_parent().get_v_scroll_bar().ratio = ( + position.y / get_parent().size.y + ) queue_redraw() - func _on_clear() -> void: if resource: name_label.hide() set_edited_resource(null, false) - func _on_edit() -> void: emit_signal("clicked") emit_signal("inspected", resource) - func _on_enable() -> void: if resource is Terrain3DMeshAsset: resource.set_enabled(!resource.is_enabled()) - func _format_number(num: int) -> String: var is_negative: bool = num < 0 var str_num: String = str(abs(num)) diff --git a/addons/terrain_3d/src/asset_dock_45.gd b/addons/terrain_3d/src/asset_dock_45.gd index 1b3da0a..41e1f37 100644 --- a/addons/terrain_3d/src/asset_dock_45.gd +++ b/addons/terrain_3d/src/asset_dock_45.gd @@ -64,7 +64,7 @@ var _godot_last_state: Window.Mode = Window.MODE_FULLSCREEN func initialize(p_plugin: EditorPlugin) -> void: if p_plugin: plugin = p_plugin - + _godot_last_state = plugin.godot_editor_window.mode placement_opt = $Box/Buttons/PlacementOpt pinned_btn = $Box/Buttons/Pinned @@ -98,7 +98,7 @@ func initialize(p_plugin: EditorPlugin) -> void: placement_opt.item_selected.connect(set_slot) floating_btn.pressed.connect(make_dock_float) pinned_btn.toggled.connect(_on_pin_changed) - pinned_btn.visible = ( window != null ) + pinned_btn.visible = (window != null) size_slider.value_changed.connect(_on_slider_changed) plugin.ui.toolbar.tool_changed.connect(_on_tool_changed) @@ -113,7 +113,7 @@ func initialize(p_plugin: EditorPlugin) -> void: func _ready() -> void: if not _initialized: return - + # Setup styles set("theme_override_styles/panel", get_theme_stylebox("panel", "Panel")) # Avoid saving icon resources in tscn when editing w/ a tool script @@ -127,12 +127,18 @@ func _ready() -> void: confirm_dialog = ConfirmationDialog.new() add_child(confirm_dialog) confirm_dialog.hide() - confirm_dialog.confirmed.connect(func(): _confirmed = true; \ - emit_signal("confirmation_closed"); \ - emit_signal("confirmation_confirmed") ) - confirm_dialog.canceled.connect(func(): _confirmed = false; \ - emit_signal("confirmation_closed"); \ - emit_signal("confirmation_canceled") ) + confirm_dialog.confirmed.connect( + func(): + _confirmed = true + emit_signal("confirmation_closed") + emit_signal("confirmation_confirmed") + ) + confirm_dialog.canceled.connect( + func(): + _confirmed = false + emit_signal("confirmation_closed") + emit_signal("confirmation_canceled") + ) func get_current_list() -> ListContainer: @@ -141,9 +147,10 @@ func get_current_list() -> ListContainer: ## Dock placement + func set_slot(p_slot: int) -> void: - p_slot = clamp(p_slot, 0, POS_MAX-1) - + p_slot = clamp(p_slot, 0, POS_MAX - 1) + if slot != p_slot: slot = p_slot placement_opt.selected = slot @@ -177,7 +184,7 @@ func remove_dock(p_force: bool = false) -> void: pinned_btn.visible = false placement_opt.visible = true state = HIDDEN - update_dock() # return window to side/bottom + update_dock() # return window to side/bottom func update_dock() -> void: @@ -207,7 +214,7 @@ func update_layout() -> void: if not window and get_parent() and get_parent().get_parent() is Window: window = get_parent().get_parent() make_dock_float() - return # Will call this function again upon display + return # Will call this function again upon display var size_parent: Control = size_slider.get_parent() # Vertical layout in window / sidebar @@ -238,8 +245,10 @@ func update_layout() -> void: func update_thumbnails() -> void: if not is_instance_valid(plugin.terrain): return - if current_list.type == Terrain3DAssets.TYPE_MESH and \ - Time.get_ticks_msec() - _last_thumb_update_time > MAX_UPDATE_TIME: + if ( + current_list.type == Terrain3DAssets.TYPE_MESH + and Time.get_ticks_msec() - _last_thumb_update_time > MAX_UPDATE_TIME + ): plugin.terrain.assets.create_mesh_thumbnails() _last_thumb_update_time = Time.get_ticks_msec() for mesh_asset in mesh_list.entries: @@ -293,7 +302,7 @@ func _on_meshes_pressed() -> void: func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor.Operation) -> void: if p_tool == Terrain3DEditor.INSTANCER: _on_meshes_pressed() - elif p_tool in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + elif p_tool in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS]: _on_textures_pressed() @@ -303,7 +312,7 @@ func _on_tool_changed(p_tool: Terrain3DEditor.Tool, p_operation: Terrain3DEditor func update_assets() -> void: if not _initialized: return - + # Verify signals to individual lists if plugin.is_terrain_valid() and plugin.terrain.assets: if not plugin.terrain.assets.textures_changed.is_connected(texture_list.update_asset_list): @@ -318,13 +327,13 @@ func update_assets() -> void: func make_dock_float() -> void: - # If not already created (eg from editor panel 'Make Floating' button) + # If not already created (eg from editor panel 'Make Floating' button) if not window: remove_dock() create_window() state = WINDOWED - visible = true # Asset dock contents are hidden when popping out of the bottom! + visible = true # Asset dock contents are hidden when popping out of the bottom! pinned_btn.visible = true floating_btn.visible = false placement_opt.visible = false @@ -366,13 +375,18 @@ func clamp_window_position() -> void: bounds = DisplayServer.screen_get_position(window.current_screen) bounds += DisplayServer.screen_get_size(window.current_screen) var margin: int = 40 - window.position.x = clamp(window.position.x, -window.size.x + 2*margin, bounds.x - margin) + window.position.x = clamp(window.position.x, -window.size.x + 2 * margin, bounds.x - margin) window.position.y = clamp(window.position.y, 25, bounds.y - margin) func _on_window_input(event: InputEvent) -> void: # Capture CTRL+S when doc focused to save scene - if event is InputEventKey and event.keycode == KEY_S and event.pressed and event.is_command_or_control_pressed(): + if ( + event is InputEventKey + and event.keycode == KEY_S + and event.pressed + and event.is_command_or_control_pressed() + ): save_editor_settings() EditorInterface.save_scene() @@ -385,7 +399,10 @@ func _on_godot_window_entered() -> void: func _on_godot_focus_entered() -> void: # If asset dock is windowed, and Godot was minimized, and now is not, restore asset dock window if is_instance_valid(window): - if _godot_last_state == Window.MODE_MINIMIZED and plugin.godot_editor_window.mode != Window.MODE_MINIMIZED: + if ( + _godot_last_state == Window.MODE_MINIMIZED + and plugin.godot_editor_window.mode != Window.MODE_MINIMIZED + ): window.show() _godot_last_state = plugin.godot_editor_window.mode plugin.godot_editor_window.grab_focus() @@ -399,6 +416,7 @@ func _on_godot_focus_exited() -> void: ## Manage Editor Settings + func load_editor_settings() -> void: floating_btn.button_pressed = plugin.get_setting(ES_DOCK_FLOATING, false) pinned_btn.button_pressed = plugin.get_setting(ES_DOCK_PINNED, true) @@ -431,8 +449,9 @@ func save_editor_settings() -> void: ## class ListContainer ############################################################## - -class ListContainer extends Container: + +class ListContainer: + extends Container var plugin: EditorPlugin var type := Terrain3DAssets.TYPE_TEXTURE var entries: Array[ListEntry] @@ -441,34 +460,34 @@ class ListContainer extends Container: var width: float = 83 var focus_style: StyleBox - func _ready() -> void: set_v_size_flags(SIZE_EXPAND_FILL) set_h_size_flags(SIZE_EXPAND_FILL) - func clear() -> void: for e in entries: e.get_parent().remove_child(e) e.queue_free() entries.clear() - func update_asset_list() -> void: clear() - + # Grab terrain var t: Terrain3D if plugin.is_terrain_valid(): t = plugin.terrain - elif is_instance_valid(plugin._last_terrain) and plugin.is_terrain_valid(plugin._last_terrain): + elif ( + is_instance_valid(plugin._last_terrain) + and plugin.is_terrain_valid(plugin._last_terrain) + ): t = plugin._last_terrain else: return - + if not t.assets: return - + if type == Terrain3DAssets.TYPE_TEXTURE: var texture_count: int = t.assets.get_texture_count() for i in texture_count: @@ -486,12 +505,11 @@ class ListContainer extends Container: if selected_id >= mesh_count or selected_id < 0: set_selected_id(0) - func add_item(p_resource: Resource = null, p_assets: Terrain3DAssets = null) -> void: var entry: ListEntry = ListEntry.new() entry.focus_style = focus_style var id: int = entries.size() - + entry.set_edited_resource(p_resource) entry.hovered.connect(_on_resource_hovered.bind(id)) entry.selected.connect(set_selected_id.bind(id)) @@ -501,55 +519,60 @@ class ListContainer extends Container: entry.asset_list = p_assets add_child(entry) entries.push_back(entry) - + if p_resource: entry.set_selected(id == selected_id) if not p_resource.id_changed.is_connected(set_selected_after_swap): p_resource.id_changed.connect(set_selected_after_swap) - func _on_resource_hovered(p_id: int): if type == Terrain3DAssets.TYPE_MESH: if plugin.terrain: plugin.terrain.assets.create_mesh_thumbnails(p_id) - - func set_selected_after_swap(p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int) -> void: + func set_selected_after_swap( + p_type: Terrain3DAssets.AssetType, p_old_id: int, p_new_id: int + ) -> void: set_selected_id(clamp(p_new_id, 0, entries.size() - 2)) - func set_selected_id(p_id: int) -> void: selected_id = p_id - + for i in entries.size(): var entry: ListEntry = entries[i] entry.set_selected(i == selected_id) - + plugin.select_terrain() # Select Paint tool if clicking a texture - if type == Terrain3DAssets.TYPE_TEXTURE and \ - not plugin.editor.get_tool() in [ Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS ]: + if ( + type == Terrain3DAssets.TYPE_TEXTURE + and not ( + plugin.editor.get_tool() + in [Terrain3DEditor.TEXTURE, Terrain3DEditor.COLOR, Terrain3DEditor.ROUGHNESS] + ) + ): var paint_btn: Button = plugin.ui.toolbar.get_node_or_null("PaintTexture") if paint_btn: paint_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.TEXTURE, Terrain3DEditor.REPLACE) - elif type == Terrain3DAssets.TYPE_MESH and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER: + elif ( + type == Terrain3DAssets.TYPE_MESH + and plugin.editor.get_tool() != Terrain3DEditor.INSTANCER + ): var instancer_btn: Button = plugin.ui.toolbar.get_node_or_null("InstanceMeshes") if instancer_btn: instancer_btn.set_pressed(true) plugin.ui._on_tool_changed(Terrain3DEditor.INSTANCER, Terrain3DEditor.ADD) - + # Update editor with selected brush plugin.ui._on_setting_changed() - func _on_resource_inspected(p_resource: Resource) -> void: await get_tree().create_timer(.01).timeout EditorInterface.edit_resource(p_resource) - - + func _on_resource_changed(p_resource: Resource, p_id: int) -> void: if not p_resource: var asset_dock: Control = get_parent().get_parent().get_parent() @@ -562,7 +585,7 @@ class ListContainer extends Container: if not asset_dock._confirmed: update_asset_list() return - + if not plugin.is_terrain_valid(): plugin.select_terrain() await get_tree().create_timer(.01).timeout @@ -577,32 +600,27 @@ class ListContainer extends Container: # If removing an entry, clear inspector if not p_resource: - EditorInterface.inspect_object(null) - - # If null resource, remove last + EditorInterface.inspect_object(null) + + # If null resource, remove last if not p_resource: var last_offset: int = 2 - if p_id == entries.size()-2: + if p_id == entries.size() - 2: last_offset = 3 set_selected_id(clamp(selected_id, 0, entries.size() - last_offset)) - func get_selected_id() -> int: return selected_id - func get_selected_asset_id() -> int: return selected_id - func set_entry_width(value: float) -> void: width = clamp(value, 66, 230) redraw() - func get_entry_width() -> float: return width - func redraw() -> void: height = 0 @@ -610,21 +628,21 @@ class ListContainer extends Container: var separation: float = 4 var columns: int = 3 columns = clamp(size.x / width, 1, 100) - + for c in get_children(): if is_instance_valid(c): c.size = Vector2(width, width) - Vector2(separation, separation) - c.position = Vector2(id % columns, id / columns) * width + \ - Vector2(separation / columns, separation / columns) + c.position = ( + Vector2(id % columns, id / columns) * width + + Vector2(separation / columns, separation / columns) + ) height = max(height, c.position.y + width) id += 1 - # Needed to enable ScrollContainer scroll bar func _get_minimum_size() -> Vector2: return Vector2(0, height) - func _notification(p_what) -> void: if p_what == NOTIFICATION_SORT_CHILDREN: redraw() @@ -635,12 +653,13 @@ class ListContainer extends Container: ############################################################## -class ListEntry extends VBoxContainer: - signal hovered() - signal selected() +class ListEntry: + extends VBoxContainer + signal hovered + signal selected signal changed(resource: Resource) signal inspected(resource: Resource) - + var resource: Resource var type := Terrain3DAssets.TYPE_TEXTURE var _thumbnail: Texture2D @@ -648,7 +667,7 @@ class ListEntry extends VBoxContainer: var is_hovered: bool = false var is_selected: bool = false var asset_list: Terrain3DAssets - + @onready var button_row := HBoxContainer.new() @onready var button_clear := TextureButton.new() @onready var button_edit := TextureButton.new() @@ -664,14 +683,12 @@ class ListEntry extends VBoxContainer: @onready var background: StyleBox = get_theme_stylebox("pressed", "Button") @onready var focus_style: StyleBox = get_theme_stylebox("focus", "Button").duplicate() - func _ready() -> void: setup_buttons() setup_label() focus_style.set_border_width_all(2) focus_style.set_border_color(Color(1, 1, 1, .67)) - func setup_buttons() -> void: var icon_size: Vector2 = Vector2(12, 12) var margin_container := MarginContainer.new() @@ -680,7 +697,7 @@ class ListEntry extends VBoxContainer: margin_container.add_theme_constant_override("margin_left", 5) margin_container.add_theme_constant_override("margin_right", 5) add_child(margin_container) - + button_row.size_flags_horizontal = Control.SIZE_EXPAND_FILL button_row.alignment = BoxContainer.ALIGNMENT_CENTER button_row.mouse_filter = Control.MOUSE_FILTER_PASS @@ -696,7 +713,7 @@ class ListEntry extends VBoxContainer: button_enabled.mouse_filter = Control.MOUSE_FILTER_PASS button_enabled.pressed.connect(enable) button_row.add_child(button_enabled) - + button_edit.set_texture_normal(edit_icon) button_edit.set_custom_minimum_size(icon_size) button_edit.set_h_size_flags(Control.SIZE_SHRINK_END) @@ -708,7 +725,7 @@ class ListEntry extends VBoxContainer: spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL spacer.mouse_filter = Control.MOUSE_FILTER_PASS button_row.add_child(spacer) - + button_clear.set_texture_normal(clear_icon) button_clear.set_custom_minimum_size(icon_size) button_clear.set_h_size_flags(Control.SIZE_SHRINK_END) @@ -717,7 +734,6 @@ class ListEntry extends VBoxContainer: button_clear.pressed.connect(clear) button_row.add_child(button_clear) - func setup_label() -> void: name_label = Label.new() add_child(name_label, true) @@ -737,13 +753,12 @@ class ListEntry extends VBoxContainer: else: name_label.text = "Add Mesh" - func _notification(p_what) -> void: match p_what: NOTIFICATION_DRAW: # Hide spacer if icons are crowding small textures spacer.visible = size.x > 70 or type == Terrain3DAssets.TYPE_TEXTURE - + var rect: Rect2 = Rect2(Vector2.ZERO, get_size()) if !resource: draw_style_box(background, rect) @@ -766,7 +781,7 @@ class ListEntry extends VBoxContainer: else: draw_rect(rect, Color(.15, .15, .15, 1.)) button_enabled.set_pressed_no_signal(!resource.is_enabled()) - name_label.add_theme_font_size_override("font_size", 4 + rect.size.x/10) + name_label.add_theme_font_size_override("font_size", 4 + rect.size.x / 10) if drop_data: draw_style_box(focus_style, rect) if is_hovered: @@ -784,7 +799,6 @@ class ListEntry extends VBoxContainer: drop_data = false queue_redraw() - func _gui_input(p_event: InputEvent) -> void: if p_event is InputEventMouseButton: if p_event.is_pressed(): @@ -806,7 +820,6 @@ class ListEntry extends VBoxContainer: if resource: clear() - func _can_drop_data(p_at_position: Vector2, p_data: Variant) -> bool: drop_data = false if typeof(p_data) == TYPE_DICTIONARY: @@ -815,7 +828,6 @@ class ListEntry extends VBoxContainer: drop_data = true return drop_data - func _drop_data(p_at_position: Vector2, p_data: Variant) -> void: if typeof(p_data) == TYPE_DICTIONARY: var res: Resource = load(p_data.files[0]) @@ -844,8 +856,6 @@ class ListEntry extends VBoxContainer: emit_signal("selected") emit_signal("inspected", resource) - - func set_edited_resource(p_res: Resource, p_no_signal: bool = true) -> void: resource = p_res if resource: @@ -853,35 +863,30 @@ class ListEntry extends VBoxContainer: resource.file_changed.connect(_on_resource_changed) if resource is Terrain3DMeshAsset: resource.instancer_setting_changed.connect(_on_resource_changed) - + if button_clear: button_clear.set_visible(resource != null) - + queue_redraw() if !p_no_signal: emit_signal("changed", resource) - func _on_resource_changed() -> void: queue_redraw() emit_signal("changed", resource) - func set_selected(value: bool) -> void: is_selected = value queue_redraw() - func clear() -> void: if resource: set_edited_resource(null, false) - func edit() -> void: emit_signal("selected") emit_signal("inspected", resource) - func enable() -> void: if resource is Terrain3DMeshAsset: resource.set_enabled(!resource.is_enabled()) diff --git a/addons/terrain_3d/src/double_slider.gd b/addons/terrain_3d/src/double_slider.gd index 34f9ab6..6536aef 100644 --- a/addons/terrain_3d/src/double_slider.gd +++ b/addons/terrain_3d/src/double_slider.gd @@ -8,11 +8,11 @@ extends Control signal value_changed(Vector2) var label: Label var suffix: String -var grabbed_handle: int = 0 # -1 left, 0 none, 1 right +var grabbed_handle: int = 0 # -1 left, 0 none, 1 right var min_value: float = 0.0 var max_value: float = 100.0 var step: float = 1.0 -var range := Vector2(0, 100) +var range := Vector2(0, 100) var display_scale: float = 1. var position_x: float = 0. var minimum_x: float = 60. @@ -43,8 +43,8 @@ func set_min(p_value: float) -> void: func get_min() -> float: return min_value - - + + func set_max(p_value: float) -> void: max_value = p_value if range.y == 0 or range.y >= max_value: @@ -55,15 +55,15 @@ func set_max(p_value: float) -> void: func get_max() -> float: return max_value - - + + func set_step(p_step: float) -> void: step = p_step - + func get_step() -> float: return step - + func set_value(p_range: Vector2) -> void: range.x = clamp(p_range.x, min_value, max_value) @@ -99,7 +99,7 @@ func _get_handle() -> int: func _gui_input(p_event: InputEvent) -> void: if p_event is InputEventMouseButton: var button: int = p_event.get_button_index() - if button in [ MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN ]: + if button in [MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP, MOUSE_BUTTON_WHEEL_DOWN]: if p_event.is_pressed(): var mid_point = (range.x + range.y) / 2.0 var xpos: float = p_event.get_position().x * 2.0 @@ -116,17 +116,19 @@ func _gui_input(p_event: InputEvent) -> void: set_slider(1., true) else: grabbed_handle = 0 - + if p_event is InputEventMouseMotion: if grabbed_handle != 0: set_slider(p_event.get_position().x) - - + + func set_slider(p_xpos: float, p_relative: bool = false) -> void: if grabbed_handle == 0: return - var xpos_step: float = clamp(snappedf((p_xpos / size.x) * max_value, step), min_value, max_value) - if(grabbed_handle < 0): + var xpos_step: float = clamp( + snappedf((p_xpos / size.x) * max_value, step), min_value, max_value + ) + if grabbed_handle < 0: if p_relative: range.x += p_xpos else: @@ -135,7 +137,7 @@ func set_slider(p_xpos: float, p_relative: bool = false) -> void: if p_relative: range.y += p_xpos else: - range.y = xpos_step + range.y = xpos_step set_value(range) @@ -146,19 +148,19 @@ func _notification(p_what: int) -> void: var bg_height: float = bg.get_minimum_size().y var mid_y: float = (size.y - bg_height) / 2.0 draw_style_box(bg, Rect2(Vector2(0, mid_y), Vector2(size.x, bg_height))) - + # Draw foreground bar var handle: Texture2D = get_theme_icon("grabber", "HSlider") var area: StyleBox = get_theme_stylebox("grabber_area", "HSlider") var startx: float = (range.x / max_value) * size.x var endx: float = (range.y / max_value) * size.x draw_style_box(area, Rect2(Vector2(startx, mid_y), Vector2(endx - startx, bg_height))) - + # Draw handles, slightly in so they don't get on the outside edges var handle_pos: Vector2 - handle_pos.x = clamp(startx - handle.get_size().x/2, -10, size.x) - handle_pos.y = clamp(endx - handle.get_size().x/2, 0, size.x - 10) + handle_pos.x = clamp(startx - handle.get_size().x / 2, -10, size.x) + handle_pos.y = clamp(endx - handle.get_size().x / 2, 0, size.x - 10) draw_texture(handle, Vector2(handle_pos.x, -mid_y - 10 * (display_scale - 1.))) draw_texture(handle, Vector2(handle_pos.y, -mid_y - 10 * (display_scale - 1.))) - + update_label() diff --git a/addons/terrain_3d/src/editor_plugin.gd b/addons/terrain_3d/src/editor_plugin.gd index 7952bf0..cd8ca13 100644 --- a/addons/terrain_3d/src/editor_plugin.gd +++ b/addons/terrain_3d/src/editor_plugin.gd @@ -3,7 +3,6 @@ @tool extends EditorPlugin - # Includes const UI: Script = preload("res://addons/terrain_3d/src/ui.gd") const RegionGizmo: Script = preload("res://addons/terrain_3d/src/region_gizmo.gd") @@ -14,7 +13,7 @@ var modifier_ctrl: bool var modifier_alt: bool var modifier_shift: bool var _last_modifiers: int = 0 -var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating +var _input_mode: int = 0 # -1: camera move, 0: none, 1: operating var rmb_release_time: int = 0 var _use_meta: bool = false @@ -22,26 +21,26 @@ var terrain: Terrain3D var _last_terrain: Terrain3D var nav_region: NavigationRegion3D -var debug: int = 0 # Set in _edit() +var debug: int = 0 # Set in _edit() var editor: Terrain3DEditor var editor_settings: EditorSettings -var ui: Node # Terrain3DUI see Godot #75388 +var ui: Node # Terrain3DUI see Godot #75388 var asset_dock: PanelContainer var region_gizmo: RegionGizmo var current_region_position: Vector2 var mouse_global_position: Vector3 = Vector3.ZERO -var godot_editor_window: Window # The Godot Editor window +var godot_editor_window: Window # The Godot Editor window func _init() -> void: if OS.get_name() == "macOS": _use_meta = true - + # Get the Godot Editor window. Structure is root:Window/EditorNode/Base Control godot_editor_window = EditorInterface.get_base_control().get_parent().get_parent() godot_editor_window.focus_entered.connect(_on_godot_focus_entered) - + func _enter_tree() -> void: editor = Terrain3DEditor.new() setup_editor_settings() @@ -77,7 +76,7 @@ func _on_godot_focus_entered() -> void: ## EditorPlugin selection function call chain isn't consistent. Here's the map of calls: -## Assume we handle Terrain3D and NavigationRegion3D +## Assume we handle Terrain3D and NavigationRegion3D # Click Terrain3D: _handles(Terrain3D), _make_visible(true), _edit(Terrain3D) # Deselect: _make_visible(false), _edit(null) # Click other node: _handles(OtherNode) @@ -89,14 +88,14 @@ func _handles(p_object: Object) -> bool: return true elif p_object is NavigationRegion3D and is_instance_valid(_last_terrain): return true - + # Terrain3DObjects requires access to EditorUndoRedoManager. The only way to make sure it # always has it, is to pass it in here. _edit is NOT called if the node is cut and pasted. elif p_object is Terrain3DObjects: p_object.editor_setup(self) elif p_object is Node3D and p_object.get_parent() is Terrain3DObjects: p_object.get_parent().editor_setup(self) - + return false @@ -119,7 +118,7 @@ func _edit(p_object: Object) -> void: _last_terrain = terrain terrain.set_plugin(self) terrain.set_editor(editor) - debug = terrain.debug_level + debug = terrain.debug_level editor.set_terrain(terrain) region_gizmo.set_node_3d(terrain) terrain.add_gizmo(region_gizmo) @@ -144,18 +143,18 @@ func _edit(p_object: Object) -> void: else: nav_region = null - + func _clear() -> void: if is_terrain_valid(): if terrain.data.region_map_changed.is_connected(update_region_grid): terrain.data.region_map_changed.disconnect(update_region_grid) - + terrain.clear_gizmos() terrain = null editor.set_terrain(null) - + ui.clear_picking() - + region_gizmo.clear() @@ -167,13 +166,13 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> if continue_input != AFTER_GUI_INPUT_CUSTOM: return continue_input ui.update_decal() - + ## Setup active camera & viewport # Always update this for all inputs, as the mouse position can move without # necessarily being a InputEventMouseMotion object. get_intersection() also # returns the last frame position, and should be updated more frequently. - - # Snap terrain to current camera + + # Snap terrain to current camera terrain.set_camera(p_viewport_camera) # Detect if viewport is set to half_resolution @@ -193,19 +192,23 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> var t = -Vector3(0, 1, 0).dot(camera_pos) / Vector3(0, 1, 0).dot(camera_dir) mouse_global_position = (camera_pos + t * camera_dir) else: - #Else look for intersection with terrain + #Else look for intersection with terrain var intersection_point: Vector3 = terrain.get_intersection(camera_pos, camera_dir, true) - if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan + if intersection_point.z > 3.4e38 or is_nan(intersection_point.y): # max double or nan return AFTER_GUI_INPUT_PASS mouse_global_position = intersection_point - + ## Handle mouse movement if p_event is InputEventMouseMotion: - - if _input_mode != -1: # Not cam rotation + if _input_mode != -1: # Not cam rotation ## Update region highlight - var region_position: Vector2 = ( Vector2(mouse_global_position.x, mouse_global_position.z) \ - / (terrain.get_region_size() * terrain.get_vertex_spacing()) ).floor() + var region_position: Vector2 = ( + ( + Vector2(mouse_global_position.x, mouse_global_position.z) + / (terrain.get_region_size() * terrain.get_vertex_spacing()) + ) + . floor() + ) if current_region_position != region_position: current_region_position = region_position update_region_grid() @@ -216,7 +219,7 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> editor.operate(mouse_global_position, p_viewport_camera.rotation.y) return AFTER_GUI_INPUT_STOP - + return AFTER_GUI_INPUT_PASS if p_event is InputEventMouseButton and _input_mode > 0: @@ -226,31 +229,35 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> ui.pick(mouse_global_position) if not ui.operation_builder or not ui.operation_builder.is_ready(): return AFTER_GUI_INPUT_STOP - + if modifier_ctrl and editor.get_tool() == Terrain3DEditor.HEIGHT: var height: float = terrain.data.get_height(mouse_global_position) ui.brush_data["height"] = height ui.tool_settings.set_setting("height", height) - + # If adjusting regions if editor.get_tool() == Terrain3DEditor.REGION: # Skip regions that already exist or don't var has_region: bool = terrain.data.has_regionp(mouse_global_position) var op: int = editor.get_operation() - if ( has_region and op == Terrain3DEditor.ADD) or \ - ( not has_region and op == Terrain3DEditor.SUBTRACT ): + if ( + (has_region and op == Terrain3DEditor.ADD) + or (not has_region and op == Terrain3DEditor.SUBTRACT) + ): return AFTER_GUI_INPUT_STOP - + # If an automatic operation is ready to go (e.g. gradient) if ui.operation_builder and ui.operation_builder.is_ready(): - ui.operation_builder.apply_operation(editor, mouse_global_position, p_viewport_camera.rotation.y) + ui.operation_builder.apply_operation( + editor, mouse_global_position, p_viewport_camera.rotation.y + ) return AFTER_GUI_INPUT_STOP - + # Mouse clicked, start editing editor.start_operation(mouse_global_position) editor.operate(mouse_global_position, p_viewport_camera.rotation.y) return AFTER_GUI_INPUT_STOP - + # _input_apply released, save undo data elif editor.is_operating(): editor.stop_operation() @@ -261,66 +268,97 @@ func _forward_3d_gui_input(p_viewport_camera: Camera3D, p_event: InputEvent) -> func _read_input(p_event: InputEvent = null) -> AfterGUIInput: ## Determine if user is moving camera or applying - if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) or \ - p_event is InputEventMouseButton and p_event.is_released() and \ - p_event.get_button_index() == MOUSE_BUTTON_LEFT: - _input_mode = 1 + if ( + Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) + or ( + p_event is InputEventMouseButton + and p_event.is_released() + and p_event.get_button_index() == MOUSE_BUTTON_LEFT + ) + ): + _input_mode = 1 else: - _input_mode = 0 - + _input_mode = 0 + match get_setting("editors/3d/navigation/navigation_scheme", 0): - 2, 1: # Modo, Maya - if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ - ( Input.is_key_pressed(KEY_ALT) and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) ): - _input_mode = -1 - if p_event is InputEventMouseButton and p_event.is_released() and \ - ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ - ( Input.is_key_pressed(KEY_ALT) and p_event.get_button_index() == MOUSE_BUTTON_LEFT )): - rmb_release_time = Time.get_ticks_msec() - 0, _: # Godot - if Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) or \ - Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE): - _input_mode = -1 - if p_event is InputEventMouseButton and p_event.is_released() and \ - ( p_event.get_button_index() == MOUSE_BUTTON_RIGHT or \ - p_event.get_button_index() == MOUSE_BUTTON_MIDDLE ): - rmb_release_time = Time.get_ticks_msec() + 2, 1: # Modo, Maya + if ( + Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) + or ( + Input.is_key_pressed(KEY_ALT) + and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) + ) + ): + _input_mode = -1 + if ( + p_event is InputEventMouseButton + and p_event.is_released() + and ( + p_event.get_button_index() == MOUSE_BUTTON_RIGHT + or ( + Input.is_key_pressed(KEY_ALT) + and p_event.get_button_index() == MOUSE_BUTTON_LEFT + ) + ) + ): + rmb_release_time = Time.get_ticks_msec() + 0, _: # Godot + if ( + Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT) + or Input.is_mouse_button_pressed(MOUSE_BUTTON_MIDDLE) + ): + _input_mode = -1 + if ( + p_event is InputEventMouseButton + and p_event.is_released() + and ( + p_event.get_button_index() == MOUSE_BUTTON_RIGHT + or p_event.get_button_index() == MOUSE_BUTTON_MIDDLE + ) + ): + rmb_release_time = Time.get_ticks_msec() if _input_mode < 0: # Camera is moving, skip input return AFTER_GUI_INPUT_PASS ## Determine modifiers pressed modifier_shift = Input.is_key_pressed(KEY_SHIFT) - - # Editor responds to modifier_ctrl so we must register touchscreen Invert + + # Editor responds to modifier_ctrl so we must register touchscreen Invert if _use_meta: modifier_ctrl = Input.is_key_pressed(KEY_META) || ui.inverted_input else: modifier_ctrl = Input.is_key_pressed(KEY_CTRL) || ui.inverted_input - + # Keybind enum: Alt,Space,Meta,Capslock var alt_key: int match get_setting("terrain3d/config/alt_key_bind", 0): - 3: alt_key = KEY_CAPSLOCK - 2: alt_key = KEY_META - 1: alt_key = KEY_SPACE - 0, _: alt_key = KEY_ALT + 3: + alt_key = KEY_CAPSLOCK + 2: + alt_key = KEY_META + 1: + alt_key = KEY_SPACE + 0, _: + alt_key = KEY_ALT modifier_alt = Input.is_key_pressed(alt_key) var current_mods: int = int(modifier_shift) | int(modifier_ctrl) << 1 | int(modifier_alt) << 2 ## Process Hotkeys - if p_event is InputEventKey and \ - current_mods == 0 and \ - p_event.is_pressed() and \ - not p_event.is_echo() and \ - consume_hotkey(p_event.keycode): + if ( + p_event is InputEventKey + and current_mods == 0 + and p_event.is_pressed() + and not p_event.is_echo() + and consume_hotkey(p_event.keycode) + ): # Hotkey found, consume event, and stop input processing EditorInterface.get_editor_viewport_3d().set_input_as_handled() return AFTER_GUI_INPUT_STOP # Brush data is cleared on set_tool, or clicking textures in the asset dock # Update modifiers if changed or missing - if _last_modifiers != current_mods or not ui.brush_data.has("modifier_shift"): + if _last_modifiers != current_mods or not ui.brush_data.has("modifier_shift"): _last_modifiers = current_mods ui.brush_data["modifier_shift"] = modifier_shift ui.brush_data["modifier_ctrl"] = modifier_ctrl @@ -337,7 +375,7 @@ func consume_hotkey(keycode: int) -> bool: KEY_1, KEY_KP_1: terrain.material.set_show_region_grid(!terrain.material.get_show_region_grid()) KEY_2, KEY_KP_2: - terrain.label_distance = 4096.0 if is_zero_approx(terrain.label_distance) else 0.0 + terrain.label_distance = 4096.0 if is_zero_approx(terrain.label_distance) else 0.0 KEY_3, KEY_KP_3: terrain.material.set_show_contours(!terrain.material.get_show_contours()) KEY_4, KEY_KP_4: @@ -384,10 +422,10 @@ func update_region_grid() -> void: region_gizmo.region_position = current_region_position region_gizmo.region_size = terrain.get_region_size() * terrain.get_vertex_spacing() region_gizmo.grid = terrain.get_data().get_region_locations() - + terrain.update_gizmos() return - + region_gizmo.show_rect = false region_gizmo.region_size = 1024 region_gizmo.grid = [Vector2i.ZERO] @@ -396,7 +434,7 @@ func update_region_grid() -> void: func _on_scene_changed(scene_root: Node) -> void: if not scene_root: return - + for node in scene_root.find_children("", "Terrain3DObjects"): node.editor_setup(self) @@ -426,10 +464,15 @@ func is_terrain_valid(p_terrain: Terrain3D = null) -> bool: func is_selected() -> bool: var selected: Array[Node] = EditorInterface.get_selection().get_selected_nodes() for node in selected: - if ( is_instance_valid(_last_terrain) and node.get_instance_id() == _last_terrain.get_instance_id() ) or \ - node is Terrain3D: - return true - return false + if ( + ( + is_instance_valid(_last_terrain) + and node.get_instance_id() == _last_terrain.get_instance_id() + ) + or node is Terrain3D + ): + return true + return false func select_terrain() -> void: @@ -453,7 +496,7 @@ func setup_editor_settings() -> void: "hint_string": "Alt,Space,Meta,Capslock" } editor_settings.add_property_info(property_info) - + func set_setting(p_str: String, p_value: Variant) -> void: editor_settings.set_setting(p_str, p_value) diff --git a/addons/terrain_3d/src/gradient_operation_builder.gd b/addons/terrain_3d/src/gradient_operation_builder.gd index 29644df..0f16a36 100644 --- a/addons/terrain_3d/src/gradient_operation_builder.gd +++ b/addons/terrain_3d/src/gradient_operation_builder.gd @@ -2,7 +2,6 @@ # Gradient Operation Builder for Terrain3D extends "res://addons/terrain_3d/src/operation_builder.gd" - const MultiPicker: Script = preload("res://addons/terrain_3d/src/multi_picker.gd") @@ -31,26 +30,28 @@ func is_ready() -> bool: return _get_point_picker().all_points_selected() and not _is_drawable() -func apply_operation(p_editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void: +func apply_operation( + p_editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float +) -> void: var points: PackedVector3Array = _get_point_picker().get_points() assert(points.size() == 2) assert(not _is_drawable()) - + var brush_size: float = _get_brush_size() assert(brush_size > 0.0) - + var start: Vector3 = points[0] var end: Vector3 = points[1] - + p_editor.start_operation(start) - + var dir: Vector3 = (end - start).normalized() - + var pos: Vector3 = start while dir.dot(end - pos) > 0.0: p_editor.operate(pos, p_camera_direction) pos += dir * brush_size * 0.2 - + p_editor.stop_operation() - + _get_point_picker().clear() diff --git a/addons/terrain_3d/src/multi_picker.gd b/addons/terrain_3d/src/multi_picker.gd index 9b93f54..4b72892 100644 --- a/addons/terrain_3d/src/multi_picker.gd +++ b/addons/terrain_3d/src/multi_picker.gd @@ -2,15 +2,12 @@ # Multipicker for Terrain3D extends HBoxContainer - signal pressed signal value_changed - const ICON_PICKER_CHECKED: String = "res://addons/terrain_3d/icons/picker_checked.svg" const MAX_POINTS: int = 2 - var icon_picker: Texture2D var icon_picker_checked: Texture2D var points: PackedVector3Array @@ -20,9 +17,9 @@ var picking_index: int = -1 func _enter_tree() -> void: icon_picker = get_theme_icon("ColorPick", "EditorIcons") icon_picker_checked = load(ICON_PICKER_CHECKED) - + points.resize(MAX_POINTS) - + for i in range(MAX_POINTS): var button := Button.new() button.icon = icon_picker @@ -30,7 +27,7 @@ func _enter_tree() -> void: button.set_meta(&"point_index", i) button.pressed.connect(_on_button_pressed.bind(i)) add_child(button) - + _update_buttons() @@ -49,7 +46,7 @@ func _update_buttons() -> void: func _update_button(button: Button) -> void: var index: int = button.get_meta(&"point_index") - + if points[index] != Vector3.ZERO: button.icon = icon_picker_checked else: @@ -69,7 +66,7 @@ func all_points_selected() -> bool: func add_point(p_value: Vector3) -> void: if points.has(p_value): return - + # If manually selecting a point individually if picking_index != -1: points[picking_index] = p_value diff --git a/addons/terrain_3d/src/operation_builder.gd b/addons/terrain_3d/src/operation_builder.gd index 99b291d..515fdfa 100644 --- a/addons/terrain_3d/src/operation_builder.gd +++ b/addons/terrain_3d/src/operation_builder.gd @@ -2,10 +2,8 @@ # Operation Builder for Terrain3D extends RefCounted - const ToolSettings: Script = preload("res://addons/terrain_3d/src/tool_settings.gd") - var tool_settings: ToolSettings @@ -21,5 +19,7 @@ func is_ready() -> bool: return false -func apply_operation(editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float) -> void: +func apply_operation( + editor: Terrain3DEditor, p_global_position: Vector3, p_camera_direction: float +) -> void: pass diff --git a/addons/terrain_3d/src/region_gizmo.gd b/addons/terrain_3d/src/region_gizmo.gd index a024ac7..9dbc7a3 100644 --- a/addons/terrain_3d/src/region_gizmo.gd +++ b/addons/terrain_3d/src/region_gizmo.gd @@ -1,7 +1,7 @@ # Copyright © 2023-2026 Cory Petkovsek, Roope Palmroos, and Contributors. # Editor Region Gizmos for Terrain3D extends EditorNode3DGizmo - + var material: StandardMaterial3D var selection_material: StandardMaterial3D var region_position: Vector2 @@ -22,34 +22,49 @@ func _init() -> void: material.set_flag(BaseMaterial3D.FLAG_ALBEDO_FROM_VERTEX_COLOR, true) material.set_shading_mode(BaseMaterial3D.SHADING_MODE_UNSHADED) material.set_albedo(Color.WHITE) - + selection_material = material.duplicate() selection_material.set_render_priority(0) func _redraw() -> void: clear() - + var rect_position = region_position * region_size if show_rect: var modulate: Color = main_color if !use_secondary_color else secondary_color - if abs(region_position.x) > Terrain3DData.REGION_MAP_SIZE*.5 or abs(region_position.y) > Terrain3DData.REGION_MAP_SIZE*.5: + if ( + abs(region_position.x) > Terrain3DData.REGION_MAP_SIZE * .5 + or abs(region_position.y) > Terrain3DData.REGION_MAP_SIZE * .5 + ): modulate = Color.GRAY - draw_rect(Vector2(region_size,region_size)*.5 + rect_position, region_size, selection_material, modulate) - + draw_rect( + Vector2(region_size, region_size) * .5 + rect_position, + region_size, + selection_material, + modulate + ) + for pos in grid: var grid_tile_position = Vector2(pos) * region_size if show_rect and grid_tile_position == rect_position: # Skip this one, otherwise focused region borders are not always visible due to draw order continue - - draw_rect(Vector2(region_size,region_size)*.5 + grid_tile_position, region_size, material, grid_color) - + + draw_rect( + Vector2(region_size, region_size) * .5 + grid_tile_position, + region_size, + material, + grid_color + ) + draw_rect(Vector2.ZERO, region_size * Terrain3DData.REGION_MAP_SIZE, material, border_color) -func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color) -> void: +func draw_rect( + p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_modulate: Color +) -> void: var lines: PackedVector3Array = [ Vector3(-1, 0, -1), Vector3(-1, 0, 1), @@ -60,9 +75,8 @@ func draw_rect(p_pos: Vector2, p_size: float, p_material: StandardMaterial3D, p_ Vector3(1, 0, -1), Vector3(-1, 0, -1), ] - + for i in lines.size(): lines[i] = ((lines[i] / 2.0) * p_size) + Vector3(p_pos.x, 0, p_pos.y) - + add_lines(lines, p_material, false, p_modulate) - diff --git a/addons/terrain_3d/src/tool_settings.gd b/addons/terrain_3d/src/tool_settings.gd index 42eea9f..1c096d8 100644 --- a/addons/terrain_3d/src/tool_settings.gd +++ b/addons/terrain_3d/src/tool_settings.gd @@ -32,13 +32,13 @@ const ES_TOOL_SETTINGS: String = "terrain3d/tool_settings/" const NONE: int = 0x0 const ALLOW_LARGER: int = 0x1 const ALLOW_SMALLER: int = 0x2 -const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER +const ALLOW_OUT_OF_BOUNDS: int = 0x3 # LARGER|SMALLER const NO_LABEL: int = 0x4 -const ADD_SEPARATOR: int = 0x8 # Add a vertical line before this entry -const ADD_SPACER: int = 0x10 # Add a space before this entry -const NO_SAVE: int = 0x20 # Don't save this in EditorSettings +const ADD_SEPARATOR: int = 0x8 # Add a vertical line before this entry +const ADD_SPACER: int = 0x10 # Add a space before this entry +const NO_SAVE: int = 0x20 # Don't save this in EditorSettings -var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors +var plugin: EditorPlugin # Actually Terrain3DEditorPlugin, but Godot still has CRC errors var brush_preview_material: ShaderMaterial var select_brush_button: Button var selected_brush_imgs: Array @@ -56,109 +56,400 @@ func _ready() -> void: for setting in ["lift_floor", "flatten_peaks", "lift_flatten", "automatic_regions"]: plugin.erase_setting(ES_TOOL_SETTINGS + setting) - # Setup buttons + # Setup buttons main_list = HFlowContainer.new() add_child(main_list, true) - + add_brushes(main_list) - add_setting({ "name":"instructions", "label":"Click the terrain to add a region. CTRL+Click to remove. Or select another tool on the left.", - "type":SettingType.LABEL, "list":main_list, "flags":NO_LABEL|NO_SAVE }) + add_setting( + { + "name": "instructions", + "label": + "Click the terrain to add a region. CTRL+Click to remove. Or select another tool on the left.", + "type": SettingType.LABEL, + "list": main_list, + "flags": NO_LABEL | NO_SAVE + } + ) - add_setting({ "name":"size", "type":SettingType.SLIDER, "list":main_list, "default":20, "unit":"m", - "range":Vector3(0.1, 200, 1), "flags":ALLOW_LARGER|ADD_SPACER }) - - add_setting({ "name":"strength", "type":SettingType.SLIDER, "list":main_list, "default":33, - "unit":"%", "range":Vector3(1, 100, 1), "flags":ALLOW_LARGER }) + add_setting( + { + "name": "size", + "type": SettingType.SLIDER, + "list": main_list, + "default": 20, + "unit": "m", + "range": Vector3(0.1, 200, 1), + "flags": ALLOW_LARGER | ADD_SPACER + } + ) - add_setting({ "name":"height", "type":SettingType.SLIDER, "list":main_list, "default":20, - "unit":"m", "range":Vector3(-500, 500, 0.1), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"height_picker", "type":SettingType.PICKER, "list":main_list, - "default":Terrain3DEditor.HEIGHT, "flags":NO_LABEL }) - - add_setting({ "name":"color", "type":SettingType.COLOR_SELECT, "list":main_list, - "default":Color.WHITE, "flags":ADD_SEPARATOR }) - add_setting({ "name":"color_picker", "type":SettingType.PICKER, "list":main_list, - "default":Terrain3DEditor.COLOR, "flags":NO_LABEL }) + add_setting( + { + "name": "strength", + "type": SettingType.SLIDER, + "list": main_list, + "default": 33, + "unit": "%", + "range": Vector3(1, 100, 1), + "flags": ALLOW_LARGER + } + ) - add_setting({ "name":"roughness", "type":SettingType.SLIDER, "list":main_list, "default":-65, - "unit":"%", "range":Vector3(-100, 100, 1), "flags":ADD_SEPARATOR }) - add_setting({ "name":"roughness_picker", "type":SettingType.PICKER, "list":main_list, - "default":Terrain3DEditor.ROUGHNESS, "flags":NO_LABEL }) + add_setting( + { + "name": "height", + "type": SettingType.SLIDER, + "list": main_list, + "default": 20, + "unit": "m", + "range": Vector3(-500, 500, 0.1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) + add_setting( + { + "name": "height_picker", + "type": SettingType.PICKER, + "list": main_list, + "default": Terrain3DEditor.HEIGHT, + "flags": NO_LABEL + } + ) - add_setting({ "name":"enable_texture", "label":"Texture", "type":SettingType.CHECKBOX, - "list":main_list, "default":true, "flags":ADD_SEPARATOR }) + add_setting( + { + "name": "color", + "type": SettingType.COLOR_SELECT, + "list": main_list, + "default": Color.WHITE, + "flags": ADD_SEPARATOR + } + ) + add_setting( + { + "name": "color_picker", + "type": SettingType.PICKER, + "list": main_list, + "default": Terrain3DEditor.COLOR, + "flags": NO_LABEL + } + ) - add_setting({ "name":"texture_filter", "label":"Texture Filter", "type":SettingType.CHECKBOX, - "list":main_list, "default":false, "flags":ADD_SEPARATOR }) + add_setting( + { + "name": "roughness", + "type": SettingType.SLIDER, + "list": main_list, + "default": -65, + "unit": "%", + "range": Vector3(-100, 100, 1), + "flags": ADD_SEPARATOR + } + ) + add_setting( + { + "name": "roughness_picker", + "type": SettingType.PICKER, + "list": main_list, + "default": Terrain3DEditor.ROUGHNESS, + "flags": NO_LABEL + } + ) - add_setting({ "name":"margin", "type":SettingType.SLIDER, "list":main_list, "default":0, - "unit":"", "range":Vector3(-50, 50, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting( + { + "name": "enable_texture", + "label": "Texture", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": true, + "flags": ADD_SEPARATOR + } + ) + + add_setting( + { + "name": "texture_filter", + "label": "Texture Filter", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": false, + "flags": ADD_SEPARATOR + } + ) + + add_setting( + { + "name": "margin", + "type": SettingType.SLIDER, + "list": main_list, + "default": 0, + "unit": "", + "range": Vector3(-50, 50, 1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) # Slope painting filter - add_setting({ "name":"slope", "type":SettingType.DOUBLE_SLIDER, "list":main_list, "default":Vector2(0, 90), - "unit":"°", "range":Vector3(0, 90, 1), "flags":ADD_SEPARATOR }) - - add_setting({ "name":"enable_angle", "label":"Angle", "type":SettingType.CHECKBOX, - "list":main_list, "default":true, "flags":ADD_SEPARATOR }) - add_setting({ "name":"angle", "type":SettingType.SLIDER, "list":main_list, "default":0, - "unit":"%", "range":Vector3(0, 337.5, 22.5), "flags":NO_LABEL }) - add_setting({ "name":"angle_picker", "type":SettingType.PICKER, "list":main_list, - "default":Terrain3DEditor.ANGLE, "flags":NO_LABEL }) - add_setting({ "name":"dynamic_angle", "label":"Dynamic", "type":SettingType.CHECKBOX, - "list":main_list, "default":false, "flags":ADD_SPACER }) - - add_setting({ "name":"enable_scale", "label":"Scale", "type":SettingType.CHECKBOX, - "list":main_list, "default":true, "flags":ADD_SEPARATOR }) - add_setting({ "name":"scale", "label":"±", "type":SettingType.SLIDER, "list":main_list, "default":0, - "unit":"%", "range":Vector3(-60, 80, 20), "flags":NO_LABEL }) - add_setting({ "name":"scale_picker", "type":SettingType.PICKER, "list":main_list, - "default":Terrain3DEditor.SCALE, "flags":NO_LABEL }) + add_setting( + { + "name": "slope", + "type": SettingType.DOUBLE_SLIDER, + "list": main_list, + "default": Vector2(0, 90), + "unit": "°", + "range": Vector3(0, 90, 1), + "flags": ADD_SEPARATOR + } + ) + + add_setting( + { + "name": "enable_angle", + "label": "Angle", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": true, + "flags": ADD_SEPARATOR + } + ) + add_setting( + { + "name": "angle", + "type": SettingType.SLIDER, + "list": main_list, + "default": 0, + "unit": "%", + "range": Vector3(0, 337.5, 22.5), + "flags": NO_LABEL + } + ) + add_setting( + { + "name": "angle_picker", + "type": SettingType.PICKER, + "list": main_list, + "default": Terrain3DEditor.ANGLE, + "flags": NO_LABEL + } + ) + add_setting( + { + "name": "dynamic_angle", + "label": "Dynamic", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": false, + "flags": ADD_SPACER + } + ) + + add_setting( + { + "name": "enable_scale", + "label": "Scale", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": true, + "flags": ADD_SEPARATOR + } + ) + add_setting( + { + "name": "scale", + "label": "±", + "type": SettingType.SLIDER, + "list": main_list, + "default": 0, + "unit": "%", + "range": Vector3(-60, 80, 20), + "flags": NO_LABEL + } + ) + add_setting( + { + "name": "scale_picker", + "type": SettingType.PICKER, + "list": main_list, + "default": Terrain3DEditor.SCALE, + "flags": NO_LABEL + } + ) ## Slope sculpting brush - add_setting({ "name":"gradient_points", "type":SettingType.MULTI_PICKER, "label":"Points", - "list":main_list, "default":Terrain3DEditor.SCULPT, "flags":ADD_SEPARATOR }) - add_setting({ "name":"drawable", "type":SettingType.CHECKBOX, "list":main_list, "default":false, - "flags":ADD_SEPARATOR }) + add_setting( + { + "name": "gradient_points", + "type": SettingType.MULTI_PICKER, + "label": "Points", + "list": main_list, + "default": Terrain3DEditor.SCULPT, + "flags": ADD_SEPARATOR + } + ) + add_setting( + { + "name": "drawable", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": false, + "flags": ADD_SEPARATOR + } + ) settings["drawable"].toggled.connect(_on_drawable_toggled) - + ## Instancer height_list = create_submenu(main_list, "Height", Layout.VERTICAL) - add_setting({ "name":"height_offset", "type":SettingType.SLIDER, "list":height_list, "default":0, - "unit":"m", "range":Vector3(-10, 10, 0.05), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"random_height", "label":"Random Height ±", "type":SettingType.SLIDER, - "list":height_list, "default":0, "unit":"m", "range":Vector3(0, 10, 0.05), - "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting( + { + "name": "height_offset", + "type": SettingType.SLIDER, + "list": height_list, + "default": 0, + "unit": "m", + "range": Vector3(-10, 10, 0.05), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) + add_setting( + { + "name": "random_height", + "label": "Random Height ±", + "type": SettingType.SLIDER, + "list": height_list, + "default": 0, + "unit": "m", + "range": Vector3(0, 10, 0.05), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) scale_list = create_submenu(main_list, "Scale", Layout.VERTICAL) - add_setting({ "name":"fixed_scale", "type":SettingType.SLIDER, "list":scale_list, "default":100, - "unit":"%", "range":Vector3(1, 1000, 1), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"random_scale", "label":"Random Scale ±", "type":SettingType.SLIDER, "list":scale_list, - "default":20, "unit":"%", "range":Vector3(0, 99, 1), "flags":ALLOW_OUT_OF_BOUNDS }) + add_setting( + { + "name": "fixed_scale", + "type": SettingType.SLIDER, + "list": scale_list, + "default": 100, + "unit": "%", + "range": Vector3(1, 1000, 1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) + add_setting( + { + "name": "random_scale", + "label": "Random Scale ±", + "type": SettingType.SLIDER, + "list": scale_list, + "default": 20, + "unit": "%", + "range": Vector3(0, 99, 1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) rotation_list = create_submenu(main_list, "Rotation", Layout.VERTICAL) - add_setting({ "name":"fixed_spin", "label":"Fixed Spin (Around Y)", "type":SettingType.SLIDER, "list":rotation_list, - "default":0, "unit":"°", "range":Vector3(0, 360, 1) }) - add_setting({ "name":"random_spin", "type":SettingType.SLIDER, "list":rotation_list, "default":360, - "unit":"°", "range":Vector3(0, 360, 1) }) - add_setting({ "name":"fixed_tilt", "label":"Fixed Tilt", "type":SettingType.SLIDER, "list":rotation_list, - "default":0, "unit":"°", "range":Vector3(-85, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"random_tilt", "label":"Random Tilt ±", "type":SettingType.SLIDER, "list":rotation_list, - "default":10, "unit":"°", "range":Vector3(0, 85, 1), "flags":ALLOW_OUT_OF_BOUNDS }) - add_setting({ "name":"align_to_normal", "type":SettingType.CHECKBOX, "list":rotation_list, "default":false }) - + add_setting( + { + "name": "fixed_spin", + "label": "Fixed Spin (Around Y)", + "type": SettingType.SLIDER, + "list": rotation_list, + "default": 0, + "unit": "°", + "range": Vector3(0, 360, 1) + } + ) + add_setting( + { + "name": "random_spin", + "type": SettingType.SLIDER, + "list": rotation_list, + "default": 360, + "unit": "°", + "range": Vector3(0, 360, 1) + } + ) + add_setting( + { + "name": "fixed_tilt", + "label": "Fixed Tilt", + "type": SettingType.SLIDER, + "list": rotation_list, + "default": 0, + "unit": "°", + "range": Vector3(-85, 85, 1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) + add_setting( + { + "name": "random_tilt", + "label": "Random Tilt ±", + "type": SettingType.SLIDER, + "list": rotation_list, + "default": 10, + "unit": "°", + "range": Vector3(0, 85, 1), + "flags": ALLOW_OUT_OF_BOUNDS + } + ) + add_setting( + { + "name": "align_to_normal", + "type": SettingType.CHECKBOX, + "list": rotation_list, + "default": false + } + ) + color_list = create_submenu(main_list, "Color", Layout.VERTICAL) - add_setting({ "name":"vertex_color", "type":SettingType.COLOR_SELECT, "list":color_list, - "default":Color.WHITE }) - add_setting({ "name":"random_hue", "label":"Random Hue Shift ±", "type":SettingType.SLIDER, - "list":color_list, "default":0, "unit":"°", "range":Vector3(0, 360, 1) }) - add_setting({ "name":"random_darken", "type":SettingType.SLIDER, "list":color_list, "default":50, - "unit":"%", "range":Vector3(0, 100, 1) }) - #add_setting({ "name":"blend_mode", "type":SettingType.OPTION, "list":color_list, "default":0, - #"range":Vector3(0, 3, 1) }) + add_setting( + { + "name": "vertex_color", + "type": SettingType.COLOR_SELECT, + "list": color_list, + "default": Color.WHITE + } + ) + add_setting( + { + "name": "random_hue", + "label": "Random Hue Shift ±", + "type": SettingType.SLIDER, + "list": color_list, + "default": 0, + "unit": "°", + "range": Vector3(0, 360, 1) + } + ) + add_setting( + { + "name": "random_darken", + "type": SettingType.SLIDER, + "list": color_list, + "default": 50, + "unit": "%", + "range": Vector3(0, 100, 1) + } + ) + #add_setting({ "name":"blend_mode", "type":SettingType.OPTION, "list":color_list, "default":0, + #"range":Vector3(0, 3, 1) }) if DisplayServer.is_touchscreen_available(): - add_setting({ "name":"invert", "label":"Invert", "type":SettingType.CHECKBOX, "list":main_list, "default":false, "flags":ADD_SEPARATOR }) + add_setting( + { + "name": "invert", + "label": "Invert", + "type": SettingType.CHECKBOX, + "list": main_list, + "default": false, + "flags": ADD_SEPARATOR + } + ) var spacer: Control = Control.new() spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL @@ -166,22 +457,67 @@ func _ready() -> void: ## Advanced Settings Menu advanced_list = create_submenu(main_list, "", Layout.VERTICAL, false) - add_setting({ "name":"auto_regions", "label":"Add regions while sculpting", "type":SettingType.CHECKBOX, - "list":advanced_list, "default":true }) - add_setting({ "name":"align_to_view", "type":SettingType.CHECKBOX, "list":advanced_list, - "default":true }) - add_setting({ "name":"show_cursor_while_painting", "type":SettingType.CHECKBOX, "list":advanced_list, - "default":true }) + add_setting( + { + "name": "auto_regions", + "label": "Add regions while sculpting", + "type": SettingType.CHECKBOX, + "list": advanced_list, + "default": true + } + ) + add_setting( + { + "name": "align_to_view", + "type": SettingType.CHECKBOX, + "list": advanced_list, + "default": true + } + ) + add_setting( + { + "name": "show_cursor_while_painting", + "type": SettingType.CHECKBOX, + "list": advanced_list, + "default": true + } + ) advanced_list.add_child(HSeparator.new(), true) - add_setting({ "name":"gamma", "type":SettingType.SLIDER, "list":advanced_list, "default":1.0, - "unit":"γ", "range":Vector3(0.1, 2.0, 0.01) }) - add_setting({ "name":"jitter", "type":SettingType.SLIDER, "list":advanced_list, "default":50, - "unit":"%", "range":Vector3(0, 100, 1) }) - add_setting({ "name":"crosshair_threshold", "type":SettingType.SLIDER, "list":advanced_list, "default":16., - "unit":"m", "range":Vector3(0, 200, 1) }) + add_setting( + { + "name": "gamma", + "type": SettingType.SLIDER, + "list": advanced_list, + "default": 1.0, + "unit": "γ", + "range": Vector3(0.1, 2.0, 0.01) + } + ) + add_setting( + { + "name": "jitter", + "type": SettingType.SLIDER, + "list": advanced_list, + "default": 50, + "unit": "%", + "range": Vector3(0, 100, 1) + } + ) + add_setting( + { + "name": "crosshair_threshold", + "type": SettingType.SLIDER, + "list": advanced_list, + "default": 16., + "unit": "m", + "range": Vector3(0, 200, 1) + } + ) -func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true) -> Container: +func create_submenu( + p_parent: Control, p_button_name: String, p_layout: Layout, p_hover_pop: bool = true +) -> Container: var menu_button: Button = Button.new() if p_button_name.is_empty(): menu_button.icon = get_theme_icon("GuiTabMenuHl", "EditorIcons") @@ -190,7 +526,7 @@ func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, menu_button.set_toggle_mode(true) menu_button.set_v_size_flags(SIZE_SHRINK_CENTER) menu_button.toggled.connect(_on_show_submenu.bind(menu_button)) - + var submenu: PopupPanel = PopupPanel.new() submenu.popup_hide.connect(menu_button.set_pressed.bind(false)) var panel_style: StyleBox = get_theme_stylebox("panel", "PopupMenu").duplicate() @@ -201,38 +537,40 @@ func create_submenu(p_parent: Control, p_button_name: String, p_layout: Layout, # Pop up menu on hover, hide on exit if p_hover_pop: menu_button.mouse_entered.connect(_on_show_submenu.bind(true, menu_button)) - + submenu.mouse_entered.connect(func(): submenu.set_meta("mouse_entered", true)) - - submenu.mouse_exited.connect(func(): - # On mouse_exit, hide popup unless LineEdit focused - var focused_element: Control = submenu.gui_get_focus_owner() - if not focused_element is LineEdit: - _on_show_submenu(false, menu_button) - submenu.set_meta("mouse_entered", false) - return - - focused_element.focus_exited.connect(func(): - # Close submenu once lineedit loses focus - if not submenu.get_meta("mouse_entered"): + + submenu.mouse_exited.connect( + func(): + # On mouse_exit, hide popup unless LineEdit focused + var focused_element: Control = submenu.gui_get_focus_owner() + if not focused_element is LineEdit: _on_show_submenu(false, menu_button) submenu.set_meta("mouse_entered", false) - ) + return + + focused_element.focus_exited.connect( + func(): + # Close submenu once lineedit loses focus + if not submenu.get_meta("mouse_entered"): + _on_show_submenu(false, menu_button) + submenu.set_meta("mouse_entered", false) + ) ) - + var sublist: Container - match(p_layout): + match p_layout: Layout.GRID: sublist = GridContainer.new() Layout.VERTICAL: sublist = VBoxContainer.new() Layout.HORIZONTAL, _: sublist = HBoxContainer.new() - + p_parent.add_child(menu_button, true) menu_button.add_child(submenu, true) submenu.add_child(sublist, true) - + return sublist @@ -240,25 +578,27 @@ func _on_show_submenu(p_toggled: bool, p_button: Button) -> void: # Don't show if mouse already down (from painting) if p_toggled and Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT): return - - # Hide menu if mouse is not in button or panel - var button_rect: Rect2 = Rect2(p_button.get_screen_transform().origin, p_button.get_global_rect().size) + + # Hide menu if mouse is not in button or panel + var button_rect: Rect2 = Rect2( + p_button.get_screen_transform().origin, p_button.get_global_rect().size + ) var in_button: bool = button_rect.has_point(DisplayServer.mouse_get_position()) var popup: PopupPanel = p_button.get_child(0) var popup_rect: Rect2 = Rect2(popup.position, popup.size) var in_popup: bool = popup_rect.has_point(DisplayServer.mouse_get_position()) - if not p_toggled and ( in_button or in_popup ): + if not p_toggled and (in_button or in_popup): return - + # Hide all submenus before possibly enabling the current one get_tree().call_group("terrain3d_submenus", "set_visible", false) popup.set_visible(p_toggled) var popup_pos: Vector2 = p_button.get_screen_transform().origin popup_pos.y -= popup.size.y - if popup.get_child_count()>0 and popup.get_child(0) == advanced_list: + if popup.get_child_count() > 0 and popup.get_child(0) == advanced_list: popup_pos.x -= popup.size.x - p_button.size.x popup.set_position(popup_pos) - + func add_brushes(p_parent: Control) -> void: var brush_list: GridContainer = create_submenu(p_parent, "Brush", Layout.GRID) @@ -267,7 +607,7 @@ func add_brushes(p_parent: Control) -> void: var brush_button_group: ButtonGroup = ButtonGroup.new() brush_button_group.pressed.connect(_on_setting_changed) var default_brush_btn: Button - + var dir: DirAccess = DirAccess.open(BRUSH_PATH) if dir: dir.list_dir_begin() @@ -283,7 +623,7 @@ func add_brushes(p_parent: Control) -> void: thumbimg = Terrain3DUtil.black_to_alpha(thumbimg) thumbimg.convert(Image.FORMAT_LA8) var thumbtex: ImageTexture = ImageTexture.create_from_image(thumbimg) - + var brush_btn: Button = Button.new() brush_btn.set_custom_minimum_size(Vector2.ONE * 100) brush_btn.set_button_icon(thumbtex) @@ -296,8 +636,8 @@ func add_brushes(p_parent: Control) -> void: brush_btn.mouse_exited.connect(_on_brush_hover.bind(false, brush_btn)) brush_list.add_child(brush_btn, true) if file_name == DEFAULT_BRUSH: - default_brush_btn = brush_btn - + default_brush_btn = brush_btn + var lbl: Label = Label.new() brush_btn.name = file_name.get_basename().to_pascal_case() brush_btn.add_child(lbl, true) @@ -308,16 +648,16 @@ func add_brushes(p_parent: Control) -> void: lbl.add_theme_constant_override("shadow_offset_x", 1) lbl.add_theme_constant_override("shadow_offset_y", 1) lbl.add_theme_font_size_override("font_size", 16) - + file_name = dir.get_next() - + brush_list.columns = sqrt(brush_list.get_child_count()) + 2 - + if not default_brush_btn: default_brush_btn = brush_button_group.get_buttons()[0] default_brush_btn.set_pressed(true) _generate_brush_texture(default_brush_btn) - + settings["brush"] = brush_button_group select_brush_button = brush_list.get_parent().get_parent() @@ -352,7 +692,11 @@ func _on_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Terrain3DEditor.ROUGHNESS: # This converts 0,1 to -100,100 # It also quantizes explicitly so picked values matches painted values - settings["roughness"].value = round(200. * float(int(p_color.a * 255.) / 255. - .5)) if not is_nan(p_color.r) else 0. + settings["roughness"].value = ( + round(200. * float(int(p_color.a * 255.) / 255. - .5)) + if not is_nan(p_color.r) + else 0. + ) Terrain3DEditor.ANGLE: settings["angle"].value = p_color.r Terrain3DEditor.SCALE: @@ -365,7 +709,9 @@ func _on_point_pick(p_type: Terrain3DEditor.Tool, p_name: String) -> void: emit_signal("picking", p_type, _on_point_picked.bind(p_name)) -func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String) -> void: +func _on_point_picked( + p_type: Terrain3DEditor.Tool, p_color: Color, p_global_position: Vector3, p_name: String +) -> void: assert(p_type == Terrain3DEditor.SCULPT) var point: Vector3 = p_global_position point.y = p_color.r @@ -375,7 +721,7 @@ func _on_point_picked(p_type: Terrain3DEditor.Tool, p_color: Color, p_global_pos func add_setting(p_args: Dictionary) -> void: var p_name: StringName = p_args.get("name", "") - var p_label: String = p_args.get("label", "") # Optional replacement for name + var p_label: String = p_args.get("label", "") # Optional replacement for name var p_type: SettingType = p_args.get("type", SettingType.TYPE_MAX) var p_list: Control = p_args.get("list") var p_default: Variant = p_args.get("default") @@ -385,13 +731,13 @@ func add_setting(p_args: Dictionary) -> void: var p_maximum: float = p_range.y var p_step: float = p_range.z var p_flags: int = p_args.get("flags", NONE) - + if p_name.is_empty() or p_type == SettingType.TYPE_MAX: return var container: HBoxContainer = HBoxContainer.new() container.set_v_size_flags(SIZE_EXPAND_FILL) - var control: Control # Houses the setting to be saved + var control: Control # Houses the setting to be saved var pending_children: Array[Control] match p_type: @@ -404,17 +750,21 @@ func add_setting(p_args: Dictionary) -> void: SettingType.CHECKBOX: var checkbox := CheckBox.new() if !(p_flags & NO_SAVE): - checkbox.set_pressed_no_signal(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) - checkbox.toggled.connect( ( - func(value, path): - plugin.set_setting(path, value) - ).bind(ES_TOOL_SETTINGS + p_name) ) + checkbox.set_pressed_no_signal( + plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default) + ) + checkbox.toggled.connect( + ( + (func(value, path): plugin.set_setting(path, value)) + . bind(ES_TOOL_SETTINGS + p_name) + ) + ) else: - checkbox.set_pressed_no_signal(p_default) + checkbox.set_pressed_no_signal(p_default) checkbox.pressed.connect(_on_setting_changed) pending_children.push_back(checkbox) control = checkbox - + SettingType.COLOR_SELECT: var picker := ColorPickerButton.new() picker.set_custom_minimum_size(Vector2(100, 25)) @@ -422,10 +772,12 @@ func add_setting(p_args: Dictionary) -> void: picker.get_picker().set_color_mode(ColorPicker.MODE_HSV) if !(p_flags & NO_SAVE): picker.set_pick_color(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) - picker.color_changed.connect( ( - func(value, path): - plugin.set_setting(path, value) - ).bind(ES_TOOL_SETTINGS + p_name) ) + picker.color_changed.connect( + ( + (func(value, path): plugin.set_setting(path, value)) + . bind(ES_TOOL_SETTINGS + p_name) + ) + ) else: picker.set_pick_color(p_default) picker.color_changed.connect(_on_setting_changed) @@ -479,12 +831,12 @@ func add_setting(p_args: Dictionary) -> void: slider.set_allow_greater(true) if p_flags & ALLOW_SMALLER: slider.set_allow_lesser(true) - + pending_children.push_back(slider) pending_children.push_back(spin_slider) control = spin_slider - - else: # DOUBLE_SLIDER + + else: # DOUBLE_SLIDER var label := Label.new() label.set_custom_minimum_size(Vector2(60, 0)) label.set_horizontal_alignment(HORIZONTAL_ALIGNMENT_CENTER) @@ -495,7 +847,7 @@ func add_setting(p_args: Dictionary) -> void: pending_children.push_back(slider) pending_children.push_back(label) control = slider - + slider.set_min(p_minimum) slider.set_max(p_maximum) slider.set_step(p_step) @@ -505,10 +857,12 @@ func add_setting(p_args: Dictionary) -> void: if !(p_flags & NO_SAVE): slider.set_value(plugin.get_setting(ES_TOOL_SETTINGS + p_name, p_default)) - slider.value_changed.connect( ( - func(value, path): - plugin.set_setting(path, value) - ).bind(ES_TOOL_SETTINGS + p_name) ) + slider.value_changed.connect( + ( + (func(value, path): plugin.set_setting(path, value)) + . bind(ES_TOOL_SETTINGS + p_name) + ) + ) else: slider.set_value(p_default) @@ -569,11 +923,14 @@ func get_setting(p_setting: String) -> Variant: value = object.get_value() # Adjust widths of all sliders on update of values var digits: float = count_digits(value) - var width: float = clamp( (1 + count_digits(value)) * 19., 50, 80) * clamp(EditorInterface.get_editor_scale(), .9, 2) + var width: float = ( + clamp((1 + count_digits(value)) * 19., 50, 80) + * clamp(EditorInterface.get_editor_scale(), .9, 2) + ) object.set_custom_minimum_size(Vector2(width, 0)) elif object is DoubleSlider: value = object.get_value() - elif object is ButtonGroup: # "brush" + elif object is ButtonGroup: # "brush" value = selected_brush_imgs elif object is CheckBox: value = object.is_pressed() @@ -588,11 +945,11 @@ func get_setting(p_setting: String) -> Variant: func set_setting(p_setting: String, p_value: Variant) -> void: var object: Object = settings.get(p_setting) - if object is DoubleSlider: # Expects p_value is Vector2 + if object is DoubleSlider: # Expects p_value is Vector2 object.set_value(p_value) elif object is Range: object.set_value(p_value) - elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ] + elif object is ButtonGroup: # Expects p_value is Array [ "button name", boolean ] if p_value is Array and p_value.size() == 2: for button in object.get_buttons(): if button.name == p_value[0]: @@ -601,8 +958,8 @@ func set_setting(p_setting: String, p_value: Variant) -> void: object.button_pressed = p_value elif object is ColorPickerButton: object.color = p_value - plugin.set_setting(ES_TOOL_SETTINGS + p_setting, p_value) # Signal doesn't fire on CPB - elif object is MultiPicker: # Expects p_value is PackedVector3Array + plugin.set_setting(ES_TOOL_SETTINGS + p_setting, p_value) # Signal doesn't fire on CPB + elif object is MultiPicker: # Expects p_value is PackedVector3Array object.points = p_value _on_setting_changed(object) @@ -644,7 +1001,7 @@ func _generate_brush_texture(p_btn: Button) -> void: img = img.duplicate() img.resize(1024, 1024, Image.INTERPOLATE_CUBIC) var tex: ImageTexture = ImageTexture.create_from_image(img) - selected_brush_imgs = [ img, tex ] + selected_brush_imgs = [img, tex] func _on_drawable_toggled(p_button_pressed: bool) -> void: @@ -671,23 +1028,22 @@ func _get_brush_preview_material() -> ShaderMaterial: return brush_preview_material -# Counts digits of a number including negative sign, decimal points, and up to 3 decimals +# Counts digits of a number including negative sign, decimal points, and up to 3 decimals func count_digits(p_value: float) -> int: var count: int = 1 for i in range(5, 0, -1): if abs(p_value) >= pow(10, i): - count = i+1 + count = i + 1 break if p_value - floor(p_value) >= .1: - count += 1 # For the decimal - if p_value*10 - floor(p_value*10.) >= .1: + count += 1 # For the decimal + if p_value * 10 - floor(p_value * 10.) >= .1: count += 1 - if p_value*100 - floor(p_value*100.) >= .1: + if p_value * 100 - floor(p_value * 100.) >= .1: count += 1 - if p_value*1000 - floor(p_value*1000.) >= .1: + if p_value * 1000 - floor(p_value * 1000.) >= .1: count += 1 # Negative sign if p_value < 0: count += 1 return count - diff --git a/addons/terrain_3d/src/toolbar.gd b/addons/terrain_3d/src/toolbar.gd index addf69c..79dd07d 100644 --- a/addons/terrain_3d/src/toolbar.gd +++ b/addons/terrain_3d/src/toolbar.gd @@ -33,61 +33,151 @@ func _ready() -> void: add_tool_group.pressed.connect(_on_tool_selected) sub_tool_group.pressed.connect(_on_tool_selected) - add_tool_button({ "tool":Terrain3DEditor.REGION, - "add_text":"Add Region (E)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_REGION_ADD, - "sub_text":"Remove Region", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_REGION_REMOVE }) - - add_child(HSeparator.new()) - - add_tool_button({ "tool":Terrain3DEditor.SCULPT, - "add_text":"Raise (R)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_ADD, - "sub_text":"Lower (R)", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_SUB }) - - add_tool_button({ "tool":Terrain3DEditor.SCULPT, - "add_text":"Smooth (Shift)", "add_op":Terrain3DEditor.AVERAGE, "add_icon":ICON_HEIGHT_SMOOTH }) - - add_tool_button({ "tool":Terrain3DEditor.HEIGHT, - "add_text":"Height (H)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HEIGHT_FLAT, - "sub_text":"Height (H)", "sub_op":Terrain3DEditor.SUBTRACT, "sub_icon":ICON_HEIGHT_FLAT }) - - add_tool_button({ "tool":Terrain3DEditor.SCULPT, - "add_text":"Slope (S)", "add_op":Terrain3DEditor.GRADIENT, "add_icon":ICON_HEIGHT_SLOPE }) + add_tool_button( + { + "tool": Terrain3DEditor.REGION, + "add_text": "Add Region (E)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_REGION_ADD, + "sub_text": "Remove Region", + "sub_op": Terrain3DEditor.SUBTRACT, + "sub_icon": ICON_REGION_REMOVE + } + ) add_child(HSeparator.new()) - add_tool_button({ "tool":Terrain3DEditor.TEXTURE, - "add_text":"Paint Texture (B)", "add_op":Terrain3DEditor.REPLACE, "add_icon":ICON_PAINT_TEXTURE }) + add_tool_button( + { + "tool": Terrain3DEditor.SCULPT, + "add_text": "Raise (R)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_HEIGHT_ADD, + "sub_text": "Lower (R)", + "sub_op": Terrain3DEditor.SUBTRACT, + "sub_icon": ICON_HEIGHT_SUB + } + ) - add_tool_button({ "tool":Terrain3DEditor.TEXTURE, - "add_text":"Spray Texture (V)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_SPRAY_TEXTURE }) + add_tool_button( + { + "tool": Terrain3DEditor.SCULPT, + "add_text": "Smooth (Shift)", + "add_op": Terrain3DEditor.AVERAGE, + "add_icon": ICON_HEIGHT_SMOOTH + } + ) - add_tool_button({ "tool":Terrain3DEditor.AUTOSHADER, - "add_text":"Paint Autoshader (A)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_AUTOSHADER, - "sub_text":"Disable Autoshader (A)", "sub_op":Terrain3DEditor.SUBTRACT }) + add_tool_button( + { + "tool": Terrain3DEditor.HEIGHT, + "add_text": "Height (H)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_HEIGHT_FLAT, + "sub_text": "Height (H)", + "sub_op": Terrain3DEditor.SUBTRACT, + "sub_icon": ICON_HEIGHT_FLAT + } + ) + + add_tool_button( + { + "tool": Terrain3DEditor.SCULPT, + "add_text": "Slope (S)", + "add_op": Terrain3DEditor.GRADIENT, + "add_icon": ICON_HEIGHT_SLOPE + } + ) add_child(HSeparator.new()) - add_tool_button({ "tool":Terrain3DEditor.COLOR, - "add_text":"Paint Color (C)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_COLOR, - "sub_text":"Remove Color (C)", "sub_op":Terrain3DEditor.SUBTRACT }) - - add_tool_button({ "tool":Terrain3DEditor.ROUGHNESS, - "add_text":"Paint Wetness (W)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_WETNESS, - "sub_text":"Remove Wetness (W)", "sub_op":Terrain3DEditor.SUBTRACT }) + add_tool_button( + { + "tool": Terrain3DEditor.TEXTURE, + "add_text": "Paint Texture (B)", + "add_op": Terrain3DEditor.REPLACE, + "add_icon": ICON_PAINT_TEXTURE + } + ) + + add_tool_button( + { + "tool": Terrain3DEditor.TEXTURE, + "add_text": "Spray Texture (V)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_SPRAY_TEXTURE + } + ) + + add_tool_button( + { + "tool": Terrain3DEditor.AUTOSHADER, + "add_text": "Paint Autoshader (A)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_AUTOSHADER, + "sub_text": "Disable Autoshader (A)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) add_child(HSeparator.new()) - add_tool_button({ "tool":Terrain3DEditor.HOLES, - "add_text":"Add Holes (X)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_HOLES, - "sub_text":"Remove Holes (X)", "sub_op":Terrain3DEditor.SUBTRACT }) + add_tool_button( + { + "tool": Terrain3DEditor.COLOR, + "add_text": "Paint Color (C)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_COLOR, + "sub_text": "Remove Color (C)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) - add_tool_button({ "tool":Terrain3DEditor.NAVIGATION, - "add_text":"Paint Navigable Area (N)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_NAVIGATION, - "sub_text":"Remove Navigable Area (N)", "sub_op":Terrain3DEditor.SUBTRACT }) + add_tool_button( + { + "tool": Terrain3DEditor.ROUGHNESS, + "add_text": "Paint Wetness (W)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_WETNESS, + "sub_text": "Remove Wetness (W)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) - add_tool_button({ "tool":Terrain3DEditor.INSTANCER, - "add_text":"Instance Meshes (I)", "add_op":Terrain3DEditor.ADD, "add_icon":ICON_INSTANCER, - "sub_text":"Remove Meshes (I)", "sub_op":Terrain3DEditor.SUBTRACT }) + add_child(HSeparator.new()) + + add_tool_button( + { + "tool": Terrain3DEditor.HOLES, + "add_text": "Add Holes (X)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_HOLES, + "sub_text": "Remove Holes (X)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) + + add_tool_button( + { + "tool": Terrain3DEditor.NAVIGATION, + "add_text": "Paint Navigable Area (N)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_NAVIGATION, + "sub_text": "Remove Navigable Area (N)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) + + add_tool_button( + { + "tool": Terrain3DEditor.INSTANCER, + "add_text": "Instance Meshes (I)", + "add_op": Terrain3DEditor.ADD, + "add_icon": ICON_INSTANCER, + "sub_text": "Remove Meshes (I)", + "sub_op": Terrain3DEditor.SUBTRACT + } + ) # Select first button var buttons: Array[BaseButton] = add_tool_group.get_buttons() @@ -98,7 +188,7 @@ func _ready() -> void: func add_tool_button(p_params: Dictionary) -> void: # Additive button var button := Button.new() - var name_str: String = p_params.get("add_text", "blank").get_slice('(', 0).to_pascal_case() + var name_str: String = p_params.get("add_text", "blank").get_slice("(", 0).to_pascal_case() button.set_name(name_str) button.set_meta("Tool", p_params.get("tool", 0)) button.set_meta("Operation", p_params.get("add_op", 0)) @@ -116,7 +206,7 @@ func add_tool_button(p_params: Dictionary) -> void: var button2: Button if p_params.has("sub_text"): button2 = Button.new() - name_str = p_params.get("sub_text", "blank").get_slice('(', 0).to_pascal_case() + name_str = p_params.get("sub_text", "blank").get_slice("(", 0).to_pascal_case() button2.set_name(name_str) button2.set_meta("Tool", p_params.get("tool", 0)) button2.set_meta("Operation", p_params.get("sub_op", 0)) @@ -151,4 +241,8 @@ func _on_tool_selected(p_button: BaseButton) -> void: var id: int = p_button.get_meta("ID", -2) for button in change_group.get_buttons(): button.set_pressed_no_signal(button.get_meta("ID", -1) == id) - emit_signal("tool_changed", p_button.get_meta("Tool", Terrain3DEditor.TOOL_MAX), p_button.get_meta("Operation", Terrain3DEditor.OP_MAX)) + emit_signal( + "tool_changed", + p_button.get_meta("Tool", Terrain3DEditor.TOOL_MAX), + p_button.get_meta("Operation", Terrain3DEditor.OP_MAX) + ) diff --git a/addons/terrain_3d/tools/importer.gd b/addons/terrain_3d/tools/importer.gd index e15f287..3783e2b 100644 --- a/addons/terrain_3d/tools/importer.gd +++ b/addons/terrain_3d/tools/importer.gd @@ -3,10 +3,12 @@ @tool extends Terrain3D - -@export var clear_all: bool = false : set = reset_settings -@export var clear_terrain: bool = false : set = reset_terrain -@export var update_height_range: bool = false : set = update_heights +@export var clear_all: bool = false: + set = reset_settings +@export var clear_terrain: bool = false: + set = reset_terrain +@export var update_height_range: bool = false: + set = update_heights func reset_settings(p_value) -> void: @@ -27,7 +29,7 @@ func reset_settings(p_value) -> void: func reset_terrain(p_value) -> void: data_directory = "" - for region:Terrain3DRegion in data.get_regions_active(): + for region: Terrain3DRegion in data.get_regions_active(): data.remove_region(region, false) data.update_maps(Terrain3DRegion.TYPE_MAX, true, false) @@ -42,15 +44,19 @@ func update_heights(p_value) -> void: @export_global_file var height_file_name: String = "" @export_global_file var control_file_name: String = "" @export_global_file var color_file_name: String = "" -@export var import_position: Vector2i = Vector2i(0, 0) : set = set_import_position +@export var import_position: Vector2i = Vector2i(0, 0): + set = set_import_position @export var import_scale: float = 1.0 @export var height_offset: float = 0.0 @export var r16_range: Vector2 = Vector2(0, 1) -@export var r16_size: Vector2i = Vector2i(1024, 1024) : set = set_r16_size -@export var run_import: bool = false : set = start_import +@export var r16_size: Vector2i = Vector2i(1024, 1024): + set = set_r16_size +@export var run_import: bool = false: + set = start_import @export_dir var destination_directory: String = "" -@export var save_to_disk: bool = false : set = save_data +@export var save_to_disk: bool = false: + set = save_data func set_import_position(p_value: Vector2i) -> void: @@ -65,14 +71,21 @@ func set_r16_size(p_value: Vector2i) -> void: func start_import(p_value: bool) -> void: if p_value: - print("Terrain3DImporter: Importing files:\n\t%s\n\t%s\n\t%s" % [ height_file_name, control_file_name, color_file_name]) + print( + ( + "Terrain3DImporter: Importing files:\n\t%s\n\t%s\n\t%s" + % [height_file_name, control_file_name, color_file_name] + ) + ) var imported_images: Array[Image] imported_images.resize(Terrain3DRegion.TYPE_MAX) var min_max := Vector2(0, 1) var img: Image if height_file_name: - img = Terrain3DUtil.load_image(height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size) + img = Terrain3DUtil.load_image( + height_file_name, ResourceLoader.CACHE_MODE_IGNORE, r16_range, r16_size + ) min_max = Terrain3DUtil.get_min_max(img) imported_images[Terrain3DRegion.TYPE_HEIGHT] = img if control_file_name: @@ -100,9 +113,10 @@ func save_data(p_value: bool) -> void: enum { TYPE_HEIGHT, TYPE_CONTROL, TYPE_COLOR } @export_enum("Height:0", "Control:1", "Color:2") var map_type: int = TYPE_HEIGHT @export var file_name_out: String = "" -@export var run_export: bool = false : set = start_export +@export var run_export: bool = false: + set = start_export + func start_export(p_value: bool) -> void: var err: int = data.export_image(file_name_out, map_type) print("Terrain3DImporter: Export error status: ", err, " ", error_string(err)) - diff --git a/addons/terrain_3d/tools/region_mover.gd b/addons/terrain_3d/tools/region_mover.gd index 78dcbd8..511561a 100644 --- a/addons/terrain_3d/tools/region_mover.gd +++ b/addons/terrain_3d/tools/region_mover.gd @@ -1,6 +1,6 @@ -# This script can be used to move your regions by an offset. +# This script can be used to move your regions by an offset. # Eventually this tool will find its way into a built in UI -# +# # Attach it to your Terrain3D node # Save and reload your scene # Select your Terrain3D node @@ -9,19 +9,18 @@ # It should unload the regions, rename files, and reload them # Clear the script and resave your scene - @tool extends Terrain3D - @export var offset: Vector2i -@export var run: bool = false : set = start_rename +@export var run: bool = false: + set = start_rename func start_rename(val: bool = false) -> void: if val == false or offset == Vector2i.ZERO: return - + var dir_name: String = data_directory data_directory = "" var dir := DirAccess.open(dir_name) @@ -36,17 +35,22 @@ func start_rename(val: bool = false) -> void: var region_loc: Vector2i = Terrain3DUtil.filename_to_location(file_name) var new_loc: Vector2i = region_loc + offset if new_loc.x < -16 or new_loc.x > 15 or new_loc.y < -16 or new_loc.y > 15: - push_error("New location %.0v out of bounds for region %.0v. Aborting" % [ new_loc, region_loc ]) + push_error( + ( + "New location %.0v out of bounds for region %.0v. Aborting" + % [new_loc, region_loc] + ) + ) return var new_name: String = "tmp_" + Terrain3DUtil.location_to_filename(new_loc) dir.rename(file_name, new_name) affected_files.push_back(new_name) - print("File: %s renamed to: %s" % [ file_name, new_name ]) - + print("File: %s renamed to: %s" % [file_name, new_name]) + for file_name in affected_files: var new_name: String = file_name.trim_prefix("tmp_") dir.rename(file_name, new_name) - print("File: %s renamed to: %s" % [ file_name, new_name ]) - + print("File: %s renamed to: %s" % [file_name, new_name]) + data_directory = dir_name - EditorInterface.get_resource_filesystem().scan() + EditorInterface.get_resource_filesystem().scan() diff --git a/addons/terrain_3d/utils/terrain_3d_objects.gd b/addons/terrain_3d/utils/terrain_3d_objects.gd index 31f9a50..13d7f07 100644 --- a/addons/terrain_3d/utils/terrain_3d_objects.gd +++ b/addons/terrain_3d/utils/terrain_3d_objects.gd @@ -5,24 +5,26 @@ extends Node3D class_name Terrain3DObjects -const TransformChangedNotifier: Script = preload("res://addons/terrain_3d/utils/transform_changed_notifier.gd") +const TransformChangedNotifier: Script = preload( + "res://addons/terrain_3d/utils/transform_changed_notifier.gd" +) const CHILD_HELPER_NAME: StringName = &"TransformChangedSignaller" const CHILD_HELPER_PATH: NodePath = ^"TransformChangedSignaller" var _undo_redo = null var _terrain_id: int -var _offsets: Dictionary # Object ID -> Vector3(X, Y offset relative to terrain height, Z) +var _offsets: Dictionary # Object ID -> Vector3(X, Y offset relative to terrain height, Z) var _ignore_transform_change: bool = false func _enter_tree() -> void: if not Engine.is_editor_hint(): return - + for child in get_children(): _on_child_entered_tree(child) - + child_entered_tree.connect(_on_child_entered_tree) child_exiting_tree.connect(_on_child_exiting_tree) @@ -30,10 +32,10 @@ func _enter_tree() -> void: func _exit_tree() -> void: if not Engine.is_editor_hint(): return - + child_entered_tree.disconnect(_on_child_entered_tree) child_exiting_tree.disconnect(_on_child_exiting_tree) - + for child in get_children(): _on_child_exiting_tree(child) @@ -45,14 +47,19 @@ func editor_setup(p_plugin) -> void: func get_terrain() -> Terrain3D: var terrain := instance_from_id(_terrain_id) as Terrain3D if not terrain or terrain.is_queued_for_deletion() or not terrain.is_inside_tree(): - var terrains: Array[Node] = Engine.get_singleton(&"EditorInterface").get_edited_scene_root().find_children("", "Terrain3D") + var terrains: Array[Node] = ( + Engine + . get_singleton(&"EditorInterface") + . get_edited_scene_root() + . find_children("", "Terrain3D") + ) if terrains.size() > 0: terrain = terrains[0] _terrain_id = terrain.get_instance_id() if terrain else 0 - + if terrain and terrain.data and not terrain.data.maps_edited.is_connected(_on_maps_edited): terrain.data.maps_edited.connect(_on_maps_edited) - + return terrain @@ -69,16 +76,16 @@ func _get_terrain_height(p_global_position: Vector3) -> float: func _on_child_entered_tree(p_node: Node) -> void: if not (p_node is Node3D): return - + assert(p_node.get_parent() == self) - + var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH) if not helper: helper = TransformChangedNotifier.new() helper.name = CHILD_HELPER_NAME p_node.add_child(helper, true, INTERNAL_MODE_BACK) assert(p_node.has_node(CHILD_HELPER_PATH)) - + # When reparenting a Node3D, Godot changes its transform _after_ reparenting it. So here, # we must use call_deferred, to avoid receiving transform_changed as a result of reparenting. _setup_child_signal.call_deferred(p_node, helper) @@ -89,7 +96,7 @@ func _setup_child_signal(p_node: Node, helper: TransformChangedNotifier) -> void return if helper.transform_changed.is_connected(_on_child_transform_changed): return - + helper.transform_changed.connect(_on_child_transform_changed.bind(p_node)) _update_child_offset(p_node) @@ -97,14 +104,14 @@ func _setup_child_signal(p_node: Node, helper: TransformChangedNotifier) -> void func _on_child_exiting_tree(p_node: Node) -> void: if not (p_node is Node3D) or not p_node.has_node(CHILD_HELPER_PATH): return - + var helper: TransformChangedNotifier = p_node.get_node_or_null(CHILD_HELPER_PATH) if helper: if helper.transform_changed.is_connected(_on_child_transform_changed): helper.transform_changed.disconnect(_on_child_transform_changed) p_node.remove_child(helper) helper.queue_free() - + _offsets.erase(p_node.get_instance_id()) @@ -116,16 +123,16 @@ func _is_node_selected(p_node: Node) -> bool: func _on_child_transform_changed(p_node: Node3D) -> void: if _ignore_transform_change: return - + var lmb_down := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) if lmb_down and (_is_node_selected(p_node) or _is_node_selected(self)): # The user may be moving the node using gizmos. # We should wait until they're done before updating otherwise gizmos + this node conflict. return - + if not _offsets.has(p_node.get_instance_id()): return - + var old_offset: Vector3 = _offsets[p_node.get_instance_id()] var old_h: float = _get_terrain_height(old_offset) var old_position: Vector3 = old_offset + Vector3(0, old_h, 0) @@ -134,17 +141,21 @@ func _on_child_transform_changed(p_node: Node3D) -> void: return var new_h: float = _get_terrain_height(new_position) var new_offset: Vector3 = new_position - Vector3(0, new_h, 0) - + var translate_without_reposition: bool = Input.is_key_pressed(KEY_SHIFT) var y_changed: bool = not is_equal_approx(old_position.y, p_node.global_position.y) if not y_changed and not translate_without_reposition: new_offset.y = old_offset.y new_position = new_offset + Vector3(0, new_h, 0) - + # Make sure that when the user undo's the translation, the offset change gets undone too! _undo_redo.create_action("Translate", UndoRedo.MERGE_ALL) - _undo_redo.add_do_method(self, &"_set_offset_and_position", p_node.get_instance_id(), new_offset, new_position) - _undo_redo.add_undo_method(self, &"_set_offset_and_position", p_node.get_instance_id(), old_offset, old_position) + _undo_redo.add_do_method( + self, &"_set_offset_and_position", p_node.get_instance_id(), new_offset, new_position + ) + _undo_redo.add_undo_method( + self, &"_set_offset_and_position", p_node.get_instance_id(), old_offset, old_position + ) _undo_redo.commit_action() @@ -152,7 +163,7 @@ func _set_offset_and_position(p_id: int, p_offset: Vector3, p_position: Vector3) var node := instance_from_id(p_id) as Node if not is_instance_valid(node): return - + _ignore_transform_change = true node.global_position = p_position _offsets[p_id] = p_offset @@ -171,7 +182,7 @@ func _update_child_offset(p_node: Node3D) -> void: func _update_child_position(p_node: Node3D) -> void: if not _offsets.has(p_node.get_instance_id()): return - + var position: Vector3 = global_transform * p_node.position var h: float = _get_terrain_height(position) var offset: Vector3 = _offsets[p_node.get_instance_id()] @@ -184,7 +195,7 @@ func _on_maps_edited(p_edited_aabb: AABB) -> void: var edited_area: AABB = p_edited_aabb.grow(1) edited_area.position.y = -INF edited_area.end.y = INF - + for child in get_children(): var node := child as Node3D if node && edited_area.has_point(node.global_position): diff --git a/demo/src/CameraManager.gd b/demo/src/CameraManager.gd index 9c40488..2a70b11 100644 --- a/demo/src/CameraManager.gd +++ b/demo/src/CameraManager.gd @@ -22,8 +22,8 @@ func _input(p_event: InputEvent) -> void: return -func rotate_camera(p_relative:Vector2) -> void: +func rotate_camera(p_relative: Vector2) -> void: _camera_yaw.rotation.y -= p_relative.x * mouse_sensitivity _camera_yaw.orthonormalize() - _camera_pitch.rotation.x += p_relative.y * mouse_sensitivity * CAMERA_RATIO * mouse_y_inversion + _camera_pitch.rotation.x += p_relative.y * mouse_sensitivity * CAMERA_RATIO * mouse_y_inversion _camera_pitch.rotation.x = clamp(_camera_pitch.rotation.x, CAMERA_MIN_PITCH, CAMERA_MAX_PITCH) diff --git a/demo/src/CaveEntrance.gd b/demo/src/CaveEntrance.gd index 70e7948..bcf7c77 100644 --- a/demo/src/CaveEntrance.gd +++ b/demo/src/CaveEntrance.gd @@ -12,7 +12,7 @@ func _on_body_entered(body: Node3D) -> void: if env: var tween: Tween = get_tree().create_tween() tween.tween_property(env.environment, "ambient_light_energy", .1, .33) - + func _on_body_exited(body: Node3D) -> void: if body.name == "Player": diff --git a/demo/src/CodeGenerated.gd b/demo/src/CodeGenerated.gd index a91652d..fc1545e 100644 --- a/demo/src/CodeGenerated.gd +++ b/demo/src/CodeGenerated.gd @@ -5,7 +5,7 @@ var terrain: Terrain3D func _ready() -> void: $UI.player = $Player - + if has_node("RunThisSceneLabel3D"): $RunThisSceneLabel3D.queue_free() @@ -20,20 +20,22 @@ func _ready() -> void: func create_terrain() -> Terrain3D: # Create textures var green_gr := Gradient.new() - green_gr.set_color(0, Color.from_hsv(100./360., .35, .3)) - green_gr.set_color(1, Color.from_hsv(120./360., .4, .37)) + green_gr.set_color(0, Color.from_hsv(100. / 360., .35, .3)) + green_gr.set_color(1, Color.from_hsv(120. / 360., .4, .37)) var green_ta: Terrain3DTextureAsset = await create_texture_asset("Grass", green_gr, 1024) green_ta.uv_scale = 0.1 green_ta.detiling_rotation = 0.1 var brown_gr := Gradient.new() - brown_gr.set_color(0, Color.from_hsv(30./360., .4, .3)) - brown_gr.set_color(1, Color.from_hsv(30./360., .4, .4)) + brown_gr.set_color(0, Color.from_hsv(30. / 360., .4, .3)) + brown_gr.set_color(1, Color.from_hsv(30. / 360., .4, .4)) var brown_ta: Terrain3DTextureAsset = await create_texture_asset("Dirt", brown_gr, 1024) brown_ta.uv_scale = 0.03 green_ta.detiling_rotation = 0.1 - - var grass_ma: Terrain3DMeshAsset = create_mesh_asset("Grass", Color.from_hsv(120./360., .4, .37)) + + var grass_ma: Terrain3DMeshAsset = create_mesh_asset( + "Grass", Color.from_hsv(120. / 360., .4, .37) + ) # Create a terrain var terrain := Terrain3D.new() @@ -77,11 +79,13 @@ func create_terrain() -> Terrain3D: return terrain -func create_texture_asset(asset_name: String, gradient: Gradient, texture_size: int = 512) -> Terrain3DTextureAsset: +func create_texture_asset( + asset_name: String, gradient: Gradient, texture_size: int = 512 +) -> Terrain3DTextureAsset: # Create noise map var fnl := FastNoiseLite.new() fnl.frequency = 0.004 - + # Create albedo noise texture var alb_noise_tex := NoiseTexture2D.new() alb_noise_tex.width = texture_size @@ -96,7 +100,7 @@ func create_texture_asset(asset_name: String, gradient: Gradient, texture_size: for x in alb_noise_img.get_width(): for y in alb_noise_img.get_height(): var clr: Color = alb_noise_img.get_pixel(x, y) - clr.a = clr.v # Noise as height + clr.a = clr.v # Noise as height alb_noise_img.set_pixel(x, y, clr) alb_noise_img.generate_mipmaps() var albedo := ImageTexture.create_from_image(alb_noise_img) @@ -113,7 +117,7 @@ func create_texture_asset(asset_name: String, gradient: Gradient, texture_size: for x in nrm_noise_img.get_width(): for y in nrm_noise_img.get_height(): var normal_rgh: Color = nrm_noise_img.get_pixel(x, y) - normal_rgh.a = 0.8 # Roughness + normal_rgh.a = 0.8 # Roughness nrm_noise_img.set_pixel(x, y, normal_rgh) nrm_noise_img.generate_mipmaps() var normal := ImageTexture.create_from_image(nrm_noise_img) diff --git a/demo/src/DemoScene.gd b/demo/src/DemoScene.gd index 979be71..b6972aa 100644 --- a/demo/src/DemoScene.gd +++ b/demo/src/DemoScene.gd @@ -9,15 +9,16 @@ func _ready(): $UI.player = $Player # Load Sky3D into the demo environment if enabled - if Engine.is_editor_hint() and has_node("Environment") and \ - Engine.get_singleton(&"EditorInterface").is_plugin_enabled("sky_3d"): - $Environment.queue_free() - var sky3d = load("res://addons/sky_3d/src/Sky3D.gd").new() - sky3d.name = "Sky3D" - add_child(sky3d, true) - move_child(sky3d, 1) - sky3d.owner = self - sky3d.current_time = 10 - sky3d.enable_editor_time = false - - + if ( + Engine.is_editor_hint() + and has_node("Environment") + and Engine.get_singleton(&"EditorInterface").is_plugin_enabled("sky_3d") + ): + $Environment.queue_free() + var sky3d = load("res://addons/sky_3d/src/Sky3D.gd").new() + sky3d.name = "Sky3D" + add_child(sky3d, true) + move_child(sky3d, 1) + sky3d.owner = self + sky3d.current_time = 10 + sky3d.enable_editor_time = false diff --git a/demo/src/Enemy.gd b/demo/src/Enemy.gd index c8d444c..7eb500c 100644 --- a/demo/src/Enemy.gd +++ b/demo/src/Enemy.gd @@ -23,7 +23,9 @@ func _process(p_delta: float) -> void: func is_on_nav_mesh() -> bool: - var closest_point := NavigationServer3D.map_get_closest_point(nav_agent.get_navigation_map(), global_position) + var closest_point := NavigationServer3D.map_get_closest_point( + nav_agent.get_navigation_map(), global_position + ) return global_position.distance_squared_to(closest_point) < nav_agent.path_max_distance ** 2 @@ -37,9 +39,9 @@ func _physics_process(p_delta: float) -> void: var velocity_xz := (next_path_position - current_agent_position).normalized() * MOVE_SPEED velocity.x = velocity_xz.x velocity.z = velocity_xz.z - + velocity.y -= 40 * p_delta - + if nav_agent.avoidance_enabled: nav_agent.set_velocity(velocity) else: diff --git a/demo/src/Player.gd b/demo/src/Player.gd index 8ae1b84..14d4c6e 100644 --- a/demo/src/Player.gd +++ b/demo/src/Player.gd @@ -2,7 +2,7 @@ extends CharacterBody3D @export var MOVE_SPEED: float = 50.0 @export var JUMP_SPEED: float = 2.0 -@export var first_person: bool = false : +@export var first_person: bool = false: set(p_value): first_person = p_value if first_person: @@ -13,17 +13,17 @@ extends CharacterBody3D $Body.visible = true create_tween().tween_property($CameraManager/Arm, "spring_length", 6.0, .33) -@export var gravity_enabled: bool = true : +@export var gravity_enabled: bool = true: set(p_value): gravity_enabled = p_value if not gravity_enabled: velocity.y = 0 - -@export var collision_enabled: bool = true : + +@export var collision_enabled: bool = true: set(p_value): collision_enabled = p_value - $CollisionShapeBody.disabled = ! collision_enabled - $CollisionShapeRay.disabled = ! collision_enabled + $CollisionShapeBody.disabled = !collision_enabled + $CollisionShapeRay.disabled = !collision_enabled func _physics_process(p_delta) -> void: @@ -41,18 +41,18 @@ func _physics_process(p_delta) -> void: # Returns the input vector relative to the camera. Forward is always the direction the camera is facing func get_camera_relative_input() -> Vector3: var input_dir: Vector3 = Vector3.ZERO - if Input.is_key_pressed(KEY_A): # Left + if Input.is_key_pressed(KEY_A): # Left input_dir -= %Camera3D.global_transform.basis.x - if Input.is_key_pressed(KEY_D): # Right + if Input.is_key_pressed(KEY_D): # Right input_dir += %Camera3D.global_transform.basis.x - if Input.is_key_pressed(KEY_W): # Forward + if Input.is_key_pressed(KEY_W): # Forward input_dir -= %Camera3D.global_transform.basis.z - if Input.is_key_pressed(KEY_S): # Backward + if Input.is_key_pressed(KEY_S): # Backward input_dir += %Camera3D.global_transform.basis.z - if Input.is_key_pressed(KEY_E) or Input.is_key_pressed(KEY_SPACE): # Up - velocity.y += JUMP_SPEED + MOVE_SPEED*.016 - if Input.is_key_pressed(KEY_Q): # Down - velocity.y -= JUMP_SPEED + MOVE_SPEED*.016 + if Input.is_key_pressed(KEY_E) or Input.is_key_pressed(KEY_SPACE): # Up + velocity.y += JUMP_SPEED + MOVE_SPEED * .016 + if Input.is_key_pressed(KEY_Q): # Down + velocity.y -= JUMP_SPEED + MOVE_SPEED * .016 if Input.is_key_pressed(KEY_KP_ADD) or Input.is_key_pressed(KEY_EQUAL): MOVE_SPEED = clamp(MOVE_SPEED + .5, 5, 9999) if Input.is_key_pressed(KEY_KP_SUBTRACT) or Input.is_key_pressed(KEY_MINUS): @@ -66,16 +66,16 @@ func _input(p_event: InputEvent) -> void: MOVE_SPEED = clamp(MOVE_SPEED + 5, 5, 9999) elif p_event.button_index == MOUSE_BUTTON_WHEEL_DOWN: MOVE_SPEED = clamp(MOVE_SPEED - 5, 5, 9999) - + elif p_event is InputEventKey: if p_event.pressed: if p_event.keycode == KEY_V: - first_person = ! first_person + first_person = !first_person elif p_event.keycode == KEY_G: - gravity_enabled = ! gravity_enabled + gravity_enabled = !gravity_enabled elif p_event.keycode == KEY_C: - collision_enabled = ! collision_enabled + collision_enabled = !collision_enabled # Else if up/down released - elif p_event.keycode in [ KEY_Q, KEY_E, KEY_SPACE ]: + elif p_event.keycode in [KEY_Q, KEY_E, KEY_SPACE]: velocity.y = 0 diff --git a/demo/src/RuntimeNavigationBaker.gd b/demo/src/RuntimeNavigationBaker.gd index be3629d..9f2a858 100644 --- a/demo/src/RuntimeNavigationBaker.gd +++ b/demo/src/RuntimeNavigationBaker.gd @@ -2,11 +2,16 @@ extends Node signal bake_finished -@export var enabled: bool = true : set = set_enabled -@export var enter_cost: float = 0.0 : set = set_enter_cost -@export var travel_cost: float = 1.0 : set = set_travel_cost -@export_flags_3d_navigation var navigation_layers: int = 1 : set = set_navigation_layers -@export var template: NavigationMesh : set = set_template +@export var enabled: bool = true: + set = set_enabled +@export var enter_cost: float = 0.0: + set = set_enter_cost +@export var travel_cost: float = 1.0: + set = set_travel_cost +@export_flags_3d_navigation var navigation_layers: int = 1: + set = set_navigation_layers +@export var template: NavigationMesh: + set = set_template @export var terrain: Terrain3D @export var player: Node3D @export var mesh_size := Vector3(256, 512, 256) @@ -16,7 +21,7 @@ signal bake_finished @export var log_timing: bool = false var _scene_geometry: NavigationMeshSourceGeometryData3D -var _current_center := Vector3(INF,INF,INF) +var _current_center := Vector3(INF, INF, INF) var _bake_task_id: int = -1 var _bake_task_timer: float = 0.0 @@ -30,16 +35,16 @@ func _ready(): _nav_region.enabled = enabled _nav_region.enter_cost = enter_cost _nav_region.travel_cost = travel_cost - + # Enabling edge connections comes with a performance penalty that causes hitches whenever # the nav mesh is updated. The navigation server has to compare each edge, and it does this on # the main thread. _nav_region.use_edge_connections = false - + add_child(_nav_region) - + _update_map_cell_size() - + # If you're using ProtonScatter, you will want to delay this next call until after all # your scatter nodes have finished setting up. Here, we just defer one frame so that nodes # after this one in the tree get set up first @@ -92,19 +97,19 @@ func _update_map_cell_size() -> void: func _process(p_delta: float) -> void: if _bake_task_id != -1: _bake_task_timer += p_delta - + if not player or _bake_task_id != -1: return - + if _bake_cooldown_timer > 0.0: _bake_cooldown_timer -= p_delta return - + var track_pos := player.global_position if player is CharacterBody3D: # Center on where the player is likely _going to be_: track_pos += player.velocity * bake_cooldown - + if track_pos.distance_squared_to(_current_center) >= min_rebake_distance * min_rebake_distance: _current_center = track_pos _rebake(_current_center) @@ -112,7 +117,9 @@ func _process(p_delta: float) -> void: func _rebake(p_center: Vector3) -> void: assert(template != null) - _bake_task_id = WorkerThreadPool.add_task(_task_bake.bind(p_center), false, "RuntimeNavigationBaker") + _bake_task_id = WorkerThreadPool.add_task( + _task_bake.bind(p_center), false, "RuntimeNavigationBaker" + ) _bake_task_timer = 0.0 _bake_cooldown_timer = bake_cooldown @@ -123,13 +130,13 @@ func _task_bake(p_center: Vector3) -> void: nav_mesh.filter_baking_aabb_offset = p_center var source_geometry: NavigationMeshSourceGeometryData3D source_geometry = _scene_geometry.duplicate() - + if terrain: var aabb: AABB = nav_mesh.filter_baking_aabb aabb.position += nav_mesh.filter_baking_aabb_offset var faces: PackedVector3Array = terrain.generate_nav_mesh_source_geometry(aabb, false) source_geometry.add_faces(faces, Transform3D.IDENTITY) - + if source_geometry.has_data(): NavigationServer3D.bake_from_source_geometry_data(nav_mesh, source_geometry) _bake_finished.call_deferred(nav_mesh) @@ -140,12 +147,12 @@ func _task_bake(p_center: Vector3) -> void: func _bake_finished(p_nav_mesh: NavigationMesh) -> void: if log_timing: print("Navigation bake took ", _bake_task_timer, "s") - + _bake_task_timer = 0.0 _bake_task_id = -1 - + if p_nav_mesh: _nav_region.navigation_mesh = p_nav_mesh - + bake_finished.emit() assert(!NavigationServer3D.region_get_use_edge_connections(_nav_region.get_region_rid())) diff --git a/demo/src/UI.gd b/demo/src/UI.gd index 970a1ac..2d9ea26 100644 --- a/demo/src/UI.gd +++ b/demo/src/UI.gd @@ -1,6 +1,5 @@ extends Control - var player: Node var visible_mode: int = 1 @@ -11,7 +10,7 @@ func _init() -> void: func _process(p_delta) -> void: $Label.text = "FPS: %d\n" % Engine.get_frames_per_second() - if(visible_mode == 1): + if visible_mode == 1: $Label.text += "Move Speed: %.1f\n" % player.MOVE_SPEED if player else "" $Label.text += "Position: %.1v\n" % player.global_position if player else "" $Label.text += """ @@ -37,12 +36,12 @@ func _unhandled_key_input(p_event: InputEvent) -> void: KEY_F8: get_tree().quit() KEY_F9: - visible_mode = (visible_mode + 1 ) % 3 + visible_mode = (visible_mode + 1) % 3 $Label/Panel.visible = (visible_mode == 1) visible = visible_mode > 0 KEY_F10: var vp = get_viewport() - vp.debug_draw = (vp.debug_draw + 1 ) % 6 + vp.debug_draw = (vp.debug_draw + 1) % 6 get_viewport().set_input_as_handled() KEY_F11: toggle_fullscreen() @@ -53,11 +52,13 @@ func _unhandled_key_input(p_event: InputEvent) -> void: else: Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE) get_viewport().set_input_as_handled() - - + + func toggle_fullscreen() -> void: - if DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN or \ - DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN: + if ( + DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_EXCLUSIVE_FULLSCREEN + or DisplayServer.window_get_mode() == DisplayServer.WINDOW_MODE_FULLSCREEN + ): DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED) DisplayServer.window_set_size(Vector2(1280, 720)) else: diff --git a/player/camera_rig.gd b/player/camera_rig.gd index 2e2105e..48ee789 100644 --- a/player/camera_rig.gd +++ b/player/camera_rig.gd @@ -61,7 +61,9 @@ func _physics_process(delta: float) -> void: desired_focus_position += cam_right * (diff_right - sign(diff_right) * deadzone_right) if abs(diff_forward) > deadzone_forward: - desired_focus_position += cam_forward * (diff_forward - sign(diff_forward) * deadzone_forward) + desired_focus_position += ( + cam_forward * (diff_forward - sign(diff_forward) * deadzone_forward) + ) focus_position = focus_position.lerp(desired_focus_position, focus_t) diff --git a/player/npc/NpcVisual.gd b/player/npc/NpcVisual.gd index 74ddd96..7b622e5 100644 --- a/player/npc/NpcVisual.gd +++ b/player/npc/NpcVisual.gd @@ -30,25 +30,28 @@ var path_pending := false var is_dead_visual := false var dead_material := StandardMaterial3D.new() + func debug_log(message: String) -> void: if DEBUG_LOGS: print("[NPCVisual] ", name, " | ", message) + func _ready() -> void: dead_material.albedo_color = Color(0.25, 0.25, 0.25, 1.0) + func setup_from_sim(npc: SimNPC) -> void: sim_id = npc.id npc_name = npc.npc_name profession = npc.profession name = "NPC_%s_%s" % [sim_id, npc_name] - set_carried_food_visible( - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0 - ) + set_carried_food_visible(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) > 0.0) + func set_carried_food_visible(is_visible: bool) -> void: carried_food_visual.visible = is_visible and not is_dead_visual + func set_target_position(pos: Vector3) -> void: path_request_id += 1 var request_id := path_request_id @@ -72,10 +75,7 @@ func set_target_position(pos: Vector3) -> void: return current_path = NavigationServer3D.map_get_path( - get_world_3d().navigation_map, - global_position, - pos, - true + get_world_3d().navigation_map, global_position, pos, true ) path_pending = false @@ -87,6 +87,7 @@ func set_target_position(pos: Vector3) -> void: else: debug_log("New target: %s Path points: %s" % [pos, current_path.size()]) + func _physics_process(delta: float) -> void: if is_dead_visual: velocity = Vector3.ZERO @@ -146,7 +147,8 @@ func _physics_process(delta: float) -> void: var target_angle := atan2(direction.x, direction.z) rotation.y = lerp_angle(rotation.y, target_angle, rotation_speed * delta) - + + func _report_arrival_once() -> void: if has_reported_arrival: return @@ -158,6 +160,7 @@ func _report_arrival_once() -> void: debug_log("Arrived at target") arrived_at_target.emit(sim_id) + func _report_navigation_failure_once() -> void: if has_reported_arrival: return @@ -172,6 +175,7 @@ func _report_navigation_failure_once() -> void: debug_log("Navigation failed") navigation_failed.emit(sim_id) + func apply_dead_visual_state() -> void: is_dead_visual = true carried_food_visual.visible = false @@ -189,7 +193,7 @@ func apply_dead_visual_state() -> void: for child in get_children(): if child is MeshInstance3D: child.material_override = dead_material - + collision_layer = 0 collision_mask = 0 debug_log("Applied dead visual state") diff --git a/player/player.gd b/player/player.gd index b4ba4bd..213d0ab 100644 --- a/player/player.gd +++ b/player/player.gd @@ -14,13 +14,9 @@ extends CharacterBody3D @export var stomach_capacity_for_food := 5 @export var interaction_range := 3.0 + func _physics_process(delta: float) -> void: - var input := Input.get_vector( - "move_left", - "move_right", - "move_forward", - "move_backward" - ) + var input := Input.get_vector("move_left", "move_right", "move_forward", "move_backward") var forward := -camera_rig.global_transform.basis.z var right := camera_rig.global_transform.basis.x @@ -37,7 +33,7 @@ func _physics_process(delta: float) -> void: velocity.x = move_toward(velocity.x, target_velocity.x, acceleration * delta) velocity.z = move_toward(velocity.z, target_velocity.z, acceleration * delta) - + if not is_on_floor(): velocity.y -= 30.0 * delta else: @@ -54,8 +50,8 @@ func _unhandled_input(event: InputEvent) -> void: # keep your camera mouse code here too if event.is_action_pressed("interact"): try_interact() - - + + func try_interact() -> void: if simulation_manager == null: return @@ -73,13 +69,13 @@ func try_interact() -> void: simulation_manager.eat_food(stomach_capacity_for_food) print("Player ate food from the village supply.") + func try_harvest_resource_node() -> bool: if not simulation_manager.has_method("find_resource_node_for_player"): push_error("Player: SimulationManager cannot find ResourceNodes") return false var node: ResourceNode = simulation_manager.find_resource_node_for_player( - global_position, - interaction_range + global_position, interaction_range ) if node == null: return false @@ -102,9 +98,10 @@ func try_harvest_resource_node() -> bool: else: print("%s could not be harvested." % node.node_id) return true - + + func is_near(zone: Marker3D) -> bool: if zone == null: return false - + return global_position.distance_to(zone.global_position) <= interaction_range diff --git a/simulation/SimNPC.gd b/simulation/SimNPC.gd index 7624480..eeac751 100644 --- a/simulation/SimNPC.gd +++ b/simulation/SimNPC.gd @@ -33,6 +33,7 @@ var last_task: StringName var random_source: RandomNumberGenerator var debug_logs := true + func _init( _id: int, _npc_name: String, @@ -53,11 +54,10 @@ func _init( hunger = random_source.randf_range(20.0, 80.0) energy = random_source.randf_range(40.0, 100.0) position = Vector3( - random_source.randf_range(-8.0, 8.0), - 0.0, - random_source.randf_range(-8.0, 8.0) + random_source.randf_range(-8.0, 8.0), 0.0, random_source.randf_range(-8.0, 8.0) ) + func die_from_starvation() -> void: is_dead = true current_task = SimulationIds.ACTION_DEAD @@ -69,35 +69,35 @@ func die_from_starvation() -> void: if debug_logs: print("[SimNPC] ", npc_name, " died from starvation.") - + + func get_inventory_amount(item_id: StringName) -> float: return float(inventory.get(String(item_id), 0.0)) + func add_inventory(item_id: StringName, amount: float) -> void: inventory[String(item_id)] = get_inventory_amount(item_id) + maxf(amount, 0.0) + func remove_inventory(item_id: StringName, requested_amount: float) -> float: - var removed := minf( - maxf(requested_amount, 0.0), - get_inventory_amount(item_id) - ) + var removed := minf(maxf(requested_amount, 0.0), get_inventory_amount(item_id)) inventory[String(item_id)] = get_inventory_amount(item_id) - removed return removed + func set_task(action_id: StringName, duration: float = -1.0) -> void: var definition := SimulationDefinitions.get_action(action_id) if definition == null: push_error("SimNPC: unknown action_id '%s'" % action_id) return current_task = action_id - task_duration = ( - duration if duration >= 0.0 else definition.default_duration - ) + task_duration = (duration if duration >= 0.0 else definition.default_duration) task_progress = 0.0 task_complete = false task_state = TASK_STATE_TRAVELING has_travel_target = false + func start_working() -> void: if task_state != TASK_STATE_TRAVELING: return diff --git a/simulation/SimVillage.gd b/simulation/SimVillage.gd index a516f6f..6bb0d23 100644 --- a/simulation/SimVillage.gd +++ b/simulation/SimVillage.gd @@ -17,6 +17,7 @@ var wood_priority := 1.0 var safety_priority := 1.0 var knowledge_priority := 1.0 + func update_modifiers() -> void: food_modifier = 1.0 wood_modifier = 1.0 @@ -30,15 +31,15 @@ func update_modifiers() -> void: safety_modifier = 2.0 if knowledge > 25: knowledge_modifier = 0.75 - - debug_log("FoodMod=%s SafetyMod=%s KnowledgeMod=%s" % - [ - food_modifier, - safety_modifier, - knowledge_modifier - ] + + debug_log( + ( + "FoodMod=%s SafetyMod=%s KnowledgeMod=%s" + % [food_modifier, safety_modifier, knowledge_modifier] + ) ) - + + func update_priorities() -> void: food_priority = 1.0 wood_priority = 1.0 @@ -68,6 +69,7 @@ func update_priorities() -> void: if debug_logs: print(get_priority_summary()) + func apply_resource_delta(resource_id: StringName, amount: float) -> void: match resource_id: &"food": @@ -87,6 +89,7 @@ func apply_resource_delta(resource_id: StringName, amount: float) -> void: update_modifiers() update_priorities() + func apply_npc_task(npc: SimNPC) -> void: if npc.is_dead: return @@ -123,27 +126,29 @@ func apply_npc_task(npc: SimNPC) -> void: update_modifiers() update_priorities() -func get_summary() -> String: - return "Village | food: %s | wood: %s | safety: %s | knowledge: %s | priorities F/W/S/K: %s/%s/%s/%s" % [ - round(food), - round(wood), - round(safety), - round(knowledge), - food_priority, - wood_priority, - safety_priority, - knowledge_priority - ] +func get_summary() -> String: + return ( + "Village | food: %s | wood: %s | safety: %s | knowledge: %s | priorities F/W/S/K: %s/%s/%s/%s" + % [ + round(food), + round(wood), + round(safety), + round(knowledge), + food_priority, + wood_priority, + safety_priority, + knowledge_priority + ] + ) func get_priority_summary() -> String: - return "Priorities | food: %s | wood: %s | safety: %s | knowledge: %s" % [ - food_priority, - wood_priority, - safety_priority, - knowledge_priority - ] + return ( + "Priorities | food: %s | wood: %s | safety: %s | knowledge: %s" + % [food_priority, wood_priority, safety_priority, knowledge_priority] + ) + func debug_log(message: String) -> void: if debug_logs: diff --git a/simulation/SimulationClock.gd b/simulation/SimulationClock.gd index 4b131f7..46913e6 100644 --- a/simulation/SimulationClock.gd +++ b/simulation/SimulationClock.gd @@ -5,9 +5,11 @@ var tick_interval: float var accumulator := 0.0 var elapsed_ticks := 0 + func _init(interval: float = 1.0) -> void: tick_interval = maxf(interval, 0.0001) + func advance(delta: float) -> int: accumulator += maxf(delta, 0.0) var ticks_due := 0 @@ -17,6 +19,7 @@ func advance(delta: float) -> int: ticks_due += 1 return ticks_due + func reset() -> void: accumulator = 0.0 elapsed_ticks = 0 diff --git a/simulation/SimulationManager.gd b/simulation/SimulationManager.gd index 3251226..c10ecc5 100644 --- a/simulation/SimulationManager.gd +++ b/simulation/SimulationManager.gd @@ -1,10 +1,6 @@ extends Node -signal npc_task_changed( - npc: SimNPC, - old_task: StringName, - new_task: StringName -) +signal npc_task_changed(npc: SimNPC, old_task: StringName, new_task: StringName) signal village_changed(village: SimVillage) signal npc_died(npc: SimNPC) signal npc_target_requested(npc: SimNPC) @@ -32,9 +28,8 @@ var action_selector := ActionSelectionSystem.new() var action_executor := ActionExecutionSystem.new() var target_resolver := ActionTargetResolver.new() -var names := [ - "Amina", "Tarik", "Jasmin" -] +var names := ["Amina", "Tarik", "Jasmin"] + func _ready() -> void: add_to_group("simulation_manager") @@ -53,20 +48,19 @@ func _ready() -> void: call_deferred("register_loaded_resource_nodes") if debug_logs: print("--- Simulation started ---") - + + func _process(delta: float) -> void: var ticks_due := clock.advance(delta) for tick in ticks_due: simulate_tick() + func generate_npcs() -> void: var profession_ids := SimulationDefinitions.get_profession_ids() for i in range(names.size()): var npc_random := _create_random_source(i, 0) - var profession_index := npc_random.randi_range( - 0, - profession_ids.size() - 1 - ) + var profession_index := npc_random.randi_range(0, profession_ids.size() - 1) var npc := SimNPC.new( i, names[i], @@ -80,21 +74,20 @@ func generate_npcs() -> void: npcs.append(npc) wander_random_sources[i] = _create_random_source(i, 1) + func _create_random_source(npc_id: int, stream_id: int) -> RandomNumberGenerator: var source := RandomNumberGenerator.new() source.seed = simulation_seed + (npc_id + 1) * 1000003 + stream_id * 7919 return source + func get_wander_offset(npc_id: int) -> Vector3: var source: RandomNumberGenerator = wander_random_sources.get(npc_id) if source == null: source = _create_random_source(npc_id, 1) wander_random_sources[npc_id] = source - return Vector3( - source.randf_range(-6.0, 6.0), - 0.0, - source.randf_range(-6.0, 6.0) - ) + return Vector3(source.randf_range(-6.0, 6.0), 0.0, source.randf_range(-6.0, 6.0)) + func simulate_tick() -> void: tick_count += 1 @@ -113,21 +106,12 @@ func simulate_tick() -> void: action_executor.advance_npc(npc, village) if ( not npc.is_dead - and old_state in [ - SimNPC.TASK_STATE_IDLE, - SimNPC.TASK_STATE_COMPLETE - ] - and npc.task_state in [ - SimNPC.TASK_STATE_IDLE, - SimNPC.TASK_STATE_COMPLETE - ] + and old_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE] + and npc.task_state in [SimNPC.TASK_STATE_IDLE, SimNPC.TASK_STATE_COMPLETE] ): var selection := action_selector.select_action(npc, village) if selection != null: - npc.set_task( - selection.action_id, - selection.duration_override - ) + npc.set_task(selection.action_id, selection.duration_override) if old_task != npc.current_task and old_target != &"": release_npc_reservation(npc.id) @@ -147,45 +131,36 @@ func simulate_tick() -> void: if not was_complete and npc.task_complete and not npc.is_dead: var completed_task := npc.current_task - var completed_definition := SimulationDefinitions.get_action( - completed_task - ) + var completed_definition := SimulationDefinitions.get_action(completed_task) if ( npc.target_id != &"" and completed_definition != null and completed_definition.target_type == SimulationIds.TARGET_RESOURCE ): var resource_state := get_resource_state(npc.target_id) - if ( - resource_state != null - and resource_state.get_reserved_by() == npc.id - ): + if resource_state != null and resource_state.get_reserved_by() == npc.id: var extracted := resource_state.extract() if extracted > 0: if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD: - npc.add_inventory( - SimulationIds.RESOURCE_FOOD, - extracted - ) + npc.add_inventory(SimulationIds.RESOURCE_FOOD, extracted) npc_inventory_changed.emit( npc, SimulationIds.RESOURCE_FOOD, - npc.get_inventory_amount( - SimulationIds.RESOURCE_FOOD - ) + npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) ) else: village.apply_resource_delta( - resource_state.get_resource_id(), - extracted + resource_state.get_resource_id(), extracted ) _record_economic_event( SimulationIds.EVENT_RESOURCE_EXTRACTED, npc.id, resource_state.get_node_id(), - _npc_inventory_id(npc.id) - if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD - else &"village", + ( + _npc_inventory_id(npc.id) + if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD + else &"village" + ), resource_state.get_resource_id(), extracted ) @@ -201,7 +176,13 @@ func simulate_tick() -> void: resource_state.get_node_id() ) elif debug_logs: - print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": target reservation was invalid") + print( + "[SimulationManager] ", + npc.npc_name, + " could not complete ", + completed_task, + ": target reservation was invalid" + ) release_npc_reservation(npc.id) elif completed_task == SimulationIds.ACTION_DEPOSIT_FOOD: deposit_npc_inventory(npc, SimulationIds.RESOURCE_FOOD) @@ -209,13 +190,19 @@ func simulate_tick() -> void: withdraw_to_npc(npc, SimulationIds.RESOURCE_FOOD, 1.0) elif completed_task == SimulationIds.ACTION_EAT: consume_npc_food(npc) - elif completed_task not in [ - SimulationIds.ACTION_GATHER_FOOD, - SimulationIds.ACTION_GATHER_WOOD - ]: + elif ( + completed_task + not in [SimulationIds.ACTION_GATHER_FOOD, SimulationIds.ACTION_GATHER_WOOD] + ): village.apply_npc_task(npc) elif debug_logs: - print("[SimulationManager] ", npc.npc_name, " could not complete ", completed_task, ": no resource target") + print( + "[SimulationManager] ", + npc.npc_name, + " could not complete ", + completed_task, + ": no resource target" + ) village_was_changed = true @@ -254,20 +241,29 @@ func simulate_tick() -> void: ) if old_state != npc.task_state: - print("[SimulationManager] State changed: ", npc.npc_name, " ", old_state, " -> ", npc.task_state) + print( + "[SimulationManager] State changed: ", + npc.npc_name, + " ", + old_state, + " -> ", + npc.task_state + ) if village_was_changed: village_changed.emit(village) if debug_logs: print(village.get_summary()) - + + func set_npc_target_id(npc_id: int, target_id: StringName) -> void: for npc in npcs: if npc.id == npc_id: npc.target_id = target_id return + func release_npc_reservation(npc_id: int) -> void: for npc in npcs: if npc.id == npc_id: @@ -278,6 +274,7 @@ func release_npc_reservation(npc_id: int) -> void: npc.target_id = &"" return + func notify_npc_arrived(npc_id: int) -> void: for npc in npcs: if npc.id == npc_id: @@ -285,9 +282,7 @@ func notify_npc_arrived(npc_id: int) -> void: return if npc.task_state == SimNPC.TASK_STATE_TRAVELING: - var definition := SimulationDefinitions.get_action( - npc.current_task - ) + var definition := SimulationDefinitions.get_action(npc.current_task) if ( npc.target_id != &"" and definition != null @@ -300,7 +295,13 @@ func notify_npc_arrived(npc_id: int) -> void: or resource_state.get_reserved_by() != npc.id ): if debug_logs: - print("[SimulationManager] ", npc.npc_name, " arrived but target ", npc.target_id, " is unavailable, replanning") + print( + "[SimulationManager] ", + npc.npc_name, + " arrived but target ", + npc.target_id, + " is unavailable, replanning" + ) notify_npc_navigation_failed(npc.id) return @@ -308,9 +309,15 @@ func notify_npc_arrived(npc_id: int) -> void: npc.start_working() if debug_logs: - print("[SimulationManager] ", npc.npc_name, " arrived and started working on: ", npc.current_task) + print( + "[SimulationManager] ", + npc.npc_name, + " arrived and started working on: ", + npc.current_task + ) return + func notify_npc_navigation_failed(npc_id: int) -> void: for npc in npcs: if npc.id != npc_id: @@ -326,12 +333,20 @@ func notify_npc_navigation_failed(npc_id: int) -> void: npc_target_requested.emit(npc) if debug_logs: - print("[SimulationManager] ", npc.npc_name, " could not navigate for ", failed_task, " and is trying a wander target") + print( + "[SimulationManager] ", + npc.npc_name, + " could not navigate for ", + failed_task, + " and is trying a wander target" + ) return + func notify_npc_target_unavailable(npc_id: int) -> void: notify_npc_navigation_failed(npc_id) + func resolve_npc_target(npc_id: int, origin: Vector3) -> bool: if active_world_adapter == null: push_error("SimulationManager: active_world_adapter is missing") @@ -341,12 +356,7 @@ func resolve_npc_target(npc_id: int, origin: Vector3) -> bool: continue if npc.is_dead or npc.task_state != SimNPC.TASK_STATE_TRAVELING: return false - var result := target_resolver.resolve( - npc, - origin, - self, - active_world_adapter - ) + var result := target_resolver.resolve(npc, origin, self, active_world_adapter) if result.is_empty(): notify_npc_target_unavailable(npc.id) return false @@ -357,6 +367,7 @@ func resolve_npc_target(npc_id: int, origin: Vector3) -> bool: return true return false + func request_current_travel(npc_id: int) -> bool: for npc in npcs: if npc.id != npc_id: @@ -370,6 +381,7 @@ func request_current_travel(npc_id: int) -> bool: return true return false + func synchronize_npc_position(npc_id: int, active_position: Vector3) -> bool: for npc in npcs: if npc.id == npc_id: @@ -377,9 +389,11 @@ func synchronize_npc_position(npc_id: int, active_position: Vector3) -> bool: return true return false + func _npc_inventory_id(npc_id: int) -> StringName: return StringName("npc_%d_inventory" % npc_id) + func _record_economic_event( event_type: StringName, actor_id: int, @@ -391,35 +405,26 @@ func _record_economic_event( if amount <= 0.0: return var event := EconomicEventRecord.create( - next_event_id, - event_type, - tick_count, - actor_id, - source_id, - destination_id, - item_id, - amount + next_event_id, event_type, tick_count, actor_id, source_id, destination_id, item_id, amount ) next_event_id += 1 economic_events.append(event) economic_event_recorded.emit(event) + func _initialize_storage() -> void: if storage_states.has(SimulationIds.STORAGE_VILLAGE_PANTRY): _sync_village_food() return - storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = ( - StorageStateRecord.create( - SimulationIds.STORAGE_VILLAGE_PANTRY, - {String(SimulationIds.RESOURCE_FOOD): village.food} - ) - ) + storage_states[SimulationIds.STORAGE_VILLAGE_PANTRY] = (StorageStateRecord.create( + SimulationIds.STORAGE_VILLAGE_PANTRY, {String(SimulationIds.RESOURCE_FOOD): village.food} + )) _sync_village_food() + func get_pantry() -> StorageStateRecord: - return storage_states.get( - SimulationIds.STORAGE_VILLAGE_PANTRY - ) as StorageStateRecord + return storage_states.get(SimulationIds.STORAGE_VILLAGE_PANTRY) as StorageStateRecord + func _sync_village_food() -> void: var pantry := get_pantry() @@ -428,6 +433,7 @@ func _sync_village_food() -> void: village.update_modifiers() village.update_priorities() + func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float: var available := npc.get_inventory_amount(item_id) var deposited := get_pantry().deposit(item_id, available) @@ -445,11 +451,8 @@ func deposit_npc_inventory(npc: SimNPC, item_id: StringName) -> float: ) return deposited -func withdraw_to_npc( - npc: SimNPC, - item_id: StringName, - amount: float -) -> float: + +func withdraw_to_npc(npc: SimNPC, item_id: StringName, amount: float) -> float: var withdrawn := get_pantry().withdraw(item_id, amount) npc.add_inventory(item_id, withdrawn) _sync_village_food() @@ -465,6 +468,7 @@ func withdraw_to_npc( ) return withdrawn + func consume_npc_food(npc: SimNPC) -> bool: if npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) < 1.0: npc.hunger = minf(npc.hunger + 5.0, 100.0) @@ -474,9 +478,7 @@ func consume_npc_food(npc: SimNPC) -> bool: npc.starvation_ticks = 0 npc.is_starving = npc.hunger >= 90.0 npc_inventory_changed.emit( - npc, - SimulationIds.RESOURCE_FOOD, - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) + npc, SimulationIds.RESOURCE_FOOD, npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) ) _record_economic_event( SimulationIds.EVENT_ITEM_CONSUMED, @@ -488,10 +490,12 @@ func consume_npc_food(npc: SimNPC) -> bool: ) return true + func register_loaded_resource_nodes() -> void: for node in ResourceNode.get_all(): register_resource_node(node) + func register_resource_node(node: ResourceNode) -> bool: if node == null or node.node_id.is_empty(): return false @@ -502,8 +506,10 @@ func register_resource_node(node: ResourceNode) -> bool: or action_definition.resource_action_id != node.action_id ): push_error( - "SimulationManager: ResourceNode '%s' has invalid action_id '%s'" - % [node.node_id, node.action_id] + ( + "SimulationManager: ResourceNode '%s' has invalid action_id '%s'" + % [node.node_id, node.action_id] + ) ) return false var resource_state := get_resource_state(node.node_id) @@ -511,46 +517,40 @@ func register_resource_node(node: ResourceNode) -> bool: resource_state = ResourceStateRecord.create_from_node(node) resource_states[node.node_id] = resource_state elif not resource_state.apply_definition(node): - push_error( - "SimulationManager: ResourceNode definition mismatch for '%s'" - % node.node_id - ) + push_error("SimulationManager: ResourceNode definition mismatch for '%s'" % node.node_id) return false return node.bind_state(resource_state) + func get_resource_state(node_id: StringName) -> ResourceStateRecord: return resource_states.get(node_id) as ResourceStateRecord + func reserve_resource(node_id: StringName, agent_id: int) -> bool: var resource_state := get_resource_state(node_id) return resource_state != null and resource_state.reserve(agent_id) + func release_resource(node_id: StringName, agent_id: int) -> void: var resource_state := get_resource_state(node_id) if resource_state != null: resource_state.release(agent_id) -func find_resource_node_for_player( - from_position: Vector3, - max_distance: float -) -> ResourceNode: + +func find_resource_node_for_player(from_position: Vector3, max_distance: float) -> ResourceNode: var best: ResourceNode var best_distance := max_distance * max_distance for node in ResourceNode.get_all(): var resource_state := get_resource_state(node.node_id) - if ( - resource_state == null - or not resource_state.can_player_use_resource() - ): + if resource_state == null or not resource_state.can_player_use_resource(): continue - var distance := from_position.distance_squared_to( - node.interaction_point.global_position - ) + var distance := from_position.distance_squared_to(node.interaction_point.global_position) if distance <= best_distance: best_distance = distance best = node return best + func harvest_resource_node(node: ResourceNode) -> float: if node == null: return 0.0 @@ -570,9 +570,11 @@ func harvest_resource_node(node: ResourceNode) -> float: SimulationIds.EVENT_RESOURCE_EXTRACTED, -1, resource_state.get_node_id(), - SimulationIds.STORAGE_VILLAGE_PANTRY - if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD - else &"village", + ( + SimulationIds.STORAGE_VILLAGE_PANTRY + if resource_state.get_resource_id() == SimulationIds.RESOURCE_FOOD + else &"village" + ), resource_state.get_resource_id(), extracted ) @@ -590,11 +592,13 @@ func harvest_resource_node(node: ResourceNode) -> float: return extracted + func eat_food(amount: float) -> void: get_pantry().withdraw(SimulationIds.RESOURCE_FOOD, amount) _sync_village_food() village_changed.emit(village) - + + func add_food(amount: float) -> void: if amount >= 0.0: get_pantry().deposit(SimulationIds.RESOURCE_FOOD, amount) @@ -603,18 +607,22 @@ func add_food(amount: float) -> void: _sync_village_food() village_changed.emit(village) + func add_wood(amount: float) -> void: village.apply_resource_delta(&"wood", amount) village_changed.emit(village) + func add_safety(amount: float) -> void: village.apply_resource_delta(&"safety", amount) village_changed.emit(village) + func add_knowledge(amount: float) -> void: village.apply_resource_delta(&"knowledge", amount) village_changed.emit(village) - + + func get_starving_count() -> int: var count := 0 for npc in npcs: @@ -622,37 +630,42 @@ func get_starving_count() -> int: count += 1 return count + func get_state_snapshot() -> Dictionary: var npc_snapshots: Array[Dictionary] = [] for npc in npcs: - npc_snapshots.append({ - "id": npc.id, - "name": npc.npc_name, - "profession": npc.profession, - "hunger": npc.hunger, - "energy": npc.energy, - "strength": npc.strength, - "intelligence": npc.intelligence, - "task": npc.current_task, - "task_state": npc.task_state, - "task_duration": npc.task_duration, - "task_progress": npc.task_progress, - "target_id": String(npc.target_id), - "position": [npc.position.x, npc.position.y, npc.position.z], - "travel_target_position": [ - npc.travel_target_position.x, - npc.travel_target_position.y, - npc.travel_target_position.z - ], - "has_travel_target": npc.has_travel_target, - "starvation_ticks": npc.starvation_ticks, - "is_dead": npc.is_dead - }) + npc_snapshots.append( + { + "id": npc.id, + "name": npc.npc_name, + "profession": npc.profession, + "hunger": npc.hunger, + "energy": npc.energy, + "strength": npc.strength, + "intelligence": npc.intelligence, + "task": npc.current_task, + "task_state": npc.task_state, + "task_duration": npc.task_duration, + "task_progress": npc.task_progress, + "target_id": String(npc.target_id), + "position": [npc.position.x, npc.position.y, npc.position.z], + "travel_target_position": + [ + npc.travel_target_position.x, + npc.travel_target_position.y, + npc.travel_target_position.z + ], + "has_travel_target": npc.has_travel_target, + "starvation_ticks": npc.starvation_ticks, + "is_dead": npc.is_dead + } + ) return { "seed": simulation_seed, "tick_count": tick_count, - "village": { + "village": + { "food": village.food, "wood": village.wood, "safety": village.safety, @@ -661,9 +674,11 @@ func get_state_snapshot() -> Dictionary: "npcs": npc_snapshots } + func get_state_checksum() -> String: return create_state_record().to_json().sha256_text() + func create_state_record() -> SimulationStateRecord: var record := SimulationStateRecord.new() var wander_streams: Array[Dictionary] = [] @@ -671,11 +686,9 @@ func create_state_record() -> SimulationStateRecord: sorted_npc_ids.sort() for npc_id in sorted_npc_ids: var source: RandomNumberGenerator = wander_random_sources[npc_id] - wander_streams.append({ - "npc_id": int(npc_id), - "seed": str(source.seed), - "state": str(source.state) - }) + wander_streams.append( + {"npc_id": int(npc_id), "seed": str(source.seed), "state": str(source.state)} + ) record.simulation = { "seed": simulation_seed, @@ -704,9 +717,11 @@ func create_state_record() -> SimulationStateRecord: record.economic_events.append(event) return record + func serialize_state() -> String: return create_state_record().to_json() + func restore_state_from_json(json_text: String) -> bool: var record := SimulationStateRecord.from_json(json_text) if record == null: @@ -714,6 +729,7 @@ func restore_state_from_json(json_text: String) -> bool: return false return restore_state(record) + func restore_state(record: SimulationStateRecord) -> bool: if record == null: return false diff --git a/simulation/actions/ActionExecutionSystem.gd b/simulation/actions/ActionExecutionSystem.gd index 921407f..f444985 100644 --- a/simulation/actions/ActionExecutionSystem.gd +++ b/simulation/actions/ActionExecutionSystem.gd @@ -1,6 +1,7 @@ class_name ActionExecutionSystem extends RefCounted + func advance_npc(npc: SimNPC, village: SimVillage) -> void: if npc.is_dead: return @@ -12,6 +13,7 @@ func advance_npc(npc: SimNPC, village: SimVillage) -> void: npc.task_complete = true npc.task_state = SimNPC.TASK_STATE_COMPLETE + func _update_needs(npc: SimNPC, village: SimVillage) -> void: npc.hunger += 3.0 * village.food_modifier match npc.task_state: diff --git a/simulation/actions/ActionSelectionResult.gd b/simulation/actions/ActionSelectionResult.gd index c5c9660..c600f2b 100644 --- a/simulation/actions/ActionSelectionResult.gd +++ b/simulation/actions/ActionSelectionResult.gd @@ -4,9 +4,7 @@ extends RefCounted var action_id: StringName var duration_override := -1.0 -func _init( - selected_action_id: StringName, - selected_duration_override: float = -1.0 -) -> void: + +func _init(selected_action_id: StringName, selected_duration_override: float = -1.0) -> void: action_id = selected_action_id duration_override = selected_duration_override diff --git a/simulation/actions/ActionSelectionSystem.gd b/simulation/actions/ActionSelectionSystem.gd index e6924c0..1e36075 100644 --- a/simulation/actions/ActionSelectionSystem.gd +++ b/simulation/actions/ActionSelectionSystem.gd @@ -1,6 +1,7 @@ class_name ActionSelectionSystem extends RefCounted + func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: if npc.is_dead: return null @@ -8,10 +9,7 @@ func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: return ActionSelectionResult.new(SimulationIds.ACTION_EAT, 1.0) if village.food > 0: - return ActionSelectionResult.new( - SimulationIds.ACTION_WITHDRAW_FOOD, - 1.0 - ) + return ActionSelectionResult.new(SimulationIds.ACTION_WITHDRAW_FOOD, 1.0) return ActionSelectionResult.new(SimulationIds.ACTION_GATHER_FOOD, 4.0) if npc.hunger > 75.0: if npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD) >= 1.0: @@ -25,34 +23,58 @@ func select_action(npc: SimNPC, village: SimVillage) -> ActionSelectionResult: return ActionSelectionResult.new(SimulationIds.ACTION_REST) return ActionSelectionResult.new(_choose_best_work_action(npc, village)) -func _choose_best_work_action( - npc: SimNPC, - village: SimVillage -) -> StringName: + +func _choose_best_work_action(npc: SimNPC, village: SimVillage) -> StringName: var scores := { - SimulationIds.ACTION_GATHER_FOOD: _calculate_score( - npc, SimulationIds.ACTION_GATHER_FOOD, village.food_priority, - village.food, 15.0, 30.0, 3.0, 1.5 + SimulationIds.ACTION_GATHER_FOOD: + _calculate_score( + npc, + SimulationIds.ACTION_GATHER_FOOD, + village.food_priority, + village.food, + 15.0, + 30.0, + 3.0, + 1.5 ), - SimulationIds.ACTION_GATHER_WOOD: _calculate_score( - npc, SimulationIds.ACTION_GATHER_WOOD, village.wood_priority, - village.wood, 10.0, 20.0, 2.5, 1.0 + SimulationIds.ACTION_GATHER_WOOD: + _calculate_score( + npc, + SimulationIds.ACTION_GATHER_WOOD, + village.wood_priority, + village.wood, + 10.0, + 20.0, + 2.5, + 1.0 ), - SimulationIds.ACTION_PATROL: _calculate_score( - npc, SimulationIds.ACTION_PATROL, village.safety_priority, - village.safety, 30.0, 50.0, 3.0, 1.5 + SimulationIds.ACTION_PATROL: + _calculate_score( + npc, + SimulationIds.ACTION_PATROL, + village.safety_priority, + village.safety, + 30.0, + 50.0, + 3.0, + 1.5 ), - SimulationIds.ACTION_STUDY: _calculate_score( - npc, SimulationIds.ACTION_STUDY, village.knowledge_priority, - village.knowledge, 10.0, 25.0, 1.5, 0.75 + SimulationIds.ACTION_STUDY: + _calculate_score( + npc, + SimulationIds.ACTION_STUDY, + village.knowledge_priority, + village.knowledge, + 10.0, + 25.0, + 1.5, + 0.75 ) } var best_action := SimulationIds.ACTION_GATHER_FOOD var best_score: float = scores[best_action] for action_id in [ - SimulationIds.ACTION_GATHER_WOOD, - SimulationIds.ACTION_PATROL, - SimulationIds.ACTION_STUDY + SimulationIds.ACTION_GATHER_WOOD, SimulationIds.ACTION_PATROL, SimulationIds.ACTION_STUDY ]: var score: float = scores[action_id] if score > best_score: @@ -60,6 +82,7 @@ func _choose_best_work_action( best_score = score return best_action + func _calculate_score( npc: SimNPC, action_id: StringName, diff --git a/simulation/actions/ActionTargetResolver.gd b/simulation/actions/ActionTargetResolver.gd index 1c3e22f..681e831 100644 --- a/simulation/actions/ActionTargetResolver.gd +++ b/simulation/actions/ActionTargetResolver.gd @@ -1,11 +1,9 @@ class_name ActionTargetResolver extends RefCounted + func resolve( - npc: SimNPC, - origin: Vector3, - simulation_manager: Node, - active_world_adapter: Node + npc: SimNPC, origin: Vector3, simulation_manager: Node, active_world_adapter: Node ) -> Dictionary: var definition := SimulationDefinitions.get_action(npc.current_task) if definition == null: @@ -13,18 +11,17 @@ func resolve( match definition.target_type: SimulationIds.TARGET_RESOURCE: return _resolve_resource( - npc, origin, definition, simulation_manager, - active_world_adapter + npc, origin, definition, simulation_manager, active_world_adapter ) SimulationIds.TARGET_ACTIVITY: return active_world_adapter.get_activity_target(npc.current_task) SimulationIds.TARGET_FREE: return { - "target_id": "", - "position": origin + simulation_manager.get_wander_offset(npc.id) + "target_id": "", "position": origin + simulation_manager.get_wander_offset(npc.id) } return {} + func _resolve_resource( npc: SimNPC, origin: Vector3, @@ -34,18 +31,10 @@ func _resolve_resource( ) -> Dictionary: var best: Dictionary = {} var best_distance := INF - for candidate in active_world_adapter.get_resource_candidates( - definition.resource_action_id - ): + for candidate in active_world_adapter.get_resource_candidates(definition.resource_action_id): var node_id := StringName(candidate["target_id"]) - var state: ResourceStateRecord = simulation_manager.get_resource_state( - node_id - ) - if ( - state == null - or not state.can_npc_use() - or not state.is_available_for(npc.id) - ): + var state: ResourceStateRecord = simulation_manager.get_resource_state(node_id) + if state == null or not state.can_npc_use() or not state.is_available_for(npc.id): continue var position: Vector3 = candidate["position"] var distance := origin.distance_squared_to(position) diff --git a/simulation/definitions/ActionDefinition.gd b/simulation/definitions/ActionDefinition.gd index 79317a5..23c3419 100644 --- a/simulation/definitions/ActionDefinition.gd +++ b/simulation/definitions/ActionDefinition.gd @@ -8,6 +8,7 @@ extends Resource @export var target_type: StringName = SimulationIds.TARGET_ACTIVITY @export var resource_action_id: StringName + func validate() -> Array[String]: var errors: Array[String] = [] if action_id.is_empty(): @@ -16,15 +17,13 @@ func validate() -> Array[String]: errors.append("display_name is empty for '%s'" % action_id) if default_duration <= 0.0: errors.append("default_duration must be positive for '%s'" % action_id) - if target_type not in [ - SimulationIds.TARGET_RESOURCE, - SimulationIds.TARGET_ACTIVITY, - SimulationIds.TARGET_FREE - ]: - errors.append("target_type '%s' is invalid for '%s'" % [target_type, action_id]) if ( - target_type == SimulationIds.TARGET_RESOURCE - and resource_action_id.is_empty() + target_type + not in [ + SimulationIds.TARGET_RESOURCE, SimulationIds.TARGET_ACTIVITY, SimulationIds.TARGET_FREE + ] ): + errors.append("target_type '%s' is invalid for '%s'" % [target_type, action_id]) + if target_type == SimulationIds.TARGET_RESOURCE and resource_action_id.is_empty(): errors.append("resource_action_id is required for '%s'" % action_id) return errors diff --git a/simulation/definitions/ProfessionDefinition.gd b/simulation/definitions/ProfessionDefinition.gd index a7ec45e..8756727 100644 --- a/simulation/definitions/ProfessionDefinition.gd +++ b/simulation/definitions/ProfessionDefinition.gd @@ -4,6 +4,7 @@ extends Resource @export var profession_id: StringName @export var display_name: String + func validate() -> Array[String]: var errors: Array[String] = [] if profession_id.is_empty(): diff --git a/simulation/definitions/SimulationDefinitions.gd b/simulation/definitions/SimulationDefinitions.gd index 8784d6f..1198bda 100644 --- a/simulation/definitions/SimulationDefinitions.gd +++ b/simulation/definitions/SimulationDefinitions.gd @@ -21,6 +21,7 @@ const PROFESSION_PATHS := [ "res://simulation/definitions/professions/wanderer.tres" ] + static func get_actions() -> Array[ActionDefinition]: var definitions: Array[ActionDefinition] = [] for path in ACTION_PATHS: @@ -29,6 +30,7 @@ static func get_actions() -> Array[ActionDefinition]: definitions.append(definition) return definitions + static func get_professions() -> Array[ProfessionDefinition]: var definitions: Array[ProfessionDefinition] = [] for path in PROFESSION_PATHS: @@ -37,26 +39,28 @@ static func get_professions() -> Array[ProfessionDefinition]: definitions.append(definition) return definitions + static func get_action(action_id: StringName) -> ActionDefinition: for definition in get_actions(): if definition.action_id == action_id: return definition return null -static func get_profession( - profession_id: StringName -) -> ProfessionDefinition: + +static func get_profession(profession_id: StringName) -> ProfessionDefinition: for definition in get_professions(): if definition.profession_id == profession_id: return definition return null + static func get_profession_ids() -> Array[StringName]: var ids: Array[StringName] = [] for definition in get_professions(): ids.append(definition.profession_id) return ids + static func validate() -> Array[String]: var errors: Array[String] = [] if get_actions().size() != ACTION_PATHS.size(): @@ -85,15 +89,19 @@ static func validate() -> Array[String]: and not profession_ids.has(definition.preferred_profession_id) ): errors.append( - "Action '%s' references unknown profession '%s'" - % [definition.action_id, definition.preferred_profession_id] + ( + "Action '%s' references unknown profession '%s'" + % [definition.action_id, definition.preferred_profession_id] + ) ) if ( definition.target_type == SimulationIds.TARGET_RESOURCE and not action_ids.has(definition.resource_action_id) ): errors.append( - "Action '%s' references unknown resource action '%s'" - % [definition.action_id, definition.resource_action_id] + ( + "Action '%s' references unknown resource action '%s'" + % [definition.action_id, definition.resource_action_id] + ) ) return errors diff --git a/simulation/persistence/SaveSlotController.gd b/simulation/persistence/SaveSlotController.gd index e153224..8707068 100644 --- a/simulation/persistence/SaveSlotController.gd +++ b/simulation/persistence/SaveSlotController.gd @@ -4,6 +4,7 @@ extends Node var store := SaveSlotStore.new() + func _unhandled_key_input(event: InputEvent) -> void: if not event.pressed or event.echo: return diff --git a/simulation/persistence/SaveSlotStore.gd b/simulation/persistence/SaveSlotStore.gd index 7b6e6ca..bbb4636 100644 --- a/simulation/persistence/SaveSlotStore.gd +++ b/simulation/persistence/SaveSlotStore.gd @@ -8,9 +8,11 @@ const MAX_SAVE_BYTES := 16 * 1024 * 1024 var save_directory: String var last_error := "" + func _init(directory: String = DEFAULT_DIRECTORY) -> void: save_directory = directory + func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: last_error = "" if manager == null or not manager.has_method("serialize_state"): @@ -55,6 +57,7 @@ func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: _remove_if_present(backup_path) return true + func load_into(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: last_error = "" if manager == null or not manager.has_method("restore_state"): @@ -72,14 +75,13 @@ func load_into(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: return _fail("Simulation refused the validated save record") return true + func has_slot(slot_name: String = DEFAULT_SLOT) -> bool: if not _is_valid_slot_name(slot_name): return false var final_path := get_slot_path(slot_name) - return ( - _read_record(final_path) != null - or _read_record(final_path + ".bak") != null - ) + return _read_record(final_path) != null or _read_record(final_path + ".bak") != null + func delete_slot(slot_name: String = DEFAULT_SLOT) -> bool: if not _is_valid_slot_name(slot_name): @@ -90,9 +92,11 @@ func delete_slot(slot_name: String = DEFAULT_SLOT) -> bool: _remove_if_present(final_path + ".bak") return true + func get_slot_path(slot_name: String = DEFAULT_SLOT) -> String: return save_directory.path_join(slot_name + ".json") + func _read_record(path: String) -> SimulationStateRecord: if not FileAccess.file_exists(path): return null @@ -106,6 +110,7 @@ func _read_record(path: String) -> SimulationStateRecord: file.close() return SimulationStateRecord.from_json(json_text) + func _ensure_directory() -> bool: var absolute_directory := ProjectSettings.globalize_path(save_directory) var error := DirAccess.make_dir_recursive_absolute(absolute_directory) @@ -113,6 +118,7 @@ func _ensure_directory() -> bool: return _fail("Could not create the save directory") return true + func _is_valid_slot_name(slot_name: String) -> bool: if slot_name.is_empty() or slot_name.length() > 32: return false @@ -122,10 +128,12 @@ func _is_valid_slot_name(slot_name: String) -> bool: return false return true + func _remove_if_present(path: String) -> void: if FileAccess.file_exists(path): DirAccess.remove_absolute(path) + func _fail(message: String) -> bool: last_error = message return false diff --git a/simulation/state/EconomicEventRecord.gd b/simulation/state/EconomicEventRecord.gd index 8bcbd22..4159b20 100644 --- a/simulation/state/EconomicEventRecord.gd +++ b/simulation/state/EconomicEventRecord.gd @@ -5,9 +5,11 @@ const SCHEMA_VERSION := 1 var data: Dictionary + func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) + static func create( event_id: int, event_type: StringName, @@ -18,25 +20,36 @@ static func create( item_id: StringName, amount: float ) -> EconomicEventRecord: - return EconomicEventRecord.new({ - "schema_version": SCHEMA_VERSION, - "event_id": event_id, - "event_type": String(event_type), - "tick": tick, - "actor_id": actor_id, - "source_id": String(source_id), - "destination_id": String(destination_id), - "item_id": String(item_id), - "amount": amount - }) + return EconomicEventRecord.new( + { + "schema_version": SCHEMA_VERSION, + "event_id": event_id, + "event_type": String(event_type), + "tick": tick, + "actor_id": actor_id, + "source_id": String(source_id), + "destination_id": String(destination_id), + "item_id": String(item_id), + "amount": amount + } + ) + static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: return null - if not record_data.has_all([ - "event_id", "event_type", "tick", "actor_id", "source_id", - "destination_id", "item_id", "amount" - ]): + if not record_data.has_all( + [ + "event_id", + "event_type", + "tick", + "actor_id", + "source_id", + "destination_id", + "item_id", + "amount" + ] + ): return null var normalized := record_data.duplicate(true) normalized["schema_version"] = SCHEMA_VERSION @@ -60,5 +73,6 @@ static func from_dictionary(record_data: Dictionary) -> EconomicEventRecord: return null return EconomicEventRecord.new(normalized) + func to_dictionary() -> Dictionary: return data.duplicate(true) diff --git a/simulation/state/NPCStateRecord.gd b/simulation/state/NPCStateRecord.gd index 42376df..bfebc5d 100644 --- a/simulation/state/NPCStateRecord.gd +++ b/simulation/state/NPCStateRecord.gd @@ -7,41 +7,47 @@ const PREVIOUS_SCHEMA_VERSION := 2 var data: Dictionary + func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) + static func capture(npc: SimNPC) -> NPCStateRecord: - return NPCStateRecord.new({ - "schema_version": SCHEMA_VERSION, - "id": npc.id, - "name": npc.npc_name, - "profession": String(npc.profession), - "hunger": npc.hunger, - "energy": npc.energy, - "strength": npc.strength, - "intelligence": npc.intelligence, - "current_task": String(npc.current_task), - "task_state": String(npc.task_state), - "position": [npc.position.x, npc.position.y, npc.position.z], - "is_starving": npc.is_starving, - "starvation_ticks": npc.starvation_ticks, - "starvation_death_threshold": npc.starvation_death_threshold, - "is_dead": npc.is_dead, - "task_duration": npc.task_duration, - "task_progress": npc.task_progress, - "task_complete": npc.task_complete, - "target_id": String(npc.target_id), - "travel_target_position": [ - npc.travel_target_position.x, - npc.travel_target_position.y, - npc.travel_target_position.z - ], - "has_travel_target": npc.has_travel_target, - "inventory": npc.inventory.duplicate(true), - "last_task": String(npc.last_task), - "random_seed": str(npc.random_source.seed), - "random_state": str(npc.random_source.state) - }) + return NPCStateRecord.new( + { + "schema_version": SCHEMA_VERSION, + "id": npc.id, + "name": npc.npc_name, + "profession": String(npc.profession), + "hunger": npc.hunger, + "energy": npc.energy, + "strength": npc.strength, + "intelligence": npc.intelligence, + "current_task": String(npc.current_task), + "task_state": String(npc.task_state), + "position": [npc.position.x, npc.position.y, npc.position.z], + "is_starving": npc.is_starving, + "starvation_ticks": npc.starvation_ticks, + "starvation_death_threshold": npc.starvation_death_threshold, + "is_dead": npc.is_dead, + "task_duration": npc.task_duration, + "task_progress": npc.task_progress, + "task_complete": npc.task_complete, + "target_id": String(npc.target_id), + "travel_target_position": + [ + npc.travel_target_position.x, + npc.travel_target_position.y, + npc.travel_target_position.z + ], + "has_travel_target": npc.has_travel_target, + "inventory": npc.inventory.duplicate(true), + "last_task": String(npc.last_task), + "random_seed": str(npc.random_source.seed), + "random_state": str(npc.random_source.state) + } + ) + static func from_dictionary(record_data: Dictionary) -> NPCStateRecord: var version := int(record_data.get("schema_version", -1)) @@ -49,23 +55,40 @@ static func from_dictionary(record_data: Dictionary) -> NPCStateRecord: record_data = _migrate_legacy(record_data, version) elif version != SCHEMA_VERSION: return null - if not record_data.has_all([ - "id", "name", "profession", "hunger", "energy", "strength", - "intelligence", "current_task", "task_state", "position", - "is_starving", "starvation_ticks", "starvation_death_threshold", - "is_dead", "task_duration", "task_progress", "task_complete", - "target_id", "travel_target_position", "has_travel_target", "inventory", - "last_task", "random_seed", "random_state" - ]): + if not record_data.has_all( + [ + "id", + "name", + "profession", + "hunger", + "energy", + "strength", + "intelligence", + "current_task", + "task_state", + "position", + "is_starving", + "starvation_ticks", + "starvation_death_threshold", + "is_dead", + "task_duration", + "task_progress", + "task_complete", + "target_id", + "travel_target_position", + "has_travel_target", + "inventory", + "last_task", + "random_seed", + "random_state" + ] + ): return null var saved_position = record_data["position"] if not saved_position is Array or saved_position.size() != 3: return null var saved_target_position = record_data["travel_target_position"] - if ( - not saved_target_position is Array - or saved_target_position.size() != 3 - ): + if not saved_target_position is Array or saved_target_position.size() != 3: return null if not record_data["inventory"] is Dictionary: return null @@ -74,36 +97,26 @@ static func from_dictionary(record_data: Dictionary) -> NPCStateRecord: return null var action_id := StringName(record_data["current_task"]) if ( - action_id not in [ - SimulationIds.ACTION_IDLE, - SimulationIds.ACTION_DEAD - ] + action_id not in [SimulationIds.ACTION_IDLE, SimulationIds.ACTION_DEAD] and SimulationDefinitions.get_action(action_id) == null ): return null var last_action_id := StringName(record_data["last_task"]) - if ( - not last_action_id.is_empty() - and SimulationDefinitions.get_action(last_action_id) == null - ): + if not last_action_id.is_empty() and SimulationDefinitions.get_action(last_action_id) == null: return null return NPCStateRecord.new(record_data) -static func _migrate_legacy( - legacy_data: Dictionary, - version: int -) -> Dictionary: + +static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION if version == LEGACY_SCHEMA_VERSION: - migrated["travel_target_position"] = legacy_data.get( - "position", - [0.0, 0.0, 0.0] - ) + migrated["travel_target_position"] = legacy_data.get("position", [0.0, 0.0, 0.0]) migrated["has_travel_target"] = false migrated["inventory"] = {} return migrated + func restore(debug_logs: bool) -> SimNPC: var random_source := RandomNumberGenerator.new() random_source.seed = String(data["random_seed"]).to_int() @@ -122,9 +135,7 @@ func restore(debug_logs: bool) -> SimNPC: npc.current_task = StringName(data["current_task"]) npc.task_state = StringName(data["task_state"]) npc.position = Vector3( - float(saved_position[0]), - float(saved_position[1]), - float(saved_position[2]) + float(saved_position[0]), float(saved_position[1]), float(saved_position[2]) ) npc.is_starving = bool(data["is_starving"]) npc.starvation_ticks = int(data["starvation_ticks"]) @@ -146,5 +157,6 @@ func restore(debug_logs: bool) -> SimNPC: npc.debug_logs = debug_logs return npc + func to_dictionary() -> Dictionary: return data.duplicate(true) diff --git a/simulation/state/ResourceStateRecord.gd b/simulation/state/ResourceStateRecord.gd index cc3998e..02e095c 100644 --- a/simulation/state/ResourceStateRecord.gd +++ b/simulation/state/ResourceStateRecord.gd @@ -9,22 +9,27 @@ const LEGACY_SCHEMA_VERSION := 1 var data: Dictionary + func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) + static func create_from_node(node: ResourceNode) -> ResourceStateRecord: - return ResourceStateRecord.new({ - "schema_version": SCHEMA_VERSION, - "node_id": String(node.node_id), - "action_id": String(node.action_id), - "resource_id": String(node.resource_id), - "amount_remaining": node.initial_amount, - "yield_per_action": node.yield_per_action, - "reserved_by": -1, - "enabled": node.initial_enabled, - "can_npcs_use": node.can_npcs_use, - "can_player_use": node.can_player_use - }) + return ResourceStateRecord.new( + { + "schema_version": SCHEMA_VERSION, + "node_id": String(node.node_id), + "action_id": String(node.action_id), + "resource_id": String(node.resource_id), + "amount_remaining": node.initial_amount, + "yield_per_action": node.yield_per_action, + "reserved_by": -1, + "enabled": node.initial_enabled, + "can_npcs_use": node.can_npcs_use, + "can_player_use": node.can_player_use + } + ) + static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord: var version := int(record_data.get("schema_version", -1)) @@ -33,26 +38,30 @@ static func from_dictionary(record_data: Dictionary) -> ResourceStateRecord: elif version != SCHEMA_VERSION: return null - if not record_data.has_all([ - "node_id", "action_id", "resource_id", "amount_remaining", - "yield_per_action", "reserved_by", "enabled", "can_npcs_use", - "can_player_use" - ]): + if not record_data.has_all( + [ + "node_id", + "action_id", + "resource_id", + "amount_remaining", + "yield_per_action", + "reserved_by", + "enabled", + "can_npcs_use", + "can_player_use" + ] + ): return null if String(record_data["node_id"]).is_empty(): return null var action_id := StringName(record_data["action_id"]) - if ( - not action_id.is_empty() - and SimulationDefinitions.get_action(action_id) == null - ): + if not action_id.is_empty() and SimulationDefinitions.get_action(action_id) == null: return null return ResourceStateRecord.new(record_data) + static func _migrate_v1(legacy_data: Dictionary) -> Dictionary: - if not legacy_data.has_all([ - "node_id", "amount_remaining", "reserved_by", "enabled" - ]): + if not legacy_data.has_all(["node_id", "amount_remaining", "reserved_by", "enabled"]): return {} var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION @@ -63,6 +72,7 @@ static func _migrate_v1(legacy_data: Dictionary) -> Dictionary: migrated["can_player_use"] = false return migrated + func apply_definition(node: ResourceNode) -> bool: if node == null or node.node_id != get_node_id(): return false @@ -85,44 +95,54 @@ func apply_definition(node: ResourceNode) -> bool: and can_player_use_resource() == node.can_player_use ) + func get_node_id() -> StringName: return StringName(data["node_id"]) + func get_action_id() -> StringName: return StringName(data["action_id"]) + func get_resource_id() -> StringName: return StringName(data["resource_id"]) + func get_amount_remaining() -> float: return float(data["amount_remaining"]) + func get_yield_per_action() -> float: return float(data["yield_per_action"]) + func get_reserved_by() -> int: return int(data["reserved_by"]) + func is_enabled() -> bool: return bool(data["enabled"]) + func can_npc_use() -> bool: return bool(data["can_npcs_use"]) + func can_player_use_resource() -> bool: return bool(data["can_player_use"]) + func is_depleted() -> bool: return get_amount_remaining() <= 0.0 + func can_extract() -> bool: return is_enabled() and not is_depleted() + func is_available_for(agent_id: int) -> bool: - return ( - can_extract() - and (get_reserved_by() == -1 or get_reserved_by() == agent_id) - ) + return can_extract() and (get_reserved_by() == -1 or get_reserved_by() == agent_id) + func reserve(agent_id: int) -> bool: if not is_available_for(agent_id): @@ -131,12 +151,14 @@ func reserve(agent_id: int) -> bool: changed.emit(self) return true + func release(agent_id: int) -> void: if get_reserved_by() != agent_id: return data["reserved_by"] = -1 changed.emit(self) + func extract(requested_amount: float = -1.0) -> float: if not can_extract(): return 0.0 @@ -152,6 +174,7 @@ func extract(requested_amount: float = -1.0) -> float: depleted.emit(self) return extracted + func set_amount_remaining(amount: float) -> void: var was_depleted := is_depleted() data["amount_remaining"] = maxf(amount, 0.0) @@ -161,9 +184,11 @@ func set_amount_remaining(amount: float) -> void: if not was_depleted and is_depleted(): depleted.emit(self) + func set_enabled(value: bool) -> void: data["enabled"] = value changed.emit(self) + func to_dictionary() -> Dictionary: return data.duplicate(true) diff --git a/simulation/state/SimulationStateRecord.gd b/simulation/state/SimulationStateRecord.gd index 15e2e79..cc318f5 100644 --- a/simulation/state/SimulationStateRecord.gd +++ b/simulation/state/SimulationStateRecord.gd @@ -13,6 +13,7 @@ var resources: Array[ResourceStateRecord] = [] var storages: Array[StorageStateRecord] = [] var economic_events: Array[EconomicEventRecord] = [] + func to_dictionary() -> Dictionary: var npc_data: Array[Dictionary] = [] for npc_record in npcs: @@ -39,15 +40,18 @@ func to_dictionary() -> Dictionary: "economic_events": event_data } + func to_json() -> String: return JSON.stringify(to_dictionary()) + static func from_json(json_text: String) -> SimulationStateRecord: var parsed = JSON.parse_string(json_text) if not parsed is Dictionary: return null return from_dictionary(parsed) + static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord: if record_data.get("schema", "") != SCHEMA_NAME: return null @@ -56,19 +60,25 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord: record_data = _migrate_legacy(record_data, version) elif version != SCHEMA_VERSION: return null - if not record_data.has_all([ - "simulation", "village", "npcs", "resources", "storages", - "economic_events" - ]): + if not record_data.has_all( + ["simulation", "village", "npcs", "resources", "storages", "economic_events"] + ): return null var simulation_data = record_data["simulation"] if not simulation_data is Dictionary: return null - if not simulation_data.has_all([ - "seed", "tick_interval", "tick_count", "clock_accumulator", - "clock_elapsed_ticks", "wander_random_streams", "next_event_id" - ]): + if not simulation_data.has_all( + [ + "seed", + "tick_interval", + "tick_count", + "clock_accumulator", + "clock_elapsed_ticks", + "wander_random_streams", + "next_event_id" + ] + ): return null var wander_streams = simulation_data["wander_random_streams"] if not wander_streams is Array: @@ -160,20 +170,22 @@ static func from_dictionary(record_data: Dictionary) -> SimulationStateRecord: return record -static func _migrate_legacy( - legacy_data: Dictionary, - version: int -) -> Dictionary: + +static func _migrate_legacy(legacy_data: Dictionary, version: int) -> Dictionary: var migrated := legacy_data.duplicate(true) migrated["schema_version"] = SCHEMA_VERSION if version == LEGACY_SCHEMA_VERSION: var village_data: Dictionary = legacy_data.get("village", {}) var initial_food := float(village_data.get("food", 0.0)) migrated["storages"] = [ - StorageStateRecord.create( - SimulationIds.STORAGE_VILLAGE_PANTRY, - {String(SimulationIds.RESOURCE_FOOD): initial_food} - ).to_dictionary() + ( + StorageStateRecord + . create( + SimulationIds.STORAGE_VILLAGE_PANTRY, + {String(SimulationIds.RESOURCE_FOOD): initial_food} + ) + . to_dictionary() + ) ] migrated["economic_events"] = [] var simulation_data: Dictionary = migrated.get("simulation", {}) diff --git a/simulation/state/StorageStateRecord.gd b/simulation/state/StorageStateRecord.gd index 6d6db49..8ab3278 100644 --- a/simulation/state/StorageStateRecord.gd +++ b/simulation/state/StorageStateRecord.gd @@ -5,20 +5,23 @@ const SCHEMA_VERSION := 1 var data: Dictionary + func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) + static func create( - storage_id: StringName, - initial_amounts: Dictionary, - capacity: float = 100.0 + storage_id: StringName, initial_amounts: Dictionary, capacity: float = 100.0 ) -> StorageStateRecord: - return StorageStateRecord.new({ - "schema_version": SCHEMA_VERSION, - "storage_id": String(storage_id), - "amounts": initial_amounts.duplicate(true), - "capacity": capacity - }) + return StorageStateRecord.new( + { + "schema_version": SCHEMA_VERSION, + "storage_id": String(storage_id), + "amounts": initial_amounts.duplicate(true), + "capacity": capacity + } + ) + static func from_dictionary(record_data: Dictionary) -> StorageStateRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: @@ -34,34 +37,36 @@ static func from_dictionary(record_data: Dictionary) -> StorageStateRecord: normalized["capacity"] = float(record_data["capacity"]) var normalized_amounts := {} for item_id in record_data["amounts"]: - normalized_amounts[String(item_id)] = float( - record_data["amounts"][item_id] - ) + normalized_amounts[String(item_id)] = float(record_data["amounts"][item_id]) normalized["amounts"] = normalized_amounts return StorageStateRecord.new(normalized) + func get_storage_id() -> StringName: return StringName(data["storage_id"]) + func get_amount(item_id: StringName) -> float: return float(data["amounts"].get(String(item_id), 0.0)) + func get_total_amount() -> float: var total := 0.0 for amount in data["amounts"].values(): total += float(amount) return total + func deposit(item_id: StringName, requested_amount: float) -> float: var accepted := minf( - maxf(requested_amount, 0.0), - maxf(float(data["capacity"]) - get_total_amount(), 0.0) + maxf(requested_amount, 0.0), maxf(float(data["capacity"]) - get_total_amount(), 0.0) ) if accepted <= 0.0: return 0.0 data["amounts"][String(item_id)] = get_amount(item_id) + accepted return accepted + func withdraw(item_id: StringName, requested_amount: float) -> float: var removed := minf(maxf(requested_amount, 0.0), get_amount(item_id)) if removed <= 0.0: @@ -69,5 +74,6 @@ func withdraw(item_id: StringName, requested_amount: float) -> float: data["amounts"][String(item_id)] = get_amount(item_id) - removed return removed + func to_dictionary() -> Dictionary: return data.duplicate(true) diff --git a/simulation/state/VillageStateRecord.gd b/simulation/state/VillageStateRecord.gd index b98de3b..1fc2b40 100644 --- a/simulation/state/VillageStateRecord.gd +++ b/simulation/state/VillageStateRecord.gd @@ -5,17 +5,22 @@ const SCHEMA_VERSION := 1 var data: Dictionary + func _init(record_data: Dictionary = {}) -> void: data = record_data.duplicate(true) + static func capture(village: SimVillage) -> VillageStateRecord: - return VillageStateRecord.new({ - "schema_version": SCHEMA_VERSION, - "food": village.food, - "wood": village.wood, - "safety": village.safety, - "knowledge": village.knowledge - }) + return VillageStateRecord.new( + { + "schema_version": SCHEMA_VERSION, + "food": village.food, + "wood": village.wood, + "safety": village.safety, + "knowledge": village.knowledge + } + ) + static func from_dictionary(record_data: Dictionary) -> VillageStateRecord: if int(record_data.get("schema_version", -1)) != SCHEMA_VERSION: @@ -24,6 +29,7 @@ static func from_dictionary(record_data: Dictionary) -> VillageStateRecord: return null return VillageStateRecord.new(record_data) + func restore(debug_logs: bool) -> SimVillage: var village := SimVillage.new() village.debug_logs = debug_logs @@ -35,5 +41,6 @@ func restore(debug_logs: bool) -> SimVillage: village.update_priorities() return village + func to_dictionary() -> Dictionary: return data.duplicate(true) diff --git a/tests/action_system_boundaries_test.gd b/tests/action_system_boundaries_test.gd index 72537ac..216cc40 100644 --- a/tests/action_system_boundaries_test.gd +++ b/tests/action_system_boundaries_test.gd @@ -1,5 +1,6 @@ extends SceneTree + class FakeActiveWorld: extends Node @@ -12,12 +13,15 @@ class FakeActiveWorld: func get_activity_target(_action_id: StringName) -> Dictionary: return {"target_id": "", "position": activity_position} + var failures: Array[String] = [] var travel_requests: Array[Vector3] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: _test_selection_and_execution_are_separate() _test_target_resolution_and_travel_are_separate() @@ -31,16 +35,11 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _test_selection_and_execution_are_separate() -> void: var village := SimVillage.new() village.debug_logs = false - var npc := SimNPC.new( - 700, - "BoundaryWorker", - SimulationIds.PROFESSION_FARMER, - 5.0, - 5.0 - ) + var npc := SimNPC.new(700, "BoundaryWorker", SimulationIds.PROFESSION_FARMER, 5.0, 5.0) npc.hunger = 80.0 var executor := ActionExecutionSystem.new() executor.advance_npc(npc, village) @@ -55,12 +54,12 @@ func _test_selection_and_execution_are_separate() -> void: "Selection should independently choose food retrieval" ) + func _test_target_resolution_and_travel_are_separate() -> void: var adapter := FakeActiveWorld.new() - adapter.resource_candidates = [{ - "target_id": "boundary_bush", - "position": Vector3(3.0, 0.0, 2.0) - }] + adapter.resource_candidates = [ + {"target_id": "boundary_bush", "position": Vector3(3.0, 0.0, 2.0)} + ] root.add_child(adapter) var manager: Node = load("res://simulation/SimulationManager.gd").new() @@ -70,18 +69,20 @@ func _test_target_resolution_and_travel_are_separate() -> void: manager.set_process(false) manager.npc_travel_requested.connect(_on_travel_requested) - var resource_state := ResourceStateRecord.from_dictionary({ - "schema_version": ResourceStateRecord.SCHEMA_VERSION, - "node_id": "boundary_bush", - "action_id": String(SimulationIds.ACTION_GATHER_FOOD), - "resource_id": String(SimulationIds.RESOURCE_FOOD), - "amount_remaining": 5.0, - "yield_per_action": 1.0, - "reserved_by": -1, - "enabled": true, - "can_npcs_use": true, - "can_player_use": true - }) + var resource_state := ResourceStateRecord.from_dictionary( + { + "schema_version": ResourceStateRecord.SCHEMA_VERSION, + "node_id": "boundary_bush", + "action_id": String(SimulationIds.ACTION_GATHER_FOOD), + "resource_id": String(SimulationIds.RESOURCE_FOOD), + "amount_remaining": 5.0, + "yield_per_action": 1.0, + "reserved_by": -1, + "enabled": true, + "can_npcs_use": true, + "can_player_use": true + } + ) manager.resource_states[resource_state.get_node_id()] = resource_state var npc: SimNPC = manager.npcs[0] @@ -91,8 +92,7 @@ func _test_target_resolution_and_travel_are_separate() -> void: "Target resolver should resolve a resource from adapter facts" ) _check( - npc.target_id == &"boundary_bush" - and resource_state.get_reserved_by() == npc.id, + npc.target_id == &"boundary_bush" and resource_state.get_reserved_by() == npc.id, "Target resolver should authoritatively assign and reserve the target" ) _check( @@ -113,12 +113,11 @@ func _test_target_resolution_and_travel_are_separate() -> void: manager.free() adapter.free() -func _on_travel_requested( - _npc: SimNPC, - target_position: Vector3 -) -> void: + +func _on_travel_requested(_npc: SimNPC, target_position: Vector3) -> void: travel_requests.append(target_position) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/deterministic_simulation_test.gd b/tests/deterministic_simulation_test.gd index 12920c2..b63188f 100644 --- a/tests/deterministic_simulation_test.gd +++ b/tests/deterministic_simulation_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var clock := SimulationClock.new(1.0) _check(clock.advance(0.4) == 0, "Clock should not tick before its interval") @@ -28,6 +30,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _run_scenario(seed_value: int) -> String: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.simulation_seed = seed_value @@ -47,6 +50,7 @@ func _run_scenario(seed_value: int) -> String: manager.free() return checksum + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/food_storage_loop_test.gd b/tests/food_storage_loop_test.gd index 95e0822..bd8e3b7 100644 --- a/tests/food_storage_loop_test.gd +++ b/tests/food_storage_loop_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.debug_logs = false @@ -14,18 +16,20 @@ func _run() -> void: var pantry: StorageStateRecord = manager.get_pantry() pantry.withdraw(SimulationIds.RESOURCE_FOOD, 1000.0) manager._sync_village_food() - var resource := ResourceStateRecord.from_dictionary({ - "schema_version": ResourceStateRecord.SCHEMA_VERSION, - "node_id": "food_loop_bush", - "action_id": String(SimulationIds.ACTION_GATHER_FOOD), - "resource_id": String(SimulationIds.RESOURCE_FOOD), - "amount_remaining": 10.0, - "yield_per_action": 2.0, - "reserved_by": -1, - "enabled": true, - "can_npcs_use": true, - "can_player_use": true - }) + var resource := ResourceStateRecord.from_dictionary( + { + "schema_version": ResourceStateRecord.SCHEMA_VERSION, + "node_id": "food_loop_bush", + "action_id": String(SimulationIds.ACTION_GATHER_FOOD), + "resource_id": String(SimulationIds.RESOURCE_FOOD), + "amount_remaining": 10.0, + "yield_per_action": 2.0, + "reserved_by": -1, + "enabled": true, + "can_npcs_use": true, + "can_player_use": true + } + ) manager.resource_states[resource.get_node_id()] = resource var npc: SimNPC = manager.npcs[0] @@ -38,10 +42,9 @@ func _run() -> void: manager.simulate_tick() _check( - is_equal_approx(resource.get_amount_remaining(), 8.0) - and is_equal_approx( - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), - 2.0 + ( + is_equal_approx(resource.get_amount_remaining(), 8.0) + and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 2.0) ), "Gathering should move food from the source into NPC inventory" ) @@ -58,10 +61,9 @@ func _run() -> void: manager.notify_npc_arrived(npc.id) manager.simulate_tick() _check( - is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 2.0) - and is_equal_approx( - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), - 0.0 + ( + is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 2.0) + and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0) ), "Deposit should transfer carried food into the pantry" ) @@ -75,28 +77,23 @@ func _run() -> void: manager.notify_npc_arrived(npc.id) manager.simulate_tick() _check( - is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 1.0) - and is_equal_approx( - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), - 1.0 + ( + is_equal_approx(pantry.get_amount(SimulationIds.RESOURCE_FOOD), 1.0) + and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 1.0) ), "Withdraw should transfer pantry food into NPC inventory" ) manager.simulate_tick() - _check( - npc.current_task == SimulationIds.ACTION_EAT, - "NPC carrying food should select eat" - ) + _check(npc.current_task == SimulationIds.ACTION_EAT, "NPC carrying food should select eat") manager.notify_npc_arrived(npc.id) var hunger_before_eating := npc.hunger manager.simulate_tick() manager.simulate_tick() _check( - npc.hunger < hunger_before_eating - and is_equal_approx( - npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), - 0.0 + ( + npc.hunger < hunger_before_eating + and is_equal_approx(npc.get_inventory_amount(SimulationIds.RESOURCE_FOOD), 0.0) ), "Eating should consume carried food and reduce hunger" ) @@ -121,8 +118,10 @@ func _run() -> void: "Economic events should restore with the simulation" ) _check( - restored.economic_events.size() == manager.economic_events.size() - and restored.next_event_id == manager.next_event_id, + ( + restored.economic_events.size() == manager.economic_events.size() + and restored.next_event_id == manager.next_event_id + ), "Economic event history and its next ID should round-trip" ) @@ -134,10 +133,12 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) + func _check_economic_event_chain(manager: Node) -> void: var expected_types: Array[StringName] = [ SimulationIds.EVENT_RESOURCE_EXTRACTED, @@ -154,28 +155,19 @@ func _check_economic_event_chain(manager: Node) -> void: for index in expected_types.size(): var event: EconomicEventRecord = manager.economic_events[index] _check( - int(event.data["event_id"]) == index - and StringName(event.data["event_type"]) == expected_types[index] - and StringName(event.data["item_id"]) - == SimulationIds.RESOURCE_FOOD, + ( + int(event.data["event_id"]) == index + and StringName(event.data["event_type"]) == expected_types[index] + and StringName(event.data["item_id"]) == SimulationIds.RESOURCE_FOOD + ), "Economic event order, identity, and item should be deterministic" ) _check( - is_equal_approx( - float(manager.economic_events[0].data["amount"]), - 2.0 - ) - and is_equal_approx( - float(manager.economic_events[1].data["amount"]), - 2.0 - ) - and is_equal_approx( - float(manager.economic_events[2].data["amount"]), - 1.0 - ) - and is_equal_approx( - float(manager.economic_events[3].data["amount"]), - 1.0 + ( + is_equal_approx(float(manager.economic_events[0].data["amount"]), 2.0) + and is_equal_approx(float(manager.economic_events[1].data["amount"]), 2.0) + and is_equal_approx(float(manager.economic_events[2].data["amount"]), 1.0) + and is_equal_approx(float(manager.economic_events[3].data["amount"]), 1.0) ), "Economic events should record exact transferred quantities" ) diff --git a/tests/jajce_runtime_integration_test.gd b/tests/jajce_runtime_integration_test.gd index e1478ca..43f6925 100644 --- a/tests/jajce_runtime_integration_test.gd +++ b/tests/jajce_runtime_integration_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var main_scene: Node = load("res://main.tscn").instantiate() root.add_child(main_scene) @@ -19,26 +21,21 @@ func _run() -> void: main_scene.has_node("JajceWorld/TerrainRoot/Terrain3D"), "Playable runtime should instance the Jajce Terrain3D world" ) - var terrain: Terrain3D = main_scene.get_node( - "JajceWorld/TerrainRoot/Terrain3D" - ) + var terrain: Terrain3D = main_scene.get_node("JajceWorld/TerrainRoot/Terrain3D") _check( terrain.get_camera() == main_scene.get_node("CameraRig/Camera3D"), "Runtime Terrain3D should bind to the gameplay camera" ) _check( - not main_scene.has_node("World") - and not main_scene.has_node("ResourceNodes") - and not main_scene.has_node("ActivityMarkers"), + ( + not main_scene.has_node("World") + and not main_scene.has_node("ResourceNodes") + and not main_scene.has_node("ActivityMarkers") + ), "Runtime should not retain duplicate flat-world objects" ) - var resource_root := main_scene.get_node( - "JajceWorld/WorldObjects/ResourceNodes" - ) - _check( - resource_root.get_child_count() == 4, - "Runtime should contain four Jajce ResourceNodes" - ) + var resource_root := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes") + _check(resource_root.get_child_count() == 4, "Runtime should contain four Jajce ResourceNodes") _check( simulation_manager.resource_states.size() == 4, "Simulation authority should bind all four Jajce resources" @@ -48,11 +45,7 @@ func _run() -> void: await _wait_for_navigation_map(navigation_map) NavigationServer3D.map_force_update(navigation_map) - var fixed_origins := [ - Vector3(-6.0, 0.0, -4.0), - Vector3(0.0, 0.0, 0.0), - Vector3(6.0, 0.0, 4.0) - ] + var fixed_origins := [Vector3(-6.0, 0.0, -4.0), Vector3(0.0, 0.0, 0.0), Vector3(6.0, 0.0, 4.0)] var destinations: Array[Vector3] = [] for marker_path in [ "JajceWorld/LegacyActivityMarkers/PantryMarker", @@ -67,25 +60,14 @@ func _run() -> void: for origin in fixed_origins: for destination in destinations: - var path := NavigationServer3D.map_get_path( - navigation_map, - origin, - destination, - true - ) + var path := NavigationServer3D.map_get_path(navigation_map, origin, destination, true) _check( not path.is_empty(), "Navigation path should exist from %s to %s" % [origin, destination] ) var village := SimVillage.new() - var worker := SimNPC.new( - 100, - "BaselineWorker", - SimulationIds.PROFESSION_SCHOLAR, - 5.0, - 5.0 - ) + var worker := SimNPC.new(100, "BaselineWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0) worker.set_task(SimulationIds.ACTION_STUDY, 2.0) worker.start_working() var executor := ActionExecutionSystem.new() @@ -93,13 +75,7 @@ func _run() -> void: executor.advance_npc(worker, village) _check(worker.task_complete, "A two-tick working task should complete") - var starving := SimNPC.new( - 101, - "BaselineStarving", - SimulationIds.PROFESSION_WANDERER, - 5.0, - 5.0 - ) + var starving := SimNPC.new(101, "BaselineStarving", SimulationIds.PROFESSION_WANDERER, 5.0, 5.0) starving.hunger = 100.0 starving.starvation_death_threshold = 1 executor.advance_npc(starving, village) @@ -114,6 +90,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _wait_for_navigation_map(navigation_map: RID) -> void: for attempt in 30: if ( @@ -125,6 +102,7 @@ func _wait_for_navigation_map(navigation_map: RID) -> void: await physics_frame _check(false, "Navigation map did not synchronize within 30 physics frames") + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/jajce_world_scaffold_test.gd b/tests/jajce_world_scaffold_test.gd index b2cc882..2969092 100644 --- a/tests/jajce_world_scaffold_test.gd +++ b/tests/jajce_world_scaffold_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var camera := Camera3D.new() camera.current = true @@ -24,8 +26,10 @@ func _run() -> void: ) _check( terrain.assets.get_texture_count() >= 3, - "Jajce terrain should expose at least three materials (found %s)" - % terrain.assets.get_texture_count() + ( + "Jajce terrain should expose at least three materials (found %s)" + % terrain.assets.get_texture_count() + ) ) _check( absf(terrain.data.get_height(Vector3.ZERO)) < 0.1, @@ -42,32 +46,22 @@ func _run() -> void: world.get_node("FoliageRoot").get_child_count() >= 10, "JajceWorld should include restrained authored foliage clusters" ) - _check( - world.has_node("FortressBlockout"), - "JajceWorld should include the fortress landmark" - ) - _check( - world.has_node("WaterRoot/WaterfallBody"), - "WaterRoot should include a waterfall drop" - ) + _check(world.has_node("FortressBlockout"), "JajceWorld should include the fortress landmark") + _check(world.has_node("WaterRoot/WaterfallBody"), "WaterRoot should include a waterfall drop") _check( world.has_node("WaterRoot/WaterfallMist"), "WaterRoot should include waterfall mist particles" ) - _check( - world.has_node("WaterRoot/RiverSurface"), - "WaterRoot should include a river surface" - ) + _check(world.has_node("WaterRoot/RiverSurface"), "WaterRoot should include a river surface") var environment: Environment = ( - world.get_node("WorldEnvironment") as WorldEnvironment - ).environment + (world.get_node("WorldEnvironment") as WorldEnvironment).environment + ) _check( environment.fog_enabled and environment.sky != null, "Jajce environment should provide sky and restrained valley fog" ) _check( - world.has_node("VillageRoot/Mill") - and world.has_node("VillageRoot/Bridge"), + world.has_node("VillageRoot/Mill") and world.has_node("VillageRoot/Bridge"), "VillageRoot should include the mill and bridge blockouts" ) var smoke_count := 0 @@ -103,11 +97,7 @@ func _run() -> void: await _wait_for_navigation_map(navigation_map) NavigationServer3D.map_force_update(navigation_map) - var origins := [ - Vector3(-6, 0, -4), - Vector3(0, 0, 0), - Vector3(6, 0, 4) - ] + var origins := [Vector3(-6, 0, -4), Vector3(0, 0, 0), Vector3(6, 0, 4)] var destinations: Array[Vector3] = [] for resource in resources: destinations.append((resource as ResourceNode).interaction_point.global_position) @@ -116,12 +106,7 @@ func _run() -> void: for origin in origins: for destination in destinations: - var path := NavigationServer3D.map_get_path( - navigation_map, - origin, - destination, - true - ) + var path := NavigationServer3D.map_get_path(navigation_map, origin, destination, true) _check( not path.is_empty(), "Jajce navigation path should exist from %s to %s" % [origin, destination] @@ -143,9 +128,7 @@ func _run() -> void: ) var selected := ResourceNode.get_by_id(npc.target_id) if selected != null: - var selected_state: ResourceStateRecord = manager.get_resource_state( - selected.node_id - ) + var selected_state: ResourceStateRecord = manager.get_resource_state(selected.node_id) _check( selected_state.get_reserved_by() == npc.id, "Selected Jajce resource should be authoritatively reserved" @@ -153,7 +136,9 @@ func _run() -> void: manager.release_resource(selected.node_id, npc.id) if failures.is_empty(): - print("[TEST] Jajce scaffold passed: Terrain3D, 6 textures, shader water, building blockouts, chimney smoke") + print( + "[TEST] Jajce scaffold passed: Terrain3D, 6 textures, shader water, building blockouts, chimney smoke" + ) quit(0) return @@ -161,6 +146,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _wait_for_navigation_map(navigation_map: RID) -> void: for attempt in 30: if ( @@ -172,6 +158,7 @@ func _wait_for_navigation_map(navigation_map: RID) -> void: await physics_frame _check(false, "Jajce navigation map did not synchronize within 30 physics frames") + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/npc_visual_lifecycle_test.gd b/tests/npc_visual_lifecycle_test.gd index b0f5430..a912955 100644 --- a/tests/npc_visual_lifecycle_test.gd +++ b/tests/npc_visual_lifecycle_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var main_scene: Node = load("res://main.tscn").instantiate() root.add_child(main_scene) @@ -21,25 +23,13 @@ func _run() -> void: var carried_food: MeshInstance3D = visual.get_node("CarriedFood") _check(not carried_food.visible, "NPC should begin without a carried-food prop") npc.add_inventory(SimulationIds.RESOURCE_FOOD, 1.0) - manager.emit_signal( - "npc_inventory_changed", - npc, - SimulationIds.RESOURCE_FOOD, - 1.0 - ) + manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 1.0) _check(carried_food.visible, "Carried food should be visible on the NPC") npc.remove_inventory(SimulationIds.RESOURCE_FOOD, 1.0) - manager.emit_signal( - "npc_inventory_changed", - npc, - SimulationIds.RESOURCE_FOOD, - 0.0 - ) + manager.emit_signal("npc_inventory_changed", npc, SimulationIds.RESOURCE_FOOD, 0.0) _check(not carried_food.visible, "Deposited or eaten food should be hidden") - var resource_state: ResourceStateRecord = manager.get_resource_state( - &"berry_bush_01" - ) + var resource_state: ResourceStateRecord = manager.get_resource_state(&"berry_bush_01") npc.set_task(SimulationIds.ACTION_GATHER_FOOD) npc.target_id = resource_state.get_node_id() npc.travel_target_position = Vector3(-6.0, 0.0, -8.0) @@ -60,9 +50,11 @@ func _run() -> void: "Unload should synchronize the last active position" ) _check( - npc.current_task == saved_action - and npc.target_id == saved_target_id - and npc.travel_target_position.is_equal_approx(saved_target_position), + ( + npc.current_task == saved_action + and npc.target_id == saved_target_id + and npc.travel_target_position.is_equal_approx(saved_target_position) + ), "Unload should preserve action and target state" ) _check( @@ -87,10 +79,7 @@ func _run() -> void: manager.get_state_checksum() == unloaded_checksum, "Loading a visual must not mutate simulation state" ) - _check( - resource_state.get_reserved_by() == npc.id, - "Reload should preserve the reservation" - ) + _check(resource_state.get_reserved_by() == npc.id, "Reload should preserve the reservation") var moved_position := authoritative_position + Vector3(0.5, 0.0, 0.25) reloaded.emit_signal("position_changed", npc.id, moved_position) @@ -124,6 +113,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/resource_node_player_parity_test.gd b/tests/resource_node_player_parity_test.gd index 5247c3a..4cda89c 100644 --- a/tests/resource_node_player_parity_test.gd +++ b/tests/resource_node_player_parity_test.gd @@ -3,9 +3,11 @@ extends SceneTree var main_scene: Node var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: main_scene = load("res://main.tscn").instantiate() root.add_child(main_scene) @@ -14,20 +16,14 @@ func _run() -> void: var simulation_manager := main_scene.get_node("SimulationManager") var player := main_scene.get_node("Player") - var bush := main_scene.get_node( - "JajceWorld/WorldObjects/ResourceNodes/BerryBush_01" - ) as ResourceNode - var tree := main_scene.get_node( - "JajceWorld/WorldObjects/ResourceNodes/Tree_01" - ) as ResourceNode + var bush := ( + main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/BerryBush_01") as ResourceNode + ) + var tree := main_scene.get_node("JajceWorld/WorldObjects/ResourceNodes/Tree_01") as ResourceNode simulation_manager.set_process(false) - var bush_state: ResourceStateRecord = simulation_manager.get_resource_state( - bush.node_id - ) - var tree_state: ResourceStateRecord = simulation_manager.get_resource_state( - tree.node_id - ) + var bush_state: ResourceStateRecord = simulation_manager.get_resource_state(bush.node_id) + var tree_state: ResourceStateRecord = simulation_manager.get_resource_state(tree.node_id) bush_state.set_amount_remaining(1.0) _check( simulation_manager.reserve_resource(bush.node_id, 999), @@ -41,11 +37,11 @@ func _run() -> void: is_equal_approx(bush_state.get_amount_remaining(), 0.0), "Player should deplete the nearby bush" ) - _check(is_equal_approx(simulation_manager.village.food, food_before + 1.0), "Village should receive exactly the bush's remaining food") _check( - bush_state.get_reserved_by() == -1, - "Depletion should release the NPC reservation" + is_equal_approx(simulation_manager.village.food, food_before + 1.0), + "Village should receive exactly the bush's remaining food" ) + _check(bush_state.get_reserved_by() == -1, "Depletion should release the NPC reservation") player.global_position = tree.interaction_point.global_position var wood_before: float = simulation_manager.village.wood @@ -53,13 +49,13 @@ func _run() -> void: player.try_interact() _check( - is_equal_approx( - tree_state.get_amount_remaining(), - tree_before - tree.yield_per_action - ), + is_equal_approx(tree_state.get_amount_remaining(), tree_before - tree.yield_per_action), "Player should extract the tree's configured yield" ) - _check(is_equal_approx(simulation_manager.village.wood, wood_before + tree.yield_per_action), "Village should receive exactly the extracted wood") + _check( + is_equal_approx(simulation_manager.village.wood, wood_before + tree.yield_per_action), + "Village should receive exactly the extracted wood" + ) if failures.is_empty(): print("[TEST] ResourceNode player parity passed") @@ -70,6 +66,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/save_slot_persistence_test.gd b/tests/save_slot_persistence_test.gd index 8479599..e7b96f6 100644 --- a/tests/save_slot_persistence_test.gd +++ b/tests/save_slot_persistence_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var test_directory := "user://save_slot_test_%s" % Time.get_ticks_usec() var store := SaveSlotStore.new(test_directory) @@ -48,17 +50,14 @@ func _run() -> void: var unchanged_checksum: String = manager.get_state_checksum() var file := FileAccess.open(final_path, FileAccess.WRITE) - file.store_string("{\"schema\":\"broken\"}") + file.store_string('{"schema":"broken"}') file.close() _check(not store.load_into(manager), "Invalid save data should be rejected") _check( manager.get_state_checksum() == unchanged_checksum, "A rejected save must not partially mutate the simulation" ) - _check( - not store.save(manager, "../escape"), - "Slot names must not escape the save directory" - ) + _check(not store.save(manager, "../escape"), "Slot names must not escape the save directory") store.delete_slot() DirAccess.remove_absolute(ProjectSettings.globalize_path(test_directory)) @@ -70,6 +69,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/simulation_definitions_test.gd b/tests/simulation_definitions_test.gd index 4a95f3c..04e120c 100644 --- a/tests/simulation_definitions_test.gd +++ b/tests/simulation_definitions_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: _test_registry_integrity() _test_definition_backed_behavior() @@ -20,12 +22,10 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _test_registry_integrity() -> void: var errors := SimulationDefinitions.validate() - _check( - errors.is_empty(), - "Definition registry should validate: %s" % [errors] - ) + _check(errors.is_empty(), "Definition registry should validate: %s" % [errors]) var action_ids: Array[StringName] = [] for definition in SimulationDefinitions.get_actions(): @@ -34,30 +34,19 @@ func _test_registry_integrity() -> void: SimulationDefinitions.get_action(definition.action_id) == definition, "Action lookup should return '%s'" % definition.action_id ) - _check( - action_ids.size() == 9, - "Registry should contain all nine executable prototype actions" - ) + _check(action_ids.size() == 9, "Registry should contain all nine executable prototype actions") var profession_ids := SimulationDefinitions.get_profession_ids() - _check( - profession_ids.size() == 5, - "Registry should contain all five prototype professions" - ) + _check(profession_ids.size() == 5, "Registry should contain all five prototype professions") for profession_id in profession_ids: _check( SimulationDefinitions.get_profession(profession_id) != null, "Profession lookup should return '%s'" % profession_id ) + func _test_definition_backed_behavior() -> void: - var npc := SimNPC.new( - 500, - "DefinitionWorker", - SimulationIds.PROFESSION_SCHOLAR, - 5.0, - 5.0 - ) + var npc := SimNPC.new(500, "DefinitionWorker", SimulationIds.PROFESSION_SCHOLAR, 5.0, 5.0) npc.set_task(SimulationIds.ACTION_STUDY) var study := SimulationDefinitions.get_action(SimulationIds.ACTION_STUDY) _check( @@ -68,23 +57,18 @@ func _test_definition_backed_behavior() -> void: study.preferred_profession_id == SimulationIds.PROFESSION_SCHOLAR, "Study should reference the stable scholar profession ID" ) - var gather := SimulationDefinitions.get_action( - SimulationIds.ACTION_GATHER_FOOD - ) + var gather := SimulationDefinitions.get_action(SimulationIds.ACTION_GATHER_FOOD) _check( - gather.target_type == SimulationIds.TARGET_RESOURCE - and gather.resource_action_id == SimulationIds.ACTION_GATHER_FOOD, + ( + gather.target_type == SimulationIds.TARGET_RESOURCE + and gather.resource_action_id == SimulationIds.ACTION_GATHER_FOOD + ), "Gather-food target metadata should come from its definition" ) + func _test_state_reference_validation() -> void: - var npc := SimNPC.new( - 501, - "ValidationWorker", - SimulationIds.PROFESSION_FARMER, - 5.0, - 5.0 - ) + var npc := SimNPC.new(501, "ValidationWorker", SimulationIds.PROFESSION_FARMER, 5.0, 5.0) var valid_data := NPCStateRecord.capture(npc).to_dictionary() var invalid_profession := valid_data.duplicate(true) invalid_profession["profession"] = "missing_profession" @@ -99,6 +83,7 @@ func _test_state_reference_validation() -> void: "NPC state should reject an unknown action ID" ) + func _test_id_round_trip() -> void: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.debug_logs = false @@ -116,8 +101,7 @@ func _test_id_round_trip() -> void: ) for npc in restored.npcs: _check( - typeof(npc.profession) == TYPE_STRING_NAME, - "Profession should restore as StringName" + typeof(npc.profession) == TYPE_STRING_NAME, "Profession should restore as StringName" ) _check( typeof(npc.current_task) == TYPE_STRING_NAME, @@ -131,6 +115,7 @@ func _test_id_round_trip() -> void: manager.free() restored.free() + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tests/simulation_state_serialization_test.gd b/tests/simulation_state_serialization_test.gd index 6dc7e2f..e728beb 100644 --- a/tests/simulation_state_serialization_test.gd +++ b/tests/simulation_state_serialization_test.gd @@ -2,9 +2,11 @@ extends SceneTree var failures: Array[String] = [] + func _initialize() -> void: call_deferred("_run") + func _run() -> void: await _test_deterministic_continuation() await _test_resource_state_round_trip() @@ -23,6 +25,7 @@ func _run() -> void: push_error("[TEST] " + failure) quit(1) + func _test_deterministic_continuation() -> void: var uninterrupted := _create_manager(91234) _advance_scenario(uninterrupted, 24) @@ -63,10 +66,9 @@ func _test_deterministic_continuation() -> void: before_save.free() restored.free() + func _test_resource_state_round_trip() -> void: - var resource: ResourceNode = load( - "res://world/resource_nodes/ResourceNode.tscn" - ).instantiate() + var resource: ResourceNode = load("res://world/resource_nodes/ResourceNode.tscn").instantiate() resource.node_id = &"SerializationTestBerry" resource.initial_amount = 9.0 resource.yield_per_action = 2.0 @@ -78,9 +80,7 @@ func _test_resource_state_round_trip() -> void: manager.register_resource_node(resource), "Resource presentation should bind to simulation authority" ) - var resource_state: ResourceStateRecord = manager.get_resource_state( - resource.node_id - ) + var resource_state: ResourceStateRecord = manager.get_resource_state(resource.node_id) _check(resource_state.reserve(42), "Resource should accept the test reservation") resource_state.extract() var saved_json: String = manager.serialize_state() @@ -103,15 +103,10 @@ func _test_resource_state_round_trip() -> void: is_equal_approx(resource_state.get_amount_remaining(), 7.0), "Resource amount should round-trip" ) - _check( - resource_state.get_reserved_by() == 42, - "Resource reservation should round-trip" - ) + _check(resource_state.get_reserved_by() == 42, "Resource reservation should round-trip") _check(resource_state.is_enabled(), "Resource enabled state should round-trip") - var rebound: ResourceNode = load( - "res://world/resource_nodes/ResourceNode.tscn" - ).instantiate() + var rebound: ResourceNode = load("res://world/resource_nodes/ResourceNode.tscn").instantiate() rebound.node_id = &"SerializationTestBerry" rebound.initial_amount = 999.0 root.add_child(rebound) @@ -128,28 +123,28 @@ func _test_resource_state_round_trip() -> void: manager.free() rebound.free() + func _test_schema_rejection() -> void: - var unsupported := JSON.stringify({ - "schema": SimulationStateRecord.SCHEMA_NAME, - "schema_version": 999 - }) + var unsupported := JSON.stringify( + {"schema": SimulationStateRecord.SCHEMA_NAME, "schema_version": 999} + ) _check( SimulationStateRecord.from_json(unsupported) == null, "Unsupported schema versions should be rejected" ) - _check( - SimulationStateRecord.from_json("[]") == null, - "Non-record JSON should be rejected" - ) + _check(SimulationStateRecord.from_json("[]") == null, "Non-record JSON should be rejected") + func _test_legacy_resource_migration() -> void: - var migrated := ResourceStateRecord.from_dictionary({ - "schema_version": 1, - "node_id": "legacy_tree", - "amount_remaining": 6.0, - "reserved_by": 12, - "enabled": true - }) + var migrated := ResourceStateRecord.from_dictionary( + { + "schema_version": 1, + "node_id": "legacy_tree", + "amount_remaining": 6.0, + "reserved_by": 12, + "enabled": true + } + ) _check(migrated != null, "ResourceStateRecord v1 should migrate explicitly") if migrated == null: return @@ -158,19 +153,13 @@ func _test_legacy_resource_migration() -> void: "Migrated resource state should use the current nested schema" ) _check( - is_equal_approx(migrated.get_amount_remaining(), 6.0) - and migrated.get_reserved_by() == 12, + is_equal_approx(migrated.get_amount_remaining(), 6.0) and migrated.get_reserved_by() == 12, "Resource migration should preserve mutable authority" ) + func _test_legacy_npc_migration() -> void: - var npc := SimNPC.new( - 88, - "LegacyNPC", - SimulationIds.PROFESSION_FARMER, - 5.0, - 5.0 - ) + var npc := SimNPC.new(88, "LegacyNPC", SimulationIds.PROFESSION_FARMER, 5.0, 5.0) var legacy_data := NPCStateRecord.capture(npc).to_dictionary() legacy_data["schema_version"] = NPCStateRecord.LEGACY_SCHEMA_VERSION legacy_data.erase("travel_target_position") @@ -184,6 +173,7 @@ func _test_legacy_npc_migration() -> void: "Legacy NPC migration should not invent an active travel target" ) + func _test_legacy_world_storage_migration() -> void: var manager := _create_manager(55) var legacy_data: Dictionary = manager.create_state_record().to_dictionary() @@ -193,35 +183,34 @@ func _test_legacy_world_storage_migration() -> void: _check(migrated != null, "World schema v1 should migrate pantry storage") if migrated != null: _check( - migrated.storages.size() == 1 - and is_equal_approx( - migrated.storages[0].get_amount( - SimulationIds.RESOURCE_FOOD - ), - manager.village.food + ( + migrated.storages.size() == 1 + and is_equal_approx( + migrated.storages[0].get_amount(SimulationIds.RESOURCE_FOOD), + manager.village.food + ) ), "World migration should preserve legacy village food in pantry" ) manager.free() + func _test_previous_world_event_migration() -> void: var manager := _create_manager(56) var previous_data: Dictionary = manager.create_state_record().to_dictionary() - previous_data["schema_version"] = ( - SimulationStateRecord.PREVIOUS_SCHEMA_VERSION - ) + previous_data["schema_version"] = (SimulationStateRecord.PREVIOUS_SCHEMA_VERSION) previous_data.erase("economic_events") previous_data["simulation"].erase("next_event_id") var migrated := SimulationStateRecord.from_dictionary(previous_data) _check(migrated != null, "World schema v2 should add an empty event stream") if migrated != null: _check( - migrated.economic_events.is_empty() - and int(migrated.simulation["next_event_id"]) == 0, + migrated.economic_events.is_empty() and int(migrated.simulation["next_event_id"]) == 0, "World v2 migration should initialize deterministic event identity" ) manager.free() + func _create_manager(seed_value: int) -> Node: var manager: Node = load("res://simulation/SimulationManager.gd").new() manager.simulation_seed = seed_value @@ -230,6 +219,7 @@ func _create_manager(seed_value: int) -> Node: manager.set_process(false) return manager + func _advance_scenario(manager: Node, steps: int) -> void: for step in steps: manager.simulate_tick() @@ -238,6 +228,7 @@ func _advance_scenario(manager: Node, steps: int) -> void: if npc.task_state == SimNPC.TASK_STATE_TRAVELING: manager.notify_npc_arrived(npc.id) + func _check(condition: bool, message: String) -> void: if not condition: failures.append(message) diff --git a/tools/generate_jajce_terrain.gd b/tools/generate_jajce_terrain.gd index c2675fe..3f760af 100644 --- a/tools/generate_jajce_terrain.gd +++ b/tools/generate_jajce_terrain.gd @@ -6,13 +6,13 @@ const HEIGHT_SCALE := 40.0 const DATA_DIRECTORY := "res://terrain/jajce/data" const TEXTURE_DIRECTORY := "res://terrain/jajce/textures" + func _initialize() -> void: call_deferred("_generate") + func _generate() -> void: - DirAccess.make_dir_recursive_absolute( - ProjectSettings.globalize_path(TEXTURE_DIRECTORY) - ) + DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(TEXTURE_DIRECTORY)) _generate_texture("grass_albedo.png", Color("#516f36"), Color("#79924c"), 17) _generate_texture("soil_albedo.png", Color("#65513b"), Color("#92734d"), 29) _generate_texture("rock_albedo.png", Color("#55534f"), Color("#89847b"), 43) @@ -22,12 +22,7 @@ func _generate() -> void: root.add_child(terrain) await process_frame - var height_map := Image.create_empty( - MAP_SIZE, - MAP_SIZE, - false, - Image.FORMAT_RF - ) + var height_map := Image.create_empty(MAP_SIZE, MAP_SIZE, false, Image.FORMAT_RF) var noise := FastNoiseLite.new() noise.seed = 1943 noise.frequency = 0.018 @@ -40,79 +35,36 @@ func _generate() -> void: var world_z := float(image_z - HALF_SIZE) var distance := Vector2(world_x, world_z).length() var outside_play_area := smoothstep(20.0, 52.0, distance) - var ridge := 28.0 * _elliptical_peak( - world_x, - world_z, - -72.0, - 18.0, - 72.0, - 48.0 - ) - var eastern_hill := 15.0 * _elliptical_peak( - world_x, - world_z, - 105.0, - 65.0, - 110.0, - 90.0 - ) - var southern_hill := 11.0 * _elliptical_peak( - world_x, - world_z, - -35.0, - -125.0, - 130.0, - 85.0 - ) - var river_trench := -5.0 * exp( - -pow((world_x - 25.0) / 18.0, 2.0) + var ridge := 28.0 * _elliptical_peak(world_x, world_z, -72.0, 18.0, 72.0, 48.0) + var eastern_hill := 15.0 * _elliptical_peak(world_x, world_z, 105.0, 65.0, 110.0, 90.0) + var southern_hill := ( + 11.0 * _elliptical_peak(world_x, world_z, -35.0, -125.0, 130.0, 85.0) ) + var river_trench := -5.0 * exp(-pow((world_x - 25.0) / 18.0, 2.0)) var broad_slope := maxf(0.0, (-world_x - 30.0) * 0.035) var detail := noise.get_noise_2d(world_x, world_z) * 2.4 var height := ( - ridge - + eastern_hill - + southern_hill - + river_trench - + broad_slope - + detail - ) * outside_play_area - height_map.set_pixel( - image_x, - image_z, - Color(height / HEIGHT_SCALE, 0.0, 0.0, 1.0) + (ridge + eastern_hill + southern_hill + river_trench + broad_slope + detail) + * outside_play_area ) + height_map.set_pixel(image_x, image_z, Color(height / HEIGHT_SCALE, 0.0, 0.0, 1.0)) terrain.data.import_images( - [height_map, null, null], - Vector3(-HALF_SIZE, 0.0, -HALF_SIZE), - 0.0, - HEIGHT_SCALE + [height_map, null, null], Vector3(-HALF_SIZE, 0.0, -HALF_SIZE), 0.0, HEIGHT_SCALE ) terrain.data.save_directory(DATA_DIRECTORY) print("[TerrainGenerator] Generated bounded Jajce terrain and textures") quit(0) + func _elliptical_peak( - x: float, - z: float, - center_x: float, - center_z: float, - width: float, - depth: float + x: float, z: float, center_x: float, center_z: float, width: float, depth: float ) -> float: - return exp( - -( - pow((x - center_x) / width, 2.0) - + pow((z - center_z) / depth, 2.0) - ) - ) + return exp(-(pow((x - center_x) / width, 2.0) + pow((z - center_z) / depth, 2.0))) + func _generate_texture( - file_name: String, - dark_color: Color, - light_color: Color, - seed_value: int + file_name: String, dark_color: Color, light_color: Color, seed_value: int ) -> void: var noise := FastNoiseLite.new() noise.seed = seed_value diff --git a/tools/generate_jajce_terrain_seed.gd b/tools/generate_jajce_terrain_seed.gd index 4418857..cf106a5 100644 --- a/tools/generate_jajce_terrain_seed.gd +++ b/tools/generate_jajce_terrain_seed.gd @@ -4,9 +4,11 @@ const REGION_SIZE := 256 const TERRAIN_SIZE := 512 const DATA_DIRECTORY := "res://terrain/jajce/data" + func _initialize() -> void: call_deferred("_run") + func _run() -> void: var camera := Camera3D.new() camera.current = true @@ -18,12 +20,7 @@ func _run() -> void: terrain.set_camera(camera) await process_frame - var height_map := Image.create_empty( - TERRAIN_SIZE, - TERRAIN_SIZE, - false, - Image.FORMAT_RF - ) + var height_map := Image.create_empty(TERRAIN_SIZE, TERRAIN_SIZE, false, Image.FORMAT_RF) height_map.fill(Color(0.0, 0.0, 0.0, 1.0)) var images: Array[Image] @@ -31,10 +28,7 @@ func _run() -> void: images[Terrain3DRegion.TYPE_HEIGHT] = height_map terrain.data.import_images( - images, - Vector3(-TERRAIN_SIZE / 2.0, 0.0, -TERRAIN_SIZE / 2.0), - 0.0, - 1.0 + images, Vector3(-TERRAIN_SIZE / 2.0, 0.0, -TERRAIN_SIZE / 2.0), 0.0, 1.0 ) terrain.data.save_directory(DATA_DIRECTORY) print("[TOOL] Generated flat Jajce Terrain3D seed data in ", DATA_DIRECTORY) diff --git a/tools/generate_terrain_textures.gd b/tools/generate_terrain_textures.gd index 11a7b7a..315ef2c 100644 --- a/tools/generate_terrain_textures.gd +++ b/tools/generate_terrain_textures.gd @@ -3,11 +3,13 @@ extends Node const TEXTURE_DIR := "res://terrain/jajce/textures" + func _ready() -> void: if not Engine.is_editor_hint(): return call_deferred("generate") + func generate() -> void: var dir := DirAccess.open(TEXTURE_DIR) if not dir: @@ -19,6 +21,7 @@ func generate() -> void: print("[TerrainTextureGenerator] Generated 3 terrain textures") + func _generate_texture(file_name: String, dark: Color, light: Color, seed_val: int) -> void: seed(seed_val) var grid_size := 16 diff --git a/world/active_world_adapter.gd b/world/active_world_adapter.gd index fd859e6..2cbd60b 100644 --- a/world/active_world_adapter.gd +++ b/world/active_world_adapter.gd @@ -6,17 +6,18 @@ extends Node @export var rest_marker: Marker3D @export var pantry_marker: Marker3D + func get_resource_candidates(action_id: StringName) -> Array[Dictionary]: var candidates: Array[Dictionary] = [] for node in ResourceNode.get_all(): if node.action_id != action_id or node.interaction_point == null: continue - candidates.append({ - "target_id": String(node.node_id), - "position": node.interaction_point.global_position - }) + candidates.append( + {"target_id": String(node.node_id), "position": node.interaction_point.global_position} + ) return candidates + func get_activity_target(action_id: StringName) -> Dictionary: var marker: Marker3D match action_id: @@ -33,10 +34,13 @@ func get_activity_target(action_id: StringName) -> Dictionary: if marker == null: return {} var target_id := "" - if action_id in [ - SimulationIds.ACTION_EAT, - SimulationIds.ACTION_DEPOSIT_FOOD, - SimulationIds.ACTION_WITHDRAW_FOOD - ]: + if ( + action_id + in [ + SimulationIds.ACTION_EAT, + SimulationIds.ACTION_DEPOSIT_FOOD, + SimulationIds.ACTION_WITHDRAW_FOOD + ] + ): target_id = String(SimulationIds.STORAGE_VILLAGE_PANTRY) return {"target_id": target_id, "position": marker.global_position} diff --git a/world/jajce/StylizedTree.gd b/world/jajce/StylizedTree.gd index e8193cc..9e4f5da 100644 --- a/world/jajce/StylizedTree.gd +++ b/world/jajce/StylizedTree.gd @@ -17,6 +17,7 @@ const COLORS := { const WIND_SHADER := preload("res://world/jajce/materials/wind.gdshader") + func _ready() -> void: for child in get_children(): child.queue_free() diff --git a/world/jajce/beauty_camera.gd b/world/jajce/beauty_camera.gd index ff998f2..45459c1 100644 --- a/world/jajce/beauty_camera.gd +++ b/world/jajce/beauty_camera.gd @@ -4,9 +4,11 @@ extends Camera3D @export var orbit_speed := 0.025 @export var animate_orbit := true + func _ready() -> void: look_at(focus_point, Vector3.UP) + func _process(delta: float) -> void: if not animate_orbit: return diff --git a/world/jajce/jajce_world.gd b/world/jajce/jajce_world.gd index aafb705..228348b 100644 --- a/world/jajce/jajce_world.gd +++ b/world/jajce/jajce_world.gd @@ -2,9 +2,11 @@ extends Node3D @onready var terrain: Terrain3D = $TerrainRoot/Terrain3D + func _ready() -> void: call_deferred("_initialize_world_presentation") + func _initialize_world_presentation() -> void: var active_camera := get_viewport().get_camera_3d() if active_camera != null: @@ -16,7 +18,7 @@ func _initialize_world_presentation() -> void: for child in $VillageRoot.get_children(): groups_to_snap.append(child) for item in groups_to_snap: - var pos := item.global_position + var pos: Vector3 = item.global_position var h: float = terrain.data.get_height(pos) if not is_nan(h): item.global_position.y = h diff --git a/world/resource_nodes/ResourceNode.gd b/world/resource_nodes/ResourceNode.gd index 4177ea6..5f6191b 100644 --- a/world/resource_nodes/ResourceNode.gd +++ b/world/resource_nodes/ResourceNode.gd @@ -21,6 +21,7 @@ static var _all: Array[ResourceNode] = [] var state: ResourceStateRecord + func _ready() -> void: if node_id.is_empty(): push_error("ResourceNode at %s has empty node_id" % get_path()) @@ -30,8 +31,10 @@ func _ready() -> void: var existing := get_by_id(node_id) if existing != null: push_error( - "Duplicate ResourceNode node_id '%s' at %s; first registered at %s" - % [node_id, get_path(), existing.get_path()] + ( + "Duplicate ResourceNode node_id '%s' at %s; first registered at %s" + % [node_id, get_path(), existing.get_path()] + ) ) initial_enabled = false _update_presentation() @@ -41,6 +44,7 @@ func _ready() -> void: _try_register_with_simulation() _update_presentation() + func _exit_tree() -> void: _all.erase(self) if state != null and state.changed.is_connected(_on_state_changed): @@ -49,6 +53,7 @@ func _exit_tree() -> void: state.depleted.disconnect(_on_state_depleted) state = null + func bind_state(resource_state: ResourceStateRecord) -> bool: if resource_state == null or resource_state.get_node_id() != node_id: return false @@ -64,6 +69,7 @@ func bind_state(resource_state: ResourceStateRecord) -> bool: _update_presentation() return true + func _try_register_with_simulation() -> void: var managers := get_tree().get_nodes_in_group("simulation_manager") if managers.size() != 1: @@ -72,21 +78,27 @@ func _try_register_with_simulation() -> void: if manager.has_method("register_resource_node"): manager.register_resource_node(self) + func get_amount_remaining() -> float: return state.get_amount_remaining() if state != null else initial_amount + func get_reserved_by() -> int: return state.get_reserved_by() if state != null else -1 + func is_enabled() -> bool: return state.is_enabled() if state != null else initial_enabled + func is_depleted() -> bool: return state.is_depleted() if state != null else initial_amount <= 0.0 + func can_extract() -> bool: return state.can_extract() if state != null else initial_enabled and not is_depleted() + func _on_state_changed(changed_state: ResourceStateRecord) -> void: if changed_state != state: return @@ -94,21 +106,19 @@ func _on_state_changed(changed_state: ResourceStateRecord) -> void: reservation_changed.emit(node_id, state.get_reserved_by()) _update_presentation() + func _on_state_depleted(depleted_state: ResourceStateRecord) -> void: if depleted_state == state: depleted.emit(node_id) + func _update_presentation() -> void: if has_node("Visual"): $Visual.visible = not (hide_visual_when_depleted and is_depleted()) if not has_node("DebugLabel"): return var label := $DebugLabel as Label3D - var text := "%s\n%s %.1f" % [ - node_id, - resource_id, - get_amount_remaining() - ] + var text := "%s\n%s %.1f" % [node_id, resource_id, get_amount_remaining()] if get_reserved_by() >= 0: text += "\nheld:%d" % get_reserved_by() if is_depleted(): @@ -117,11 +127,13 @@ func _update_presentation() -> void: text += "\nUNBOUND" label.text = text + static func get_by_id(search_id: StringName) -> ResourceNode: for node in _all: if node.node_id == search_id: return node return null + static func get_all() -> Array[ResourceNode]: return _all.duplicate() diff --git a/world/ui/ui.gd b/world/ui/ui.gd index 06c1549..0af9eae 100644 --- a/world/ui/ui.gd +++ b/world/ui/ui.gd @@ -6,6 +6,7 @@ const DEBUG_LOGS := false @onready var village_stats_label: Label = $VillagePanel/MarginContainer/VillageStatsLabel + func _ready() -> void: if simulation_manager == null: push_error("VillageUI: simulation_manager missing") @@ -19,8 +20,10 @@ func _ready() -> void: if "village" in simulation_manager: _on_village_changed(simulation_manager.village) + func _on_village_changed(village: SimVillage) -> void: - village_stats_label.text = """ + village_stats_label.text = ( + """ Village Food: %s @@ -32,13 +35,15 @@ func _on_village_changed(village: SimVillage) -> void: Food Mod: %.2f Safety Mod: %.2f Knowledge Mod: %.2f - """ % [ - round(village.food), - round(village.wood), - round(village.safety), - round(village.knowledge), - simulation_manager.get_starving_count(), - village.food_modifier, - village.safety_modifier, - village.knowledge_modifier - ] + """ + % [ + round(village.food), + round(village.wood), + round(village.safety), + round(village.knowledge), + simulation_manager.get_starving_count(), + village.food_modifier, + village.safety_modifier, + village.knowledge_modifier + ] + ) diff --git a/world/world_view_manager.gd b/world/world_view_manager.gd index b1e77f1..cf5b674 100644 --- a/world/world_view_manager.gd +++ b/world/world_view_manager.gd @@ -8,25 +8,24 @@ const DEBUG_LOGS := true var active_npc_visuals := {} + func debug_log(message: String) -> void: if DEBUG_LOGS: print("[WorldViewManager] ", message) + func _ready() -> void: call_deferred("initialize_world_view") + func initialize_world_view() -> void: await get_tree().physics_frame if simulation_manager.has_signal("npc_target_requested"): - simulation_manager.npc_target_requested.connect( - _on_npc_target_requested - ) + simulation_manager.npc_target_requested.connect(_on_npc_target_requested) else: push_error("WorldViewManager: missing npc_target_requested signal") if simulation_manager.has_signal("npc_travel_requested"): - simulation_manager.npc_travel_requested.connect( - _on_npc_travel_requested - ) + simulation_manager.npc_travel_requested.connect(_on_npc_travel_requested) else: push_error("WorldViewManager: missing npc_travel_requested signal") if simulation_manager.has_signal("npc_died"): @@ -34,19 +33,16 @@ func initialize_world_view() -> void: else: push_error("WorldViewManager: SimulationManager has no npc_died signal") if simulation_manager.has_signal("npc_inventory_changed"): - simulation_manager.npc_inventory_changed.connect( - _on_npc_inventory_changed - ) + simulation_manager.npc_inventory_changed.connect(_on_npc_inventory_changed) else: - push_error( - "WorldViewManager: SimulationManager has no npc_inventory_changed signal" - ) + push_error("WorldViewManager: SimulationManager has no npc_inventory_changed signal") if simulation_manager.has_signal("state_restored"): simulation_manager.state_restored.connect(_on_simulation_state_restored) else: push_error("WorldViewManager: SimulationManager has no state_restored signal") spawn_initial_npcs() + func spawn_initial_npcs() -> void: if simulation_manager == null: push_error("WorldViewManager: simulation_manager is missing") @@ -61,6 +57,7 @@ func spawn_initial_npcs() -> void: for npc in simulation_manager.npcs: spawn_npc_visual(npc) + func spawn_npc_visual(npc: SimNPC) -> void: if active_npc_visuals.has(npc.id): return @@ -87,18 +84,17 @@ func spawn_npc_visual(npc: SimNPC) -> void: elif npc.task_state == SimNPC.TASK_STATE_TRAVELING: simulation_manager.request_current_travel(npc.id) + func despawn_npc_visual(npc_id: int) -> bool: var visual = active_npc_visuals.get(npc_id) as Node3D if visual == null: return false - simulation_manager.synchronize_npc_position( - npc_id, - visual.global_position - ) + simulation_manager.synchronize_npc_position(npc_id, visual.global_position) active_npc_visuals.erase(npc_id) visual.queue_free() return true + func reload_npc_visual(npc_id: int) -> bool: if active_npc_visuals.has(npc_id): return false @@ -108,25 +104,22 @@ func reload_npc_visual(npc_id: int) -> bool: return true return false + func _on_npc_target_requested(npc: SimNPC) -> void: var visual = active_npc_visuals.get(npc.id) as Node3D if visual == null: return - if not simulation_manager.resolve_npc_target( - npc.id, - visual.global_position - ): + if not simulation_manager.resolve_npc_target(npc.id, visual.global_position): debug_log("Target resolution failed for %s" % npc.npc_name) -func _on_npc_travel_requested( - npc: SimNPC, - target_position: Vector3 -) -> void: + +func _on_npc_travel_requested(npc: SimNPC, target_position: Vector3) -> void: var visual = active_npc_visuals.get(npc.id) if visual == null: return visual.set_target_position(target_position) + func _on_npc_visual_arrived(sim_id: int) -> void: debug_log("NPC visual arrived. sim_id=%s" % sim_id) if simulation_manager == null: @@ -134,12 +127,10 @@ func _on_npc_visual_arrived(sim_id: int) -> void: return var visual = active_npc_visuals.get(sim_id) as Node3D if visual != null: - simulation_manager.synchronize_npc_position( - sim_id, - visual.global_position - ) + simulation_manager.synchronize_npc_position(sim_id, visual.global_position) simulation_manager.notify_npc_arrived(sim_id) + func _on_npc_visual_navigation_failed(sim_id: int) -> void: debug_log("NPC visual navigation failed. sim_id=%s" % sim_id) if simulation_manager == null: @@ -147,18 +138,14 @@ func _on_npc_visual_navigation_failed(sim_id: int) -> void: return var visual = active_npc_visuals.get(sim_id) as Node3D if visual != null: - simulation_manager.synchronize_npc_position( - sim_id, - visual.global_position - ) + simulation_manager.synchronize_npc_position(sim_id, visual.global_position) simulation_manager.notify_npc_navigation_failed(sim_id) -func _on_npc_visual_position_changed( - sim_id: int, - active_position: Vector3 -) -> void: + +func _on_npc_visual_position_changed(sim_id: int, active_position: Vector3) -> void: simulation_manager.synchronize_npc_position(sim_id, active_position) + func _on_npc_died(npc: SimNPC) -> void: if not active_npc_visuals.has(npc.id): return @@ -168,11 +155,8 @@ func _on_npc_died(npc: SimNPC) -> void: else: push_error("WorldViewManager: NPCVisual has no apply_dead_visual_state") -func _on_npc_inventory_changed( - npc: SimNPC, - item_id: StringName, - amount: float -) -> void: + +func _on_npc_inventory_changed(npc: SimNPC, item_id: StringName, amount: float) -> void: if item_id != SimulationIds.RESOURCE_FOOD: return var visual = active_npc_visuals.get(npc.id) @@ -181,9 +165,8 @@ func _on_npc_inventory_changed( if visual.has_method("set_carried_food_visible"): visual.set_carried_food_visible(amount > 0.0) else: - push_error( - "WorldViewManager: NPCVisual has no set_carried_food_visible method" - ) + push_error("WorldViewManager: NPCVisual has no set_carried_food_visible method") + func _on_simulation_state_restored() -> void: for visual in active_npc_visuals.values():