From 23cf53fa1418f5e048002c49931b2c5de0071406 Mon Sep 17 00:00:00 2001 From: Voyager Date: Wed, 2 Sep 2026 17:20:25 -0400 Subject: [PATCH] Add seasonal tree palettes --- main/main.gd | 22 ++ tests/generated_world_runtime_validation.gd | 123 +++++++- tests/terrain_seasonal_material_validation.gd | 274 ++++++++++++++++++ ...errain_seasonal_material_validation.gd.uid | 1 + tests/world_layout_validation.gd | 11 + world/generation/generated_world_region.gd | 248 +++++++++++++++- world/generation/props/assets/prop_palm.glb | Bin 26244 -> 26268 bytes world/generation/props/assets/prop_pine.glb | Bin 10168 -> 10172 bytes .../props/assets/prop_pine_large.glb | Bin 9500 -> 9488 bytes world/generation/props/assets/prop_tree_1.glb | Bin 8416 -> 8420 bytes world/generation/props/assets/prop_tree_2.glb | Bin 6328 -> 6336 bytes world/generation/props/assets/prop_tree_3.glb | Bin 7284 -> 7292 bytes .../props/assets/prop_tree_large.glb | Bin 13200 -> 13204 bytes .../props/definitions/prop_palm.tres | 7 +- .../props/definitions/prop_pine.tres | 7 +- .../props/definitions/prop_pine_large.tres | 7 +- .../props/definitions/prop_tree_1.tres | 10 +- .../props/definitions/prop_tree_2.tres | 10 +- .../props/definitions/prop_tree_3.tres | 10 +- .../props/definitions/prop_tree_large.tres | 10 +- .../props/materials/leaf_autumn.tres | 9 + .../props/materials/leaf_autumn_dark.tres | 9 + .../props/materials/leaf_autumn_light.tres | 9 + .../props/materials/leaf_autumn_mid.tres | 9 + .../generation/props/materials/leaf_dark.tres | 2 +- .../props/materials/leaf_light.tres | 2 +- .../generation/props/materials/leaf_mid.tres | 2 +- .../props/materials/palm_autumn.tres | 9 + .../props/materials/palm_summer.tres | 9 + .../props/materials/pine_autumn.tres | 9 + .../props/materials/pine_summer.tres | 9 + world/generation/terrain_prop_catalog.gd | 36 ++- world/generation/terrain_prop_definition.gd | 32 ++ world/regions/world_region.gd | 4 + world/test_world.gd | 14 + 35 files changed, 861 insertions(+), 43 deletions(-) create mode 100644 tests/terrain_seasonal_material_validation.gd create mode 100644 tests/terrain_seasonal_material_validation.gd.uid create mode 100644 world/generation/props/materials/leaf_autumn.tres create mode 100644 world/generation/props/materials/leaf_autumn_dark.tres create mode 100644 world/generation/props/materials/leaf_autumn_light.tres create mode 100644 world/generation/props/materials/leaf_autumn_mid.tres create mode 100644 world/generation/props/materials/palm_autumn.tres create mode 100644 world/generation/props/materials/palm_summer.tres create mode 100644 world/generation/props/materials/pine_autumn.tres create mode 100644 world/generation/props/materials/pine_summer.tres diff --git a/main/main.gd b/main/main.gd index 20a07da..c6dda6c 100644 --- a/main/main.gd +++ b/main/main.gd @@ -108,6 +108,7 @@ const NetworkProfileServiceType = preload( "res://network/network_profile_service.gd" ) const WorldTimeServiceType = preload("res://world/world_time_service.gd") +const CalendarSeasonType = preload("res://world/calendar_season.gd") const WorldTimeVisualControllerType = preload( "res://world/world_time_visual_controller.gd" ) @@ -406,6 +407,20 @@ func _on_world_weather_changed( _apply_weather_visual_motion(weather) +func _on_world_calendar_date_changed(_date_id: String) -> void: + _apply_world_calendar_season() + + +func _apply_world_calendar_season() -> void: + var season: int = _world_time.get_season() + if season == CalendarSeasonType.UNKNOWN: + var system_date: Dictionary = Time.get_date_dict_from_system() + season = CalendarSeasonType.from_month( + int(system_date.get("month", 0)) + ) + _test_world.set_calendar_season(season) + + func _apply_weather_visual_motion( weather: WorldWeatherService.Weather, ) -> void: @@ -659,6 +674,13 @@ func _initialize_application(dedicated: bool) -> void: if _application_initialized: return _application_initialized = true + if not _world_time.calendar_date_changed.is_connected( + _on_world_calendar_date_changed + ): + _world_time.calendar_date_changed.connect( + _on_world_calendar_date_changed + ) + _apply_world_calendar_season() _player.global_transform = _test_world.get_player_spawn_transform() _player.velocity = Vector3.ZERO _player_spawn_service.setup( diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 524e7b3..fcf6526 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -21,12 +21,16 @@ const Gatherables: GatherableCatalog = preload( const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) +const CalendarSeasonType = preload("res://world/calendar_season.gd") const FOLIAGE_WIND_SHADER: Shader = preload( "res://world/materials/foliage_wind.gdshader" ) const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = ( &"foliage_wind_source_materials" ) +const PROP_PRIMARY_MATERIAL_ROLL_META: StringName = ( + &"terrain_prop_primary_material_roll" +) const FIRST_SEED := 13001 const SECOND_SEED := 13002 const RIVER_FALLBACK_SEED := 13012 @@ -81,6 +85,7 @@ func _run() -> void: configured_generator.elevated_cliff_third_tier_base_chance = 0.0 configured_generator.elevated_cliff_third_tier_stack_chance = 1.0 configured_generator.elevated_cliff_double_third_tier_chance = 0.0 + region.set_calendar_season(CalendarSeasonType.Season.FALL) root.add_child(region) await process_frame assert(region.generate_world(FIRST_SEED)) @@ -757,24 +762,23 @@ func _validate_generated_region( float(child.get_meta(&"terrain_prop_visual_scale", 1.0)) ) tree_yaws[snappedf((child as Node3D).rotation.y, 0.01)] = true - if prop_id in [ - &"prop_tree_1", - &"prop_tree_2", - &"prop_tree_3", - &"prop_tree_large", - ]: + if not definition.variant_material_slot_names.is_empty(): assert( _has_material_variant_override( child.get_node_or_null("Visual"), - definition.material_variants, + definition.primary_material_variants_for_season( + CalendarSeasonType.Season.FALL + ), ) ) + if not definition.secondary_material_variants.is_empty(): assert( _has_material_variant_override( child.get_node_or_null("Visual"), definition.secondary_material_variants, ) ) + _validate_seasonal_foliage_switch(region, decorations) assert(not palm_clusters.is_empty()) for members: Array in palm_clusters.values(): assert(members.size() >= 1 and members.size() <= 3) @@ -1169,17 +1173,33 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void: assert(tree.minimum_visual_scale < tree.maximum_visual_scale) assert(tree.material_variants.size() >= 3) assert(not tree.variant_material_slot_names.is_empty()) + assert(tree.summer_material_variants.size() == 3) + assert(tree.fall_material_variants.size() == 4) + assert( + tree.primary_material_variants_for_season( + CalendarSeasonType.Season.SPRING + ) == tree.summer_material_variants + ) + assert( + tree.primary_material_variants_for_season( + CalendarSeasonType.Season.WINTER + ) == tree.summer_material_variants + ) assert(tree.secondary_material_variants.size() >= 3) assert(not tree.secondary_variant_material_slot_names.is_empty()) var pine := catalog.definition_for_id(&"prop_pine") assert(pine != null) assert(pine.minimum_visual_scale < pine.maximum_visual_scale) assert(pine.material_variants.is_empty()) + assert(pine.summer_material_variants.size() == 1) + assert(pine.fall_material_variants.size() == 1) assert(not pine.has_gatherable_surface()) var large_pine := catalog.definition_for_id(&"prop_pine_large") assert(large_pine != null) assert(large_pine.minimum_visual_scale < large_pine.maximum_visual_scale) assert(large_pine.material_variants.is_empty()) + assert(large_pine.summer_material_variants.size() == 1) + assert(large_pine.fall_material_variants.size() == 1) assert(not large_pine.has_gatherable_surface()) var palm := catalog.definition_for_id(&"prop_palm") assert(palm != null) @@ -1189,6 +1209,8 @@ func _validate_prop_catalog(region: GeneratedWorldRegion) -> void: assert(palm.maximum_cluster_size == 3) assert(palm.minimum_cluster_radius > palm.clearance_radius) assert(palm.maximum_cluster_radius > palm.minimum_cluster_radius) + assert(palm.summer_material_variants.size() == 1) + assert(palm.fall_material_variants.size() == 1) assert(palm.prefer_ocean_facing) assert(not palm.local_overhang_direction.is_zero_approx()) var aligned_tree_collisions: Dictionary[StringName, Vector2] = { @@ -1594,6 +1616,82 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D: return null +func _validate_seasonal_foliage_switch( + region: GeneratedWorldRegion, + decorations: Node3D, +) -> void: + var original_signature := _decoration_signature(region) + var expected_rolls: Dictionary[String, float] = {} + var found_broadleaf := false + var found_pine := false + var found_palm := false + for prop_node: Node in decorations.get_children(): + var fall_prop := prop_node as Node3D + var prop_id := StringName(fall_prop.get_meta(&"terrain_prop_id", &"")) + var fall_definition := region.get_prop_catalog().definition_for_id(prop_id) + if ( + fall_definition == null + or fall_definition.variant_material_slot_names.is_empty() + ): + continue + assert(fall_prop.has_meta(PROP_PRIMARY_MATERIAL_ROLL_META)) + var roll := float(fall_prop.get_meta(PROP_PRIMARY_MATERIAL_ROLL_META)) + assert(roll >= 0.0 and roll < 1.0) + expected_rolls[fall_prop.name] = roll + assert(_has_material_variant_override( + fall_prop.get_node_or_null("Visual"), + fall_definition.primary_material_variants_for_season( + CalendarSeasonType.Season.FALL + ), + )) + found_broadleaf = found_broadleaf or String(prop_id).begins_with( + "prop_tree_" + ) + found_pine = found_pine or prop_id in [ + &"prop_pine", + &"prop_pine_large", + ] + found_palm = found_palm or prop_id == &"prop_palm" + assert(found_broadleaf and found_pine and found_palm) + + region.set_calendar_season(CalendarSeasonType.Season.SUMMER) + assert(_decoration_signature(region) == original_signature) + for prop_node: Node in decorations.get_children(): + var summer_prop := prop_node as Node3D + if not expected_rolls.has(summer_prop.name): + continue + assert(is_equal_approx( + float(summer_prop.get_meta(PROP_PRIMARY_MATERIAL_ROLL_META)), + expected_rolls[summer_prop.name], + )) + var summer_definition := region.get_prop_catalog().definition_for_id( + StringName(summer_prop.get_meta(&"terrain_prop_id", &"")) + ) + assert(_has_material_variant_override( + summer_prop.get_node_or_null("Visual"), + summer_definition.primary_material_variants_for_season( + CalendarSeasonType.Season.SUMMER + ), + )) + assert(_first_foliage_wind_material(decorations) != null) + + region.set_calendar_season(CalendarSeasonType.Season.FALL) + assert(_decoration_signature(region) == original_signature) + for prop_node: Node in decorations.get_children(): + var restored_prop := prop_node as Node3D + if not expected_rolls.has(restored_prop.name): + continue + var restored_definition := region.get_prop_catalog().definition_for_id( + StringName(restored_prop.get_meta(&"terrain_prop_id", &"")) + ) + assert(_has_material_variant_override( + restored_prop.get_node_or_null("Visual"), + restored_definition.primary_material_variants_for_season( + CalendarSeasonType.Season.FALL + ), + )) + + func _first_foliage_wind_material(root_node: Node) -> ShaderMaterial: var mesh_instance := root_node as MeshInstance3D if mesh_instance != null and mesh_instance.mesh != null: @@ -1652,8 +1750,15 @@ func _has_material_variant_override( surface_index, null, ) as Material - if source_material != null and source_material in variants: - return true + var source_standard := source_material as StandardMaterial3D + if source_standard != null and source_material in variants: + var visible_color: Color = wind_material.get_shader_parameter( + "albedo_color" + ) + if visible_color.is_equal_approx( + source_standard.albedo_color + ): + return true for child: Node in root_node.get_children(): if _has_material_variant_override(child, variants): return true diff --git a/tests/terrain_seasonal_material_validation.gd b/tests/terrain_seasonal_material_validation.gd new file mode 100644 index 0000000..4771e52 --- /dev/null +++ b/tests/terrain_seasonal_material_validation.gd @@ -0,0 +1,274 @@ +extends SceneTree + +const CalendarSeasonType = preload("res://world/calendar_season.gd") +const PropCatalog: TerrainPropCatalog = preload( + "res://world/generation/props/terrain_prop_catalog.tres" +) +const RegionScene: PackedScene = preload( + "res://world/generation/generated_world_region.tscn" +) + +const BROADLEAF_IDS: Array[StringName] = [ + &"prop_tree_1", + &"prop_tree_2", + &"prop_tree_3", + &"prop_tree_large", +] +const IMPORTED_FOLIAGE_NAMES: Dictionary = { + &"prop_tree_1": &"leaf_autumn_light", + &"prop_tree_2": &"leaf_autumn", + &"prop_tree_3": &"leaf_autumn_dark", + &"prop_tree_large": &"leaf_autumn_mid", + &"prop_pine": &"pine_summer", + &"prop_pine_large": &"pine_autumn", + &"prop_palm": &"palm_autumn", +} +const PROP_PRIMARY_MATERIAL_ROLL_META: StringName = ( + &"terrain_prop_primary_material_roll" +) +const PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META: StringName = ( + &"terrain_prop_legacy_primary_material_index" +) + + +func _initialize() -> void: + call_deferred(&"_run") + + +func _run() -> void: + assert(PropCatalog.validation_errors().is_empty()) + for stable_id: StringName in BROADLEAF_IDS: + var definition := PropCatalog.definition_for_id(stable_id) + assert(definition != null) + _assert_palette( + definition.summer_material_variants, + PackedStringArray([ + "leaf_variant_light", + "leaf_variant_mid", + "leaf_variant_dark", + ]), + [ + Color(0.3387566, 0.7186728, 0, 1), + Color(0.3146831, 0.4631305, 0, 1), + Color(0.1712407, 0.2520192, 0, 1), + ], + ) + _assert_palette( + definition.fall_material_variants, + PackedStringArray([ + "leaf_autumn_light", + "leaf_autumn", + "leaf_autumn_mid", + "leaf_autumn_dark", + ]), + [ + Color(0.8222013, 0.794216, 0, 1), + Color(0.6186816, 0.4523746, 0, 1), + Color(0.6841143, 0.3515543, 0, 1), + Color(0.3853541, 0.00134692, 0, 1), + ], + ) + assert( + definition.primary_material_variants_for_season( + CalendarSeasonType.Season.SPRING + ) == definition.summer_material_variants + ) + assert( + definition.primary_material_variants_for_season( + CalendarSeasonType.Season.WINTER + ) == definition.summer_material_variants + ) + + _assert_single_seasonal_tree( + &"prop_pine", + &"pine_summer", + Color(0.02171588, 0.2242201, 0.1360403, 1), + &"pine_autumn", + Color(0.02388079, 0.1285609, 0, 1), + ) + _assert_single_seasonal_tree( + &"prop_pine_large", + &"pine_summer", + Color(0.02171588, 0.2242201, 0.1360403, 1), + &"pine_autumn", + Color(0.02388079, 0.1285609, 0, 1), + ) + _assert_single_seasonal_tree( + &"prop_palm", + &"palm_summer", + Color(0.3146831, 0.4631305, 0, 1), + &"palm_autumn", + Color(0.2951757, 0.3575695, 0.162893, 1), + ) + + for stable_id: StringName in IMPORTED_FOLIAGE_NAMES: + var definition := PropCatalog.definition_for_id(stable_id) + var instance := definition.packed_scene.instantiate() + var material_names := PackedStringArray() + _collect_material_names(instance, material_names) + assert(IMPORTED_FOLIAGE_NAMES[stable_id] in material_names) + instance.free() + + var region := RegionScene.instantiate() as GeneratedWorldRegion + assert(region != null) + assert(bool(region.call( + "_material_name_matches_targets", + &"leaf_autumn_mid", + PackedStringArray(["leaf"]), + ))) + assert(bool(region.call( + "_material_name_matches_targets", + &"pine_winter", + PackedStringArray(["pine"]), + ))) + assert(bool(region.call( + "_material_name_matches_targets", + &"palm_spring", + PackedStringArray(["palm"]), + ))) + _validate_palette_roll_mapping(region) + _validate_primary_roll_random_state(region) + region.free() + print("Terrain seasonal material validation: PASS") + quit() + + +func _assert_single_seasonal_tree( + stable_id: StringName, + summer_name: StringName, + summer_color: Color, + fall_name: StringName, + fall_color: Color, +) -> void: + var definition := PropCatalog.definition_for_id(stable_id) + assert(definition != null) + _assert_palette( + definition.summer_material_variants, + PackedStringArray([summer_name]), + [summer_color], + ) + _assert_palette( + definition.fall_material_variants, + PackedStringArray([fall_name]), + [fall_color], + ) + + +func _assert_palette( + materials: Array[Material], + expected_names: PackedStringArray, + expected_colors: Array[Color], +) -> void: + assert(materials.size() == expected_names.size()) + assert(materials.size() == expected_colors.size()) + for index: int in materials.size(): + var material := materials[index] as StandardMaterial3D + assert(material != null) + assert(material.resource_name == expected_names[index]) + assert(material.albedo_color.is_equal_approx(expected_colors[index])) + + +func _validate_palette_roll_mapping(region: GeneratedWorldRegion) -> void: + var definition := PropCatalog.definition_for_id(&"prop_tree_1") + assert(definition != null) + var instance := definition.packed_scene.instantiate() + var palette := definition.fall_material_variants + for index: int in palette.size(): + var roll := (float(index) + 0.5) / float(palette.size()) + region.call( + "_apply_material_variant_for_roll", + instance, + definition.variant_material_slot_names, + palette, + roll, + ) + assert(_has_surface_override(instance, palette[index])) + instance.free() + + +func _validate_primary_roll_random_state( + region: GeneratedWorldRegion, +) -> void: + var broadleaf := PropCatalog.definition_for_id(&"prop_tree_1") + var baseline := RandomNumberGenerator.new() + baseline.seed = 29092026 + var actual := RandomNumberGenerator.new() + actual.seed = baseline.seed + var legacy_index := baseline.randi_range( + 0, + broadleaf.material_variants.size() - 1, + ) + var selection := region.call( + "_primary_material_selection", + broadleaf, + actual, + ) as Dictionary + var roll := float(selection.get("roll", -1.0)) + assert(roll >= 0.0 and roll < 1.0) + assert(int(selection.get("legacy_index", -1)) == legacy_index) + assert(actual.state == baseline.state) + var prop := Node3D.new() + prop.set_meta(PROP_PRIMARY_MATERIAL_ROLL_META, 0.125) + prop.set_meta( + PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META, + legacy_index, + ) + var summer_roll := float(region.call( + "_primary_material_roll_for_palette", + prop, + broadleaf, + broadleaf.summer_material_variants, + )) + assert( + floori(summer_roll * broadleaf.summer_material_variants.size()) + == legacy_index + ) + var fall_roll := float(region.call( + "_primary_material_roll_for_palette", + prop, + broadleaf, + broadleaf.fall_material_variants, + )) + assert(is_equal_approx(fall_roll, 0.125)) + prop.free() + + var pine := PropCatalog.definition_for_id(&"prop_pine") + var pine_random := RandomNumberGenerator.new() + pine_random.seed = 29092026 + var pine_initial_state := pine_random.state + var pine_selection := region.call( + "_primary_material_selection", + pine, + pine_random, + ) as Dictionary + assert(int(pine_selection.get("legacy_index", -2)) == -1) + assert(pine_random.state == pine_initial_state) + + +func _has_surface_override(root_node: Node, expected: Material) -> bool: + var mesh_instance := root_node as MeshInstance3D + if mesh_instance != null and mesh_instance.mesh != null: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + if ( + mesh_instance.get_surface_override_material(surface_index) + == expected + ): + return true + for child: Node in root_node.get_children(): + if _has_surface_override(child, expected): + return true + return false + + +func _collect_material_names( + node: Node, + material_names: PackedStringArray, +) -> void: + var mesh_instance := node as MeshInstance3D + if mesh_instance != null and mesh_instance.mesh != null: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.mesh.surface_get_material(surface_index) + if material != null: + material_names.append(material.resource_name) + for child: Node in node.get_children(): + _collect_material_names(child, material_names) diff --git a/tests/terrain_seasonal_material_validation.gd.uid b/tests/terrain_seasonal_material_validation.gd.uid new file mode 100644 index 0000000..a1c59b9 --- /dev/null +++ b/tests/terrain_seasonal_material_validation.gd.uid @@ -0,0 +1 @@ +uid://dicorkv8nbkb5 diff --git a/tests/world_layout_validation.gd b/tests/world_layout_validation.gd index 473b45e..66047c8 100644 --- a/tests/world_layout_validation.gd +++ b/tests/world_layout_validation.gd @@ -4,6 +4,7 @@ const TestWorldScene: PackedScene = preload("res://world/test_world.tscn") const SetupPageScene: PackedScene = preload( "res://ui/new_game_setup_page.tscn" ) +const CalendarSeasonType = preload("res://world/calendar_season.gd") func _initialize() -> void: @@ -84,6 +85,7 @@ func _validate_world_switching() -> void: root.add_child(world) await process_frame assert(world.get_world_layout() == WorldLayout.GENERATED) + world.set_calendar_season(CalendarSeasonType.Season.FALL) world.set_foliage_wind_strength_multiplier(2.0) world.set_water_motion_strength_multiplier(2.0) var initial_generator := world.get_node( @@ -134,6 +136,15 @@ func _validate_world_switching() -> void: "Regions/GeneratedWorldRegion" ) as GeneratedWorldRegion assert(generated_region != null) + assert( + int(generated_region.get("_calendar_season")) + == CalendarSeasonType.Season.FALL + ) + world.set_calendar_season(CalendarSeasonType.Season.SUMMER) + assert( + int(generated_region.get("_calendar_season")) + == CalendarSeasonType.Season.SUMMER + ) assert(is_equal_approx( float(generated_region.get("_foliage_wind_strength_multiplier")), 2.0 )) diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 6ba1786..c06d5db 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -20,6 +20,7 @@ const RVUpgradeInteractionType = preload( const PrecipitationOcclusionType = preload( "res://world/environment/precipitation_occlusion.gd" ) +const CalendarSeasonType = preload("res://world/calendar_season.gd") const FOLIAGE_WIND_SHADER: Shader = preload( "res://world/materials/foliage_wind.gdshader" ) @@ -88,7 +89,14 @@ const FOLIAGE_MATERIAL_NAMES: Array[StringName] = [ &"leaf_variant_light", &"leaf_variant_mid", &"leaf_variant_dark", + &"palm", ] +const PROP_PRIMARY_MATERIAL_ROLL_META: StringName = ( + &"terrain_prop_primary_material_roll" +) +const PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META: StringName = ( + &"terrain_prop_legacy_primary_material_index" +) # Retain the exact generated material override while foliage wind is active so # profile changes restore both the selected palette and authored surface state. const FOLIAGE_WIND_SOURCE_MATERIALS_META: StringName = ( @@ -138,6 +146,7 @@ var _water_recovery_triangles_by_coordinate: Dictionary[Vector2i, Array] = {} var _home_accessible_elevations_by_coordinate: Dictionary[Vector2i, Array] = {} var _foliage_wind_enabled: bool = false var _foliage_wind_strength_multiplier: float = 1.0 +var _calendar_season: int = CalendarSeasonType.UNKNOWN func _ready() -> void: @@ -148,6 +157,11 @@ func _ready() -> void: _set_foliage_wind_enabled(not _light_performance_profile) +func _exit_tree() -> void: + if _decorations != null: + _apply_foliage_wind_to_node(_decorations, false) + + func generate_world(world_seed: int) -> bool: if world_seed <= 0 or world_seed > PlayerSaveManager.MAX_WORLD_SEED: return false @@ -232,6 +246,18 @@ func set_water_motion_strength_multiplier(multiplier: float) -> void: surface_motion.set_motion_strength_multiplier(multiplier) +func set_calendar_season(season: int) -> void: + var normalized_season := ( + season + if season >= 0 and season < CalendarSeasonType.Season.size() + else CalendarSeasonType.UNKNOWN + ) + if normalized_season == _calendar_season: + return + _calendar_season = normalized_season + _refresh_seasonal_prop_materials() + + func _get_ocean_surface_motion() -> WaterSurfaceMotion: return _ocean.get_node_or_null(^"VisualWater") as WaterSurfaceMotion @@ -319,6 +345,7 @@ func _on_generation_completed(summary: Dictionary) -> void: _cache_home_accessible_elevations(records) _assign_biomes(records, summary) var spawn_position := Vector3.ZERO + _apply_foliage_wind_to_node(_decorations, false) _clear_children(_decorations) _clear_children(_tree_anchors) _placed_prop_positions.clear() @@ -655,7 +682,7 @@ func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void: ) if not source_material is StandardMaterial3D: continue - if not FOLIAGE_MATERIAL_NAMES.has( + if not _is_foliage_material_name( StringName(source_material.resource_name) ): continue @@ -1156,6 +1183,18 @@ func _instantiate_prop( prop.set_meta(&"terrain_prop_visual_scale", visual_scale) prop.set_meta(&"terrain_prop_cluster_id", cluster_id) prop.set_meta(&"terrain_prop_cluster_member_index", cluster_member_index) + var primary_material_selection := _primary_material_selection( + definition, + random, + ) + prop.set_meta( + PROP_PRIMARY_MATERIAL_ROLL_META, + float(primary_material_selection.get("roll", 0.5)), + ) + prop.set_meta( + PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META, + int(primary_material_selection.get("legacy_index", -1)), + ) prop.position = placement prop.rotation.y = yaw _decorations.add_child(prop) @@ -1164,7 +1203,7 @@ func _instantiate_prop( visual_root.position = definition.visual_offset visual_root.scale = Vector3.ONE * visual_scale _configure_prop_visuals(visual_root) - _apply_prop_material_variant(visual_root, definition, random) + _apply_prop_material_variants(prop, visual_root, definition, random) _add_prop_collision(prop, definition, visual_scale) _placed_prop_positions.append(placement) _placed_prop_clearance_radii.append( @@ -1526,16 +1565,24 @@ func _configure_prop_visuals(root_node: Node) -> void: _configure_prop_visuals(child) -func _apply_prop_material_variant( +func _apply_prop_material_variants( + prop: Node3D, root_node: Node, definition: TerrainPropDefinition, random: RandomNumberGenerator, ) -> void: - _apply_random_material_variant( + var primary_variants := definition.primary_material_variants_for_season( + _calendar_season + ) + _apply_material_variant_for_roll( root_node, definition.variant_material_slot_names, - definition.material_variants, - random, + primary_variants, + _primary_material_roll_for_palette( + prop, + definition, + primary_variants, + ), ) _apply_random_material_variant( root_node, @@ -1545,6 +1592,69 @@ func _apply_prop_material_variant( ) +func _primary_material_selection( + definition: TerrainPropDefinition, + random: RandomNumberGenerator, +) -> Dictionary: + var preview_random := RandomNumberGenerator.new() + preview_random.state = random.state + var roll := preview_random.randf() + # Preserve the legacy foliage draw so this presentation-only feature never + # changes which props a known world seed places afterward. Retain its chosen + # index too, so existing summer worlds keep their exact historical tint. + var legacy_index := -1 + if not definition.material_variants.is_empty(): + legacy_index = random.randi_range( + 0, + definition.material_variants.size() - 1, + ) + # New seasonal-only definitions such as pine and palm peek at the current + # deterministic state without consuming it, preserving their old layout. + return { + "roll": roll, + "legacy_index": legacy_index, + } + + +func _primary_material_roll_for_palette( + prop: Node3D, + definition: TerrainPropDefinition, + variants: Array[Material], +) -> float: + var legacy_index := int(prop.get_meta( + PROP_LEGACY_PRIMARY_MATERIAL_INDEX_META, + -1, + )) + var uses_summer_palette := ( + not definition.summer_material_variants.is_empty() + and variants == definition.summer_material_variants + ) + if uses_summer_palette and legacy_index >= 0: + var clamped_index := mini(legacy_index, variants.size() - 1) + return (float(clamped_index) + 0.5) / float(variants.size()) + return float(prop.get_meta(PROP_PRIMARY_MATERIAL_ROLL_META, 0.5)) + + +func _apply_material_variant_for_roll( + root_node: Node, + target_material_names: PackedStringArray, + variants: Array[Material], + roll: float, +) -> void: + if variants.is_empty() or target_material_names.is_empty(): + return + var variant_index := mini( + floori(clampf(roll, 0.0, 0.999999) * float(variants.size())), + variants.size() - 1, + ) + _apply_material_variant_to_meshes( + root_node, + target_material_names, + variants[variant_index], + true, + ) + + func _apply_random_material_variant( root_node: Node, target_material_names: PackedStringArray, @@ -1558,6 +1668,7 @@ func _apply_random_material_variant( root_node, target_material_names, variant, + false, ) @@ -1565,16 +1676,28 @@ func _apply_material_variant_to_meshes( root_node: Node, target_material_names: PackedStringArray, variant: Material, + match_material_family: bool, ) -> void: var mesh_instance := root_node as MeshInstance3D if mesh_instance != null and mesh_instance.mesh != null: for surface_index: int in mesh_instance.mesh.get_surface_count(): - var active_material := mesh_instance.get_active_material(surface_index) - if ( - active_material != null - and active_material.resource_name in target_material_names - ): - mesh_instance.set_surface_override_material( + var source_material := mesh_instance.mesh.surface_get_material( + surface_index + ) + var material_matches := ( + source_material != null + and ( + _material_name_matches_targets( + StringName(source_material.resource_name), + target_material_names, + ) + if match_material_family + else source_material.resource_name in target_material_names + ) + ) + if material_matches: + _apply_material_variant_to_surface( + mesh_instance, surface_index, variant, ) @@ -1583,6 +1706,107 @@ func _apply_material_variant_to_meshes( child, target_material_names, variant, + match_material_family, + ) + + +func _apply_material_variant_to_surface( + mesh_instance: MeshInstance3D, + surface_index: int, + variant: Material, +) -> void: + var wind_material := mesh_instance.get_surface_override_material( + surface_index + ) as ShaderMaterial + var standard_variant := variant as StandardMaterial3D + if ( + wind_material != null + and wind_material.shader == FOLIAGE_WIND_SHADER + ): + # Seasonal foliage is intentionally authored as StandardMaterial3D. Keep + # the current wind presentation intact if a malformed catalog entry slips + # through instead of silently replacing it with an incompatible shader. + if standard_variant == null: + return + var source_materials: Dictionary = mesh_instance.get_meta( + FOLIAGE_WIND_SOURCE_MATERIALS_META, + {}, + ) + source_materials[surface_index] = variant + mesh_instance.set_meta( + FOLIAGE_WIND_SOURCE_MATERIALS_META, + source_materials, + ) + wind_material.set_shader_parameter( + "albedo_color", + standard_variant.albedo_color, + ) + wind_material.set_shader_parameter( + "material_roughness", + standard_variant.roughness, + ) + wind_material.set_shader_parameter( + "material_metallic", + standard_variant.metallic, + ) + return + mesh_instance.set_surface_override_material(surface_index, variant) + + +func _material_name_matches_targets( + material_name: StringName, + target_material_names: PackedStringArray, +) -> bool: + var normalized_name := String(material_name).to_lower() + for target_name: String in target_material_names: + var normalized_target := target_name.to_lower() + if ( + normalized_name == normalized_target + or normalized_name.begins_with(normalized_target + "_") + ): + return true + return false + + +func _is_foliage_material_name(material_name: StringName) -> bool: + var normalized_name := String(material_name).to_lower() + for target_name: StringName in FOLIAGE_MATERIAL_NAMES: + var normalized_target := String(target_name).to_lower() + if ( + normalized_name == normalized_target + or normalized_name.begins_with(normalized_target + "_") + ): + return true + return false + + +func _refresh_seasonal_prop_materials() -> void: + if _decorations == null: + return + for prop_node: Node in _decorations.get_children(): + var prop := prop_node as Node3D + if prop == null: + continue + var definition := prop_catalog.definition_for_id( + StringName(prop.get_meta(&"terrain_prop_id", &"")) + ) if prop_catalog != null else null + if definition == null: + continue + var visual_root := prop.get_node_or_null("Visual") + if visual_root == null: + continue + var primary_variants := definition.primary_material_variants_for_season( + _calendar_season + ) + _apply_material_variant_for_roll( + visual_root, + definition.variant_material_slot_names, + primary_variants, + _primary_material_roll_for_palette( + prop, + definition, + primary_variants, + ), ) diff --git a/world/generation/props/assets/prop_palm.glb b/world/generation/props/assets/prop_palm.glb index 6c34fb95d528d5077b259b6f52c1ba585d2204a4..0d2f32bd5fa0694e756f135560ce2d31995605b8 100644 GIT binary patch delta 111 zcmZoU%Q)vOBX4?6h#M0F1H+s&1_p_VyjQpj5_59n6H7}XD_8(Eqd z7#bNHSQr`^>KN!58kicGm|K{b7@Hax8=G5N0L2W9jLa;}O)QN~jEyZ#jf{;a-)EHA O9K?8lZLT@-162zHb0Y(DV*^7YQ%eIA0}CAk kJwppa6AKFiGc!v=Lo-Wr6CH!iA&fWJ7!@{aa}>q_0FLz)EC2ui diff --git a/world/generation/props/assets/prop_pine.glb b/world/generation/props/assets/prop_pine.glb index a9e560a82078591cc97545abdfec79e28d07c64a..eb6ee742a8b0bb5937a84d641006954cc14d8c7f 100644 GIT binary patch delta 43 zcmdntzsH|9JtxGCiGhJ(k2(Vb&qUrE?D55=xw)xDlRFpQ)+K1Ksg149cF z3u99YLrYU5b5jE|3u7GvJp)5i0}}%?V+#vYGgAvob4wE)gU#O=RTw8PU}c%SkY(ZI zJhWo1FQ;@ zV_1YIe`eL1EXgi7S)WZCL^GS4Sxh!y)0#XT$Tl=Jp1ht-XYzG+0cIlulgaOZYGm1j zSd2^zK#FWwObm_9CKmu1rWR%!2!G{LJEVM2!p-A8DzgBLf!o#nRemR^D}jqu_1uo-M&*YB@Mxe*!8g9A5}3Z*~5 zQ_ymyW?0$5sUK3sm_#?|{TpM%pLwv2R+6C%`grzR)P5IO1+t!e$AlanpAgK#cC`p^ KqblCGtp5Tiqbz+b7>R#y4RzATE9zX0i8AZ^ShKlv<+ z$mBFOt;yZ&f|D1qX@h7+v&jnVI+Hm$1X&D?EX^h_VAq=L0pge&Sxim@awf0|F&i0J uOjcyq2C-R;jSY>!s(9H2SxiigAzFNZ9FW5&$8uP2-XWpEv3UlEf&c(=$4W;4 delta 266 zcmaFj_`s1jJtxGCiGhLPfdT_V%|_lZMy{OH#I*RF%=C*C2~Eb^1D zvnoz51=8Ydij%hh>2NlY$ilgL0%3)79$fA3oyqA$T2oDGnt&st~Gfon;?scp%F;N rn%TtMe6k{k_GEr`K^9XJs9rx1$HLTXaw3QI<|6_M0-NV>Xm9`kUwlct diff --git a/world/generation/props/assets/prop_tree_2.glb b/world/generation/props/assets/prop_tree_2.glb index 08d21028ae0118294ae0b91794a6459e816edf47..06401b7b7402d3e0c13e00b3a87fca51e404f005 100644 GIT binary patch delta 272 zcmdmCc)*Z1JtxGCiGhLPfCK|W$wuBV#>pQUTPI&(l;zGzO-zeVEG;R`&6}LgsIO*h zVqk7zVPR-!Vq$7!Y-Xfmpl4`qWNL11U}S7+U}k7$Vrr^musM!Ng>f=7>(a?8Eb^0& zvnotZ2Ge|#Mc9NUF9Nb%*+eE^Wzn15#Jp(o3Rd~a&MXR(p9AS8Ag#tGKlvbw$mB>i zt;yBwf|IAPX@h7+lgR??I+K603o=`nnM|I*t~J?$Ly*PL(9~#hAds`4O_15p*mANU zyEcf;Vq|1s0ao=F$T2oBf@rZ}2Xai!EGGMM7;`Ep7zi*hIC=VQb{8&W-Q2-(fC~Vs CElMi@ delta 276 zcmX?LxWkY)JtxGCiGhJ(hXeyd&PLuaMwXn^#I(r*O!{gD76#@<2Ij^FhDN5A1||j; zItF@%7KSDk76xW!mWGCAmgXip29qx^nodq;Y~5VT#KSn5mvzx(Jr?=N=U5dc=Ynaz z$lh***K5QbB@3ZJl?qps%c|EKAWM3AA$?w24NR8p-(<~yB6WFvSA7B@pJey4$ zL^E3$noXVq6#LIE$YN+_Yy{@maR{;)8Je3-4rkYz+{Fgun45xRtXYf=Elj{lSb*w{ z%^;dZjZ6%5l#(h-QvK7?ic?FJtc-!)nH}2Igi4mOyN7W@u!l cW1weXYG`I*ZfR&}Xli6(Y-zT+fJwy=0HVYZApigX diff --git a/world/generation/props/definitions/prop_palm.tres b/world/generation/props/definitions/prop_palm.tres index c8021d4..3f547d9 100644 --- a/world/generation/props/definitions/prop_palm.tres +++ b/world/generation/props/definitions/prop_palm.tres @@ -1,7 +1,9 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=5 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_palm.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/palm_summer.tres" id="3_palm_summer"] +[ext_resource type="Material" path="res://world/generation/props/materials/palm_autumn.tres" id="4_palm_autumn"] [resource] script = ExtResource("1_definition") @@ -18,6 +20,9 @@ minimum_cluster_size = 2 maximum_cluster_size = 3 minimum_cluster_radius = 1.4 maximum_cluster_radius = 3.0 +variant_material_slot_names = PackedStringArray("leaf", "palm") +summer_material_variants = Array[Material]([ExtResource("3_palm_summer")]) +fall_material_variants = Array[Material]([ExtResource("4_palm_autumn")]) prefer_ocean_facing = true local_overhang_direction = Vector2(-0.883, 0.469) ocean_facing_spread_degrees = 55.0 diff --git a/world/generation/props/definitions/prop_pine.tres b/world/generation/props/definitions/prop_pine.tres index 5dd460a..7810dda 100644 --- a/world/generation/props/definitions/prop_pine.tres +++ b/world/generation/props/definitions/prop_pine.tres @@ -1,7 +1,9 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=5 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_pine.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/pine_summer.tres" id="3_pine_summer"] +[ext_resource type="Material" path="res://world/generation/props/materials/pine_autumn.tres" id="4_pine_autumn"] [resource] script = ExtResource("1_definition") @@ -15,6 +17,9 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.55 minimum_visual_scale = 0.65 maximum_visual_scale = 1.2 +variant_material_slot_names = PackedStringArray("pine") +summer_material_variants = Array[Material]([ExtResource("3_pine_summer")]) +fall_material_variants = Array[Material]([ExtResource("4_pine_autumn")]) collision_radius = 0.85 collision_height = 4.0 collision_offset = Vector3(-0.044, 0, 0.083) diff --git a/world/generation/props/definitions/prop_pine_large.tres b/world/generation/props/definitions/prop_pine_large.tres index bc70868..830e1b7 100644 --- a/world/generation/props/definitions/prop_pine_large.tres +++ b/world/generation/props/definitions/prop_pine_large.tres @@ -1,7 +1,9 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=3 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=5 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_pine_large.glb" id="2_scene"] +[ext_resource type="Material" path="res://world/generation/props/materials/pine_summer.tres" id="3_pine_summer"] +[ext_resource type="Material" path="res://world/generation/props/materials/pine_autumn.tres" id="4_pine_autumn"] [resource] script = ExtResource("1_definition") @@ -15,6 +17,9 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.65 minimum_visual_scale = 0.75 maximum_visual_scale = 1.2 +variant_material_slot_names = PackedStringArray("pine") +summer_material_variants = Array[Material]([ExtResource("3_pine_summer")]) +fall_material_variants = Array[Material]([ExtResource("4_pine_autumn")]) collision_radius = 0.75 collision_height = 9.5 collision_offset = Vector3(-0.04, 0, 0.075) diff --git a/world/generation/props/definitions/prop_tree_1.tres b/world/generation/props/definitions/prop_tree_1.tres index 4688578..77499e1 100644 --- a/world/generation/props/definitions/prop_tree_1.tres +++ b/world/generation/props/definitions/prop_tree_1.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=13 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_1.glb" id="2_scene"] @@ -8,6 +8,10 @@ [ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_light.tres" id="9_leaf_autumn_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn.tres" id="10_leaf_autumn"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_mid.tres" id="11_leaf_autumn_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_dark.tres" id="12_leaf_autumn_dark"] [resource] script = ExtResource("1_definition") @@ -21,8 +25,10 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.3 minimum_visual_scale = 0.65 maximum_visual_scale = 1.2 -variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +variant_material_slot_names = PackedStringArray("leaf") material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +summer_material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +fall_material_variants = Array[Material]([ExtResource("9_leaf_autumn_light"), ExtResource("10_leaf_autumn"), ExtResource("11_leaf_autumn_mid"), ExtResource("12_leaf_autumn_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.4 diff --git a/world/generation/props/definitions/prop_tree_2.tres b/world/generation/props/definitions/prop_tree_2.tres index a6e7895..10e8e1c 100644 --- a/world/generation/props/definitions/prop_tree_2.tres +++ b/world/generation/props/definitions/prop_tree_2.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=13 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_2.glb" id="2_scene"] @@ -8,6 +8,10 @@ [ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_light.tres" id="9_leaf_autumn_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn.tres" id="10_leaf_autumn"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_mid.tres" id="11_leaf_autumn_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_dark.tres" id="12_leaf_autumn_dark"] [resource] script = ExtResource("1_definition") @@ -21,8 +25,10 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.5 minimum_visual_scale = 0.7 maximum_visual_scale = 1.2 -variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +variant_material_slot_names = PackedStringArray("leaf") material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +summer_material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +fall_material_variants = Array[Material]([ExtResource("9_leaf_autumn_light"), ExtResource("10_leaf_autumn"), ExtResource("11_leaf_autumn_mid"), ExtResource("12_leaf_autumn_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.55 diff --git a/world/generation/props/definitions/prop_tree_3.tres b/world/generation/props/definitions/prop_tree_3.tres index 9f833c9..1657447 100644 --- a/world/generation/props/definitions/prop_tree_3.tres +++ b/world/generation/props/definitions/prop_tree_3.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=13 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_3.glb" id="2_scene"] @@ -8,6 +8,10 @@ [ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_light.tres" id="9_leaf_autumn_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn.tres" id="10_leaf_autumn"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_mid.tres" id="11_leaf_autumn_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_dark.tres" id="12_leaf_autumn_dark"] [resource] script = ExtResource("1_definition") @@ -21,8 +25,10 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 1.8 minimum_visual_scale = 0.7 maximum_visual_scale = 1.2 -variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +variant_material_slot_names = PackedStringArray("leaf") material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +summer_material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +fall_material_variants = Array[Material]([ExtResource("9_leaf_autumn_light"), ExtResource("10_leaf_autumn"), ExtResource("11_leaf_autumn_mid"), ExtResource("12_leaf_autumn_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 0.55 diff --git a/world/generation/props/definitions/prop_tree_large.tres b/world/generation/props/definitions/prop_tree_large.tres index ee91d62..112ed28 100644 --- a/world/generation/props/definitions/prop_tree_large.tres +++ b/world/generation/props/definitions/prop_tree_large.tres @@ -1,4 +1,4 @@ -[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=9 format=3] +[gd_resource type="Resource" script_class="TerrainPropDefinition" load_steps=13 format=3] [ext_resource type="Script" path="res://world/generation/terrain_prop_definition.gd" id="1_definition"] [ext_resource type="PackedScene" path="res://world/generation/props/assets/prop_tree_large.glb" id="2_scene"] @@ -8,6 +8,10 @@ [ext_resource type="Material" path="res://world/generation/props/materials/wood_light.tres" id="6_wood_light"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_mid.tres" id="7_wood_mid"] [ext_resource type="Material" path="res://world/generation/props/materials/wood_dark.tres" id="8_wood_dark"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_light.tres" id="9_leaf_autumn_light"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn.tres" id="10_leaf_autumn"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_mid.tres" id="11_leaf_autumn_mid"] +[ext_resource type="Material" path="res://world/generation/props/materials/leaf_autumn_dark.tres" id="12_leaf_autumn_dark"] [resource] script = ExtResource("1_definition") @@ -21,8 +25,10 @@ minimum_spawn_chunk_distance = 2 clearance_radius = 2.5 minimum_visual_scale = 0.75 maximum_visual_scale = 1.2 -variant_material_slot_names = PackedStringArray("leaf_light", "leaf", "leaf_dark") +variant_material_slot_names = PackedStringArray("leaf") material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +summer_material_variants = Array[Material]([ExtResource("3_leaf_light"), ExtResource("4_leaf_mid"), ExtResource("5_leaf_dark")]) +fall_material_variants = Array[Material]([ExtResource("9_leaf_autumn_light"), ExtResource("10_leaf_autumn"), ExtResource("11_leaf_autumn_mid"), ExtResource("12_leaf_autumn_dark")]) secondary_variant_material_slot_names = PackedStringArray("wood", "wood_light", "wood_mid") secondary_material_variants = Array[Material]([ExtResource("6_wood_light"), ExtResource("7_wood_mid"), ExtResource("8_wood_dark")]) collision_radius = 1.05 diff --git a/world/generation/props/materials/leaf_autumn.tres b/world/generation/props/materials/leaf_autumn.tres new file mode 100644 index 0000000..10cb76b --- /dev/null +++ b/world/generation/props/materials/leaf_autumn.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_autumn" +albedo_color = Color(0.6186816, 0.4523746, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_autumn_dark.tres b/world/generation/props/materials/leaf_autumn_dark.tres new file mode 100644 index 0000000..93a39c8 --- /dev/null +++ b/world/generation/props/materials/leaf_autumn_dark.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_autumn_dark" +albedo_color = Color(0.3853541, 0.00134692, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_autumn_light.tres b/world/generation/props/materials/leaf_autumn_light.tres new file mode 100644 index 0000000..9b42d13 --- /dev/null +++ b/world/generation/props/materials/leaf_autumn_light.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_autumn_light" +albedo_color = Color(0.8222013, 0.794216, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_autumn_mid.tres b/world/generation/props/materials/leaf_autumn_mid.tres new file mode 100644 index 0000000..7a4f0b5 --- /dev/null +++ b/world/generation/props/materials/leaf_autumn_mid.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "leaf_autumn_mid" +albedo_color = Color(0.6841143, 0.3515543, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/leaf_dark.tres b/world/generation/props/materials/leaf_dark.tres index d75f991..bbdcf55 100644 --- a/world/generation/props/materials/leaf_dark.tres +++ b/world/generation/props/materials/leaf_dark.tres @@ -2,7 +2,7 @@ [resource] resource_name = "leaf_variant_dark" -albedo_color = Color(0.1712, 0.252, 0, 1) +albedo_color = Color(0.1712407, 0.2520192, 0, 1) metallic = 0.0 roughness = 1.0 shading_mode = 1 diff --git a/world/generation/props/materials/leaf_light.tres b/world/generation/props/materials/leaf_light.tres index d49f786..06a69b9 100644 --- a/world/generation/props/materials/leaf_light.tres +++ b/world/generation/props/materials/leaf_light.tres @@ -2,7 +2,7 @@ [resource] resource_name = "leaf_variant_light" -albedo_color = Color(0.3388, 0.7187, 0, 1) +albedo_color = Color(0.3387566, 0.7186728, 0, 1) metallic = 0.0 roughness = 1.0 shading_mode = 1 diff --git a/world/generation/props/materials/leaf_mid.tres b/world/generation/props/materials/leaf_mid.tres index d0ee646..11e7577 100644 --- a/world/generation/props/materials/leaf_mid.tres +++ b/world/generation/props/materials/leaf_mid.tres @@ -2,7 +2,7 @@ [resource] resource_name = "leaf_variant_mid" -albedo_color = Color(0.3147, 0.4631, 0, 1) +albedo_color = Color(0.3146831, 0.4631305, 0, 1) metallic = 0.0 roughness = 1.0 shading_mode = 1 diff --git a/world/generation/props/materials/palm_autumn.tres b/world/generation/props/materials/palm_autumn.tres new file mode 100644 index 0000000..7956bc7 --- /dev/null +++ b/world/generation/props/materials/palm_autumn.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "palm_autumn" +albedo_color = Color(0.2951757, 0.3575695, 0.162893, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/palm_summer.tres b/world/generation/props/materials/palm_summer.tres new file mode 100644 index 0000000..9084f0d --- /dev/null +++ b/world/generation/props/materials/palm_summer.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "palm_summer" +albedo_color = Color(0.3146831, 0.4631305, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/pine_autumn.tres b/world/generation/props/materials/pine_autumn.tres new file mode 100644 index 0000000..8195f2c --- /dev/null +++ b/world/generation/props/materials/pine_autumn.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "pine_autumn" +albedo_color = Color(0.02388079, 0.1285609, 0, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/props/materials/pine_summer.tres b/world/generation/props/materials/pine_summer.tres new file mode 100644 index 0000000..79a3a47 --- /dev/null +++ b/world/generation/props/materials/pine_summer.tres @@ -0,0 +1,9 @@ +[gd_resource type="StandardMaterial3D" format=3] + +[resource] +resource_name = "pine_summer" +albedo_color = Color(0.02171588, 0.2242201, 0.1360403, 1) +metallic = 0.0 +roughness = 1.0 +shading_mode = 1 +texture_filter = 0 diff --git a/world/generation/terrain_prop_catalog.gd b/world/generation/terrain_prop_catalog.gd index ed37c68..e562f00 100644 --- a/world/generation/terrain_prop_catalog.gd +++ b/world/generation/terrain_prop_catalog.gd @@ -113,20 +113,40 @@ func validation_errors() -> PackedStringArray: "%s has an invalid loose-cluster radius." % definition.stable_id ) + var primary_material_palettes: Array[Array] = [ + definition.material_variants, + definition.spring_material_variants, + definition.summer_material_variants, + definition.fall_material_variants, + definition.winter_material_variants, + ] + var has_primary_material_variants := false + for palette: Array in primary_material_palettes: + has_primary_material_variants = ( + has_primary_material_variants or not palette.is_empty() + ) + for material: Material in palette: + if material == null: + errors.append( + "%s contains an empty primary material variant." + % definition.stable_id + ) + elif not material is StandardMaterial3D: + errors.append( + ( + "%s primary material variants must use " + + "StandardMaterial3D so foliage wind is retained." + ) + % definition.stable_id + ) if ( - not definition.material_variants.is_empty() + has_primary_material_variants and definition.variant_material_slot_names.is_empty() ): errors.append( - "%s has material variants but no target material slots." + "%s has primary material variants but no target material slots." % definition.stable_id ) - for material: Material in definition.material_variants: - if material == null: - errors.append( - "%s contains an empty material variant." - % definition.stable_id - ) if ( not definition.secondary_material_variants.is_empty() and definition.secondary_variant_material_slot_names.is_empty() diff --git a/world/generation/terrain_prop_definition.gd b/world/generation/terrain_prop_definition.gd index d2524eb..71f1c83 100644 --- a/world/generation/terrain_prop_definition.gd +++ b/world/generation/terrain_prop_definition.gd @@ -1,6 +1,8 @@ class_name TerrainPropDefinition extends Resource +const CalendarSeasonType = preload("res://world/calendar_season.gd") + @export var stable_id: StringName @export var label := "" @export var packed_scene: PackedScene @@ -33,7 +35,15 @@ extends Resource ## A single material is chosen per prop and applied only to matching imported ## material slots. This keeps trunks untouched while varying foliage. @export var variant_material_slot_names := PackedStringArray() +## Legacy/default palette. It remains the final fallback for definitions that +## have not been migrated to explicit seasonal palettes. @export var material_variants: Array[Material] = [] +@export_group("Seasonal Primary Materials") +@export var spring_material_variants: Array[Material] = [] +@export var summer_material_variants: Array[Material] = [] +@export var fall_material_variants: Array[Material] = [] +@export var winter_material_variants: Array[Material] = [] +@export_group("") ## An independent second material channel, used when both foliage and wood ## should vary without coupling their color choices. @export var secondary_variant_material_slot_names := PackedStringArray() @@ -71,6 +81,28 @@ func supports_chunk_tags(chunk_tags: PackedStringArray) -> bool: return true +func primary_material_variants_for_season( + season: int, +) -> Array[Material]: + var requested: Array[Material] = [] + match season: + CalendarSeasonType.Season.SPRING: + requested = spring_material_variants + CalendarSeasonType.Season.SUMMER: + requested = summer_material_variants + CalendarSeasonType.Season.FALL: + requested = fall_material_variants + CalendarSeasonType.Season.WINTER: + requested = winter_material_variants + if not requested.is_empty(): + return requested + # Spring and winter intentionally retain summer foliage until their authored + # palettes arrive. Unknown dates use the same stable visual fallback. + if not summer_material_variants.is_empty(): + return summer_material_variants + return material_variants + + func has_collision() -> bool: return has_cylinder_collision() or has_box_collision() diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index b19175d..9d4ccc6 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -42,6 +42,10 @@ func set_water_motion_strength_multiplier(_multiplier: float) -> void: pass +func set_calendar_season(_season: int) -> void: + pass + + func get_playable_half_extents() -> Vector2: return Vector2(50.0, 50.0) diff --git a/world/test_world.gd b/world/test_world.gd index 64dd157..13cd0ca 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -14,6 +14,7 @@ const RVUpgradeInteractionType = preload( "res://world/rv_upgrade_interaction.gd" ) const WorldLayoutType = preload("res://world/world_layout.gd") +const CalendarSeasonType = preload("res://world/calendar_season.gd") const GENERATED_REGION_SCENE: PackedScene = preload( "res://world/generation/generated_world_region.tscn" ) @@ -38,6 +39,7 @@ var _world_seed: int = PlayerSaveManager.DEFAULT_WORLD_SEED var _light_performance_profile: bool = false var _foliage_wind_strength_multiplier: float = 1.0 var _water_motion_strength_multiplier: float = 1.0 +var _calendar_season: int = CalendarSeasonType.UNKNOWN var _dedicated_simulation: bool = false var _local_home_interior_presentation_active: bool = false @@ -118,6 +120,16 @@ func set_water_motion_strength_multiplier(multiplier: float) -> void: ) +func set_calendar_season(season: int) -> void: + _calendar_season = ( + season + if season >= 0 and season < CalendarSeasonType.Season.size() + else CalendarSeasonType.UNKNOWN + ) + if _active_region != null: + _active_region.set_calendar_season(_calendar_season) + + func set_local_home_interior_presentation_active(enabled: bool) -> void: _local_home_interior_presentation_active = enabled if _active_region != null and not _dedicated_simulation: @@ -296,6 +308,7 @@ func get_gatherable_spawn_positions( func _ready() -> void: if _active_region != null: _world_layout = _active_region.region_id + _active_region.set_calendar_season(_calendar_season) _configure_world_coverage() @@ -318,6 +331,7 @@ func _replace_active_region(layout: StringName, world_seed: int) -> bool: return false if layout == WorldLayoutType.GENERATED: replacement.set("initial_seed", world_seed) + replacement.set_calendar_season(_calendar_season) if _active_region != null: _regions_root.remove_child(_active_region) _active_region.free()