From 46175ee4051207ec6c0b8c42a3a56dc8482187b6 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 27 Jul 2026 23:44:27 -0400 Subject: [PATCH 1/3] Migrate pause menu to bubble UI --- .../bubble_menu/bubble_transition_flurry.gd | 195 +++++++++ .../bubble_transition_flurry.gd.uid | 1 + ui/game_ui.gd | 2 +- ui/pause_menu.gd | 375 +++++++++++++++--- ui/pause_menu.tscn | 187 ++++++--- ui/settings_panel.gd | 43 +- 6 files changed, 694 insertions(+), 109 deletions(-) create mode 100644 ui/components/bubble_menu/bubble_transition_flurry.gd create mode 100644 ui/components/bubble_menu/bubble_transition_flurry.gd.uid diff --git a/ui/components/bubble_menu/bubble_transition_flurry.gd b/ui/components/bubble_menu/bubble_transition_flurry.gd new file mode 100644 index 0000000..49a2c59 --- /dev/null +++ b/ui/components/bubble_menu/bubble_transition_flurry.gd @@ -0,0 +1,195 @@ +class_name BubbleTransitionFlurry +extends Control + +const 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 BUBBLE_COUNT_MIN: int = 4 +const BUBBLE_COUNT_MAX: int = 5 +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_TRAVEL_DURATION_MIN: float = 7.0 +const BUBBLE_TRAVEL_DURATION_MAX: float = 14.0 +const BUBBLE_EDGE_MARGIN: float = 16.0 +const BURST_X_MIN: float = 0.36 +const BURST_X_MAX: float = 0.64 +const BURST_Y_MIN: float = 0.60 +const BURST_Y_MAX: float = 0.78 +const MAX_ACTIVE_BUBBLES: int = 30 + +var _rng := RandomNumberGenerator.new() +var _generation: int = 0 +var _bubbles: Array[TextureRect] = [] +var _tweens: Dictionary[int, Tween] = {} + + +func _ready() -> void: + _rng.randomize() + + +func emit_flurry() -> void: + if not visible or BUBBLE_TEXTURES.is_empty(): + return + var bubble_count: int = _rng.randi_range( + BUBBLE_COUNT_MIN, + BUBBLE_COUNT_MAX + ) + for _bubble_index: int in bubble_count: + if not _spawn_bubble( + _rng.randf_range(BURST_X_MIN, BURST_X_MAX), + _rng.randf_range(BURST_Y_MIN, BURST_Y_MAX) + ): + break + + +func clear_flurries() -> void: + _generation += 1 + for tween: Tween in _tweens.values(): + if tween != null and tween.is_valid(): + tween.kill() + _tweens.clear() + for bubble: TextureRect in _bubbles: + if is_instance_valid(bubble): + bubble.queue_free() + _bubbles.clear() + + +func get_active_bubble_count() -> int: + return _bubbles.size() + + +func _spawn_bubble(normalized_x: float, start_y_ratio: float) -> bool: + if _bubbles.size() >= MAX_ACTIVE_BUBBLES: + return false + var layer_size: Vector2 = size + if layer_size.x <= 1.0 or layer_size.y <= 1.0: + return false + var texture: Texture2D = BUBBLE_TEXTURES[ + _rng.randi_range(0, BUBBLE_TEXTURES.size() - 1) + ] + var presentation_size: Vector2 = texture.get_size() * _rng.randf_range( + BUBBLE_SCALE_MIN, + BUBBLE_SCALE_MAX + ) + var bubble := TextureRect.new() + bubble.name = "TransitionBubble" + 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 = _rng.randf_range( + BUBBLE_OPACITY_MIN, + BUBBLE_OPACITY_MAX + ) + add_child(bubble) + _bubbles.append(bubble) + + var drift_direction: float = ( + 1.0 if _rng.randi_range(0, 1) == 1 else -1.0 + ) + var horizontal_drift: float = _rng.randf_range( + BUBBLE_DRIFT_MIN, + BUBBLE_DRIFT_MAX + ) * drift_direction + var wobble_amplitude: float = _rng.randf_range( + BUBBLE_WOBBLE_MIN, + BUBBLE_WOBBLE_MAX + ) + var wobble_cycles: float = _rng.randf_range(0.8, 1.6) + var wobble_phase: float = _rng.randf_range(0.0, TAU) + var full_start_y: float = ( + layer_size.y + presentation_size.y + BUBBLE_EDGE_MARGIN + ) + var start_y: float = ( + start_y_ratio * layer_size.y - presentation_size.y * 0.5 + ) + var end_y: float = -presentation_size.y - BUBBLE_EDGE_MARGIN + var full_distance: float = maxf(full_start_y - end_y, 1.0) + var remaining_distance: float = maxf(start_y - end_y, 1.0) + var travel_duration: float = _rng.randf_range( + BUBBLE_TRAVEL_DURATION_MIN, + BUBBLE_TRAVEL_DURATION_MAX + ) * remaining_distance / full_distance + var generation: int = _generation + _update_bubble( + 0.0, + bubble, + normalized_x, + start_y_ratio, + horizontal_drift, + wobble_amplitude, + wobble_cycles, + wobble_phase, + generation + ) + var tween: Tween = create_tween() + _tweens[bubble.get_instance_id()] = tween + tween.tween_method( + _update_bubble.bind( + bubble, + normalized_x, + start_y_ratio, + horizontal_drift, + wobble_amplitude, + wobble_cycles, + wobble_phase, + generation + ), + 0.0, + 1.0, + travel_duration + ) + tween.finished.connect( + _finish_bubble.bind(bubble, generation), + CONNECT_ONE_SHOT + ) + return true + + +func _update_bubble( + progress: float, + bubble: TextureRect, + normalized_x: float, + start_y_ratio: float, + horizontal_drift: float, + wobble_amplitude: float, + wobble_cycles: float, + wobble_phase: float, + generation: int, +) -> void: + if generation != _generation or not is_instance_valid(bubble): + return + var start_y: float = start_y_ratio * size.y - bubble.size.y * 0.5 + var end_y: float = -bubble.size.y - BUBBLE_EDGE_MARGIN + var wobble: float = sin( + wobble_phase + progress * TAU * wobble_cycles + ) * wobble_amplitude + bubble.position = Vector2( + normalized_x * size.x + + horizontal_drift * progress + + wobble + - bubble.size.x * 0.5, + lerpf(start_y, end_y, progress) + ) + + +func _finish_bubble(bubble: TextureRect, generation: int) -> void: + if generation != _generation or not is_instance_valid(bubble): + return + _tweens.erase(bubble.get_instance_id()) + _bubbles.erase(bubble) + bubble.queue_free() + + +func _exit_tree() -> void: + clear_flurries() diff --git a/ui/components/bubble_menu/bubble_transition_flurry.gd.uid b/ui/components/bubble_menu/bubble_transition_flurry.gd.uid new file mode 100644 index 0000000..1c56e59 --- /dev/null +++ b/ui/components/bubble_menu/bubble_transition_flurry.gd.uid @@ -0,0 +1 @@ +uid://bxqtrj8vujks3 diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 2a71c0a..d63f940 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -56,7 +56,7 @@ signal interactive_pointer_ui_changed(is_open: bool) $UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel ) @onready var _pause_settings_panel: SettingsPanelType = ( - $UIRoot/PauseMenu/SettingsCenter/SettingsPanel + $UIRoot/PauseMenu/ResponsivePauseStage/PausePresentationScaleRoot/SettingsCenter/SettingsPanel ) var _showcase_active: bool = false diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index 57e8d8b..9d569af 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -2,6 +2,13 @@ class_name PauseMenu extends Control const INPUT_OWNER: StringName = &"game_menu" +const PAUSE_DESKTOP_REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) +const PAUSE_COMPACT_REFERENCE_SIZE: Vector2 = Vector2(640.0, 480.0) +const COMPACT_HEIGHT_THRESHOLD: float = 560.0 +const PAGE_OUTGOING_DURATION: float = 1.70 +const PAGE_INCOMING_DURATION: float = 1.85 +const VISIBILITY_FADE_DURATION: float = 0.55 +const BACKDROP_TARGET_ALPHA: float = 0.68 const PlayerType = preload("res://player/player.gd") const SaveManagerType = preload("res://save/player_save_manager.gd") const SettingsManagerType = preload( @@ -32,15 +39,20 @@ enum CloseReason { TEARDOWN, } -@onready var _root_panel: PanelContainer = %RootPanel +@onready var _presentation_scale_root: Control = %PausePresentationScaleRoot +@onready var _root_page: SettingsBubblePage = %RootPage @onready var _settings_panel: SettingsPanelType = %SettingsPanel +@onready var _dim_background: ColorRect = %DimBackground +@onready var _transition_flurry: BubbleTransitionFlurry = ( + %TransitionBubbleLayer +) @onready var _confirmation_panel: PanelContainer = %ConfirmationPanel @onready var _confirmation_title: Label = %ConfirmationTitle @onready var _confirmation_text: Label = %ConfirmationText @onready var _confirm_button: Button = %ConfirmButton @onready var _cancel_button: Button = %CancelConfirmButton @onready var _feedback: Label = %FeedbackLabel -@onready var _save_button: Button = %SaveButton +@onready var _save_button: BubbleButton = %SaveButton var _player: PlayerType var _save_manager: SaveManagerType @@ -53,6 +65,14 @@ var _control_snapshot_stored: bool = false var _mouse_snapshot_stored: bool = false var _confirmation_action: ConfirmationAction = ConfirmationAction.NONE var _action_in_progress: bool = false +var _root_transition_active: bool = false +var _root_transition_generation: int = 0 +var _closing_menu: bool = false +var _backdrop_fade: Tween +var _backdrop_fade_generation: int = 0 +var _confirmation_fade: Tween +var _confirmation_fade_generation: int = 0 +var _confirmation_fade_active: bool = false func _ready() -> void: @@ -66,6 +86,14 @@ func _ready() -> void: _cancel_button.pressed.connect(_close_confirmation) _settings_panel.applied.connect(_on_settings_applied) _settings_panel.closed.connect(_on_settings_closed) + _settings_panel.navigation_transition_started.connect( + _emit_transition_flurry + ) + _dim_background.color.a = 0.0 + _confirmation_panel.modulate.a = 0.0 + _set_confirmation_interactive(false) + resized.connect(_update_responsive_pause_stage) + call_deferred("_update_responsive_pause_stage") func setup( @@ -95,19 +123,29 @@ func open_menu() -> void: _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _feedback.text = "" - _root_panel.show() _settings_panel.hide() _confirmation_panel.hide() _confirmation_action = ConfirmationAction.NONE + _action_in_progress = false + _root_page.hide_page() show() - %ResumeButton.grab_focus() + _fade_backdrop(true) + _update_responsive_pause_stage() + _begin_root_entry(false) menu_visibility_changed.emit(true) func resume() -> void: - if not visible or _action_in_progress: + if ( + not visible + or _action_in_progress + or _root_transition_active + or _settings_panel.visible + or _confirmation_action != ConfirmationAction.NONE + ): return - close_menu(CloseReason.USER_RETURN) + _fade_backdrop(false) + _begin_root_exit(_finish_user_close) func close_for_title_transition() -> void: @@ -124,28 +162,14 @@ func close_menu( ) -> void: if not visible: return - if _settings_panel.visible: - _settings_panel.close_panel() - hide() - _root_panel.show() - _settings_panel.hide() - _confirmation_panel.hide() - _confirmation_action = ConfirmationAction.NONE - _action_in_progress = false - if _fishing_spot != null and is_instance_valid(_fishing_spot): - _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) - get_viewport().gui_release_focus() - if restore_controls: - _restore_controls() - else: - _control_snapshot_stored = false - _apply_mouse_close_policy(reason) - menu_visibility_changed.emit(false) + _finish_close(reason, restore_controls) func handle_escape() -> bool: if not visible: return false + if _root_transition_active or _confirmation_fade_active: + return true if _confirmation_panel.visible: _close_confirmation() elif _settings_panel.visible: @@ -156,7 +180,7 @@ func handle_escape() -> bool: func _save_now() -> void: - if _action_in_progress: + if _action_in_progress or _root_transition_active: return _action_in_progress = true _save_button.disabled = true @@ -173,24 +197,35 @@ func _reenable_save_button() -> void: func _open_settings() -> void: - if _action_in_progress or _confirmation_panel.visible: + if ( + _action_in_progress + or _root_transition_active + or _confirmation_action != ConfirmationAction.NONE + ): return - _root_panel.hide() + _begin_root_exit(_finish_open_settings) + + +func _finish_open_settings() -> void: _settings_panel.open_panel( _settings_manager, - SettingsPanelType.PresentationMode.GAMEPLAY_MODAL + SettingsPanelType.PresentationMode.GAMEPLAY_MODAL, + true, + true ) func _on_settings_applied() -> void: - _root_panel.show() + if _closing_menu: + return _feedback.text = "settings saved." - %SettingsButton.grab_focus() + _begin_root_entry(true) func _on_settings_closed() -> void: - _root_panel.show() - %SettingsButton.grab_focus() + if _closing_menu: + return + _begin_root_entry(true) func _confirm_return_to_title() -> void: @@ -218,7 +253,7 @@ func _confirm_reset_progress() -> void: func _request_quit() -> void: - if _action_in_progress: + if _action_in_progress or _root_transition_active: return _action_in_progress = true var settings_saved: bool = _settings_manager.save_if_dirty() @@ -243,7 +278,7 @@ func _open_confirmation( confirm_text: String, dangerous: bool, ) -> void: - if _action_in_progress: + if _action_in_progress or _root_transition_active: return _confirmation_action = action _confirmation_title.text = title @@ -252,8 +287,18 @@ func _open_confirmation( _confirm_button.theme_type_variation = ( &"DangerButton" if dangerous else StringName() ) - _root_panel.hide() - _confirmation_panel.show() + _begin_root_exit(_show_confirmation.bind(dangerous)) + + +func _show_confirmation(dangerous: bool) -> void: + _fade_confirmation( + true, + _finish_confirmation_open.bind(dangerous) + ) + + +func _finish_confirmation_open(dangerous: bool) -> void: + _set_confirmation_interactive(true) if dangerous: _cancel_button.grab_focus() else: @@ -261,41 +306,281 @@ func _open_confirmation( func _close_confirmation() -> void: + if _root_transition_active or _confirmation_fade_active: + return _confirmation_action = ConfirmationAction.NONE - _confirmation_panel.hide() - _root_panel.show() - %ResumeButton.grab_focus() + _fade_confirmation(false, _finish_confirmation_cancel) + + +func _finish_confirmation_cancel() -> void: + _begin_root_entry(false) func _accept_confirmation() -> void: - if _action_in_progress: + if ( + _action_in_progress + or _root_transition_active + or _confirmation_fade_active + ): return var action: ConfirmationAction = _confirmation_action _confirmation_action = ConfirmationAction.NONE - _confirmation_panel.hide() _action_in_progress = true + _fade_confirmation( + false, + _finish_confirmation_accept.bind(action) + ) + + +func _finish_confirmation_accept(action: ConfirmationAction) -> void: match action: ConfirmationAction.RETURN_TO_TITLE: if _save_manager.save_now(): return_to_title_requested.emit() else: _feedback.text = "save failed. previous save was preserved." - _root_panel.show() + _action_in_progress = false + _begin_root_entry(false) + return ConfirmationAction.RESET_PROGRESS: reset_progress_requested.emit() ConfirmationAction.QUIT_ANYWAY: quit_requested.emit() _: - _root_panel.show() + _action_in_progress = false + _begin_root_entry(false) + return _action_in_progress = false func report_reset_failure() -> void: _action_in_progress = false - _root_panel.show() - _confirmation_panel.hide() + _confirmation_action = ConfirmationAction.NONE _feedback.text = "reset failed. your progression was preserved." - %ResumeButton.grab_focus() + if visible: + _begin_root_entry(false) + + +func _begin_root_entry(flurry_already_emitted: bool) -> void: + if _root_transition_active: + return + _root_transition_generation += 1 + _root_transition_active = true + if not flurry_already_emitted: + _emit_transition_flurry() + var generation: int = _root_transition_generation + _root_page.transition_in( + true, + _finish_root_entry.bind(generation), + PAGE_INCOMING_DURATION + ) + + +func _finish_root_entry(generation: int) -> void: + if generation != _root_transition_generation or not visible: + return + _root_transition_active = false + + +func _begin_root_exit(completed: Callable) -> void: + if _root_transition_active or not _root_page.visible: + return + _root_transition_generation += 1 + _root_transition_active = true + _emit_transition_flurry() + var generation: int = _root_transition_generation + _root_page.transition_out( + _finish_root_exit.bind(generation, completed), + PAGE_OUTGOING_DURATION + ) + + +func _finish_root_exit( + generation: int, + completed: Callable, +) -> void: + if generation != _root_transition_generation or not visible: + return + _root_transition_active = false + completed.call() + + +func _finish_user_close() -> void: + _finish_close(CloseReason.USER_RETURN, true) + + +func _finish_close(reason: CloseReason, restore_controls: bool) -> void: + _closing_menu = true + if _settings_panel.visible: + _settings_panel.close_panel(true) + _closing_menu = false + _root_transition_generation += 1 + _root_transition_active = false + _cancel_backdrop_fade() + _cancel_confirmation_fade() + _dim_background.color.a = 0.0 + _dim_background.hide() + _confirmation_panel.modulate.a = 0.0 + _set_confirmation_interactive(false) + _settings_panel.hide() + _root_page.hide_page() + _confirmation_panel.hide() + _confirmation_action = ConfirmationAction.NONE + _action_in_progress = false + _transition_flurry.clear_flurries() + hide() + if _fishing_spot != null and is_instance_valid(_fishing_spot): + _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false) + get_viewport().gui_release_focus() + if restore_controls: + _restore_controls() + else: + _control_snapshot_stored = false + _apply_mouse_close_policy(reason) + menu_visibility_changed.emit(false) + + +func _emit_transition_flurry() -> void: + _transition_flurry.emit_flurry() + + +func _fade_backdrop(fade_in: bool) -> void: + _backdrop_fade_generation += 1 + _cancel_backdrop_fade() + _dim_background.show() + var target_alpha: float = BACKDROP_TARGET_ALPHA if fade_in else 0.0 + if is_equal_approx(_dim_background.color.a, target_alpha): + _finish_backdrop_fade(_backdrop_fade_generation, fade_in) + return + var generation: int = _backdrop_fade_generation + _backdrop_fade = create_tween() + _backdrop_fade.tween_property( + _dim_background, + "color:a", + target_alpha, + VISIBILITY_FADE_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + _backdrop_fade.finished.connect( + _finish_backdrop_fade.bind(generation, fade_in), + CONNECT_ONE_SHOT + ) + + +func _finish_backdrop_fade(generation: int, faded_in: bool) -> void: + if generation != _backdrop_fade_generation: + return + _backdrop_fade = null + _dim_background.color.a = ( + BACKDROP_TARGET_ALPHA if faded_in else 0.0 + ) + if not faded_in: + _dim_background.hide() + + +func _cancel_backdrop_fade() -> void: + if _backdrop_fade != null: + _backdrop_fade.kill() + _backdrop_fade = null + + +func _fade_confirmation( + fade_in: bool, + completed: Callable, +) -> void: + _confirmation_fade_generation += 1 + _cancel_confirmation_fade() + _confirmation_fade_active = true + _set_confirmation_interactive(false) + _confirmation_panel.show() + var target_alpha: float = 1.0 if fade_in else 0.0 + if is_equal_approx(_confirmation_panel.modulate.a, target_alpha): + _finish_confirmation_fade( + _confirmation_fade_generation, + fade_in, + completed + ) + return + var generation: int = _confirmation_fade_generation + _confirmation_fade = create_tween() + _confirmation_fade.tween_property( + _confirmation_panel, + "modulate:a", + target_alpha, + VISIBILITY_FADE_DURATION + ).set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT) + _confirmation_fade.finished.connect( + _finish_confirmation_fade.bind( + generation, + fade_in, + completed + ), + CONNECT_ONE_SHOT + ) + + +func _finish_confirmation_fade( + generation: int, + faded_in: bool, + completed: Callable, +) -> void: + if generation != _confirmation_fade_generation or not visible: + return + _confirmation_fade = null + _confirmation_fade_active = false + _confirmation_panel.modulate.a = 1.0 if faded_in else 0.0 + if not faded_in: + _confirmation_panel.hide() + completed.call() + + +func _cancel_confirmation_fade() -> void: + _confirmation_fade_generation += 1 + _confirmation_fade_active = false + if _confirmation_fade != null: + _confirmation_fade.kill() + _confirmation_fade = null + + +func _set_confirmation_interactive(interactive: bool) -> void: + _confirmation_panel.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + for button: Button in [_confirm_button, _cancel_button]: + if not interactive and button.has_focus(): + button.release_focus() + button.focus_mode = ( + Control.FOCUS_ALL if interactive else Control.FOCUS_NONE + ) + button.mouse_filter = ( + Control.MOUSE_FILTER_STOP + if interactive + else Control.MOUSE_FILTER_IGNORE + ) + + +func _update_responsive_pause_stage() -> void: + if not is_node_ready(): + return + var display_size := Vector2( + maxf(1.0, size.x), + maxf(1.0, size.y) + ) + var reference_size: Vector2 = ( + PAUSE_COMPACT_REFERENCE_SIZE + if display_size.y < COMPACT_HEIGHT_THRESHOLD + else PAUSE_DESKTOP_REFERENCE_SIZE + ) + var presentation_scale: float = minf( + display_size.x / reference_size.x, + display_size.y / reference_size.y + ) + _presentation_scale_root.size = reference_size + _presentation_scale_root.scale = Vector2.ONE * presentation_scale + _presentation_scale_root.position = ( + display_size - reference_size * presentation_scale + ) * 0.5 func _restore_controls() -> void: diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index ec504ac..79d8f1c 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -1,8 +1,13 @@ -[gd_scene load_steps=4 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://ui/pause_menu.gd" id="1_script"] [ext_resource type="PackedScene" path="res://ui/settings_panel.tscn" id="2_settings"] [ext_resource type="Theme" path="res://ui/game_theme.tres" id="3_theme"] +[ext_resource type="Script" path="res://ui/settings/settings_bubble_page.gd" id="4_page"] +[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="5_bubble"] +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_cluster.gd" id="6_cluster"] +[ext_resource type="Resource" path="res://ui/components/bubble_menu/bubble_menu_profile.tres" id="7_profile"] +[ext_resource type="Script" path="res://ui/components/bubble_menu/bubble_transition_flurry.gd" id="8_flurry"] [node name="PauseMenu" type="Control"] unique_name_in_owner = true @@ -19,6 +24,7 @@ theme = ExtResource("3_theme") script = ExtResource("1_script") [node name="DimBackground" type="ColorRect" parent="."] +unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -26,84 +32,162 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 0 -color = Color(0.02, 0.05, 0.07, 0.78) +color = Color(0.025, 0.12, 0.19, 0.68) -[node name="RootCenter" type="CenterContainer" parent="."] +[node name="TransitionBubbleLayer" 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 +script = ExtResource("8_flurry") -[node name="RootPanel" type="PanelContainer" parent="RootCenter"] +[node name="ResponsivePauseStage" type="Control" parent="."] unique_name_in_owner = true -custom_minimum_size = Vector2(560, 0) -layout_mode = 2 +z_index = 2 +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 1 -[node name="Margin" type="MarginContainer" parent="RootCenter/RootPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 32 -theme_override_constants/margin_top = 26 -theme_override_constants/margin_right = 32 -theme_override_constants/margin_bottom = 26 - -[node name="Content" type="VBoxContainer" parent="RootCenter/RootPanel/Margin"] -layout_mode = 2 -theme_override_constants/separation = 11 - -[node name="Title" type="Label" parent="RootCenter/RootPanel/Margin/Content"] -layout_mode = 2 -theme_override_font_sizes/font_size = 45 -text = "game menu" -horizontal_alignment = 1 - -[node name="ResumeButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="PausePresentationScaleRoot" type="Control" parent="ResponsivePauseStage"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 -text = "return to game" +layout_mode = 0 +offset_right = 1280.0 +offset_bottom = 720.0 +mouse_filter = 1 -[node name="SaveButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="RootPage" type="Control" parent="ResponsivePauseStage/PausePresentationScaleRoot"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 -text = "save now" +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("4_page") +page_id = &"pause" +bubble_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")]) +focus_paths = Array[NodePath]([NodePath("BubbleCluster/ResumeButton"), NodePath("BubbleCluster/SaveButton"), NodePath("BubbleCluster/SettingsButton"), NodePath("BubbleCluster/ReturnToTitleButton"), NodePath("BubbleCluster/ResetProgressButton"), NodePath("BubbleCluster/QuitButton")]) +initial_focus_path = NodePath("BubbleCluster/ResumeButton") +back_focus_path = NodePath("BubbleCluster/ResumeButton") +maximum_layout_size = Vector2(720, 520) +compact_maximum_layout_size = Vector2(544, 400) +compact_width_threshold = 680.0 +compact_height_threshold = 500.0 +outgoing_rise_duration = 1.7 +incoming_rise_duration = 1.85 -[node name="SettingsButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="BubbleCluster" type="Control" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage"] +layout_mode = 0 +offset_right = 720.0 +offset_bottom = 520.0 +script = ExtResource("6_cluster") +profile = ExtResource("7_profile") +desktop_reference_size = Vector2(720, 520) +compact_reference_size = Vector2(608, 448) + +[node name="ResumeButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 +custom_minimum_size = Vector2(0, 0) +text = "return\nto game" +accessibility_name = "return to game" +neutral_size = Vector2(174, 164) +desktop_anchor = Vector2(335, 220) +compact_anchor = Vector2(350, 225) +compact_minimum_size = Vector2(132, 126) +minimum_font_size = 18 +maximum_font_size = 27 +motion_phase = 0.3 + +[node name="SaveButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) +text = "save\nnow" +accessibility_name = "save now" +neutral_size = Vector2(126, 120) +desktop_anchor = Vector2(185, 185) +compact_anchor = Vector2(165, 180) +compact_minimum_size = Vector2(100, 96) +minimum_font_size = 15 +maximum_font_size = 21 +motion_phase = 1.15 + +[node name="SettingsButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] +unique_name_in_owner = true +custom_minimum_size = Vector2(0, 0) text = "settings" +neutral_size = Vector2(144, 136) +desktop_anchor = Vector2(485, 170) +compact_anchor = Vector2(515, 175) +compact_minimum_size = Vector2(112, 108) +minimum_font_size = 16 +maximum_font_size = 23 +motion_phase = 2.05 -[node name="ReturnToTitleButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="ReturnToTitleButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 -text = "return to title" +custom_minimum_size = Vector2(0, 0) +text = "return\nto title" +accessibility_name = "return to title" +neutral_size = Vector2(132, 126) +desktop_anchor = Vector2(520, 340) +compact_anchor = Vector2(535, 360) +compact_minimum_size = Vector2(106, 102) +minimum_font_size = 14 +maximum_font_size = 21 +motion_phase = 3.0 -[node name="ResetProgressButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="ResetProgressButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 -theme_type_variation = &"DangerButton" -text = "reset progress" +custom_minimum_size = Vector2(0, 0) +text = "reset\nprogress" +accessibility_name = "reset progress" +neutral_size = Vector2(126, 120) +desktop_anchor = Vector2(245, 365) +compact_anchor = Vector2(200, 380) +compact_minimum_size = Vector2(102, 98) +minimum_font_size = 14 +maximum_font_size = 20 +motion_phase = 3.9 -[node name="QuitButton" type="Button" parent="RootCenter/RootPanel/Margin/Content"] +[node name="QuitButton" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster" instance=ExtResource("5_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 64) -layout_mode = 2 +custom_minimum_size = Vector2(0, 0) text = "quit" +neutral_size = Vector2(102, 98) +desktop_anchor = Vector2(390, 395) +compact_anchor = Vector2(390, 410) +compact_minimum_size = Vector2(84, 82) +minimum_font_size = 14 +maximum_font_size = 18 +motion_phase = 4.75 -[node name="FeedbackLabel" type="Label" parent="RootCenter/RootPanel/Margin/Content"] +[node name="FeedbackLabel" type="Label" parent="ResponsivePauseStage/PausePresentationScaleRoot/RootPage/BubbleCluster"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 54) -layout_mode = 2 +layout_mode = 1 +anchors_preset = 12 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_top = -38.0 +offset_bottom = -6.0 +grow_horizontal = 2 +grow_vertical = 0 +mouse_filter = 2 horizontal_alignment = 1 vertical_alignment = 1 autowrap_mode = 2 -[node name="SettingsCenter" type="CenterContainer" parent="."] +[node name="SettingsCenter" type="CenterContainer" parent="ResponsivePauseStage/PausePresentationScaleRoot"] +unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -112,11 +196,12 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="SettingsPanel" parent="SettingsCenter" instance=ExtResource("2_settings")] +[node name="SettingsPanel" parent="ResponsivePauseStage/PausePresentationScaleRoot/SettingsCenter" instance=ExtResource("2_settings")] unique_name_in_owner = true layout_mode = 2 [node name="ConfirmationCenter" type="CenterContainer" parent="."] +z_index = 3 layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 850db9d..1b3d0d7 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -69,6 +69,7 @@ var _pages: Dictionary[StringName, SettingsBubblePage] = {} var _page_transition_generation: int = 0 var _page_transition_active: bool = false var _presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL +var _animate_gameplay_host_transitions: bool = false var _auto_click_enabled: bool = false var _auto_click_interval_value: float = 0.20 var _mouse_sensitivity: float = 0.005 @@ -145,10 +146,16 @@ func _ready() -> void: func open_panel( settings_manager: SettingsManagerType, presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL, + animate_gameplay_host_transitions: bool = false, + uses_external_backdrop: bool = false, ) -> void: _cancel_page_transition() _presentation_mode = presentation_mode - _backdrop.visible = presentation_mode == PresentationMode.GAMEPLAY_MODAL + _animate_gameplay_host_transitions = animate_gameplay_host_transitions + _backdrop.visible = ( + presentation_mode == PresentationMode.GAMEPLAY_MODAL + and not uses_external_backdrop + ) _settings_manager = settings_manager _load_controls() _feedback.text = "" @@ -156,7 +163,10 @@ func open_panel( _page_stack.clear() _page_stack.append(PAGE_ROOT) panel_visibility_changed.emit(true) - if presentation_mode == PresentationMode.TITLE_EMBEDDED: + if ( + presentation_mode == PresentationMode.TITLE_EMBEDDED + or _animate_gameplay_host_transitions + ): for page: SettingsBubblePage in _pages.values(): page.hide_page() _page_transition_generation += 1 @@ -164,7 +174,7 @@ func open_panel( var generation: int = _page_transition_generation _root_page.transition_in( true, - _finish_embedded_open.bind(generation), + _finish_animated_open.bind(generation), TITLE_HOST_INCOMING_DURATION ) else: @@ -172,13 +182,19 @@ func open_panel( opened.emit() -func close_panel() -> void: +func close_panel(immediate: bool = false) -> void: if not visible: return - if _presentation_mode == PresentationMode.TITLE_EMBEDDED: + if immediate: + _finish_panel_close(false) + return + if ( + _presentation_mode == PresentationMode.TITLE_EMBEDDED + or _animate_gameplay_host_transitions + ): if _page_transition_active: return - _begin_embedded_close(false) + _begin_animated_close(false) return _finish_panel_close(false) @@ -266,14 +282,14 @@ func _finish_incoming_page(generation: int) -> void: _page_transition_active = false -func _finish_embedded_open(generation: int) -> void: +func _finish_animated_open(generation: int) -> void: if generation != _page_transition_generation or not _page_transition_active: return _page_transition_active = false opened.emit() -func _begin_embedded_close(applied_result: bool) -> void: +func _begin_animated_close(applied_result: bool) -> void: var active_page: SettingsBubblePage = _get_active_page() if active_page == null: return @@ -282,12 +298,12 @@ func _begin_embedded_close(applied_result: bool) -> void: navigation_transition_started.emit() var generation: int = _page_transition_generation active_page.transition_out( - _finish_embedded_close.bind(generation, applied_result), + _finish_animated_close.bind(generation, applied_result), TITLE_HOST_OUTGOING_DURATION ) -func _finish_embedded_close( +func _finish_animated_close( generation: int, applied_result: bool, ) -> void: @@ -329,8 +345,11 @@ func _apply_settings() -> void: edited.world_pixel_size = _settings_manager.current_settings.world_pixel_size edited.ui_pixel_size = _settings_manager.current_settings.ui_pixel_size if _settings_manager.apply_settings(edited): - if _presentation_mode == PresentationMode.TITLE_EMBEDDED: - _begin_embedded_close(true) + if ( + _presentation_mode == PresentationMode.TITLE_EMBEDDED + or _animate_gameplay_host_transitions + ): + _begin_animated_close(true) else: _finish_panel_close(true) else: From 6f51f91324ccacd50863082e19f85aab575ab306 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 27 Jul 2026 23:45:11 -0400 Subject: [PATCH 2/3] Add resolution-independent pixelation --- main/main.gd | 26 ++++-- main/main.tscn | 6 +- main/world_pixelation_postprocess.gd | 51 +++++++++++ main/world_pixelation_postprocess.gd.uid | 1 + main/world_pixelation_postprocess.gdshader | 15 ++++ .../world_pixelation_postprocess.gdshader.uid | 1 + main/world_pixelation_postprocess.tscn | 21 +++++ settings/player_settings.gd | 85 +++++++++++++++++-- ui/title_screen.gd | 27 ++++-- ui/ui_pixelation_presenter.gd | 20 ++--- 10 files changed, 220 insertions(+), 33 deletions(-) create mode 100644 main/world_pixelation_postprocess.gd create mode 100644 main/world_pixelation_postprocess.gd.uid create mode 100644 main/world_pixelation_postprocess.gdshader create mode 100644 main/world_pixelation_postprocess.gdshader.uid create mode 100644 main/world_pixelation_postprocess.tscn diff --git a/main/main.gd b/main/main.gd index 21e621f..51e645b 100644 --- a/main/main.gd +++ b/main/main.gd @@ -30,6 +30,9 @@ const UIPixelationPresenterType = preload( const PixelationResetOverlayType = preload( "res://ui/pixelation_reset_overlay.gd" ) +const WorldPixelationPostprocessType = preload( + "res://main/world_pixelation_postprocess.gd" +) const TITLE_MUSIC_SILENCE_DB: float = -80.0 @@ -53,6 +56,9 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0 @onready var _pixelation_reset: PixelationResetOverlayType = ( %PixelationResetOverlay ) +@onready var _world_pixelation: WorldPixelationPostprocessType = ( + %WorldPixelationPostprocess +) var _gameplay_started: bool = false var _shop_interaction: FishingShopInteractionType @@ -241,14 +247,7 @@ func _process(_delta: float) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void: if settings == null: return - var root_viewport: Viewport = get_viewport() - root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST - root_viewport.scaling_3d_scale = PlayerSettingsType.get_world_render_scale( - settings.world_pixel_size - ) - root_viewport.msaa_3d = Viewport.MSAA_DISABLED - root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED - root_viewport.use_taa = false + _apply_world_pixelation(settings.world_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size) _game_ui.get_title_screen().set_world_pixelation( settings.world_pixel_size @@ -264,6 +263,16 @@ func _apply_runtime_settings(settings: PlayerSettingsType) -> void: ) +func _apply_world_pixelation(pixel_size: int) -> void: + var root_viewport: Viewport = get_viewport() + root_viewport.scaling_3d_mode = Viewport.SCALING_3D_MODE_NEAREST + root_viewport.scaling_3d_scale = 1.0 + root_viewport.msaa_3d = Viewport.MSAA_DISABLED + root_viewport.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED + root_viewport.use_taa = false + _world_pixelation.set_pixel_size(pixel_size) + + func _on_effective_ui_pixel_size_changed( _requested_pixel_size: int, effective_pixel_size: int, @@ -281,6 +290,7 @@ func _reset_pixelation() -> void: func _set_gameplay_active(active: bool) -> void: _gameplay_started = active + _world_pixelation.set_gameplay_active(active) _ui_pixelation.set_gameplay_active(active) if not active: _player.item_effects.reset_all() diff --git a/main/main.tscn b/main/main.tscn index 9729966..192178b 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=16 format=3] +[gd_scene load_steps=17 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"] @@ -15,6 +15,7 @@ [ext_resource type="AudioStream" path="res://audio/music/title/as_in_four_wolves.ogg" id="13_title_music"] [ext_resource type="Script" path="res://ui/ui_pixelation_presenter.gd" id="14_pixelation"] [ext_resource type="PackedScene" path="res://ui/pixelation_reset_overlay.tscn" id="15_reset"] +[ext_resource type="PackedScene" path="res://main/world_pixelation_postprocess.tscn" id="16_world_pixelation"] [node name="Main" type="Node3D"] script = ExtResource("3_main") @@ -53,6 +54,9 @@ stream = ExtResource("13_title_music") volume_db = -80.0 bus = &"Music" +[node name="WorldPixelationPostprocess" parent="." instance=ExtResource("16_world_pixelation")] +unique_name_in_owner = true + [node name="UIPresentation" type="SubViewportContainer" parent="."] unique_name_in_owner = true texture_filter = 1 diff --git a/main/world_pixelation_postprocess.gd b/main/world_pixelation_postprocess.gd new file mode 100644 index 0000000..303fa3e --- /dev/null +++ b/main/world_pixelation_postprocess.gd @@ -0,0 +1,51 @@ +class_name WorldPixelationPostprocess +extends CanvasLayer + +@onready var _screen_grid: ColorRect = %ScreenGrid + +var _pixel_size: int = PlayerSettings.DEFAULT_WORLD_PIXEL_SIZE +var _gameplay_active: bool = false + + +func _ready() -> void: + get_viewport().size_changed.connect(_refresh_grid) + _refresh_grid() + + +func set_pixel_size(pixel_size: int) -> void: + _pixel_size = clampi( + pixel_size, + PlayerSettings.MIN_WORLD_PIXEL_SIZE, + PlayerSettings.MAX_WORLD_PIXEL_SIZE + ) + _refresh_grid() + + +func set_gameplay_active(active: bool) -> void: + _gameplay_active = active + _refresh_grid() + + +func get_grid_size() -> Vector2i: + return PlayerSettings.get_world_grid_size( + _pixel_size, + get_window().size + ) + + +func _refresh_grid() -> void: + if not is_node_ready(): + return + var effect_enabled: bool = ( + _gameplay_active + and _pixel_size != PlayerSettings.MIN_WORLD_PIXEL_SIZE + ) + _screen_grid.visible = effect_enabled + if not effect_enabled: + return + var shader_material := _screen_grid.material as ShaderMaterial + if shader_material != null: + shader_material.set_shader_parameter( + "logical_grid_size", + Vector2(get_grid_size()) + ) diff --git a/main/world_pixelation_postprocess.gd.uid b/main/world_pixelation_postprocess.gd.uid new file mode 100644 index 0000000..b77b05c --- /dev/null +++ b/main/world_pixelation_postprocess.gd.uid @@ -0,0 +1 @@ +uid://c3n47x8egoc8x diff --git a/main/world_pixelation_postprocess.gdshader b/main/world_pixelation_postprocess.gdshader new file mode 100644 index 0000000..4018a3f --- /dev/null +++ b/main/world_pixelation_postprocess.gdshader @@ -0,0 +1,15 @@ +shader_type canvas_item; +render_mode unshaded; + +uniform sampler2D screen_texture + : hint_screen_texture, filter_nearest, repeat_disable; +uniform vec2 logical_grid_size = vec2(640.0, 360.0); + + +void fragment() { + vec2 safe_grid = max(logical_grid_size, vec2(1.0)); + vec2 snapped_uv = ( + floor(SCREEN_UV * safe_grid) + vec2(0.5) + ) / safe_grid; + COLOR = texture(screen_texture, snapped_uv); +} diff --git a/main/world_pixelation_postprocess.gdshader.uid b/main/world_pixelation_postprocess.gdshader.uid new file mode 100644 index 0000000..0b61936 --- /dev/null +++ b/main/world_pixelation_postprocess.gdshader.uid @@ -0,0 +1 @@ +uid://sq224d8sn1b4 diff --git a/main/world_pixelation_postprocess.tscn b/main/world_pixelation_postprocess.tscn new file mode 100644 index 0000000..b6f952b --- /dev/null +++ b/main/world_pixelation_postprocess.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=3 format=3] + +[ext_resource type="Script" path="res://main/world_pixelation_postprocess.gd" id="1_script"] +[ext_resource type="Shader" path="res://main/world_pixelation_postprocess.gdshader" id="2_shader"] + +[sub_resource type="ShaderMaterial" id="ShaderMaterial_world_pixelation"] +shader = ExtResource("2_shader") + +[node name="WorldPixelationPostprocess" type="CanvasLayer"] +layer = -1 +script = ExtResource("1_script") + +[node name="ScreenGrid" type="ColorRect" parent="."] +unique_name_in_owner = true +material = SubResource("ShaderMaterial_world_pixelation") +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 diff --git a/settings/player_settings.gd b/settings/player_settings.gd index 0d3657e..44c00a3 100644 --- a/settings/player_settings.gd +++ b/settings/player_settings.gd @@ -13,8 +13,11 @@ const DEFAULT_WORLD_PIXEL_SIZE: int = 3 const MIN_UI_PIXEL_SIZE: int = 1 const MAX_UI_PIXEL_SIZE: int = 5 const DEFAULT_UI_PIXEL_SIZE: int = 3 -const WORLD_RENDER_SCALES: Array[float] = [1.0, 0.9, 0.8, 0.65, 0.45] -const UI_RENDER_SCALES: Array[float] = [1.0, 0.975, 0.95, 0.925, 0.9] +const COMPACT_DISPLAY_HEIGHT: int = 560 +const WORLD_DESKTOP_GRID_HEIGHTS: Array[int] = [0, 540, 360, 216, 96] +const WORLD_COMPACT_GRID_HEIGHTS: Array[int] = [0, 360, 240, 144, 72] +const UI_DESKTOP_RENDER_HEIGHTS: Array[int] = [0, 612, 504, 396, 288] +const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192] @export var auto_click_enabled: bool = false @export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20 @@ -55,19 +58,89 @@ func copy() -> PlayerSettings: return result -static func get_world_render_scale(pixel_size: int) -> float: +static func get_world_grid_height( + pixel_size: int, + displayed_height: int, +) -> int: var index: int = clampi( pixel_size, MIN_WORLD_PIXEL_SIZE, MAX_WORLD_PIXEL_SIZE ) - 1 - return WORLD_RENDER_SCALES[index] + if index == 0: + return maxi(1, displayed_height) + var targets: Array[int] = ( + WORLD_COMPACT_GRID_HEIGHTS + if displayed_height < COMPACT_DISPLAY_HEIGHT + else WORLD_DESKTOP_GRID_HEIGHTS + ) + return mini(maxi(1, displayed_height), targets[index]) -static func get_ui_render_scale(pixel_size: int) -> float: +static func get_world_grid_size( + pixel_size: int, + displayed_size: Vector2i, +) -> Vector2i: + var safe_size := Vector2i( + maxi(1, displayed_size.x), + maxi(1, displayed_size.y) + ) + var target_height: int = get_world_grid_height( + pixel_size, + safe_size.y + ) + return Vector2i( + maxi( + 1, + roundi( + float(target_height) + * float(safe_size.x) + / float(safe_size.y) + ) + ), + target_height + ) + + +static func get_ui_render_height( + pixel_size: int, + displayed_height: int, +) -> int: var index: int = clampi( pixel_size, MIN_UI_PIXEL_SIZE, MAX_UI_PIXEL_SIZE ) - 1 - return UI_RENDER_SCALES[index] + if index == 0: + return maxi(1, displayed_height) + var targets: Array[int] = ( + UI_COMPACT_RENDER_HEIGHTS + if displayed_height < COMPACT_DISPLAY_HEIGHT + else UI_DESKTOP_RENDER_HEIGHTS + ) + return mini(maxi(1, displayed_height), targets[index]) + + +static func get_ui_render_size( + pixel_size: int, + displayed_size: Vector2i, +) -> Vector2i: + var safe_size := Vector2i( + maxi(1, displayed_size.x), + maxi(1, displayed_size.y) + ) + var target_height: int = get_ui_render_height( + pixel_size, + safe_size.y + ) + return Vector2i( + maxi( + 1, + roundi( + float(target_height) + * float(safe_size.x) + / float(safe_size.y) + ) + ), + target_height + ) diff --git a/ui/title_screen.gd b/ui/title_screen.gd index c3552fa..aab4921 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -379,20 +379,25 @@ func set_world_pixelation(pixel_size: int) -> void: func _update_world_preview_resolution() -> void: - var render_scale: float = PlayerSettings.get_world_render_scale( - _world_pixel_size + var displayed_size := Vector2i( + maxi(1, roundi(size.x)), + maxi(1, roundi(size.y)) + ) + var grid_size: Vector2i = PlayerSettings.get_world_grid_size( + _world_pixel_size, + displayed_size ) var shader_material := _background.material as ShaderMaterial if shader_material != null: shader_material.set_shader_parameter( "virtual_pixel_density", - Vector2( - maxf(1.0, size.x * render_scale), - maxf(1.0, size.y * render_scale) - ) + Vector2(grid_size) ) var logo_material := _title_logo.material as ShaderMaterial if logo_material != null: + var render_scale: float = ( + float(grid_size.y) / float(displayed_size.y) + ) var effect_scale: float = 1.0 / render_scale logo_material.set_shader_parameter( "horizontal_displacement_pixels", @@ -405,9 +410,15 @@ func _update_world_preview_resolution() -> void: func _snap_world_preview(value: Vector2) -> Vector2: - var render_scale: float = PlayerSettings.get_world_render_scale( - _world_pixel_size + var displayed_size := Vector2i( + maxi(1, roundi(size.x)), + maxi(1, roundi(size.y)) ) + var grid_size: Vector2i = PlayerSettings.get_world_grid_size( + _world_pixel_size, + displayed_size + ) + var render_scale: float = float(grid_size.y) / float(displayed_size.y) var step: float = 1.0 / render_scale return Vector2( roundf(value.x / step) * step, diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index ca41c15..476f3a8 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -1,7 +1,7 @@ class_name UIPixelationPresenter extends SubViewportContainer -const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(320, 180) +const MIN_UI_VIEWPORT_SIZE: Vector2i = Vector2i(256, 180) signal effective_pixel_size_changed( requested_pixel_size: int, @@ -70,12 +70,13 @@ func _resize_presentation() -> void: return var previous_effective_size: int = _effective_pixel_size _effective_pixel_size = _resolve_effective_pixel_size(root_size) - var render_scale: float = PlayerSettings.get_ui_render_scale( - _effective_pixel_size + var viewport_size: Vector2i = PlayerSettings.get_ui_render_size( + _effective_pixel_size, + root_size ) - var viewport_size := Vector2i( - ceili(float(root_size.x) * render_scale), - ceili(float(root_size.y) * render_scale) + var render_scale: float = minf( + 1.0, + float(viewport_size.y) / float(root_size.y) ) var presentation_size: Vector2 = Vector2(viewport_size) / render_scale set_anchors_preset(Control.PRESET_TOP_LEFT) @@ -98,10 +99,9 @@ func _resize_presentation() -> void: func _resolve_effective_pixel_size(root_size: Vector2i) -> int: for candidate: int in range(_requested_pixel_size, 0, -1): - var render_scale: float = PlayerSettings.get_ui_render_scale(candidate) - var candidate_size := Vector2i( - ceili(float(root_size.x) * render_scale), - ceili(float(root_size.y) * render_scale) + var candidate_size: Vector2i = PlayerSettings.get_ui_render_size( + candidate, + root_size ) if ( candidate_size.x >= MIN_UI_VIEWPORT_SIZE.x From 48a3ab03ea1c2062d80a53df8be2946efc349440 Mon Sep 17 00:00:00 2001 From: Voyager Date: Mon, 27 Jul 2026 23:46:14 -0400 Subject: [PATCH 3/3] Prepare v0.2.0-prealpha.2 --- export_presets.cfg | 10 +++++----- playtest/README-PLAYTEST.txt | 4 ++-- project.godot | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/export_presets.cfg b/export_presets.cfg index 6386c22..e01b43e 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -9,7 +9,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="builds/*,playtest/*,scripts/*" -export_path="builds/playtest-0.1/windows-x86_64/netfishing.exe" +export_path="builds/playtest-0.2.0-prealpha.2/windows-x86_64/netfishing.exe" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" @@ -35,11 +35,11 @@ application/modify_resources=true application/icon="" application/console_wrapper_icon="" application/icon_interpolation=4 -application/file_version="0.1.0.0" -application/product_version="0.1.0.0" +application/file_version="0.2.0.2" +application/product_version="0.2.0.2" application/company_name="" application/product_name="NETFISHING" -application/file_description="NETFISHING Pre-Alpha Playtest 0.1" +application/file_description="NETFISHING Pre-Alpha Playtest 0.2" application/copyright="" application/trademarks="" application/export_angle=0 @@ -62,7 +62,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="builds/*,playtest/*,scripts/*" -export_path="builds/playtest-0.1/linux-x86_64/netfishing" +export_path="builds/playtest-0.2.0-prealpha.2/linux-x86_64/netfishing" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/playtest/README-PLAYTEST.txt b/playtest/README-PLAYTEST.txt index 060f1cf..f781b31 100644 --- a/playtest/README-PLAYTEST.txt +++ b/playtest/README-PLAYTEST.txt @@ -1,6 +1,6 @@ NETFISHING -Pre-Alpha Playtest 0.1 -Version 0.1.0-prealpha +Pre-Alpha Playtest 0.2 +Version 0.2.0-prealpha.2 Thank you for trying this early private playtest. diff --git a/project.godot b/project.godot index 3b0f605..525d8f0 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="NETFISHING" -config/version="0.1.0-prealpha" +config/version="0.2.0-prealpha.2" run/main_scene="res://main/main.tscn" config/features=PackedStringArray("4.7", "GL Compatibility") config/icon="res://icon.svg"