diff --git a/docs/PORTMASTER.md b/docs/PORTMASTER.md index 6fbeb26..262f930 100644 --- a/docs/PORTMASTER.md +++ b/docs/PORTMASTER.md @@ -76,9 +76,11 @@ builds/v/straywild.zip ``` `--package-only` is reserved for repackaging an already validated -PortMaster-specific ARM64 export. It does not rebuild game content. The -standard Linux ARM64 release staging directory remains separate so building -PortMaster cannot replace the normal Linux ARM64 payload with reduced assets. +PortMaster-specific ARM64 export. It does not rebuild game content, and verifies +the tagged-export marker written by a prior full build so a generic Linux ARM64 +binary cannot accidentally be repackaged as PortMaster. The standard Linux +ARM64 release staging directory remains separate so building PortMaster cannot +replace the normal Linux ARM64 payload with reduced assets. For a packaging-only correction to an already published game release, commit only PortMaster launcher, packaging-script, or PortMaster documentation changes @@ -217,7 +219,11 @@ The light profile passes these Godot options: - `--audio-output-latency 40` It also passes `straywild_PERFORMANCE_PROFILE=light` and -`straywild_LOW_END=1`. The game renders the 3D world at 37.5% linear +`straywild_LOW_END=1`. Those selectors are honored only by the dedicated +PortMaster export, which carries the immutable `straywild_portmaster` Godot +feature tag; desktop, Android, macOS, generic ARM64, and dedicated-server +builds always use the full presentation even if those environment variables +are present. The game renders the 3D world at 37.5% linear resolution with nearest-neighbor scaling, reducing 3D pixel work by about 86% while the separately rendered UI retains its canonical resolution. The light profile also: @@ -263,10 +269,11 @@ On a typical muOS installation, the full path is: /mnt/mmc/ports/straywild/conf/performance_profile ``` -Set the file to `light`, or remove it, to restore the default. An externally -provided `straywild_PERFORMANCE_PROFILE=normal` or `light` environment value -takes precedence over the persistent file. Invalid values safely fall back to -the light profile and are reported in `straywild/log.txt`. +Set the file to `light`, or remove it, to restore the default. Within the +PortMaster package, an externally provided +`straywild_PERFORMANCE_PROFILE=normal` or `light` environment value takes +precedence over the persistent file. Invalid values safely fall back to the +light profile and are reported in `straywild/log.txt`. Do not use Godot's low processor mode for the light profile. It reduces idle CPU usage by sleeping between updates and is not a game-performance diff --git a/export_presets.cfg b/export_presets.cfg index 76fdef3..7c19ad7 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -239,3 +239,33 @@ ssh_remote_deploy/extra_args_ssh="" ssh_remote_deploy/extra_args_scp="" ssh_remote_deploy/run_script="#!/usr/bin/env bash\nexport DISPLAY=:0\nunzip -o -q \"{temp_dir}/{archive_name}\" -d \"{temp_dir}\"\nchmod +x \"{temp_dir}/{exe_name}\"\n\"{temp_dir}/{exe_name}\" {cmd_args}" ssh_remote_deploy/cleanup_script="#!/usr/bin/env bash\nkill $(pgrep -x -f \"{temp_dir}/{exe_name} {cmd_args}\")\nrm -rf \"{temp_dir}\"" + +[preset.6] + +name="PortMaster ARM64" +platform="Linux" +runnable=false +advanced_options=false +dedicated_server=false +custom_features="straywild_portmaster" +export_filter="all_resources" +include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt" +exclude_filter="builds/*,scripts/*" +export_path="builds/v0.20.3-alpha/portmaster-arm64/straywild.arm64" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + +[preset.6.options] + +custom_template/debug="" +custom_template/release="" +debug/export_console_wrapper=0 +binary_format/embed_pck=false +binary_format/architecture="arm64" +texture_format/s3tc_bptc=false +texture_format/etc2_astc=true diff --git a/main/main.gd b/main/main.gd index a42e8c1..20120d6 100644 --- a/main/main.gd +++ b/main/main.gd @@ -146,6 +146,7 @@ const TITLE_MUSIC_PATH: String = ( const NEW_GAME_MUSIC_PATH: String = "res://audio/music/world/tulips.wav" const DUSK_MUSIC_PATH: String = "res://audio/music/world/craft.mp3" const TIME_CROSSING_EPSILON_HOURS: float = 0.000001 +const RAIN_VISUAL_MOTION_MULTIPLIER: float = 2.0 const DEDICATED_IDLE_PHYSICS_TICKS_PER_SECOND: int = 10 const DEDICATED_ACTIVE_PHYSICS_TICKS_PER_SECOND: int = 30 const HOME_ENTRY_INPUT_OWNER: StringName = &"home_entry_transition" @@ -382,6 +383,25 @@ func _configure_audio_performance_profile(light_profile: bool) -> void: _rain_ambience.set_audio_enabled(true) +func _on_world_weather_changed( + weather: WorldWeatherService.Weather, + _seconds_remaining: float, +) -> void: + _apply_weather_visual_motion(weather) + + +func _apply_weather_visual_motion( + weather: WorldWeatherService.Weather, +) -> void: + var multiplier: float = ( + RAIN_VISUAL_MOTION_MULTIPLIER + if weather == WorldWeatherService.Weather.RAINY + else 1.0 + ) + _test_world.set_foliage_wind_strength_multiplier(multiplier) + _test_world.set_water_motion_strength_multiplier(multiplier) + + func _load_optional_audio_stream(path: String) -> AudioStream: if not ResourceLoader.exists(path, "AudioStream"): return null @@ -654,6 +674,11 @@ func _initialize_application(dedicated: bool) -> void: Callable(_player, "get_active_gameplay_camera"), _performance_profile.is_light(), ) + if not _world_weather.weather_changed.is_connected( + _on_world_weather_changed + ): + _world_weather.weather_changed.connect(_on_world_weather_changed) + _apply_weather_visual_motion(_world_weather.get_weather()) if not _world_time.natural_time_advanced.is_connected( _on_natural_time_advanced ): diff --git a/main/runtime_performance_profile.gd b/main/runtime_performance_profile.gd index f18e63a..3f44e38 100644 --- a/main/runtime_performance_profile.gd +++ b/main/runtime_performance_profile.gd @@ -9,6 +9,7 @@ const NETFISHING_PROFILE_ENVIRONMENT_VARIABLE := ( "NETFISHING_PERFORMANCE_PROFILE" ) const NETFISHING_LIGHT_ENVIRONMENT_VARIABLE := "NETFISHING_LOW_END" +const PORTMASTER_BUILD_FEATURE: StringName = &"straywild_portmaster" const NORMAL_PROFILE: StringName = &"normal" const LIGHT_PROFILE: StringName = &"light" const NORMAL_WORLD_RENDER_SCALE: float = 1.0 @@ -18,6 +19,16 @@ var _profile_name: StringName = NORMAL_PROFILE static func from_environment() -> RuntimePerformanceProfile: + return from_environment_for_portmaster_build( + OS.has_feature(PORTMASTER_BUILD_FEATURE) + ) + + +static func from_environment_for_portmaster_build( + is_portmaster_build: bool, +) -> RuntimePerformanceProfile: + if not is_portmaster_build: + return from_name(NORMAL_PROFILE) var profile_name: String = OS.get_environment( PROFILE_ENVIRONMENT_VARIABLE ) diff --git a/scripts/build_portmaster.sh b/scripts/build_portmaster.sh index 077167f..cb4c101 100755 --- a/scripts/build_portmaster.sh +++ b/scripts/build_portmaster.sh @@ -44,6 +44,8 @@ readonly RELEASE_ROOT="${PROJECT_ROOT}/builds/${RELEASE_TAG}" readonly ARM64_ROOT="${RELEASE_ROOT}/portmaster-arm64" readonly ARM64_EXECUTABLE="${ARM64_ROOT}/straywild.arm64" readonly ARM64_PCK="${ARM64_ROOT}/straywild.pck" +readonly PORTMASTER_FEATURE_TAG="straywild_portmaster" +readonly PORTMASTER_FEATURE_MARKER="${ARM64_ROOT}/.portmaster-feature-tag" readonly STAGE_ROOT="${RELEASE_ROOT}/portmaster-stage" readonly GAME_ROOT="${STAGE_ROOT}/straywild" readonly ARCHIVE="${RELEASE_ROOT}/straywild.zip" @@ -103,7 +105,10 @@ if [[ ${PACKAGE_ONLY} -eq 0 ]]; then rm -rf -- "${EXPORT_PROJECT_ROOT}" } trap cleanup_export_project EXIT INT TERM - rm -f -- "${ARM64_EXECUTABLE}" "${ARM64_PCK}" + rm -f -- \ + "${ARM64_EXECUTABLE}" \ + "${ARM64_PCK}" \ + "${PORTMASTER_FEATURE_MARKER}" rsync -a \ --exclude '/.git/' \ --exclude '/.godot/' \ @@ -124,14 +129,24 @@ if [[ ${PACKAGE_ONLY} -eq 0 ]]; then "${GODOT_BIN}" \ --headless \ --path "${EXPORT_PROJECT_ROOT}" \ - --export-release "Linux ARM64" \ + --export-release "PortMaster ARM64" \ "${ARM64_EXECUTABLE}" + printf '%s\n%s\n' \ + "${PORTMASTER_FEATURE_TAG}" \ + "${TAG_COMMIT}" >"${PORTMASTER_FEATURE_MARKER}" cleanup_export_project trap - EXIT INT TERM fi test -s "${ARM64_EXECUTABLE}" test -s "${ARM64_PCK}" +if [[ ! -f "${PORTMASTER_FEATURE_MARKER}" ]] \ + || ! grep -Fxq "${PORTMASTER_FEATURE_TAG}" "${PORTMASTER_FEATURE_MARKER}" \ + || ! grep -Fxq "${TAG_COMMIT}" "${PORTMASTER_FEATURE_MARKER}"; then + echo "PortMaster export is missing its tagged-preset marker." >&2 + echo "Run a full PortMaster build before using --package-only." >&2 + exit 1 +fi file "${ARM64_EXECUTABLE}" | grep -q "ARM aarch64" rm -rf -- "${STAGE_ROOT}" diff --git a/tests/controller_ui_navigation_validation.gd b/tests/controller_ui_navigation_validation.gd index 24f39f9..2471617 100644 --- a/tests/controller_ui_navigation_validation.gd +++ b/tests/controller_ui_navigation_validation.gd @@ -282,6 +282,16 @@ func _validate_low_end_profile_contract() -> void: ) assert(builder.contains("prepare_portmaster_assets.sh")) assert(builder.contains("straywild-portmaster-export")) + assert(builder.contains('--export-release "PortMaster ARM64"')) + assert(builder.contains("PORTMASTER_FEATURE_MARKER")) + assert(builder.contains("Run a full PortMaster build before using --package-only.")) + var export_presets: String = FileAccess.get_file_as_string( + "res://export_presets.cfg" + ) + assert(export_presets.contains('name="PortMaster ARM64"')) + assert(export_presets.contains( + 'custom_features="straywild_portmaster"' + )) var asset_profile: String = FileAccess.get_file_as_string( "res://scripts/prepare_portmaster_assets.sh" ) diff --git a/tests/generated_world_runtime_validation.gd b/tests/generated_world_runtime_validation.gd index 9df5f1d..524e7b3 100644 --- a/tests/generated_world_runtime_validation.gd +++ b/tests/generated_world_runtime_validation.gd @@ -474,6 +474,19 @@ func _validate_generated_region( assert(ocean != null and fresh_root != null) assert(ocean.water_type == WaterType.Type.SALT_WATER) assert(is_equal_approx(ocean.position.y, GeneratedWorldRegion.WATER_HEIGHT)) + var ocean_motion := ocean.get_node("VisualWater") as WaterSurfaceMotion + assert(ocean_motion != null and ocean_motion.is_processing()) + var normal_water_amplitude: float = ocean_motion.get_effective_amplitude() + assert(normal_water_amplitude > 0.0) + region.set_water_motion_strength_multiplier(2.0) + assert(is_equal_approx( + ocean_motion.get_effective_amplitude(), + normal_water_amplitude * 2.0, + )) + region.set_water_motion_strength_multiplier(1.0) + assert(is_equal_approx( + ocean_motion.get_effective_amplitude(), normal_water_amplitude + )) var ocean_recovery := ocean.get_node("RecoveryRegion") as PlayerWaterTrigger assert( ocean_recovery.entry_height_reference @@ -635,6 +648,20 @@ func _validate_generated_region( "GatherableAnchors/ReachableTreeTrunks" ) as GatherableAnchorSet3D assert(decorations != null and decorations.get_child_count() > 0) + var normal_foliage_wind := _first_foliage_wind_material(decorations) + assert(normal_foliage_wind != null) + var normal_foliage_strength: float = float( + normal_foliage_wind.get_shader_parameter("local_wind_strength") + ) + assert(normal_foliage_strength > 0.0) + region.set_foliage_wind_strength_multiplier(2.0) + var rainy_foliage_wind := _first_foliage_wind_material(decorations) + assert(rainy_foliage_wind != null) + assert(is_equal_approx( + float(rainy_foliage_wind.get_shader_parameter("local_wind_strength")), + normal_foliage_strength * 2.0, + )) + region.set_foliage_wind_strength_multiplier(1.0) assert(anchors != null) assert(anchors.get_spawn_positions().size() > 0) _validate_tree_gatherable_anchors(region, decorations, anchors) @@ -1567,6 +1594,22 @@ func _find_mesh_instance(root_node: Node) -> MeshInstance3D: return null +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: + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.get_surface_override_material( + surface_index + ) as ShaderMaterial + if material != null and material.shader == FOLIAGE_WIND_SHADER: + return material + for child: Node in root_node.get_children(): + var found := _first_foliage_wind_material(child) + if found != null: + return found + return null + + func _has_precipitation_occlusion_layer(root_node: Node) -> bool: var mesh_instance := root_node as MeshInstance3D if ( diff --git a/tests/light_performance_profile_validation.gd b/tests/light_performance_profile_validation.gd index 5252193..ada8f93 100644 --- a/tests/light_performance_profile_validation.gd +++ b/tests/light_performance_profile_validation.gd @@ -4,6 +4,9 @@ const MainScene: PackedScene = preload("res://main/main.tscn") const RuntimePerformanceProfileType = preload( "res://main/runtime_performance_profile.gd" ) +const FOLIAGE_WIND_SHADER: Shader = preload( + "res://world/materials/foliage_wind.gdshader" +) func _initialize() -> void: call_deferred("_run") @@ -30,8 +33,11 @@ func _run() -> void: PlayerSaveManager.DEFAULT_WORLD_SEED, true, ))) - _validate_main_profile(main) - _validate_minimal_weather(main) + # A regular editor/desktop runtime must ignore a light-profile environment + # variable. Only the tagged PortMaster export is allowed to honor it. + _validate_normal_profile(main) + _validate_rain_visual_motion(main) + _validate_new_game_music_transition(main) _stop_audio_players(main) main.queue_free() for _frame: int in 8: @@ -39,28 +45,7 @@ func _run() -> void: OS.unset_environment( RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE ) - var normal_main: Node = MainScene.instantiate() - root.add_child(normal_main) - for _frame: int in 4: - await process_frame - if not bool(normal_main.get("_application_initialized")): - normal_main.call("_activate_selected_data_path", "", true) - for _frame: int in 8: - await process_frame - assert(bool(normal_main.get("_application_initialized"))) - assert(bool(normal_main.call( - "_apply_world", - WorldLayout.GENERATED, - PlayerSaveManager.DEFAULT_WORLD_SEED, - true, - ))) - _validate_normal_profile(normal_main) - _validate_new_game_music_transition(normal_main) - _stop_audio_players(normal_main) - normal_main.queue_free() - for _frame: int in 8: - await process_frame - print("Light performance profile validation: PASS") + print("Runtime performance profile validation: PASS") quit() @@ -73,53 +58,31 @@ func _validate_profile_resolution() -> void: assert(light.is_light()) assert(is_equal_approx(light.get_world_render_scale(), 0.375)) assert(legacy.is_light()) - - -func _validate_main_profile(main: Node) -> void: - var profile: RuntimePerformanceProfile = main.get("_performance_profile") - assert(profile != null and profile.is_light()) - assert(is_equal_approx(root.scaling_3d_scale, 0.375)) - var pixelation: Node = main.get_node("%WorldPixelationPostprocess") - assert(bool(pixelation.call("is_light_performance_profile"))) - pixelation.call("set_gameplay_active", true) - var screen_grid := pixelation.get_node("ScreenGrid") as ColorRect - assert(screen_grid != null and not screen_grid.visible) - - var pond := _first_fresh_water_visual(main) - var ocean := main.get_node( - "TestWorld/Regions/GeneratedWorldRegion/WaterBodies/OceanWater/VisualWater" - ) as MeshInstance3D - assert(not pond.visible) - assert(ocean.material_override is StandardMaterial3D) - assert(not ocean.material_override is ShaderMaterial) - _validate_ocean_fishing_coverage(main, ocean) - var region := main.get_node( - "TestWorld/Regions/GeneratedWorldRegion" - ) as GeneratedWorldRegion - assert(region != null) - assert(region.get_node("Decorations").get_child_count() > 0) - - var title_background := main.get_node("%TitleBackground") as ColorRect - var title_material := title_background.material as ShaderMaterial - assert(title_material != null) - assert(not bool(title_material.get_shader_parameter( - "animation_enabled" - ))) - _validate_no_full_screen_menu_backdrops(main) - var game_ui := main.get_node("%GameUI") as GameUI - var title_screen := game_ui.get_title_screen() - assert(title_screen != null) - var player_menu := game_ui.get("_player_menu") as PlayerMenu - assert(player_menu != null) - assert(not player_menu.is_cooler_water_effect_enabled()) - assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null) - assert((main.get_node("%NewGameMusic") as AudioStreamPlayer).stream != null) - assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null) - assert( - (main.get_node("%WavesAudio") as AudioStreamPlayer).stream == null + OS.set_environment( + RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE, + "light", ) - assert( - (main.get_node("RainAmbience") as AudioStreamPlayer).stream == null + assert(not RuntimePerformanceProfileType.from_environment_for_portmaster_build( + false + ).is_light()) + assert(RuntimePerformanceProfileType.from_environment_for_portmaster_build( + true + ).is_light()) + OS.unset_environment( + RuntimePerformanceProfileType.PROFILE_ENVIRONMENT_VARIABLE + ) + OS.set_environment( + RuntimePerformanceProfileType.LEGACY_LIGHT_ENVIRONMENT_VARIABLE, + "1", + ) + assert(not RuntimePerformanceProfileType.from_environment_for_portmaster_build( + false + ).is_light()) + assert(RuntimePerformanceProfileType.from_environment_for_portmaster_build( + true + ).is_light()) + OS.unset_environment( + RuntimePerformanceProfileType.LEGACY_LIGHT_ENVIRONMENT_VARIABLE ) @@ -176,6 +139,54 @@ func _validate_normal_profile(main: Node) -> void: ) +func _validate_rain_visual_motion(main: Node) -> void: + var region := main.get_node( + "TestWorld/Regions/GeneratedWorldRegion" + ) as GeneratedWorldRegion + assert(region != null) + var ocean_motion := region.get_node( + "WaterBodies/OceanWater/VisualWater" + ) as WaterSurfaceMotion + assert(ocean_motion != null) + var normal_water_amplitude: float = ocean_motion.get_effective_amplitude() + var decorations := region.get_node("Decorations") as Node3D + assert(decorations != null) + var normal_wind := _first_foliage_wind_material(decorations) + assert(normal_wind != null) + var normal_wind_strength: float = float( + normal_wind.get_shader_parameter("local_wind_strength") + ) + var weather := main.get_node("%WorldWeatherService") as WorldWeatherService + assert(weather != null) + weather.apply_authoritative_snapshot( + WorldWeatherService.Weather.RAINY, + 60.0, + ) + assert(is_equal_approx( + ocean_motion.get_effective_amplitude(), + normal_water_amplitude * 2.0, + )) + var rainy_wind := _first_foliage_wind_material(decorations) + assert(rainy_wind != null) + assert(is_equal_approx( + float(rainy_wind.get_shader_parameter("local_wind_strength")), + normal_wind_strength * 2.0, + )) + weather.apply_authoritative_snapshot( + WorldWeatherService.Weather.SUNNY, + 60.0, + ) + assert(is_equal_approx( + ocean_motion.get_effective_amplitude(), normal_water_amplitude + )) + var restored_wind := _first_foliage_wind_material(decorations) + assert(restored_wind != null) + assert(is_equal_approx( + float(restored_wind.get_shader_parameter("local_wind_strength")), + normal_wind_strength, + )) + + func _validate_new_game_music_transition(main: Node) -> void: var title_music := main.get_node("%TitleMusic") as AudioStreamPlayer var new_game_music := main.get_node("%NewGameMusic") as AudioStreamPlayer @@ -201,6 +212,22 @@ func _first_fresh_water_visual(main: Node) -> MeshInstance3D: return fresh_water_root.get_child(0).get_node("VisualWater") as MeshInstance3D +func _first_foliage_wind_material(root_node: Node) -> ShaderMaterial: + for node: Node in root_node.find_children( + "*", "MeshInstance3D", true, false + ): + var mesh_instance := node as MeshInstance3D + if mesh_instance == null or mesh_instance.mesh == null: + continue + for surface_index: int in mesh_instance.mesh.get_surface_count(): + var material := mesh_instance.get_surface_override_material( + surface_index + ) as ShaderMaterial + if material != null and material.shader == FOLIAGE_WIND_SHADER: + return material + return null + + func _validate_ocean_fishing_coverage( main: Node, ocean: MeshInstance3D, @@ -230,41 +257,3 @@ func _stop_audio_players(root_node: Node) -> void: (root_node as AudioStreamPlayer3D).stop() for child: Node in root_node.get_children(): _stop_audio_players(child) - - -func _validate_minimal_weather(main: Node) -> void: - var visuals: WorldTimeVisualController = main.get_node( - "%WorldTimeVisualController" - ) - var rain := visuals.get_node("LocalRain") as GPUParticles3D - assert(rain.amount == WorldTimeVisualController.LIGHT_RAIN_PARTICLE_AMOUNT) - assert(rain.fixed_fps == WorldTimeVisualController.LIGHT_RAIN_FIXED_FPS) - assert(rain.amount == 48) - assert(rain.fixed_fps == 8) - var clouds := visuals.get_node("LocalStormClouds") as LocalStormCloudLayer - assert(clouds.get_patch_count() == 1) - var ceiling := clouds.get_node("CloudCeiling") as MeshInstance3D - assert(ceiling != null and ceiling.mesh is PlaneMesh) - assert(ceiling.material_override == null) - assert((ceiling.mesh as PlaneMesh).material is StandardMaterial3D) - - visuals.apply_weather_immediately(WorldWeatherService.Weather.RAINY) - assert(rain.emitting and rain.amount_ratio > 0.99) - assert(clouds.visible and clouds.get_storm_amount() > 0.87) - var cloud_material := ( - (ceiling.mesh as PlaneMesh).material as StandardMaterial3D - ) - assert(cloud_material.albedo_color.a < 0.65) - var test_world := main.get_node("TestWorld") as TestWorld - var runtime_environment := ( - test_world.get_world_environment().environment as Environment - ) - var sky_material := ( - runtime_environment.sky.sky_material as ShaderMaterial - ) - assert(is_zero_approx(float( - sky_material.get_shader_parameter("cloud_coverage") - ))) - assert(is_zero_approx(float( - sky_material.get_shader_parameter("cloud_opacity") - ))) diff --git a/tests/world_layout_validation.gd b/tests/world_layout_validation.gd index f21120c..473b45e 100644 --- a/tests/world_layout_validation.gd +++ b/tests/world_layout_validation.gd @@ -84,6 +84,8 @@ func _validate_world_switching() -> void: root.add_child(world) await process_frame assert(world.get_world_layout() == WorldLayout.GENERATED) + world.set_foliage_wind_strength_multiplier(2.0) + world.set_water_motion_strength_multiplier(2.0) var initial_generator := world.get_node( "Regions/GeneratedWorldRegion/Terrain/TerrainChunkGenerator" ) as TerrainChunkGenerator @@ -97,6 +99,20 @@ func _validate_world_switching() -> void: assert(world.get_generation_seed() == 13579) assert(world.get_node_or_null("Regions/StarterIslandRegion") != null) assert(world.get_node_or_null("Regions/GeneratedWorldRegion") == null) + var starter_region := world.get_node( + "Regions/StarterIslandRegion" + ) as StarterIslandRegion + assert(starter_region != null) + assert(is_equal_approx( + float(starter_region.get("_foliage_wind_strength_multiplier")), 2.0 + )) + var starter_ocean_motion := starter_region.get_node( + "WaterBodies/Ocean/VisualWater" + ) as WaterSurfaceMotion + assert(starter_ocean_motion != null) + assert(is_equal_approx( + starter_ocean_motion.get_motion_strength_multiplier(), 2.0 + )) assert(world.get_fishing_shop() != null) assert(world.get_player_storage() != null) assert(not world.get_fishable_water_regions().is_empty()) @@ -114,6 +130,20 @@ func _validate_world_switching() -> void: assert(world.get_generation_seed() == 24680) assert(world.get_node_or_null("Regions/GeneratedWorldRegion") != null) assert(world.get_node_or_null("Regions/StarterIslandRegion") == null) + var generated_region := world.get_node( + "Regions/GeneratedWorldRegion" + ) as GeneratedWorldRegion + assert(generated_region != null) + assert(is_equal_approx( + float(generated_region.get("_foliage_wind_strength_multiplier")), 2.0 + )) + var generated_ocean_motion := generated_region.get_node( + "WaterBodies/OceanWater/VisualWater" + ) as WaterSurfaceMotion + assert(generated_ocean_motion != null) + assert(is_equal_approx( + generated_ocean_motion.get_motion_strength_multiplier(), 2.0 + )) world.queue_free() await process_frame diff --git a/world/generation/generated_world_region.gd b/world/generation/generated_world_region.gd index 9c59692..9c7568d 100644 --- a/world/generation/generated_world_region.gd +++ b/world/generation/generated_world_region.gd @@ -138,6 +138,7 @@ var _biome_assignments: Dictionary[Vector2i, StringName] = {} 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 func _ready() -> void: @@ -218,6 +219,24 @@ func set_light_performance_profile(enabled: bool) -> void: _light_performance_profile = enabled _set_foliage_wind_enabled(not enabled) _apply_water_materials() + var surface_motion := _get_ocean_surface_motion() + if surface_motion != null: + surface_motion.set_motion_enabled(not enabled) + + +func set_foliage_wind_strength_multiplier(multiplier: float) -> void: + _foliage_wind_strength_multiplier = maxf(multiplier, 0.0) + _set_foliage_wind_enabled(_foliage_wind_enabled) + + +func set_water_motion_strength_multiplier(multiplier: float) -> void: + var surface_motion := _get_ocean_surface_motion() + if surface_motion != null: + surface_motion.set_motion_strength_multiplier(multiplier) + + +func _get_ocean_surface_motion() -> WaterSurfaceMotion: + return _ocean.get_node_or_null(^"VisualWater") as WaterSurfaceMotion func get_spawn_surface_triangles( @@ -611,13 +630,29 @@ func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void: (mesh_global_scale.x + mesh_global_scale.z) * 0.5, 0.001, ) - var local_strength: float = foliage_wind_strength / horizontal_scale + var local_strength: float = ( + foliage_wind_strength * _foliage_wind_strength_multiplier + / horizontal_scale + ) var phase: float = fposmod( mesh_instance.global_position.x * 0.73 + mesh_instance.global_position.z * 0.41, TAU, ) for surface_index: int in mesh_instance.mesh.get_surface_count(): + var override_material: Material = ( + mesh_instance.get_surface_override_material(surface_index) + ) + var existing_wind := override_material as ShaderMaterial + if ( + existing_wind != null + and existing_wind.shader == FOLIAGE_WIND_SHADER + ): + existing_wind.set_shader_parameter( + "local_wind_strength", + local_strength, + ) + continue var source_material: Material = mesh_instance.get_active_material( surface_index ) diff --git a/world/generation/generated_world_region.tscn b/world/generation/generated_world_region.tscn index c996b88..b4b3dba 100644 --- a/world/generation/generated_world_region.tscn +++ b/world/generation/generated_world_region.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=17 format=3] [ext_resource type="Script" path="res://world/generation/generated_world_region.gd" id="1_region"] [ext_resource type="Script" path="res://world/generation/terrain_chunk_generator.gd" id="2_generator"] @@ -15,6 +15,7 @@ [ext_resource type="Resource" path="res://world/generation/biomes/terrain_biome_catalog.tres" id="13_biome_catalog"] [ext_resource type="PackedScene" path="res://world/interactables/decor_shop_world.tscn" id="14_decor_shop"] [ext_resource type="PackedScene" path="res://world/interactables/rv_upgrade_shop_world.tscn" id="15_rv_upgrade"] +[ext_resource type="Script" path="res://world/water_surface_motion.gd" id="16_water_motion"] [node name="GeneratedWorldRegion" type="Node3D"] script = ExtResource("1_region") @@ -60,6 +61,9 @@ fish_pool = ExtResource("10_ocean_pool") water_type = 1 location_tags = Array[StringName]([&"coast", &"ocean", &"generated_ocean"]) +[node name="VisualWater" parent="WaterBodies/OceanWater" index="0"] +script = ExtResource("16_water_motion") + [node name="FreshWaterBodies" type="Node3D" parent="WaterBodies"] unique_name_in_owner = true diff --git a/world/regions/starter_island_region.gd b/world/regions/starter_island_region.gd index c690e4b..5352c39 100644 --- a/world/regions/starter_island_region.gd +++ b/world/regions/starter_island_region.gd @@ -72,6 +72,7 @@ var rv_upgrade_shop_path: NodePath = ( @export_range(0.0, 4.0, 0.05) var foliage_wind_speed: float = 0.9 var _foliage_wind_enabled: bool = false +var _foliage_wind_strength_multiplier: float = 1.0 func _ready() -> void: @@ -141,6 +142,19 @@ func set_light_performance_profile(enabled: bool) -> void: surface_motion.set_motion_enabled(not enabled) +func set_foliage_wind_strength_multiplier(multiplier: float) -> void: + _foliage_wind_strength_multiplier = maxf(multiplier, 0.0) + _set_foliage_wind_enabled(_foliage_wind_enabled) + + +func set_water_motion_strength_multiplier(multiplier: float) -> void: + var ocean := get_node_or_null( + ^"WaterBodies/Ocean/VisualWater" + ) as WaterSurfaceMotion + if ocean != null: + ocean.set_motion_strength_multiplier(multiplier) + + func is_foliage_wind_enabled() -> bool: return _foliage_wind_enabled @@ -342,13 +356,29 @@ func _apply_foliage_wind_to_mesh(mesh_instance: MeshInstance3D) -> void: (mesh_global_scale.x + mesh_global_scale.z) * 0.5, 0.001, ) - var local_strength: float = foliage_wind_strength / horizontal_scale + var local_strength: float = ( + foliage_wind_strength * _foliage_wind_strength_multiplier + / horizontal_scale + ) var phase: float = fposmod( mesh_instance.global_position.x * 0.73 + mesh_instance.global_position.z * 0.41, TAU, ) for surface_index: int in mesh_instance.mesh.get_surface_count(): + var override_material: Material = ( + mesh_instance.get_surface_override_material(surface_index) + ) + var existing_wind := override_material as ShaderMaterial + if ( + existing_wind != null + and existing_wind.shader == FOLIAGE_WIND_SHADER + ): + existing_wind.set_shader_parameter( + "local_wind_strength", + local_strength, + ) + continue var source_material: Material = mesh_instance.get_active_material( surface_index ) diff --git a/world/regions/world_region.gd b/world/regions/world_region.gd index d054938..ac5b2ff 100644 --- a/world/regions/world_region.gd +++ b/world/regions/world_region.gd @@ -38,6 +38,14 @@ func set_light_performance_profile(_enabled: bool) -> void: pass +func set_foliage_wind_strength_multiplier(_multiplier: float) -> void: + pass + + +func set_water_motion_strength_multiplier(_multiplier: float) -> 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 9d81d4d..b036626 100644 --- a/world/test_world.gd +++ b/world/test_world.gd @@ -36,6 +36,8 @@ const WORLD_BOUNDARY_SHORELINE_CLEARANCE := 18.0 var _world_layout: StringName = WorldLayoutType.GENERATED 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 _dedicated_simulation: bool = false var _local_home_interior_presentation_active: bool = false @@ -100,6 +102,22 @@ func set_light_performance_profile(enabled: bool) -> void: _active_region.set_light_performance_profile(enabled) +func set_foliage_wind_strength_multiplier(multiplier: float) -> void: + _foliage_wind_strength_multiplier = maxf(multiplier, 0.0) + if _active_region != null: + _active_region.set_foliage_wind_strength_multiplier( + _foliage_wind_strength_multiplier + ) + + +func set_water_motion_strength_multiplier(multiplier: float) -> void: + _water_motion_strength_multiplier = maxf(multiplier, 0.0) + if _active_region != null: + _active_region.set_water_motion_strength_multiplier( + _water_motion_strength_multiplier + ) + + func set_local_home_interior_presentation_active(enabled: bool) -> void: _local_home_interior_presentation_active = enabled if _active_region != null and not _dedicated_simulation: @@ -310,6 +328,12 @@ func _replace_active_region(layout: StringName, world_seed: int) -> bool: _active_region = replacement _regions_root.add_child(_active_region) _active_region.set_light_performance_profile(_light_performance_profile) + _active_region.set_foliage_wind_strength_multiplier( + _foliage_wind_strength_multiplier + ) + _active_region.set_water_motion_strength_multiplier( + _water_motion_strength_multiplier + ) if _dedicated_simulation: _set_region_presentation_enabled(_active_region, false) else: diff --git a/world/water_surface_motion.gd b/world/water_surface_motion.gd index 02e397e..d8dbd4d 100644 --- a/world/water_surface_motion.gd +++ b/world/water_surface_motion.gd @@ -18,6 +18,7 @@ const DEFAULT_PHASE_OFFSET: float = 0.0 @export_range(0.0, TAU, 0.01, "radians") var phase_offset: float = 0.0 var _base_height: float +var _motion_strength_multiplier: float = 1.0 func _ready() -> void: @@ -28,7 +29,7 @@ func _ready() -> void: func _process(_delta: float) -> void: position.y = _base_height + calculate_height_offset( _current_time_seconds(), - amplitude, + get_effective_amplitude(), cycle_seconds, secondary_swell, phase_offset, @@ -41,6 +42,20 @@ func set_motion_enabled(enabled: bool) -> void: position.y = _base_height +func set_motion_strength_multiplier(multiplier: float) -> void: + _motion_strength_multiplier = maxf(multiplier, 0.0) + if _motion_strength_multiplier <= 0.0: + position.y = _base_height + + +func get_motion_strength_multiplier() -> float: + return _motion_strength_multiplier + + +func get_effective_amplitude() -> float: + return amplitude * _motion_strength_multiplier + + static func get_default_height_offset() -> float: return calculate_height_offset( _current_time_seconds(),