diff --git a/art/source/ui/title/bubbles/bubble1.xcf b/art/source/ui/title/bubbles/bubble1.xcf new file mode 100644 index 0000000..aaf6b4d Binary files /dev/null and b/art/source/ui/title/bubbles/bubble1.xcf differ diff --git a/art/source/ui/title/bubbles/bubble2.xcf b/art/source/ui/title/bubbles/bubble2.xcf new file mode 100644 index 0000000..11018d1 Binary files /dev/null and b/art/source/ui/title/bubbles/bubble2.xcf differ diff --git a/art/source/ui/title/bubbles/bubble3.xcf b/art/source/ui/title/bubbles/bubble3.xcf new file mode 100644 index 0000000..bf77096 Binary files /dev/null and b/art/source/ui/title/bubbles/bubble3.xcf differ diff --git a/audio/music/title/as_in_four_wolves.ogg b/audio/music/title/as_in_four_wolves.ogg new file mode 100644 index 0000000..64b2463 Binary files /dev/null and b/audio/music/title/as_in_four_wolves.ogg differ diff --git a/audio/music/title/as_in_four_wolves.ogg.import b/audio/music/title/as_in_four_wolves.ogg.import new file mode 100644 index 0000000..109abb6 --- /dev/null +++ b/audio/music/title/as_in_four_wolves.ogg.import @@ -0,0 +1,19 @@ +[remap] + +importer="oggvorbisstr" +type="AudioStreamOggVorbis" +uid="uid://bgkfluumuen4m" +path="res://.godot/imported/as_in_four_wolves.ogg-3d5f167a45d25a52b56a9dd816196d44.oggvorbisstr" + +[deps] + +source_file="res://audio/music/title/as_in_four_wolves.ogg" +dest_files=["res://.godot/imported/as_in_four_wolves.ogg-3d5f167a45d25a52b56a9dd816196d44.oggvorbisstr"] + +[params] + +loop=true +loop_offset=0 +bpm=0 +beat_count=0 +bar_beats=4 diff --git a/default_bus_layout.tres b/default_bus_layout.tres new file mode 100644 index 0000000..ca38684 --- /dev/null +++ b/default_bus_layout.tres @@ -0,0 +1,15 @@ +[gd_resource type="AudioBusLayout" format=3] + +[resource] +bus/0/name = &"Master" +bus/0/solo = false +bus/0/mute = false +bus/0/bypass_fx = false +bus/0/volume_db = 0.0 +bus/0/send = &"" +bus/1/name = &"Music" +bus/1/solo = false +bus/1/mute = false +bus/1/bypass_fx = false +bus/1/volume_db = 0.0 +bus/1/send = &"Master" diff --git a/main/main.gd b/main/main.gd index abf93dd..b2bf6c6 100644 --- a/main/main.gd +++ b/main/main.gd @@ -25,10 +25,16 @@ const FishingShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) +const TITLE_MUSIC_SILENCE_DB: float = -80.0 + @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType @export var main_shop_buyer_profile: FishBuyerProfileType @export var item_catalog: ItemCatalogType +@export_category("Title Music") +@export_range(-40.0, 0.0, 0.5) var title_music_volume_db: float = -14.0 +@export_range(0.0, 5.0, 0.05) var title_music_fade_in_seconds: float = 1.0 +@export_range(0.0, 5.0, 0.05) var title_music_fade_out_seconds: float = 0.75 @onready var _test_world: TestWorldType = $TestWorld @onready var _player: PlayerType = %Player @@ -37,9 +43,14 @@ const FishingShopInteractionType = preload( @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager +@onready var _title_music: AudioStreamPlayer = %TitleMusic var _gameplay_started: bool = false var _shop_interaction: FishingShopInteractionType +var _title_music_tween: Tween +var _title_music_transition_generation: int = 0 +var _title_music_requested: bool = false +var _quit_in_progress: bool = false func _ready() -> void: @@ -144,6 +155,7 @@ func _ready() -> void: _player.hotbar.get_selected_slot(), _player.hotbar.get_selected_item_id() ) + _show_title_music() func _input(event: InputEvent) -> void: @@ -226,20 +238,26 @@ func _set_gameplay_active(active: bool) -> void: func _on_gameplay_requested() -> void: - if _gameplay_started: + if _gameplay_started or _quit_in_progress: return + _fade_out_title_music() _game_ui.get_title_screen().hide() _set_gameplay_active(true) func _on_return_to_title_requested() -> void: + if _quit_in_progress: + return var pause_menu: PauseMenuType = _game_ui.get_pause_menu() pause_menu.close_for_title_transition() _set_gameplay_active(false) _game_ui.get_title_screen().reopen() + _show_title_music() func _on_reset_progress_requested() -> void: + if _quit_in_progress: + return var pause_menu: PauseMenuType = _game_ui.get_pause_menu() if not _save_manager.delete_progression_save(): pause_menu.report_reset_failure() @@ -250,6 +268,7 @@ func _on_reset_progress_requested() -> void: pause_menu.close_for_title_transition() _set_gameplay_active(false) _game_ui.get_title_screen().reopen() + _show_title_music() func _on_water_recovery_starting() -> void: @@ -282,12 +301,116 @@ func _refresh_active_hotbar_item() -> void: func _on_quit_requested() -> void: + if _quit_in_progress: + return + _quit_in_progress = true _settings_manager.save_if_dirty() if _gameplay_started: _save_manager.save_if_dirty() + _fade_out_title_music(_finish_quit) + + +func _show_title_music() -> void: + _title_music_requested = true + var generation: int = _replace_title_music_transition() + if not _title_music.playing: + _title_music.volume_db = TITLE_MUSIC_SILENCE_DB + _title_music.play() + if is_equal_approx(_title_music.volume_db, title_music_volume_db): + return + if title_music_fade_in_seconds <= 0.0: + _title_music.volume_db = title_music_volume_db + return + _title_music_tween = create_tween() + _title_music_tween.set_trans(Tween.TRANS_CUBIC) + _title_music_tween.set_ease(Tween.EASE_OUT) + _title_music_tween.tween_property( + _title_music, + "volume_db", + title_music_volume_db, + title_music_fade_in_seconds + ) + _title_music_tween.finished.connect( + _on_title_music_faded_in.bind(generation), + CONNECT_ONE_SHOT + ) + + +func _fade_out_title_music(on_complete: Callable = Callable()) -> void: + _title_music_requested = false + var generation: int = _replace_title_music_transition() + if not _title_music.playing: + _title_music.volume_db = title_music_volume_db + if on_complete.is_valid(): + on_complete.call() + return + if title_music_fade_out_seconds <= 0.0: + _complete_title_music_fade_out(generation, on_complete) + return + _title_music_tween = create_tween() + _title_music_tween.set_trans(Tween.TRANS_CUBIC) + _title_music_tween.set_ease(Tween.EASE_IN) + _title_music_tween.tween_property( + _title_music, + "volume_db", + TITLE_MUSIC_SILENCE_DB, + title_music_fade_out_seconds + ) + _title_music_tween.finished.connect( + _complete_title_music_fade_out.bind(generation, on_complete), + CONNECT_ONE_SHOT + ) + + +func _replace_title_music_transition() -> int: + _title_music_transition_generation += 1 + if _title_music_tween != null: + _title_music_tween.kill() + _title_music_tween = null + return _title_music_transition_generation + + +func _on_title_music_faded_in(generation: int) -> void: + if ( + generation != _title_music_transition_generation + or not _title_music_requested + ): + return + _title_music_tween = null + _title_music.volume_db = title_music_volume_db + + +func _complete_title_music_fade_out( + generation: int, + on_complete: Callable, +) -> void: + if ( + generation != _title_music_transition_generation + or _title_music_requested + ): + return + _title_music_tween = null + _title_music.stop() + _title_music.volume_db = title_music_volume_db + if on_complete.is_valid(): + on_complete.call() + + +func _finish_quit() -> void: + _title_music.queue_free() + await get_tree().process_frame get_tree().quit() +func _exit_tree() -> void: + if _title_music_tween != null: + _title_music_tween.kill() + _title_music_tween = null + if is_instance_valid(_title_music): + _title_music.stop() + _title_music.stream = null + + func _on_shop_range_changed(in_range: bool) -> void: if not in_range: _game_ui.get_fishing_shop().close_for_range_exit() diff --git a/main/main.tscn b/main/main.tscn index aacf883..d8565ec 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=13 format=3] +[gd_scene load_steps=14 format=3] [ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"] [ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] @@ -12,6 +12,7 @@ [ext_resource type="Script" 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" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"] [node name="Main" type="Node3D"] script = ExtResource("3_main") @@ -44,5 +45,11 @@ script = ExtResource("9_save") unique_name_in_owner = true script = ExtResource("10_settings") +[node name="TitleMusic" type="AudioStreamPlayer" parent="."] +unique_name_in_owner = true +stream = ExtResource("13_title_music") +volume_db = -80.0 +bus = &"Music" + [node name="GameUI" parent="." instance=ExtResource("5_ui")] unique_name_in_owner = true diff --git a/ui/assets/title/bubbles/bubble1.png b/ui/assets/title/bubbles/bubble1.png new file mode 100644 index 0000000..e3003e2 Binary files /dev/null and b/ui/assets/title/bubbles/bubble1.png differ diff --git a/ui/assets/title/bubbles/bubble1.png.import b/ui/assets/title/bubbles/bubble1.png.import new file mode 100644 index 0000000..276aa31 --- /dev/null +++ b/ui/assets/title/bubbles/bubble1.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dkvilbotou8bp" +path="res://.godot/imported/bubble1.png-c853cfc8f5bc5b89bb69a71c23de669d.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble1.png" +dest_files=["res://.godot/imported/bubble1.png-c853cfc8f5bc5b89bb69a71c23de669d.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/assets/title/bubbles/bubble2.png b/ui/assets/title/bubbles/bubble2.png new file mode 100644 index 0000000..a2a4d7d Binary files /dev/null and b/ui/assets/title/bubbles/bubble2.png differ diff --git a/ui/assets/title/bubbles/bubble2.png.import b/ui/assets/title/bubbles/bubble2.png.import new file mode 100644 index 0000000..e77ac0e --- /dev/null +++ b/ui/assets/title/bubbles/bubble2.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dh11c1271egs7" +path="res://.godot/imported/bubble2.png-3fe11ab91543c1b1505cfc1100bfd0c7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble2.png" +dest_files=["res://.godot/imported/bubble2.png-3fe11ab91543c1b1505cfc1100bfd0c7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/assets/title/bubbles/bubble3.png b/ui/assets/title/bubbles/bubble3.png new file mode 100644 index 0000000..fdacb6a Binary files /dev/null and b/ui/assets/title/bubbles/bubble3.png differ diff --git a/ui/assets/title/bubbles/bubble3.png.import b/ui/assets/title/bubbles/bubble3.png.import new file mode 100644 index 0000000..0986a8e --- /dev/null +++ b/ui/assets/title/bubbles/bubble3.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b5praw1x4vukl" +path="res://.godot/imported/bubble3.png-7e3e34f881bccf21cec803168829b93e.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/bubbles/bubble3.png" +dest_files=["res://.godot/imported/bubble3.png-7e3e34f881bccf21cec803168829b93e.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/assets/title/netfishing_logo.png b/ui/assets/title/netfishing_logo.png new file mode 100644 index 0000000..71c8ac8 Binary files /dev/null and b/ui/assets/title/netfishing_logo.png differ diff --git a/ui/assets/title/netfishing_logo.png.import b/ui/assets/title/netfishing_logo.png.import new file mode 100644 index 0000000..2aa351a --- /dev/null +++ b/ui/assets/title/netfishing_logo.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ckqayis1ckb2v" +path="res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://ui/assets/title/netfishing_logo.png" +dest_files=["res://.godot/imported/netfishing_logo.png-82ab9fce5851dcc5239eca178e8cde99.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/ui/title_logo_underwater.gdshader b/ui/title_logo_underwater.gdshader new file mode 100644 index 0000000..cdcfce8 --- /dev/null +++ b/ui/title_logo_underwater.gdshader @@ -0,0 +1,48 @@ +shader_type canvas_item; + +uniform float horizontal_displacement_pixels : hint_range(0.0, 2.0, 0.25) = 1.5; +uniform float wave_speed : hint_range(0.0, 2.0, 0.05) = 0.55; +uniform float wave_scale : hint_range(0.0, 0.20, 0.005) = 0.055; +uniform vec3 underwater_tint : source_color = vec3(0.78, 0.96, 1.08); +uniform float tint_strength : hint_range(0.0, 0.35, 0.01) = 0.18; +uniform float highlight_strength : hint_range(0.0, 0.15, 0.01) = 0.06; +uniform float bob_amount_pixels : hint_range(0.0, 4.0, 1.0) = 4.0; +uniform float bob_speed : hint_range(0.0, 2.0, 0.05) = 0.65; + +void vertex() { + VERTEX.y += round( + sin(TIME * bob_speed) * bob_amount_pixels + ); +} + +void fragment() { + float stepped_time = floor(TIME * 12.0) / 12.0; + float source_row = floor(UV.y / max(TEXTURE_PIXEL_SIZE.y, 0.000001)); + float wave = sin( + source_row * wave_scale + stepped_time * wave_speed + ); + float pixel_offset = clamp( + round(wave * horizontal_displacement_pixels), + -2.0, + 2.0 + ); + vec2 sample_uv = UV + vec2(pixel_offset * TEXTURE_PIXEL_SIZE.x, 0.0); + vec4 source = texture(TEXTURE, sample_uv); + + vec3 treated_color = mix( + source.rgb, + source.rgb * underwater_tint, + tint_strength + ); + float highlight_wave = sin( + UV.y * 26.0 - stepped_time * wave_speed * 0.8 + ) * 0.5 + 0.5; + float highlight_level = floor(highlight_wave * 3.0) / 3.0; + treated_color += ( + vec3(0.12, 0.30, 0.34) + * highlight_level + * highlight_strength + * source.a + ); + COLOR = vec4(treated_color, source.a); +} diff --git a/ui/title_logo_underwater.gdshader.uid b/ui/title_logo_underwater.gdshader.uid new file mode 100644 index 0000000..77c6f67 --- /dev/null +++ b/ui/title_logo_underwater.gdshader.uid @@ -0,0 +1 @@ +uid://bu14y18kegufh diff --git a/ui/title_screen.gd b/ui/title_screen.gd index be83160..2c70655 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -7,6 +7,52 @@ const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const SettingsPanelType = preload("res://ui/settings_panel.gd") +const DECORATIVE_FISH_TEXTURES: Array[Texture2D] = [ + preload("res://fish/species/bass/fish_bass_striped.png"), + preload("res://fish/species/bluegill/fish_bluegill.png"), + preload("res://fish/species/carp/fish_carp_common.png"), + preload("res://fish/species/sunfish/fish_sunfish.png"), +] +const DECORATIVE_BUBBLE_TEXTURES: Array[Texture2D] = [ + preload("res://ui/assets/title/bubbles/bubble1.png"), + preload("res://ui/assets/title/bubbles/bubble2.png"), + preload("res://ui/assets/title/bubbles/bubble3.png"), +] + +const FIRST_FISH_DELAY_MIN: float = 2.0 +const FIRST_FISH_DELAY_MAX: float = 5.0 +const NEXT_FISH_DELAY_MIN: float = 5.0 +const NEXT_FISH_DELAY_MAX: float = 12.0 +const CROSSING_DURATION_MIN: float = 8.0 +const CROSSING_DURATION_MAX: float = 16.0 +const FISH_LONGEST_SIDE_MIN: float = 120.0 +const FISH_LONGEST_SIDE_MAX: float = 210.0 +const FISH_EDGE_MARGIN: float = 24.0 +const BUBBLE_EVENT_DELAY_MIN: float = 4.0 +const BUBBLE_EVENT_DELAY_MAX: float = 10.0 +const BUBBLE_CLUSTER_DELAY_MIN: float = 0.25 +const BUBBLE_CLUSTER_DELAY_MAX: float = 0.65 +const BUBBLE_TRAVEL_DURATION_MIN: float = 7.0 +const BUBBLE_TRAVEL_DURATION_MAX: float = 14.0 +const BUBBLE_SCALE_MIN: float = 0.65 +const BUBBLE_SCALE_MAX: float = 1.15 +const BUBBLE_OPACITY_MIN: float = 0.55 +const BUBBLE_OPACITY_MAX: float = 0.90 +const BUBBLE_DRIFT_MIN: float = 10.0 +const BUBBLE_DRIFT_MAX: float = 45.0 +const BUBBLE_WOBBLE_MIN: float = 3.0 +const BUBBLE_WOBBLE_MAX: float = 9.0 +const BUBBLE_EDGE_MARGIN: float = 16.0 +const MAX_DECORATIVE_BUBBLES: int = 6 +const LOGO_ASPECT_RATIO: float = 2560.0 / 760.0 +const LOGO_WIDTH_FACTOR: float = 0.52 +const LOGO_MIN_WIDTH: float = 320.0 +const LOGO_MAX_WIDTH: float = 720.0 +const BUTTON_COLUMN_WIDTH: float = 360.0 +const TITLE_HORIZONTAL_MARGIN: float = 48.0 +const START_PROMPT_MIN_SCALE: float = 0.985 +const START_PROMPT_MAX_SCALE: float = 1.015 +const START_PROMPT_CYCLE_SECONDS: float = 3.0 signal gameplay_requested signal quit_requested @@ -21,17 +67,41 @@ enum ConfirmationAction { @onready var _new_game_button: Button = %NewGameButton @onready var _settings_button: Button = %SettingsButton @onready var _delete_button: Button = %DeleteSaveButton +@onready var _quit_button: Button = %QuitButton @onready var _feedback_label: Label = %FeedbackLabel @onready var _confirmation_panel: PanelContainer = %ConfirmationPanel @onready var _confirmation_text: Label = %ConfirmationText @onready var _confirm_button: Button = %ConfirmButton @onready var _settings_panel: SettingsPanelType = %SettingsPanel +@onready var _decorative_fish_layer: Control = %DecorativeFishLayer +@onready var _decorative_fish_timer: Timer = %DecorativeFishTimer +@onready var _decorative_bubble_layer: Control = %DecorativeBubbleLayer +@onready var _decorative_bubble_event_timer: Timer = %DecorativeBubbleEventTimer +@onready var _decorative_bubble_cluster_timer: Timer = %DecorativeBubbleClusterTimer +@onready var _title_logo: TextureRect = %TitleLogo +@onready var _button_center: CenterContainer = %ButtonCenter +@onready var _button_stack: VBoxContainer = %ButtonStack +@onready var _start_prompt_center: CenterContainer = %StartPromptCenter +@onready var _start_prompt_label: Label = %StartPromptLabel var _save_manager: SaveManagerType var _settings_manager: SettingsManagerType var _inspection: SaveInspectionType var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _action_in_progress: bool = false +var _decorative_rng := RandomNumberGenerator.new() +var _decorative_fish: TextureRect +var _decorative_fish_tween: Tween +var _decorative_presentation_ready: bool = false +var _decorative_presentation_active: bool = false +var _decorative_generation: int = 0 +var _decorative_bubbles: Array[TextureRect] = [] +var _decorative_bubble_tweens: Dictionary[int, Tween] = {} +var _pending_cluster_bubbles: int = 0 +var _awaiting_start_input: bool = false +var _start_prompt_elapsed: float = 0.0 +var _navigation_focus_active: bool = false +var _modal_restore_navigation_focus: bool = false func _ready() -> void: @@ -44,6 +114,20 @@ func _ready() -> void: %CancelConfirmButton.pressed.connect(_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) + _decorative_fish_timer.timeout.connect(_on_decorative_fish_timer_timeout) + _decorative_bubble_event_timer.timeout.connect( + _on_decorative_bubble_event_timer_timeout + ) + _decorative_bubble_cluster_timer.timeout.connect( + _on_decorative_bubble_cluster_timer_timeout + ) + visibility_changed.connect(_on_title_visibility_changed) + resized.connect(_update_title_layout) + _start_prompt_label.resized.connect(_update_start_prompt_pivot) + _decorative_rng.randomize() + set_process(false) + call_deferred("_update_title_layout") + call_deferred("_update_start_prompt_pivot") func setup( @@ -55,20 +139,104 @@ func setup( Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _refresh_save_inspection() show() - call_deferred("_focus_initial_button") + _decorative_presentation_ready = true + _start_decorative_presentation() + _begin_title_entry() func reopen() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + _prepare_awaiting_start_input() _close_confirmation() _settings_panel.hide() _refresh_save_inspection() show() - call_deferred("_focus_initial_button") + _start_decorative_presentation() + _start_entry_prompt_animation() + + +func is_awaiting_start_input() -> bool: + return _awaiting_start_input + + +func is_decorative_presentation_active() -> bool: + return _decorative_presentation_active + + +func get_active_decorative_fish_count() -> int: + return 1 if is_instance_valid(_decorative_fish) else 0 + + +func get_active_decorative_bubble_count() -> int: + return _decorative_bubbles.size() + + +func is_decorative_bubble_scheduler_active() -> bool: + return ( + _decorative_presentation_active + and ( + not _decorative_bubble_event_timer.is_stopped() + or not _decorative_bubble_cluster_timer.is_stopped() + ) + ) + + +func _update_title_layout() -> void: + if not is_node_ready(): + return + var available_width: float = maxf(1.0, size.x - TITLE_HORIZONTAL_MARGIN) + var logo_width: float = minf( + clampf( + size.x * LOGO_WIDTH_FACTOR, + LOGO_MIN_WIDTH, + LOGO_MAX_WIDTH + ), + available_width + ) + _title_logo.custom_minimum_size = Vector2( + logo_width, + logo_width / LOGO_ASPECT_RATIO + ) + _button_stack.custom_minimum_size = Vector2( + minf(BUTTON_COLUMN_WIDTH, available_width), + 0.0 + ) + + +func _process(delta: float) -> void: + if ( + not _awaiting_start_input + or not visible + or not _start_prompt_center.visible + ): + return + _start_prompt_elapsed = fmod( + _start_prompt_elapsed + delta, + START_PROMPT_CYCLE_SECONDS + ) + var phase: float = _start_prompt_elapsed / START_PROMPT_CYCLE_SECONDS + var pulse_weight: float = (sin(phase * TAU - PI * 0.5) + 1.0) * 0.5 + var prompt_scale: float = lerpf( + START_PROMPT_MIN_SCALE, + START_PROMPT_MAX_SCALE, + pulse_weight + ) + _start_prompt_label.scale = Vector2.ONE * prompt_scale func _input(event: InputEvent) -> void: - if not visible or not event.is_action_pressed("ui_cancel"): + if not visible: + return + if _awaiting_start_input: + if not _is_start_prompt_reveal_event(event): + return + _reveal_primary_menu() + get_viewport().set_input_as_handled() + return + if _handle_primary_menu_focus_input(event): + get_viewport().set_input_as_handled() + return + if not event.is_action_pressed("ui_cancel"): return if _confirmation_panel.visible: _close_confirmation() @@ -79,6 +247,119 @@ func _input(event: InputEvent) -> void: get_viewport().set_input_as_handled() +func _is_start_prompt_reveal_event(event: InputEvent) -> bool: + if event is InputEventKey: + var key_event := event as InputEventKey + return key_event.pressed and not key_event.echo + if event is InputEventJoypadButton: + return (event as InputEventJoypadButton).pressed + if event is InputEventMouseButton: + return (event as InputEventMouseButton).pressed + return false + + +func _handle_primary_menu_focus_input(event: InputEvent) -> bool: + if ( + not _button_center.visible + or _confirmation_panel.visible + or _settings_panel.visible + ): + return false + if event is InputEventMouseMotion: + _navigation_focus_active = false + _release_primary_menu_focus() + return false + if event is InputEventKey and (event as InputEventKey).echo: + return false + var moves_forward: bool = ( + event.is_action_pressed("ui_down") + or event.is_action_pressed("ui_right") + ) + var moves_backward: bool = ( + event.is_action_pressed("ui_up") + or event.is_action_pressed("ui_left") + ) + if not moves_forward and not moves_backward: + return false + _navigation_focus_active = true + if _primary_menu_has_focus(): + return false + if moves_forward: + _get_first_available_menu_button().grab_focus() + else: + _quit_button.grab_focus() + return true + + +func _get_first_available_menu_button() -> Button: + return _continue_button if not _continue_button.disabled else _new_game_button + + +func _primary_menu_has_focus() -> bool: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + return focus_owner != null and _button_center.is_ancestor_of(focus_owner) + + +func _release_primary_menu_focus() -> void: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and _button_center.is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _release_title_focus() -> void: + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _begin_title_entry() -> void: + _prepare_awaiting_start_input() + _start_entry_prompt_animation() + + +func _prepare_awaiting_start_input() -> void: + _awaiting_start_input = true + _navigation_focus_active = false + _modal_restore_navigation_focus = false + _button_center.hide() + _feedback_label.hide() + _start_prompt_center.show() + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if focus_owner != null and is_ancestor_of(focus_owner): + focus_owner.release_focus() + + +func _start_entry_prompt_animation() -> void: + _start_prompt_elapsed = 0.0 + _start_prompt_label.scale = Vector2.ONE * START_PROMPT_MIN_SCALE + _update_start_prompt_pivot() + set_process(true) + + +func _stop_entry_prompt_animation() -> void: + set_process(false) + _start_prompt_elapsed = 0.0 + _start_prompt_label.scale = Vector2.ONE + + +func _update_start_prompt_pivot() -> void: + if not is_node_ready(): + return + _start_prompt_label.pivot_offset = _start_prompt_label.size * 0.5 + + +func _reveal_primary_menu() -> void: + if not _awaiting_start_input: + return + _awaiting_start_input = false + _stop_entry_prompt_animation() + _start_prompt_center.hide() + _button_center.show() + _feedback_label.show() + _navigation_focus_active = false + _release_title_focus() + + func _on_continue_pressed() -> void: if ( _action_in_progress @@ -90,11 +371,11 @@ func _on_continue_pressed() -> void: return _action_in_progress = true if _save_manager.load_player_data(): - _feedback_label.text = "Save loaded." + _feedback_label.text = "save loaded." gameplay_requested.emit() else: _refresh_save_inspection() - _feedback_label.text = "Failed to load save. The original was preserved." + _feedback_label.text = "failed to load save. the original was preserved." _action_in_progress = false @@ -112,17 +393,17 @@ func _on_new_game_pressed() -> void: return if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION: _feedback_label.text = ( - "This save is from a newer game version. " - + "Use Delete Save before starting over." + "this save is from a newer game version. " + + "use delete save before starting over." ) return if _inspection.status == SaveInspectionType.Status.IO_ERROR: - _feedback_label.text = "The existing save cannot be accessed safely." + _feedback_label.text = "the existing save cannot be accessed safely." return _open_confirmation( ConfirmationAction.NEW_GAME, - "Start a new game? Existing progression will be deleted.", - "Start New Game" + "start a new game? existing progression will be deleted.", + "start new game" ) @@ -137,8 +418,8 @@ func _on_delete_pressed() -> void: return _open_confirmation( ConfirmationAction.DELETE_SAVE, - "Delete your saved progression? This cannot be undone.", - "Delete" + "delete your saved progression? this cannot be undone.", + "delete" ) @@ -149,7 +430,7 @@ func _on_confirmation_accepted() -> void: _close_confirmation() _action_in_progress = true if not _save_manager.delete_progression_save(): - _feedback_label.text = "Failed to delete saved progression." + _feedback_label.text = "failed to delete saved progression." _action_in_progress = false _refresh_save_inspection() return @@ -158,7 +439,7 @@ func _on_confirmation_accepted() -> void: if action == ConfirmationAction.NEW_GAME: gameplay_requested.emit() else: - _feedback_label.text = "Saved progression deleted." + _feedback_label.text = "saved progression deleted." _action_in_progress = false @@ -167,6 +448,7 @@ func _open_confirmation( message: String, confirm_text: String, ) -> void: + _modal_restore_navigation_focus = _navigation_focus_active _confirmation_action = action _confirmation_text.text = message _confirm_button.text = confirm_text @@ -175,14 +457,23 @@ func _open_confirmation( func _close_confirmation() -> void: + var restore_navigation_focus: bool = _modal_restore_navigation_focus + _modal_restore_navigation_focus = false _confirmation_action = ConfirmationAction.NONE _confirmation_panel.visible = false - _focus_initial_button() + if _awaiting_start_input: + return + if restore_navigation_focus: + _focus_initial_button() + else: + _navigation_focus_active = false + _release_title_focus() func _open_settings() -> void: if _confirmation_panel.visible or _action_in_progress: return + _modal_restore_navigation_focus = _navigation_focus_active _feedback_label.text = "" _settings_panel.open_panel(_settings_manager) @@ -192,12 +483,23 @@ func _close_settings() -> void: func _on_settings_applied() -> void: - _feedback_label.text = "Settings saved." - _settings_button.grab_focus() + _feedback_label.text = "settings saved." + _restore_settings_focus() func _on_settings_closed() -> void: - _settings_button.grab_focus() + _restore_settings_focus() + + +func _restore_settings_focus() -> void: + var restore_navigation_focus: bool = _modal_restore_navigation_focus + _modal_restore_navigation_focus = false + if restore_navigation_focus: + _navigation_focus_active = true + _settings_button.grab_focus() + else: + _navigation_focus_active = false + _release_title_focus() func _refresh_save_inspection() -> void: @@ -217,12 +519,18 @@ func _refresh_save_inspection() -> void: func _focus_initial_button() -> void: - if not is_node_ready() or not visible: + if ( + not is_node_ready() + or not visible + or _awaiting_start_input + or not _button_center.visible + ): return if not _continue_button.disabled: _continue_button.grab_focus() else: _new_game_button.grab_focus() + _navigation_focus_active = true func _on_quit_pressed() -> void: @@ -232,3 +540,430 @@ func _on_quit_pressed() -> void: and not _settings_panel.visible ): quit_requested.emit() + + +func _on_title_visibility_changed() -> void: + if not _decorative_presentation_ready: + return + if visible: + _start_decorative_presentation() + else: + _stop_entry_prompt_animation() + _navigation_focus_active = false + _modal_restore_navigation_focus = false + _stop_decorative_presentation() + + +func _start_decorative_presentation() -> void: + if ( + not _decorative_presentation_ready + or not visible + or _decorative_presentation_active + ): + return + _decorative_generation += 1 + _decorative_presentation_active = true + _schedule_next_decorative_fish( + FIRST_FISH_DELAY_MIN, + FIRST_FISH_DELAY_MAX + ) + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _stop_decorative_presentation() -> void: + _decorative_generation += 1 + _decorative_presentation_active = false + _decorative_fish_timer.stop() + if _decorative_fish_tween != null: + _decorative_fish_tween.kill() + _decorative_fish_tween = null + if is_instance_valid(_decorative_fish): + _decorative_fish.queue_free() + _decorative_fish = null + _decorative_bubble_event_timer.stop() + _decorative_bubble_cluster_timer.stop() + _pending_cluster_bubbles = 0 + for bubble_tween: Tween in _decorative_bubble_tweens.values(): + if bubble_tween != null: + bubble_tween.kill() + _decorative_bubble_tweens.clear() + for bubble: TextureRect in _decorative_bubbles: + if is_instance_valid(bubble): + bubble.queue_free() + _decorative_bubbles.clear() + + +func _schedule_next_decorative_fish( + minimum_delay: float, + maximum_delay: float, +) -> void: + if not _decorative_presentation_active or not visible: + return + _decorative_fish_timer.start( + _decorative_rng.randf_range(minimum_delay, maximum_delay) + ) + + +func _on_decorative_fish_timer_timeout() -> void: + if ( + not _decorative_presentation_active + or not visible + or is_instance_valid(_decorative_fish) + ): + return + _spawn_decorative_fish(_decorative_generation) + + +func _spawn_decorative_fish(generation: int) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not visible + or DECORATIVE_FISH_TEXTURES.is_empty() + ): + return + var layer_size: Vector2 = _decorative_fish_layer.size + if layer_size.x <= 1.0 or layer_size.y <= 1.0: + _schedule_next_decorative_fish(0.5, 1.0) + return + var texture: Texture2D = DECORATIVE_FISH_TEXTURES[ + _decorative_rng.randi_range(0, DECORATIVE_FISH_TEXTURES.size() - 1) + ] + var source_size: Vector2 = texture.get_size() + if source_size.x <= 0.0 or source_size.y <= 0.0: + _schedule_next_decorative_fish( + NEXT_FISH_DELAY_MIN, + NEXT_FISH_DELAY_MAX + ) + return + var longest_side: float = _decorative_rng.randf_range( + FISH_LONGEST_SIDE_MIN, + FISH_LONGEST_SIDE_MAX + ) + var presentation_scale: float = longest_side / maxf( + source_size.x, + source_size.y + ) + var presentation_size: Vector2 = source_size * presentation_scale + var fish_control := TextureRect.new() + fish_control.name = "DecorativeFish" + fish_control.texture = texture + fish_control.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + fish_control.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + fish_control.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + fish_control.mouse_filter = Control.MOUSE_FILTER_IGNORE + fish_control.custom_minimum_size = presentation_size + fish_control.size = presentation_size + fish_control.modulate.a = 0.46 + + var direction: float = ( + 1.0 + if _decorative_rng.randi_range(0, 1) == 1 + else -1.0 + ) + fish_control.flip_h = direction > 0.0 + var minimum_y: float = maxf(24.0, layer_size.y * 0.08) + var maximum_y: float = maxf( + minimum_y, + layer_size.y - presentation_size.y - maxf(24.0, layer_size.y * 0.08) + ) + var base_y: float = _decorative_rng.randf_range(minimum_y, maximum_y) + var bob_amplitude: float = _decorative_rng.randf_range(3.0, 8.0) + var bob_speed: float = _decorative_rng.randf_range(0.65, 1.15) + var crossing_duration: float = _decorative_rng.randf_range( + CROSSING_DURATION_MIN, + CROSSING_DURATION_MAX + ) + fish_control.position = Vector2( + -presentation_size.x - FISH_EDGE_MARGIN + if direction > 0.0 + else layer_size.x + FISH_EDGE_MARGIN, + base_y + ) + _decorative_fish_layer.add_child(fish_control) + _decorative_fish = fish_control + _decorative_fish_tween = create_tween() + _decorative_fish_tween.tween_method( + _update_decorative_fish.bind( + fish_control, + direction, + base_y, + bob_amplitude, + bob_speed, + crossing_duration, + generation + ), + 0.0, + 1.0, + crossing_duration + ) + _decorative_fish_tween.finished.connect( + _on_decorative_fish_finished.bind(fish_control, generation), + CONNECT_ONE_SHOT + ) + + +func _update_decorative_fish( + progress: float, + fish_control: TextureRect, + direction: float, + base_y: float, + bob_amplitude: float, + bob_speed: float, + crossing_duration: float, + generation: int, +) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not is_instance_valid(fish_control) + ): + return + var start_x: float = ( + -fish_control.size.x - FISH_EDGE_MARGIN + if direction > 0.0 + else _decorative_fish_layer.size.x + FISH_EDGE_MARGIN + ) + var end_x: float = ( + _decorative_fish_layer.size.x + FISH_EDGE_MARGIN + if direction > 0.0 + else -fish_control.size.x - FISH_EDGE_MARGIN + ) + fish_control.position.x = lerpf(start_x, end_x, progress) + var bobbed_y: float = ( + base_y + + sin(progress * crossing_duration * bob_speed) * bob_amplitude + ) + fish_control.position.y = clampf( + bobbed_y, + 8.0, + maxf(8.0, _decorative_fish_layer.size.y - fish_control.size.y - 8.0) + ) + + +func _on_decorative_fish_finished( + fish_control: TextureRect, + generation: int, +) -> void: + if generation != _decorative_generation: + return + _decorative_fish_tween = null + if is_instance_valid(fish_control): + fish_control.queue_free() + if _decorative_fish == fish_control: + _decorative_fish = null + _schedule_next_decorative_fish( + NEXT_FISH_DELAY_MIN, + NEXT_FISH_DELAY_MAX + ) + + +func _schedule_next_decorative_bubble_event( + minimum_delay: float, + maximum_delay: float, +) -> void: + if not _decorative_presentation_active or not visible: + return + _decorative_bubble_event_timer.start( + _decorative_rng.randf_range(minimum_delay, maximum_delay) + ) + + +func _on_decorative_bubble_event_timer_timeout() -> void: + if not _decorative_presentation_active or not visible: + return + var available_slots: int = ( + MAX_DECORATIVE_BUBBLES - _decorative_bubbles.size() + ) + if available_slots <= 0: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + return + var event_size: int = _choose_decorative_bubble_event_size() + event_size = mini(event_size, available_slots) + if not _spawn_decorative_bubble(_decorative_generation): + _schedule_next_decorative_bubble_event(0.5, 1.0) + return + _pending_cluster_bubbles = event_size - 1 + if _pending_cluster_bubbles > 0: + _start_decorative_bubble_cluster_timer() + else: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _choose_decorative_bubble_event_size() -> int: + var roll: float = _decorative_rng.randf() + if roll < 0.75: + return 1 + if roll < 0.95: + return 2 + return 3 + + +func _start_decorative_bubble_cluster_timer() -> void: + _decorative_bubble_cluster_timer.start( + _decorative_rng.randf_range( + BUBBLE_CLUSTER_DELAY_MIN, + BUBBLE_CLUSTER_DELAY_MAX + ) + ) + + +func _on_decorative_bubble_cluster_timer_timeout() -> void: + if ( + not _decorative_presentation_active + or not visible + or _pending_cluster_bubbles <= 0 + ): + _pending_cluster_bubbles = 0 + return + if _decorative_bubbles.size() < MAX_DECORATIVE_BUBBLES: + _spawn_decorative_bubble(_decorative_generation) + _pending_cluster_bubbles -= 1 + if _pending_cluster_bubbles > 0: + _start_decorative_bubble_cluster_timer() + else: + _schedule_next_decorative_bubble_event( + BUBBLE_EVENT_DELAY_MIN, + BUBBLE_EVENT_DELAY_MAX + ) + + +func _spawn_decorative_bubble(generation: int) -> bool: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not visible + or _decorative_bubbles.size() >= MAX_DECORATIVE_BUBBLES + or DECORATIVE_BUBBLE_TEXTURES.is_empty() + ): + return false + var layer_size: Vector2 = _decorative_bubble_layer.size + if layer_size.x <= 1.0 or layer_size.y <= 1.0: + return false + var texture: Texture2D = DECORATIVE_BUBBLE_TEXTURES[ + _decorative_rng.randi_range( + 0, + DECORATIVE_BUBBLE_TEXTURES.size() - 1 + ) + ] + var presentation_scale: float = _decorative_rng.randf_range( + BUBBLE_SCALE_MIN, + BUBBLE_SCALE_MAX + ) + var presentation_size: Vector2 = texture.get_size() * presentation_scale + var bubble := TextureRect.new() + bubble.name = "DecorativeBubble" + bubble.texture = texture + bubble.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + bubble.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED + bubble.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST + bubble.mouse_filter = Control.MOUSE_FILTER_IGNORE + bubble.size = presentation_size + bubble.modulate.a = _decorative_rng.randf_range( + BUBBLE_OPACITY_MIN, + BUBBLE_OPACITY_MAX + ) + _decorative_bubble_layer.add_child(bubble) + _decorative_bubbles.append(bubble) + + var normalized_x: float = _decorative_rng.randf_range(0.08, 0.92) + var drift_direction: float = ( + 1.0 if _decorative_rng.randi_range(0, 1) == 1 else -1.0 + ) + var horizontal_drift: float = ( + _decorative_rng.randf_range( + BUBBLE_DRIFT_MIN, + BUBBLE_DRIFT_MAX + ) + * drift_direction + ) + var wobble_amplitude: float = _decorative_rng.randf_range( + BUBBLE_WOBBLE_MIN, + BUBBLE_WOBBLE_MAX + ) + var wobble_cycles: float = _decorative_rng.randf_range(0.8, 1.6) + var wobble_phase: float = _decorative_rng.randf_range(0.0, TAU) + var travel_duration: float = _decorative_rng.randf_range( + BUBBLE_TRAVEL_DURATION_MIN, + BUBBLE_TRAVEL_DURATION_MAX + ) + var bubble_tween: Tween = create_tween() + _decorative_bubble_tweens[bubble.get_instance_id()] = bubble_tween + bubble_tween.tween_method( + _update_decorative_bubble.bind( + bubble, + normalized_x, + horizontal_drift, + wobble_amplitude, + wobble_cycles, + wobble_phase, + generation + ), + 0.0, + 1.0, + travel_duration + ) + bubble_tween.finished.connect( + _on_decorative_bubble_finished.bind(bubble, generation), + CONNECT_ONE_SHOT + ) + return true + + +func _update_decorative_bubble( + progress: float, + bubble: TextureRect, + normalized_x: float, + horizontal_drift: float, + wobble_amplitude: float, + wobble_cycles: float, + wobble_phase: float, + generation: int, +) -> void: + if ( + generation != _decorative_generation + or not _decorative_presentation_active + or not is_instance_valid(bubble) + ): + return + var layer_size: Vector2 = _decorative_bubble_layer.size + var start_y: float = ( + layer_size.y + bubble.size.y + BUBBLE_EDGE_MARGIN + ) + var end_y: float = -bubble.size.y - BUBBLE_EDGE_MARGIN + var base_x: float = normalized_x * layer_size.x + var wobble: float = sin( + wobble_phase + progress * TAU * wobble_cycles + ) * wobble_amplitude + bubble.position = Vector2( + base_x + + horizontal_drift * progress + + wobble + - bubble.size.x * 0.5, + lerpf(start_y, end_y, progress) + ) + + +func _on_decorative_bubble_finished( + bubble: TextureRect, + generation: int, +) -> void: + if generation != _decorative_generation: + return + if is_instance_valid(bubble): + _decorative_bubble_tweens.erase(bubble.get_instance_id()) + _decorative_bubbles.erase(bubble) + bubble.queue_free() + + +func _exit_tree() -> void: + _stop_decorative_presentation() diff --git a/ui/title_screen.tscn b/ui/title_screen.tscn index dce846f..b26271f 100644 --- a/ui/title_screen.tscn +++ b/ui/title_screen.tscn @@ -1,8 +1,17 @@ -[gd_scene load_steps=4 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://ui/title_screen.gd" id="1_script"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="3_settings"] +[ext_resource type="Shader" path="res://ui/title_water_background.gdshader" id="4_water_shader"] +[ext_resource type="Texture2D" path="res://ui/assets/title/netfishing_logo.png" id="5_logo"] +[ext_resource type="Shader" path="res://ui/title_logo_underwater.gdshader" id="6_logo_shader"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water"] +shader = ExtResource("4_water_shader") + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_logo"] +shader = ExtResource("6_logo_shader") [node name="TitleScreen" type="Control"] unique_name_in_owner = true @@ -18,15 +27,78 @@ theme = ExtResource("2_theme") script = ExtResource("1_script") [node name="Background" type="ColorRect" parent="."] +material = SubResource("ShaderMaterial_title_water") layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -color = Color(0.047, 0.102, 0.145, 1) +mouse_filter = 2 + +[node name="DecorativeFishLayer" type="Control" parent="."] +unique_name_in_owner = true +z_index = 1 +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="DecorativeFishTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="DecorativeBubbleLayer" type="Control" parent="."] +unique_name_in_owner = true +z_index = 2 +clip_contents = true +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="DecorativeBubbleEventTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="DecorativeBubbleClusterTimer" type="Timer" parent="."] +unique_name_in_owner = true +one_shot = true + +[node name="StartPromptCenter" type="CenterContainer" parent="."] +unique_name_in_owner = true +z_index = 12 +layout_mode = 1 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -104.0 +offset_bottom = -48.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 + +[node name="StartPromptLabel" type="Label" parent="StartPromptCenter"] +unique_name_in_owner = true +layout_mode = 2 +mouse_filter = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +theme_override_colors/font_outline_color = Color(0.02, 0.075, 0.11, 0.9) +theme_override_constants/outline_size = 3 +theme_override_font_sizes/font_size = 26 +text = "press any key to start" +horizontal_alignment = 1 +vertical_alignment = 1 [node name="Center" type="CenterContainer" parent="."] +z_index = 10 layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -34,75 +106,77 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 -[node name="MainPanel" type="PanelContainer" parent="Center"] -custom_minimum_size = Vector2(440, 0) +[node name="MainContent" type="VBoxContainer" parent="Center"] layout_mode = 2 +theme_override_constants/separation = 8 +alignment = 1 -[node name="Margin" type="MarginContainer" parent="Center/MainPanel"] +[node name="TitleLogo" type="TextureRect" parent="Center/MainContent"] +unique_name_in_owner = true +custom_minimum_size = Vector2(640, 190) +material = SubResource("ShaderMaterial_title_logo") layout_mode = 2 -theme_override_constants/margin_left = 36 -theme_override_constants/margin_top = 30 -theme_override_constants/margin_right = 36 -theme_override_constants/margin_bottom = 30 +mouse_filter = 2 +texture_filter = 1 +texture = ExtResource("5_logo") +expand_mode = 1 +stretch_mode = 5 -[node name="Content" type="VBoxContainer" parent="Center/MainPanel/Margin"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="Title" type="Label" parent="Center/MainPanel/Margin/Content"] -layout_mode = 2 -theme_override_font_sizes/font_size = 36 -text = "NETFISHING" -horizontal_alignment = 1 - -[node name="Subtitle" type="Label" parent="Center/MainPanel/Margin/Content"] -layout_mode = 2 -text = "A cozy social fishing game" -horizontal_alignment = 1 - -[node name="PlaytestLabel" type="Label" parent="Center/MainPanel/Margin/Content"] +[node name="PlaytestLabel" type="Label" parent="Center/MainContent"] layout_mode = 2 theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_font_sizes/font_size = 13 -text = "Pre-Alpha Playtest 0.1" +text = "pre-alpha playtest 0.1" horizontal_alignment = 1 -[node name="Spacer" type="Control" parent="Center/MainPanel/Margin/Content"] -custom_minimum_size = Vector2(0, 12) +[node name="Spacer" type="Control" parent="Center/MainContent"] +custom_minimum_size = Vector2(0, 4) layout_mode = 2 -[node name="ContinueButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="ButtonCenter" type="CenterContainer" parent="Center/MainContent"] +unique_name_in_owner = true +visible = false +layout_mode = 2 + +[node name="ButtonStack" type="VBoxContainer" parent="Center/MainContent/ButtonCenter"] +unique_name_in_owner = true +custom_minimum_size = Vector2(360, 0) +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="ContinueButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Continue" +text = "continue" -[node name="NewGameButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="NewGameButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "New Game" +text = "new game" -[node name="SettingsButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="SettingsButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Settings" +text = "settings" -[node name="DeleteSaveButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="DeleteSaveButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Delete Save" +text = "delete save" -[node name="QuitButton" type="Button" parent="Center/MainPanel/Margin/Content"] +[node name="QuitButton" type="Button" parent="Center/MainContent/ButtonCenter/ButtonStack"] unique_name_in_owner = true custom_minimum_size = Vector2(0, 44) layout_mode = 2 -text = "Quit" +text = "quit" -[node name="FeedbackLabel" type="Label" parent="Center/MainPanel/Margin/Content"] +[node name="FeedbackLabel" type="Label" parent="Center/MainContent"] unique_name_in_owner = true +visible = false custom_minimum_size = Vector2(0, 42) layout_mode = 2 text = "" @@ -141,7 +215,7 @@ theme_override_constants/separation = 14 [node name="ConfirmationText" type="Label" parent="ConfirmationPanel/Margin/Content"] unique_name_in_owner = true layout_mode = 2 -text = "Confirm?" +text = "confirm?" horizontal_alignment = 1 autowrap_mode = 2 @@ -154,13 +228,13 @@ alignment = 1 unique_name_in_owner = true custom_minimum_size = Vector2(150, 42) layout_mode = 2 -text = "Confirm" +text = "confirm" [node name="CancelConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"] unique_name_in_owner = true custom_minimum_size = Vector2(120, 42) layout_mode = 2 -text = "Cancel" +text = "cancel" [node name="SettingsPanel" parent="." instance=ExtResource("3_settings")] unique_name_in_owner = true diff --git a/ui/title_water_background.gdshader b/ui/title_water_background.gdshader new file mode 100644 index 0000000..c7faabe --- /dev/null +++ b/ui/title_water_background.gdshader @@ -0,0 +1,95 @@ +shader_type canvas_item; + +uniform vec4 surface_color : source_color = vec4(0.10, 0.45, 0.62, 1.0); +uniform vec4 middle_color : source_color = vec4(0.055, 0.285, 0.40, 1.0); +uniform vec4 depth_color : source_color = vec4(0.02, 0.16, 0.27, 1.0); +uniform vec2 virtual_pixel_density = vec2(320.0, 180.0); +uniform float color_step_count : hint_range(2.0, 16.0, 1.0) = 14.0; +uniform float continuous_depth_blend : hint_range(0.0, 0.25, 0.01) = 0.12; +uniform float wave_speed : hint_range(0.0, 0.5, 0.01) = 0.10; +uniform float wave_scale : hint_range(0.5, 8.0, 0.1) = 2.4; +uniform float wave_strength : hint_range(0.0, 0.05, 0.001) = 0.012; +uniform float caustic_strength : hint_range(0.0, 0.08, 0.001) = 0.018; + +void fragment() { + vec2 grid_size = max(virtual_pixel_density, vec2(1.0)); + vec2 pixel_uv = (floor(UV * grid_size) + vec2(0.5)) / grid_size; + float motion = TIME * wave_speed; + + float broad_wave = sin( + pixel_uv.x * wave_scale + motion + ); + float secondary_wave = sin( + pixel_uv.x * 1.1 + pixel_uv.y * 1.6 - motion * 0.65 + ); + float movement_falloff = 1.0 - pixel_uv.y * 0.75; + float depth_displacement = ( + (broad_wave * 0.65 + secondary_wave * 0.35) + * wave_strength + * movement_falloff + ); + float continuous_depth = clamp( + pixel_uv.y + + depth_displacement, + 0.0, + 1.0 + ); + float depth_steps = max(2.0, floor(color_step_count)); + float band_index = floor( + continuous_depth * (depth_steps - 1.0) + 0.5 + ); + float band_depth = band_index / (depth_steps - 1.0); + float display_depth = mix( + band_depth, + continuous_depth, + clamp(continuous_depth_blend, 0.0, 0.25) + ); + + vec3 water_color; + if (display_depth < 0.5) { + water_color = mix( + surface_color.rgb, + middle_color.rgb, + display_depth * 2.0 + ); + } else { + water_color = mix( + middle_color.rgb, + depth_color.rgb, + (display_depth - 0.5) * 2.0 + ); + } + + float caustic_wave = clamp( + ( + sin( + pixel_uv.x * 4.0 + + pixel_uv.y * 1.2 + + motion * 0.7 + ) + * sin( + pixel_uv.x * 1.7 + - pixel_uv.y * 2.2 + - motion * 0.55 + ) + - 0.25 + ) / 0.75, + 0.0, + 1.0 + ); + float caustic_level = floor( + caustic_wave * 3.0 + ) / 3.0; + float upper_water_mask = 1.0 - smoothstep( + 0.18, + 0.34, + pixel_uv.y + ); + water_color += ( + vec3(0.05, 0.12, 0.14) + * caustic_level + * upper_water_mask + * caustic_strength + ); + COLOR = vec4(clamp(water_color, vec3(0.0), vec3(1.0)), 1.0); +} diff --git a/ui/title_water_background.gdshader.uid b/ui/title_water_background.gdshader.uid new file mode 100644 index 0000000..fda761b --- /dev/null +++ b/ui/title_water_background.gdshader.uid @@ -0,0 +1 @@ +uid://c8s1g7tn771du