Add seasonal tree palettes

This commit is contained in:
Alexander Sellite 2026-09-02 17:20:25 -04:00
parent 1163cef320
commit 23cf53fa14
35 changed files with 861 additions and 43 deletions

View file

@ -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(

View file

@ -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

View file

@ -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)

View file

@ -0,0 +1 @@
uid://dicorkv8nbkb5

View file

@ -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
))

View file

@ -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,
),
)

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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()