Optimize PortMaster low-memory profile

This commit is contained in:
Alexander Sellite 2026-08-18 01:02:13 -04:00
parent b27883d115
commit 021de5aca4
16 changed files with 327 additions and 31 deletions

View file

@ -207,21 +207,38 @@ The light profile passes these Godot options:
- `--audio-output-latency 40`
It also passes `NETFISHING_PERFORMANCE_PROFILE=light` and
`NETFISHING_LOW_END=1`. The game renders the 3D world at 50% linear resolution
with nearest-neighbor scaling, reducing 3D pixel work by 75% while the
separately rendered UI retains its canonical resolution. The light profile
also:
`NETFISHING_LOW_END=1`. 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:
- disables the additional full-screen world-pixelation pass;
- replaces animated depth-aware water shaders with opaque, per-vertex water;
- disables ocean surface motion;
- disables foliage wind;
- keeps the title water and full-window menu patterns static;
- reduces rain to 128 particles simulated at 12 FPS; and
- replaces the animated cooler water and decorative bubbles with flat water;
- disables decorative title-screen fish and bubbles;
- omits title music, dusk music, rain ambience, and shoreline ambience from the
package while retaining gameplay sound effects, speech, and character calls;
- reduces rain to 48 particles simulated at 8 FPS; and
- replaces procedural sky clouds and 81 moving local cloud patches with one
flat cloud ceiling.
The normal profile retains the full visual presentation.
PortMaster exports are built in an isolated project copy with a dedicated
micro-texture profile. Every raster texture is capped at 128 pixels on its
longest side. High-volume gameplay catalogs, including fish, character parts,
environment art, equipment, gathering art, and item icons, are capped at 64
pixels. All remaining raster textures use ETC2 VRAM compression. Short sound
effects, speech, and calls are imported as 22.05 kHz mono QOA samples. The
authoritative artwork and the desktop, Android, and macOS imports are not
modified. Source artwork, design documents, tests, platform-specific icons,
music, and ambience are omitted from the temporary export project. This
build-time profile is always present in the PortMaster package; selecting the
normal runtime profile restores visual effects, but does not restore omitted
audio or replace the micro textures with desktop-resolution assets.
The normal profile otherwise retains the full visual presentation.
To opt a capable device into the normal profile, create the persistent file
`netfishing/conf/performance_profile` containing exactly:

View file

@ -118,6 +118,10 @@ const NetworkWorldSpawnServiceType = preload(
)
const TITLE_MUSIC_SILENCE_DB: float = -80.0
const TITLE_MUSIC_PATH: String = (
"res://audio/music/title/as_in_four_wolves.ogg"
)
const DUSK_MUSIC_PATH: String = "res://audio/music/world/craft.mp3"
const TIME_CROSSING_EPSILON_HOURS: float = 0.000001
const PLAYER_MENU_PATTERN_SCALE: float = 0.85
const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0)
@ -273,6 +277,7 @@ func _ready() -> void:
_rain_ambience.name = "RainAmbience"
add_child(_rain_ambience)
_rain_ambience.configure(_world_weather)
_configure_audio_performance_profile(_performance_profile.is_light())
_shoreline_ambience.configure(
_player,
_test_world.get_saltwater_shoreline_mesh(),
@ -297,6 +302,8 @@ func _ready() -> void:
func _configure_presentation_performance_profile(light_profile: bool) -> void:
_game_ui.get_title_screen().set_minimal_presentation(light_profile)
_game_ui.set_light_performance_profile(light_profile)
var title_water_material := _title_background.material as ShaderMaterial
if title_water_material != null:
title_water_material.set_shader_parameter(
@ -343,6 +350,25 @@ func _configure_presentation_performance_profile(light_profile: bool) -> void:
)
func _configure_audio_performance_profile(light_profile: bool) -> void:
if light_profile:
_title_music.stream = null
_dusk_music.stream = null
_shoreline_ambience.set_audio_enabled(false)
_rain_ambience.set_audio_enabled(false)
return
_title_music.stream = _load_optional_audio_stream(TITLE_MUSIC_PATH)
_dusk_music.stream = _load_optional_audio_stream(DUSK_MUSIC_PATH)
_shoreline_ambience.set_audio_enabled(true)
_rain_ambience.set_audio_enabled(true)
func _load_optional_audio_stream(path: String) -> AudioStream:
if not ResourceLoader.exists(path, "AudioStream"):
return null
return load(path) as AudioStream
func _is_dedicated_server_runtime() -> bool:
if OS.has_feature("dedicated_server"):
return true
@ -1488,7 +1514,8 @@ func _on_natural_time_advanced(advanced_hours: float) -> void:
)
):
_dusk_music_played_for_natural_day = true
_dusk_music.play(0.0)
if _dusk_music.stream != null:
_dusk_music.play(0.0)
static func _natural_interval_crosses_hour(
@ -1956,6 +1983,8 @@ func _on_quit_requested() -> void:
func _show_title_music(restart_from_beginning: bool = false) -> void:
_title_music_requested = true
_replace_title_music_transition()
if _title_music.stream == null:
return
if restart_from_beginning:
_title_music.stop()
_title_music.volume_db = title_music_volume_db

View file

@ -12,7 +12,6 @@
[ext_resource type="Script" uid="uid://dwv3nvfke5xtu" path="res://settings/player_settings_manager.gd" id="10_settings"]
[ext_resource type="Resource" path="res://items/catalog/item_catalog.tres" id="11_items"]
[ext_resource type="Resource" path="res://economy/buyers/main_fishing_shop.tres" id="12_main_shop"]
[ext_resource type="AudioStream" uid="uid://bgkfluumuen4m" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"]
[ext_resource type="Script" uid="uid://bdtb05vkiicd" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"]
[ext_resource type="PackedScene" path="res://ui/pixelation_reset_overlay.tscn" id="15_reset"]
[ext_resource type="PackedScene" path="res://main/world_pixelation_postprocess.tscn" id="16_world_pixelation"]
@ -53,11 +52,9 @@
[ext_resource type="Script" uid="uid://daqtje84gqgu0" path="res://jobs/player_job_service.gd" id="52_player_jobs"]
[ext_resource type="Script" uid="uid://drlyytr2o3uw8" path="res://network/network_job_service.gd" id="53_network_jobs"]
[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
[ext_resource type="AudioStream" uid="uid://dck3tf8qkadea" path="res://audio/ambience/waves.wav" id="55_waves"]
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
[ext_resource type="Script" path="res://settings/keyboard_mouse_mapping_manager.gd" id="keyboard_mouse_mapping"]
[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
[ext_resource type="AudioStream" uid="uid://c4y8puv0n6xk8" path="res://audio/music/world/craft.mp3" id="dusk_music"]
[ext_resource type="Resource" path="res://gathering/catalog/gatherable_catalog.tres" id="58_gatherable_catalog"]
[ext_resource type="Script" path="res://network/network_world_spawn_service.gd" id="59_world_spawns"]
[ext_resource type="Script" path="res://gathering/gathering_controller.gd" id="60_gathering_controller"]
@ -398,13 +395,11 @@ script = ExtResource("keyboard_mouse_mapping")
[node name="TitleMusic" type="AudioStreamPlayer" parent="." unique_id=1292939956]
unique_name_in_owner = true
stream = ExtResource("13_title_music")
volume_db = -80.0
bus = &"Music"
[node name="DuskMusic" type="AudioStreamPlayer" parent="." unique_id=992744241]
unique_name_in_owner = true
stream = ExtResource("dusk_music")
bus = &"Music"
[node name="ShorelineAmbience" type="Node" parent="." unique_id=1948037177]
@ -413,7 +408,6 @@ script = ExtResource("54_shoreline_ambience")
[node name="WavesAudio" type="AudioStreamPlayer" parent="ShorelineAmbience" unique_id=193530658]
unique_name_in_owner = true
stream = ExtResource("55_waves")
bus = &"Environment"
[node name="WorldPixelationPostprocess" parent="." unique_id=344378000 instance=ExtResource("16_world_pixelation")]

View file

@ -8,7 +8,7 @@ const LEGACY_LIGHT_ENVIRONMENT_VARIABLE: String = "NETFISHING_LOW_END"
const NORMAL_PROFILE: StringName = &"normal"
const LIGHT_PROFILE: StringName = &"light"
const NORMAL_WORLD_RENDER_SCALE: float = 1.0
const LIGHT_WORLD_RENDER_SCALE: float = 0.5
const LIGHT_WORLD_RENDER_SCALE: float = 0.375
var _profile_name: StringName = NORMAL_PROFILE

View file

@ -5,6 +5,7 @@ set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
readonly TEMPLATE_ROOT="${SCRIPT_DIR}/portmaster"
readonly ASSET_PROFILE_SCRIPT="${SCRIPT_DIR}/prepare_portmaster_assets.sh"
readonly GODOT_BIN="${GODOT_BIN:-godot}"
PACKAGE_ONLY=0
@ -86,7 +87,7 @@ if [[ "${NETFISHING_ALLOW_DIRTY_RELEASE:-0}" != "1" ]]; then
fi
fi
for command_name in git file zip unzip sha256sum; do
for command_name in git file zip unzip sha256sum rsync; do
command -v "${command_name}" >/dev/null 2>&1 || {
echo "Required command not found: ${command_name}" >&2
exit 1
@ -95,11 +96,39 @@ done
mkdir -p -- "${ARM64_ROOT}" "${RELEASE_ROOT}"
if [[ ${PACKAGE_ONLY} -eq 0 ]]; then
readonly EXPORT_PROJECT_ROOT="$(
mktemp -d -t netfishing-portmaster-export.XXXXXX
)"
cleanup_export_project() {
rm -rf -- "${EXPORT_PROJECT_ROOT}"
}
trap cleanup_export_project EXIT INT TERM
rm -f -- "${ARM64_EXECUTABLE}" "${ARM64_PCK}"
rsync -a \
--exclude '/.git/' \
--exclude '/.godot/' \
--exclude '/art/source/' \
--exclude '/art/exported/system_icons/netfishing.ico' \
--exclude '/art/exported/system_icons/netfishing_1024.png' \
--exclude '/art/exported/system_icons/netfishing_1024.png.import' \
--exclude '/audio/ambience/' \
--exclude '/audio/music/' \
--exclude '/builds/' \
--exclude '/docs/design/' \
--exclude '/tests/' \
"${PROJECT_ROOT}/" "${EXPORT_PROJECT_ROOT}/"
"${ASSET_PROFILE_SCRIPT}" "${EXPORT_PROJECT_ROOT}"
"${GODOT_BIN}" \
--headless \
--path "${PROJECT_ROOT}" \
--export-release "Linux ARM64"
--path "${EXPORT_PROJECT_ROOT}" \
--import
"${GODOT_BIN}" \
--headless \
--path "${EXPORT_PROJECT_ROOT}" \
--export-release "Linux ARM64" \
"${ARM64_EXECUTABLE}"
cleanup_export_project
trap - EXIT INT TERM
fi
test -s "${ARM64_EXECUTABLE}"
@ -153,6 +182,8 @@ Engine/export template: $("${GODOT_BIN}" --version)
Architecture: AArch64
Minimum linked GLIBC symbol version: GLIBC_2.28
Rendering method: gl_compatibility
Texture profile: 128 px maximum; 64 px gameplay catalogs; ETC2 compressed
Audio profile: 22.05 kHz mono QOA effects; music and ambience omitted
The executable and PCK were exported from the release commit listed above.
Repository working-tree changes were not included in game content.
@ -187,8 +218,9 @@ selected data root. Device-local configuration remains under
\`netfishing/conf/\`.
PortMaster launches use the light performance profile by default. Put the word
\`normal\` in \`netfishing/conf/performance_profile\` to opt a capable device
into the normal rendering profile. See \`netfishing/licenses/\` for bundled
credits and license information.
into the normal rendering profile. PortMaster packages omit music and ambience
in both profiles. See \`netfishing/licenses/\` for bundled credits and license
information.
## Controls

View file

@ -0,0 +1,96 @@
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "usage: scripts/prepare_portmaster_assets.sh <temporary-project-root>" >&2
exit 2
fi
readonly PROJECT_ROOT="$1"
readonly GENERAL_TEXTURE_LIMIT=128
readonly GAMEPLAY_TEXTURE_LIMIT=64
readonly AUDIO_SAMPLE_RATE=22050
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
echo "PortMaster asset project is missing project.godot: ${PROJECT_ROOT}" >&2
exit 1
fi
limit_texture_imports() {
local asset_root="$1"
local size_limit="$2"
local import_file
local updated=0
if [[ ! -d "${asset_root}" ]]; then
return
fi
while IFS= read -r -d '' import_file; do
if ! grep -q '^importer="texture"$' "${import_file}"; then
continue
fi
if ! grep -q '^process/size_limit=' "${import_file}"; then
continue
fi
sed -i \
-e 's|^compress/mode=.*$|compress/mode=2|' \
-e "s|^process/size_limit=.*$|process/size_limit=${size_limit}|" \
"${import_file}"
updated=$((updated + 1))
done < <(find "${asset_root}" -type f -name '*.import' -print0)
printf 'PortMaster texture limit %d px: %d imports under %s\n' \
"${size_limit}" "${updated}" "${asset_root#${PROJECT_ROOT}/}"
}
configure_audio_imports() {
local asset_root="$1"
local import_file
local updated=0
if [[ ! -d "${asset_root}" ]]; then
return
fi
while IFS= read -r -d '' import_file; do
if ! grep -q '^importer="wav"$' "${import_file}"; then
continue
fi
sed -i \
-e 's|^force/mono=.*$|force/mono=true|' \
-e 's|^force/max_rate=.*$|force/max_rate=true|' \
-e "s|^force/max_rate_hz=.*$|force/max_rate_hz=${AUDIO_SAMPLE_RATE}|" \
-e 's|^compress/mode=.*$|compress/mode=2|' \
"${import_file}"
updated=$((updated + 1))
done < <(find "${asset_root}" -type f -name '*.wav.import' -print0)
printf 'PortMaster audio profile %d Hz mono: %d imports under %s\n' \
"${AUDIO_SAMPLE_RATE}" "${updated}" \
"${asset_root#${PROJECT_ROOT}/}"
}
# The entire PortMaster project uses a deliberately small, VRAM-compressed
# texture ceiling. Fonts are unaffected, preserving readable interface text
# while raster artwork is reduced for one-gigabyte shared-memory devices.
limit_texture_imports "${PROJECT_ROOT}" "${GENERAL_TEXTURE_LIMIT}"
# These catalogs contain most of the authored high-resolution textures and
# account for the largest runtime residency. At a 640x480 output, 64 pixels
# remains sufficient for the deliberately low-fidelity light profile.
for gameplay_root in \
"art/exported/characters" \
"art/exported/environment" \
"art/exported/items" \
"fish/species" \
"gathering" \
"items/icons" \
"world"; do
limit_texture_imports \
"${PROJECT_ROOT}/${gameplay_root}" \
"${GAMEPLAY_TEXTURE_LIMIT}"
done
# Short speech, call, and gameplay effects remain available, but use a small
# mono QOA representation to reduce both package and resident sample memory.
configure_audio_imports "${PROJECT_ROOT}/audio/sfx"
configure_audio_imports "${PROJECT_ROOT}/sound/dialogue"

View file

@ -187,6 +187,22 @@ func _validate_low_end_profile_contract() -> void:
assert(launcher.contains("NETFISHING_LOW_END=1"))
assert(launcher.contains("--max-fps 30"))
assert(launcher.contains("--audio-output-latency 40"))
var builder: String = FileAccess.get_file_as_string(
"res://scripts/build_portmaster.sh"
)
assert(builder.contains("prepare_portmaster_assets.sh"))
assert(builder.contains("netfishing-portmaster-export"))
var asset_profile: String = FileAccess.get_file_as_string(
"res://scripts/prepare_portmaster_assets.sh"
)
assert(asset_profile.contains("GENERAL_TEXTURE_LIMIT=128"))
assert(asset_profile.contains("GAMEPLAY_TEXTURE_LIMIT=64"))
assert(asset_profile.contains("AUDIO_SAMPLE_RATE=22050"))
assert(asset_profile.contains("compress/mode=2"))
assert(asset_profile.contains("fish/species"))
assert(asset_profile.contains("art/exported/characters"))
assert(builder.contains("--exclude '/audio/ambience/'"))
assert(builder.contains("--exclude '/audio/music/'"))
func _make_button(

View file

@ -63,14 +63,14 @@ func _validate_profile_resolution() -> void:
assert(not normal.is_light())
assert(is_equal_approx(normal.get_world_render_scale(), 1.0))
assert(light.is_light())
assert(is_equal_approx(light.get_world_render_scale(), 0.5))
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.5))
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)
@ -118,6 +118,21 @@ func _validate_main_profile(main: Node) -> void:
"scroll_velocity_pixels"
) == Vector2.ZERO
)
var game_ui := main.get_node("%GameUI") as GameUI
var title_screen := game_ui.get_title_screen()
assert(title_screen != null)
assert(not title_screen.is_decorative_presentation_active())
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("%DuskMusic") as AudioStreamPlayer).stream == null)
assert(
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream == null
)
assert(
(main.get_node("RainAmbience") as AudioStreamPlayer).stream == null
)
func _validate_normal_profile(main: Node) -> void:
@ -141,6 +156,10 @@ func _validate_normal_profile(main: Node) -> void:
assert(_count_foliage_wind_overrides(
island.get_node("Terrain/Visual")
) > 0)
var game_ui := main.get_node("%GameUI") as GameUI
var player_menu := game_ui.get("_player_menu") as PlayerMenu
assert(player_menu != null)
assert(player_menu.is_cooler_water_effect_enabled())
var title_background := main.get_node("%TitleBackground") as ColorRect
var title_material := title_background.material as ShaderMaterial
@ -167,6 +186,14 @@ func _validate_normal_profile(main: Node) -> void:
var rain := visuals.get_node("LocalRain") as GPUParticles3D
assert(rain.amount == WorldTimeVisualController.RAIN_PARTICLE_AMOUNT)
assert(rain.fixed_fps == 30)
assert((main.get_node("%TitleMusic") as AudioStreamPlayer).stream != null)
assert((main.get_node("%DuskMusic") as AudioStreamPlayer).stream != null)
assert(
(main.get_node("%WavesAudio") as AudioStreamPlayer).stream != null
)
assert(
(main.get_node("RainAmbience") as AudioStreamPlayer).stream != null
)
func _validate_ocean_fishing_coverage(
@ -222,8 +249,8 @@ func _validate_minimal_weather(main: Node) -> void:
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 == 128)
assert(rain.fixed_fps == 12)
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

View file

@ -35,6 +35,8 @@ func _run() -> void:
root.add_child(controller)
await process_frame
assert(runtime_audio.bus == &"Environment")
assert(runtime_audio.stream == null)
controller.set_audio_enabled(true)
assert(controller.near_distance < controller.far_distance)
assert(controller.near_volume_db > controller.far_volume_db)
var runtime_waves := runtime_audio.stream as AudioStreamWAV

View file

@ -1723,6 +1723,10 @@ func get_title_screen() -> TitleScreenType:
return _title_screen
func set_light_performance_profile(light_profile: bool) -> void:
_player_menu.set_light_performance_profile(light_profile)
func set_effective_ui_pixel_size(pixel_size: int) -> void:
_title_settings_panel.set_effective_ui_pixel_size(pixel_size)
_pause_settings_panel.set_effective_ui_pixel_size(pixel_size)

View file

@ -149,6 +149,7 @@ const BAG_STORAGE_COLUMNS: int = 5
const BAG_STORAGE_MINIMUM_SLOTS: int = 15
const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0)
const CONTROLLER_PICKUP_HOLD_SECONDS: float = 0.42
const LIGHT_COOLER_WATER_COLOR := Color(0.037, 0.27, 0.375, 1.0)
@onready var _navigation_cluster: BubbleClusterType = %NavigationCluster
@onready var _presentation_scale_root: Control = %PlayerMenuPresentationScaleRoot
@ -253,6 +254,7 @@ var _sale_buyer_override: FishBuyerProfileType
var _shop_cooler_context_active: bool = false
var _cooler_original_parent: Node
var _cooler_original_index: int = -1
var _authored_cooler_water_material: ShaderMaterial
var _confirmation_original_parent: Node
var _confirmation_original_index: int = -1
var _catalog: FishPoolType
@ -325,6 +327,9 @@ var _motion_elapsed: float = 0.0
func _ready() -> void:
_authored_cooler_water_material = (
_cooler_water_surface.material as ShaderMaterial
)
_configure_inventory_transition_group()
_cooler_original_parent = _cooler_page.get_parent()
_cooler_original_index = _cooler_page.get_index()
@ -445,6 +450,21 @@ func _update_cooler_water_mask() -> void:
)
func set_light_performance_profile(light_profile: bool) -> void:
_cooler_water_surface.material = (
null if light_profile else _authored_cooler_water_material
)
_cooler_water_surface.color = (
LIGHT_COOLER_WATER_COLOR if light_profile else Color.WHITE
)
if not light_profile:
_update_cooler_water_mask()
func is_cooler_water_effect_enabled() -> bool:
return _cooler_water_surface.material is ShaderMaterial
func setup(
player: PlayerType,
inventory: FishInventoryType,

View file

@ -18,7 +18,7 @@ script = ExtResource("1")
[node name="Viewport" type="SubViewport" parent="."]
transparent_bg = true
size = Vector2i(320, 330)
render_target_update_mode = 4
render_target_update_mode = 2
world_3d = SubResource("PreviewWorld")
[node name="WorldEnvironment" type="WorldEnvironment" parent="Viewport"]

View file

@ -155,6 +155,7 @@ var _decorative_bubbles: Array[TextureRect] = []
var _decorative_bubble_tweens: Dictionary[int, Tween] = {}
var _transition_bubble_ids: Dictionary[int, bool] = {}
var _pending_cluster_bubbles: int = 0
var _minimal_presentation: bool = false
var _awaiting_start_input: bool = false
var _start_prompt_elapsed: float = 0.0
var _navigation_focus_active: bool = false
@ -260,6 +261,14 @@ func setup(
_begin_title_entry()
func set_minimal_presentation(enabled: bool) -> void:
_minimal_presentation = enabled
if _minimal_presentation:
_stop_decorative_presentation()
elif visible:
_start_decorative_presentation()
func reopen() -> void:
_cancel_title_entry_transition()
_cancel_title_settings_transition()
@ -1513,6 +1522,7 @@ func _start_decorative_presentation() -> void:
not _decorative_presentation_ready
or not visible
or _decorative_presentation_active
or _minimal_presentation
):
return
_decorative_generation += 1
@ -1977,7 +1987,11 @@ func _get_idle_decorative_bubble_count() -> int:
func _emit_navigation_bubble_flurry() -> void:
if not _decorative_presentation_active or not visible:
if (
_minimal_presentation
or not _decorative_presentation_active
or not visible
):
return
var bubble_count: int = _decorative_rng.randi_range(
TRANSITION_BUBBLE_COUNT_MIN,

View file

@ -12,10 +12,24 @@ const SILENT_VOLUME_DB: float = -60.0
var _weather_service: WorldWeatherService
var _fade_tween: Tween
var _is_active: bool = false
var _audio_enabled: bool = false
func _ready() -> void:
bus = &"Environment"
volume_db = SILENT_VOLUME_DB
func set_audio_enabled(enabled: bool) -> void:
if _audio_enabled == enabled:
return
_audio_enabled = enabled
if not _audio_enabled:
_silence_immediately()
stream = null
return
if not ResourceLoader.exists(RAIN_STREAM_PATH, "AudioStreamOggVorbis"):
return
var rain_stream := load(RAIN_STREAM_PATH) as AudioStreamOggVorbis
var runtime_stream := (
rain_stream.duplicate() as AudioStreamOggVorbis
@ -25,7 +39,10 @@ func _ready() -> void:
if runtime_stream != null:
runtime_stream.loop = true
stream = runtime_stream
volume_db = SILENT_VOLUME_DB
if _is_active:
_transition_to_rain(
_weather_service != null and _weather_service.is_raining()
)
func configure(weather_service: WorldWeatherService) -> void:
@ -53,6 +70,8 @@ func set_active(active: bool) -> void:
if not active:
_silence_immediately()
return
if not _audio_enabled:
return
_transition_to_rain(
_weather_service != null and _weather_service.is_raining()
)
@ -66,7 +85,7 @@ func _on_weather_changed(
func _transition_to_rain(is_raining: bool) -> void:
if not _is_active:
if not _is_active or not _audio_enabled:
return
_stop_fade()
if is_raining:

View file

@ -1,6 +1,8 @@
class_name ShorelineAmbience
extends Node
const WAVES_STREAM_PATH: String = "res://audio/ambience/waves.wav"
@export_range(0.0, 20.0, 0.1) var near_distance: float = 2.0
@export_range(1.0, 100.0, 0.5) var far_distance: float = 24.0
@export_range(-40.0, 12.0, 0.5) var near_volume_db: float = 1.0
@ -15,15 +17,37 @@ var _shoreline_segments: Array[PackedVector2Array] = []
var _distance_update_remaining: float = 0.0
var _target_volume_db: float = -80.0
var _is_active := false
var _audio_enabled: bool = false
func _ready() -> void:
_waves_audio.bus = &"Environment"
_configure_audio_loop(_waves_audio.stream)
_waves_audio.stream = null
_waves_audio.volume_db = -80.0
set_process(false)
func set_audio_enabled(enabled: bool) -> void:
if _audio_enabled == enabled:
return
_audio_enabled = enabled
if not _audio_enabled:
_waves_audio.stop()
_waves_audio.stream = null
set_process(false)
return
if not ResourceLoader.exists(WAVES_STREAM_PATH, "AudioStream"):
return
_waves_audio.stream = load(WAVES_STREAM_PATH) as AudioStream
_configure_audio_loop(_waves_audio.stream)
if _is_active:
set_process(true)
_update_target_volume()
_waves_audio.volume_db = _target_volume_db
if _waves_audio.stream != null:
_waves_audio.play()
func configure(listener: Node3D, shoreline_mesh: MeshInstance3D) -> void:
_listener = listener
_shoreline_segments = _extract_shoreline_segments(shoreline_mesh)
@ -34,8 +58,10 @@ func configure(listener: Node3D, shoreline_mesh: MeshInstance3D) -> void:
func set_active(active: bool) -> void:
_is_active = active
set_process(active)
set_process(active and _audio_enabled)
if active:
if not _audio_enabled:
return
_distance_update_remaining = 0.0
_update_target_volume()
_waves_audio.volume_db = _target_volume_db

View file

@ -12,8 +12,8 @@ const RAIN_VISIBILITY_AABB := AABB(
Vector3(27.0, 12.0, 27.0),
)
const RAIN_PARTICLE_AMOUNT: int = 2240
const LIGHT_RAIN_PARTICLE_AMOUNT: int = 128
const LIGHT_RAIN_FIXED_FPS: int = 12
const LIGHT_RAIN_PARTICLE_AMOUNT: int = 48
const LIGHT_RAIN_FIXED_FPS: int = 8
const RAIN_VELOCITY_MIN: float = 16.0
const RAIN_VELOCITY_MAX: float = 20.0
const RAIN_DROP_SIZE := Vector3(0.014, 0.34, 0.014)