Revamp underwater title presentation

This commit is contained in:
Alexander Sellite 2026-07-26 23:19:25 -04:00
parent e560cc0b3e
commit 1c801509af
22 changed files with 1341 additions and 63 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

15
default_bus_layout.tres Normal file
View file

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

View file

@ -25,10 +25,16 @@ const FishingShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd" "res://world/fishing_shop_interaction.gd"
) )
const TITLE_MUSIC_SILENCE_DB: float = -80.0
@export var fish_catalog: FishPoolType @export var fish_catalog: FishPoolType
@export var pelican_buyer_profile: FishBuyerProfileType @export var pelican_buyer_profile: FishBuyerProfileType
@export var main_shop_buyer_profile: FishBuyerProfileType @export var main_shop_buyer_profile: FishBuyerProfileType
@export var item_catalog: ItemCatalogType @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 _test_world: TestWorldType = $TestWorld
@onready var _player: PlayerType = %Player @onready var _player: PlayerType = %Player
@ -37,9 +43,14 @@ const FishingShopInteractionType = preload(
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
@onready var _title_music: AudioStreamPlayer = %TitleMusic
var _gameplay_started: bool = false var _gameplay_started: bool = false
var _shop_interaction: FishingShopInteractionType 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: func _ready() -> void:
@ -144,6 +155,7 @@ func _ready() -> void:
_player.hotbar.get_selected_slot(), _player.hotbar.get_selected_slot(),
_player.hotbar.get_selected_item_id() _player.hotbar.get_selected_item_id()
) )
_show_title_music()
func _input(event: InputEvent) -> void: func _input(event: InputEvent) -> void:
@ -226,20 +238,26 @@ func _set_gameplay_active(active: bool) -> void:
func _on_gameplay_requested() -> void: func _on_gameplay_requested() -> void:
if _gameplay_started: if _gameplay_started or _quit_in_progress:
return return
_fade_out_title_music()
_game_ui.get_title_screen().hide() _game_ui.get_title_screen().hide()
_set_gameplay_active(true) _set_gameplay_active(true)
func _on_return_to_title_requested() -> void: func _on_return_to_title_requested() -> void:
if _quit_in_progress:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu() var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
pause_menu.close_for_title_transition() pause_menu.close_for_title_transition()
_set_gameplay_active(false) _set_gameplay_active(false)
_game_ui.get_title_screen().reopen() _game_ui.get_title_screen().reopen()
_show_title_music()
func _on_reset_progress_requested() -> void: func _on_reset_progress_requested() -> void:
if _quit_in_progress:
return
var pause_menu: PauseMenuType = _game_ui.get_pause_menu() var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
if not _save_manager.delete_progression_save(): if not _save_manager.delete_progression_save():
pause_menu.report_reset_failure() pause_menu.report_reset_failure()
@ -250,6 +268,7 @@ func _on_reset_progress_requested() -> void:
pause_menu.close_for_title_transition() pause_menu.close_for_title_transition()
_set_gameplay_active(false) _set_gameplay_active(false)
_game_ui.get_title_screen().reopen() _game_ui.get_title_screen().reopen()
_show_title_music()
func _on_water_recovery_starting() -> void: func _on_water_recovery_starting() -> void:
@ -282,12 +301,116 @@ func _refresh_active_hotbar_item() -> void:
func _on_quit_requested() -> void: func _on_quit_requested() -> void:
if _quit_in_progress:
return
_quit_in_progress = true
_settings_manager.save_if_dirty() _settings_manager.save_if_dirty()
if _gameplay_started: if _gameplay_started:
_save_manager.save_if_dirty() _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() 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: func _on_shop_range_changed(in_range: bool) -> void:
if not in_range: if not in_range:
_game_ui.get_fishing_shop().close_for_range_exit() _game_ui.get_fishing_shop().close_for_range_exit()

View file

@ -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://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] [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="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://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="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"] [node name="Main" type="Node3D"]
script = ExtResource("3_main") script = ExtResource("3_main")
@ -44,5 +45,11 @@ script = ExtResource("9_save")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("10_settings") 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")] [node name="GameUI" parent="." instance=ExtResource("5_ui")]
unique_name_in_owner = true unique_name_in_owner = true

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 B

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View file

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

View file

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

View file

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

View file

@ -7,6 +7,52 @@ const SettingsManagerType = preload(
"res://settings/player_settings_manager.gd" "res://settings/player_settings_manager.gd"
) )
const SettingsPanelType = preload("res://ui/settings_panel.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 gameplay_requested
signal quit_requested signal quit_requested
@ -21,17 +67,41 @@ enum ConfirmationAction {
@onready var _new_game_button: Button = %NewGameButton @onready var _new_game_button: Button = %NewGameButton
@onready var _settings_button: Button = %SettingsButton @onready var _settings_button: Button = %SettingsButton
@onready var _delete_button: Button = %DeleteSaveButton @onready var _delete_button: Button = %DeleteSaveButton
@onready var _quit_button: Button = %QuitButton
@onready var _feedback_label: Label = %FeedbackLabel @onready var _feedback_label: Label = %FeedbackLabel
@onready var _confirmation_panel: PanelContainer = %ConfirmationPanel @onready var _confirmation_panel: PanelContainer = %ConfirmationPanel
@onready var _confirmation_text: Label = %ConfirmationText @onready var _confirmation_text: Label = %ConfirmationText
@onready var _confirm_button: Button = %ConfirmButton @onready var _confirm_button: Button = %ConfirmButton
@onready var _settings_panel: SettingsPanelType = %SettingsPanel @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 _save_manager: SaveManagerType
var _settings_manager: SettingsManagerType var _settings_manager: SettingsManagerType
var _inspection: SaveInspectionType var _inspection: SaveInspectionType
var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE
var _action_in_progress: bool = false 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: func _ready() -> void:
@ -44,6 +114,20 @@ func _ready() -> void:
%CancelConfirmButton.pressed.connect(_close_confirmation) %CancelConfirmButton.pressed.connect(_close_confirmation)
_settings_panel.applied.connect(_on_settings_applied) _settings_panel.applied.connect(_on_settings_applied)
_settings_panel.closed.connect(_on_settings_closed) _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( func setup(
@ -55,20 +139,104 @@ func setup(
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_refresh_save_inspection() _refresh_save_inspection()
show() show()
call_deferred("_focus_initial_button") _decorative_presentation_ready = true
_start_decorative_presentation()
_begin_title_entry()
func reopen() -> void: func reopen() -> void:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_prepare_awaiting_start_input()
_close_confirmation() _close_confirmation()
_settings_panel.hide() _settings_panel.hide()
_refresh_save_inspection() _refresh_save_inspection()
show() 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: 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 return
if _confirmation_panel.visible: if _confirmation_panel.visible:
_close_confirmation() _close_confirmation()
@ -79,6 +247,119 @@ func _input(event: InputEvent) -> void:
get_viewport().set_input_as_handled() 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: func _on_continue_pressed() -> void:
if ( if (
_action_in_progress _action_in_progress
@ -90,11 +371,11 @@ func _on_continue_pressed() -> void:
return return
_action_in_progress = true _action_in_progress = true
if _save_manager.load_player_data(): if _save_manager.load_player_data():
_feedback_label.text = "Save loaded." _feedback_label.text = "save loaded."
gameplay_requested.emit() gameplay_requested.emit()
else: else:
_refresh_save_inspection() _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 _action_in_progress = false
@ -112,17 +393,17 @@ func _on_new_game_pressed() -> void:
return return
if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION: if _inspection.status == SaveInspectionType.Status.UNSUPPORTED_VERSION:
_feedback_label.text = ( _feedback_label.text = (
"This save is from a newer game version. " "this save is from a newer game version. "
+ "Use Delete Save before starting over." + "use delete save before starting over."
) )
return return
if _inspection.status == SaveInspectionType.Status.IO_ERROR: 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 return
_open_confirmation( _open_confirmation(
ConfirmationAction.NEW_GAME, ConfirmationAction.NEW_GAME,
"Start a new game? Existing progression will be deleted.", "start a new game? existing progression will be deleted.",
"Start New Game" "start new game"
) )
@ -137,8 +418,8 @@ func _on_delete_pressed() -> void:
return return
_open_confirmation( _open_confirmation(
ConfirmationAction.DELETE_SAVE, ConfirmationAction.DELETE_SAVE,
"Delete your saved progression? This cannot be undone.", "delete your saved progression? this cannot be undone.",
"Delete" "delete"
) )
@ -149,7 +430,7 @@ func _on_confirmation_accepted() -> void:
_close_confirmation() _close_confirmation()
_action_in_progress = true _action_in_progress = true
if not _save_manager.delete_progression_save(): 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 _action_in_progress = false
_refresh_save_inspection() _refresh_save_inspection()
return return
@ -158,7 +439,7 @@ func _on_confirmation_accepted() -> void:
if action == ConfirmationAction.NEW_GAME: if action == ConfirmationAction.NEW_GAME:
gameplay_requested.emit() gameplay_requested.emit()
else: else:
_feedback_label.text = "Saved progression deleted." _feedback_label.text = "saved progression deleted."
_action_in_progress = false _action_in_progress = false
@ -167,6 +448,7 @@ func _open_confirmation(
message: String, message: String,
confirm_text: String, confirm_text: String,
) -> void: ) -> void:
_modal_restore_navigation_focus = _navigation_focus_active
_confirmation_action = action _confirmation_action = action
_confirmation_text.text = message _confirmation_text.text = message
_confirm_button.text = confirm_text _confirm_button.text = confirm_text
@ -175,14 +457,23 @@ func _open_confirmation(
func _close_confirmation() -> void: func _close_confirmation() -> void:
var restore_navigation_focus: bool = _modal_restore_navigation_focus
_modal_restore_navigation_focus = false
_confirmation_action = ConfirmationAction.NONE _confirmation_action = ConfirmationAction.NONE
_confirmation_panel.visible = false _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: func _open_settings() -> void:
if _confirmation_panel.visible or _action_in_progress: if _confirmation_panel.visible or _action_in_progress:
return return
_modal_restore_navigation_focus = _navigation_focus_active
_feedback_label.text = "" _feedback_label.text = ""
_settings_panel.open_panel(_settings_manager) _settings_panel.open_panel(_settings_manager)
@ -192,12 +483,23 @@ func _close_settings() -> void:
func _on_settings_applied() -> void: func _on_settings_applied() -> void:
_feedback_label.text = "Settings saved." _feedback_label.text = "settings saved."
_settings_button.grab_focus() _restore_settings_focus()
func _on_settings_closed() -> void: 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: func _refresh_save_inspection() -> void:
@ -217,12 +519,18 @@ func _refresh_save_inspection() -> void:
func _focus_initial_button() -> 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 return
if not _continue_button.disabled: if not _continue_button.disabled:
_continue_button.grab_focus() _continue_button.grab_focus()
else: else:
_new_game_button.grab_focus() _new_game_button.grab_focus()
_navigation_focus_active = true
func _on_quit_pressed() -> void: func _on_quit_pressed() -> void:
@ -232,3 +540,430 @@ func _on_quit_pressed() -> void:
and not _settings_panel.visible and not _settings_panel.visible
): ):
quit_requested.emit() 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()

View file

@ -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="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="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="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"] [node name="TitleScreen" type="Control"]
unique_name_in_owner = true unique_name_in_owner = true
@ -18,15 +27,78 @@ theme = ExtResource("2_theme")
script = ExtResource("1_script") script = ExtResource("1_script")
[node name="Background" type="ColorRect" parent="."] [node name="Background" type="ColorRect" parent="."]
material = SubResource("ShaderMaterial_title_water")
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 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="."] [node name="Center" type="CenterContainer" parent="."]
z_index = 10
layout_mode = 1 layout_mode = 1
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@ -34,75 +106,77 @@ anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
[node name="MainPanel" type="PanelContainer" parent="Center"] [node name="MainContent" type="VBoxContainer" parent="Center"]
custom_minimum_size = Vector2(440, 0)
layout_mode = 2 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 layout_mode = 2
theme_override_constants/margin_left = 36 mouse_filter = 2
theme_override_constants/margin_top = 30 texture_filter = 1
theme_override_constants/margin_right = 36 texture = ExtResource("5_logo")
theme_override_constants/margin_bottom = 30 expand_mode = 1
stretch_mode = 5
[node name="Content" type="VBoxContainer" parent="Center/MainPanel/Margin"] [node name="PlaytestLabel" type="Label" parent="Center/MainContent"]
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"]
layout_mode = 2 layout_mode = 2
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1) theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
theme_override_font_sizes/font_size = 13 theme_override_font_sizes/font_size = 13
text = "Pre-Alpha Playtest 0.1" text = "pre-alpha playtest 0.1"
horizontal_alignment = 1 horizontal_alignment = 1
[node name="Spacer" type="Control" parent="Center/MainPanel/Margin/Content"] [node name="Spacer" type="Control" parent="Center/MainContent"]
custom_minimum_size = Vector2(0, 12) custom_minimum_size = Vector2(0, 4)
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 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 unique_name_in_owner = true
custom_minimum_size = Vector2(0, 44) custom_minimum_size = Vector2(0, 44)
layout_mode = 2 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 unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(0, 42) custom_minimum_size = Vector2(0, 42)
layout_mode = 2 layout_mode = 2
text = "" text = ""
@ -141,7 +215,7 @@ theme_override_constants/separation = 14
[node name="ConfirmationText" type="Label" parent="ConfirmationPanel/Margin/Content"] [node name="ConfirmationText" type="Label" parent="ConfirmationPanel/Margin/Content"]
unique_name_in_owner = true unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "Confirm?" text = "confirm?"
horizontal_alignment = 1 horizontal_alignment = 1
autowrap_mode = 2 autowrap_mode = 2
@ -154,13 +228,13 @@ alignment = 1
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(150, 42) custom_minimum_size = Vector2(150, 42)
layout_mode = 2 layout_mode = 2
text = "Confirm" text = "confirm"
[node name="CancelConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"] [node name="CancelConfirmButton" type="Button" parent="ConfirmationPanel/Margin/Content/Buttons"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(120, 42) custom_minimum_size = Vector2(120, 42)
layout_mode = 2 layout_mode = 2
text = "Cancel" text = "cancel"
[node name="SettingsPanel" parent="." instance=ExtResource("3_settings")] [node name="SettingsPanel" parent="." instance=ExtResource("3_settings")]
unique_name_in_owner = true unique_name_in_owner = true

View file

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

View file

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