From 6b8c3eb2f7317badcf39a544fdf8e6c829b4e3fc Mon Sep 17 00:00:00 2001 From: Voyager Date: Fri, 31 Jul 2026 21:20:23 -0400 Subject: [PATCH] Improve UI scaling and fishing commerce --- art/patterns/pattern_moneyfish.png | Bin 0 -> 1352 bytes art/patterns/pattern_moneyfish.png.import | 40 ++ fishing/fishing_spot.gd | 6 +- main/main.gd | 62 ++- main/main.tscn | 18 +- network/network_sale_protocol.gd | 6 + network/network_sale_service.gd | 145 +++--- tests/economy_regression_validation.gd | 260 ++++++++-- tests/fish_catalog_content_validation.gd | 7 +- tests/inventory_notepad_art_validation.gd | 273 ++++++++++ tests/inventory_notepad_art_validation.gd.uid | 1 + tests/ui_scaling_runtime_validation.gd | 195 +++++++ tests/ui_scaling_runtime_validation.gd.uid | 1 + ui/chat_ui.gd | 9 +- ui/components/inventory_notepad.gd | 43 +- ui/fishing_shop.gd | 476 +++++------------- ui/fishing_shop.tscn | 257 ++++------ ui/game_ui.gd | 52 +- ui/game_ui.tscn | 44 +- ui/pause_menu.gd | 50 -- ui/pause_menu.tscn | 11 - ui/player_menu.gd | 253 ++++++++-- ui/player_menu.tscn | 20 +- ui/settings_panel.gd | 6 - ui/settings_panel.tscn | 10 - ui/title_screen.gd | 39 +- ui/ui_pixelation_presenter.gd | 29 +- ui/ui_reference_presentation.gd | 17 + world/interactables/pelican_buyer_world.tscn | 2 +- 29 files changed, 1515 insertions(+), 817 deletions(-) create mode 100644 art/patterns/pattern_moneyfish.png create mode 100644 art/patterns/pattern_moneyfish.png.import create mode 100644 tests/inventory_notepad_art_validation.gd create mode 100644 tests/inventory_notepad_art_validation.gd.uid create mode 100644 tests/ui_scaling_runtime_validation.gd create mode 100644 tests/ui_scaling_runtime_validation.gd.uid diff --git a/art/patterns/pattern_moneyfish.png b/art/patterns/pattern_moneyfish.png new file mode 100644 index 0000000000000000000000000000000000000000..d223c4406dbb407fa966ad67c4554ca0620a9bf9 GIT binary patch literal 1352 zcmV-O1-JT%P)z@;j|==^1poj5AY({UO#lFTCIA3{ga82g0001h=l}q9FaQARU;qF* zm;eA5aGbhPJOBUy32;bRa{vG?BLDy{BLR4&KXw2B00(qQO+^Rl2mt{B6!F9{>i_@* zo=HSORCwC$n$K$-Wf;dl?>q1QN)wx^t(92pMH)0TVj<9gL@cs2r{JX^y;KUl3L;+g zR1wr;d(^){JP3_=ups_Qw1?KRm`K@ZDppG{iMu=5o%iuDyPMr+jkPngJF}Yy26kqa znR&nSJfC^znXfu{c2r1c$g>vvav3>VMc>Gv-_LFDJ7V$PT?fFN-w&?pcnp)+!JqW7 zrz>!{hW;{({wVuE$N7H(JTAx)yJNHZ>sl&zw8UwPzN&dEi_0Vg`LB176D?j#=z0 zW&gKTAqb%S#lBF*o-X<8&07KH+-Um+L>xanwZLx&S2%I&MM?{Qv$RrZ?wg!A!O8Q} zT;Bf<2soFAVM_%QKqf8nl8Zg=g49*j0v6EVU0TO^ZDiLIg&Bp^B7(==UkUWkN{s~-gKHY~}aDrt(8)&X~291o^_N<6H>gQV7SVFI*{cAaHky9?kV?R>d zxQX-m5bEPpFb0GLnn3ZWV9&Vi*R{{*kW5suhw8r%c*qkqa23vHIn*bqU=R!wG=XUU zkYJ}O0O;!(f1|GN91_$BPy;F&wVZ}EecyxMRe&%-6DZp^75YX7JK>_14eASnyj6p$ zpcc~pjagTS*l-_%VS*-5+)?Nca>zvwZ?=T?h9D)VUk&P(gZw4_qv!#{bOSVDg!A?= z-g}$axe^#bfA66F(1DL@x&fNdM0j%;@8TvhR)b{&#o%S9TR~%|Dv%R@xd${V2dbjc z%W3qrOxFkBnhENz;{(|eP6k8`n$SgfV_Lr6c z%Ul(CdJCDW#zuh1dV>0^9Av~F@Qi;NJGF(3ddOoI)%OBiNh3!sa@fP>{nuF;tD;s@ zsDkPL44|k`MF%x*u+!yjVdk@_t4{wjpjr1%*8IoKYKk~Uq`4?F9x_&K-g%n`o3Ehp zvp{CHTTH#mif9EGueDD)iheoRJZQ2_Hu$NS?THYPmVhdlX6oB~rCAO#R`u7%wjYN! zUuhGhFET(~b?SGzW!pV0P(_0(m>q)jg#fK0bet^ZeMqW;LC|U42YRG5Tf%3Hi!Jk*V(3wcEa`RtC&LFaoQnM>y0UxF-O6-vvTx-6daufUUd4mq5_(1OEU&ndl0000< KMNUMnLSTZc>u|{c literal 0 HcmV?d00001 diff --git a/art/patterns/pattern_moneyfish.png.import b/art/patterns/pattern_moneyfish.png.import new file mode 100644 index 0000000..790317c --- /dev/null +++ b/art/patterns/pattern_moneyfish.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://b6xws1d2dnbnn" +path="res://.godot/imported/pattern_moneyfish.png-c20edef3e829648b3e6aae4e190efaed.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://art/patterns/pattern_moneyfish.png" +dest_files=["res://.godot/imported/pattern_moneyfish.png-c20edef3e829648b3e6aae4e190efaed.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index 2a0b242..6f2555f 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -496,7 +496,7 @@ func _begin_aiming(player: PlayerType) -> void: _cast_charge = 0.0 _cast_target = _calculate_cast_target(minimum_cast_distance) state = FishingState.AIMING_CAST - status_changed.emit("hold left click to aim • release to cast") + status_changed.emit("") var target_is_fishable: bool = is_target_fishable(_cast_target) var preview_position: Vector3 = _resolve_preview_surface_position(_cast_target) _presentation.begin_aim( @@ -603,7 +603,7 @@ func _confirm_cast() -> void: return state = FishingState.CASTING - status_changed.emit("casting...") + status_changed.emit("") _presentation.begin_cast(_cast_target) _presentation.set_line_mode(FishingPresentationType.LineMode.TAUT) @@ -618,7 +618,7 @@ func _on_cast_completed() -> void: _cast_charge, _build_network_evidence() ) - status_changed.emit("checking the water...") + status_changed.emit("") return _selected_water_region = get_fishable_water_region(_cast_target) diff --git a/main/main.gd b/main/main.gd index f62c696..f845ae3 100644 --- a/main/main.gd +++ b/main/main.gd @@ -74,6 +74,7 @@ const NetworkProfileServiceType = preload( const TITLE_MUSIC_SILENCE_DB: float = -80.0 const PLAYER_MENU_PATTERN_SCALE: float = 0.85 const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0) +const SHOP_PATTERN_SCALE: float = 1.75 @export var fish_catalog: FishPoolType @export var pelican_buyer_profile: FishBuyerProfileType @@ -136,6 +137,7 @@ const PLAYER_MENU_PATTERN_SCROLL_VELOCITY := Vector2(-7.0, -5.0) @onready var _players_root: Node3D = $Players @onready var _title_background: ColorRect = %TitleBackground @onready var _player_menu_backdrop: ColorRect = %PlayerMenuBackdrop +@onready var _shop_backdrop: ColorRect = %ShopBackdrop var _gameplay_started: bool = false var _shop_interaction: FishingShopInteractionType @@ -146,6 +148,7 @@ var _quit_in_progress: bool = false var _join_requested_from_title: bool = false var _join_requested_from_pause: bool = false var _player_menu_backdrop_tween: Tween +var _shop_backdrop_tween: Tween var _pending_join_endpoint: String = "" var _server_trust_dialog: ConfirmationDialog var _pending_trust_changed: bool = false @@ -173,6 +176,16 @@ func _ready() -> void: "scroll_velocity_pixels", PLAYER_MENU_PATTERN_SCROLL_VELOCITY, ) + var shop_pattern_material := _shop_backdrop.material as ShaderMaterial + if shop_pattern_material != null: + shop_pattern_material.set_shader_parameter( + "display_scale", + SHOP_PATTERN_SCALE, + ) + shop_pattern_material.set_shader_parameter( + "scroll_velocity_pixels", + PLAYER_MENU_PATTERN_SCROLL_VELOCITY, + ) get_window().size_changed.connect(_resize_native_overlays) _resize_native_overlays() if not _settings_manager.settings_changed.is_connected( @@ -338,17 +351,21 @@ func _initialize_after_data_root() -> void: fish_catalog, _network_item_use ) + var sale_buyers: Array[FishBuyerProfileType] = [ + pelican_buyer_profile, + main_shop_buyer_profile, + ] _network_sale.setup( _network_session, _player_spawn_service, _network_fishing, + _shop_interaction, _player.inventory, _player.wallet, _player.fish_sale_service, _save_manager, fish_catalog, - pelican_buyer_profile, - _test_world.get_pelican_convenience_landmark(), + sale_buyers, _asset_reservations ) _network_shop.setup( @@ -455,6 +472,9 @@ func _initialize_after_data_root() -> void: _game_ui.player_menu_backdrop_visibility_changed.connect( _set_player_menu_backdrop_visible ) + _game_ui.shop_backdrop_visibility_changed.connect( + _set_shop_backdrop_visible + ) _pixelation_reset.return_to_settings_requested.connect( _game_ui.focus_open_settings_back_button ) @@ -916,6 +936,7 @@ func _set_gameplay_active(active: bool) -> void: _refresh_active_hotbar_item() else: _set_player_menu_backdrop_visible(false) + _set_shop_backdrop_visible(false) func _set_player_menu_backdrop_visible(is_visible: bool) -> void: @@ -953,11 +974,48 @@ func _set_player_menu_backdrop_visible(is_visible: bool) -> void: ) +func _set_shop_backdrop_visible(is_visible: bool) -> void: + if _shop_backdrop_tween != null: + _shop_backdrop_tween.kill() + _shop_backdrop_tween = null + var should_show: bool = is_visible and _gameplay_started + if should_show: + var was_visible: bool = _shop_backdrop.visible + _shop_backdrop.visible = true + if not was_visible: + _shop_backdrop.modulate.a = 0.0 + _shop_backdrop_tween = create_tween() + _shop_backdrop_tween.tween_property( + _shop_backdrop, + "modulate:a", + 1.0, + UIMotion.PLAYER_MENU_ENTER_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) + return + if not _shop_backdrop.visible: + return + _shop_backdrop_tween = create_tween() + _shop_backdrop_tween.tween_property( + _shop_backdrop, + "modulate:a", + 0.0, + UIMotion.PLAYER_MENU_EXIT_DURATION, + ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) + _shop_backdrop_tween.finished.connect( + func() -> void: + _shop_backdrop.visible = false + _shop_backdrop.modulate.a = 1.0 + _shop_backdrop_tween = null + ) + + func _resize_native_overlays() -> void: if not is_node_ready(): return _player_menu_backdrop.position = Vector2.ZERO _player_menu_backdrop.size = Vector2(get_window().size) + _shop_backdrop.position = Vector2.ZERO + _shop_backdrop.size = Vector2(get_window().size) func _on_new_game_requested() -> void: diff --git a/main/main.tscn b/main/main.tscn index 2be860d..bc3afcd 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -1,4 +1,4 @@ -[gd_scene load_steps=45 format=3] +[gd_scene load_steps=47 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"] @@ -42,6 +42,7 @@ [ext_resource type="Shader" path="res://ui/title_water_background.gdshader" id="40_title_water"] [ext_resource type="Texture2D" path="res://art/patterns/pattern_tealdot.png" id="41_menu_pattern"] [ext_resource type="Shader" path="res://ui/player_menu_pattern.gdshader" id="42_menu_pattern"] +[ext_resource type="Texture2D" path="res://art/patterns/pattern_moneyfish.png" id="43_shop_pattern"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"] shader = ExtResource("40_title_water") @@ -53,6 +54,13 @@ shader_parameter/source_tile_size = Vector2(128, 128) shader_parameter/display_scale = 0.85 shader_parameter/scroll_velocity_pixels = Vector2(-7, -5) +[sub_resource type="ShaderMaterial" id="ShaderMaterial_shop_pattern"] +shader = ExtResource("42_menu_pattern") +shader_parameter/pattern_texture = ExtResource("43_shop_pattern") +shader_parameter/source_tile_size = Vector2(64, 64) +shader_parameter/display_scale = 1.75 +shader_parameter/scroll_velocity_pixels = Vector2(-7, -5) + [node name="Main" type="Node3D"] script = ExtResource("3_main") fish_catalog = ExtResource("6_pool") @@ -82,6 +90,14 @@ mouse_filter = 2 material = SubResource("ShaderMaterial_player_menu_pattern") color = Color(1, 1, 1, 1) +[node name="ShopBackdrop" type="ColorRect" parent="."] +unique_name_in_owner = true +visible = false +z_index = 10 +mouse_filter = 2 +material = SubResource("ShaderMaterial_shop_pattern") +color = Color(1, 1, 1, 1) + [node name="InterfaceFontController" type="Node" parent="."] unique_name_in_owner = true script = ExtResource("39_fonts") diff --git a/network/network_sale_protocol.gd b/network/network_sale_protocol.gd index f304480..ef91ccf 100644 --- a/network/network_sale_protocol.gd +++ b/network/network_sale_protocol.gd @@ -101,6 +101,12 @@ static func validate_result(data: Variant) -> bool: and str(payload["message"]).length() <= MAX_MESSAGE_LENGTH ): return false + if payload.has("buyer_id") and ( + typeof(payload["buyer_id"]) not in [TYPE_STRING, TYPE_STRING_NAME] + or str(payload["buyer_id"]).is_empty() + or str(payload["buyer_id"]).length() > MAX_ID_LENGTH + ): + return false var seen: Dictionary[String, bool] = {} for value: Variant in payload["catch_ids"]: if typeof(value) not in [TYPE_STRING, TYPE_STRING_NAME]: diff --git a/network/network_sale_service.gd b/network/network_sale_service.gd index daf962b..06eefb8 100644 --- a/network/network_sale_service.gd +++ b/network/network_sale_service.gd @@ -9,7 +9,8 @@ const FishSaleServiceType = preload("res://economy/fish_sale_service.gd") const FishSaleResultType = preload("res://economy/fish_sale_result.gd") const MAX_LEDGER_ENTRIES_PER_PEER: int = 64 -const PELICAN_INTERACTION_RANGE: float = 7.5 +const PELICAN_BUYER_ID: StringName = &"pelicans" +const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop" signal local_sale_pending(request_id: String) signal local_sale_finished( @@ -23,13 +24,13 @@ signal local_sale_finished( var _session: NetworkSession var _spawn_service: PlayerSpawnService var _network_fishing: NetworkFishingService +var _shop_interaction: FishingShopInteraction var _inventory: FishInventory var _wallet: PlayerWallet var _sale_service: FishSaleServiceType var _save_manager: PlayerSaveManager var _fish_catalog: FishPoolType -var _buyer: FishBuyerProfileType -var _pelican_landmark: Node3D +var _buyers: Dictionary[StringName, FishBuyerProfileType] = {} var _request_ledgers: Dictionary[int, Dictionary] = {} var _pending_by_peer: Dictionary[int, String] = {} var _result_owners: Dictionary[String, int] = {} @@ -38,6 +39,7 @@ var _applied_results: Dictionary[String, bool] = {} var _received_results: Dictionary[String, bool] = {} var _pending_local_request_id: String = "" var _pending_local_catch_ids: Array[StringName] = [] +var _pending_local_buyer_id: StringName var _reservations: PlayerAssetReservationService @@ -45,25 +47,28 @@ func setup( session: NetworkSession, spawn_service: PlayerSpawnService, network_fishing: NetworkFishingService, + shop_interaction: FishingShopInteraction, inventory: FishInventory, wallet: PlayerWallet, sale_service: FishSaleServiceType, save_manager: PlayerSaveManager, fish_catalog: FishPoolType, - buyer: FishBuyerProfileType, - pelican_landmark: Node3D, + buyers: Array[FishBuyerProfileType], reservations: PlayerAssetReservationService, ) -> void: _session = session _spawn_service = spawn_service _network_fishing = network_fishing + _shop_interaction = shop_interaction _inventory = inventory _wallet = wallet _sale_service = sale_service _save_manager = save_manager _fish_catalog = fish_catalog - _buyer = buyer - _pelican_landmark = pelican_landmark + _buyers.clear() + for buyer: FishBuyerProfileType in buyers: + if buyer != null and buyer.is_valid(): + _buyers[buyer.id] = buyer _reservations = reservations if not _session.peer_removed.is_connected(_on_peer_removed): _session.peer_removed.connect(_on_peer_removed) @@ -71,9 +76,18 @@ func setup( _session.state_changed.connect(_on_session_state_changed) -func can_request_sale() -> bool: +func is_local_sale_pending() -> bool: + return not _pending_local_request_id.is_empty() + + +func can_request_sale( + buyer_id: StringName = PELICAN_BUYER_ID, +) -> bool: + var buyer: FishBuyerProfileType = _buyers.get(buyer_id) return ( - _session != null + buyer != null + and buyer.is_valid() + and _session != null and _session.is_gameplay_session_active() and ( _session.is_host() @@ -84,11 +98,10 @@ func can_request_sale() -> bool: ) -func is_local_sale_pending() -> bool: - return not _pending_local_request_id.is_empty() - - -func request_local_sale(catch_ids: Array[StringName]) -> String: +func request_local_sale( + catch_ids: Array[StringName], + buyer_id: StringName = PELICAN_BUYER_ID, +) -> String: for catch_id: StringName in catch_ids: if _reservations != null and _reservations.is_fish_reserved(catch_id): local_sale_finished.emit( @@ -100,7 +113,7 @@ func request_local_sale(catch_ids: Array[StringName]) -> String: "", false, "Selling…", _empty_catch_ids(), 0 ) return "" - if not can_request_sale(): + if not can_request_sale(buyer_id): local_sale_finished.emit( "", false, @@ -114,6 +127,12 @@ func request_local_sale(catch_ids: Array[StringName]) -> String: "", false, "Sale could not be completed.", _empty_catch_ids(), 0 ) return "" + var buyer: FishBuyerProfileType = _buyers.get(buyer_id) + if buyer == null or not buyer.is_valid(): + local_sale_finished.emit( + "", false, "The buyer is unavailable.", _empty_catch_ids(), 0 + ) + return "" var evidence: Array[Dictionary] = [] for catch_id: StringName in catch_ids: var fish_catch: FishCatch = _inventory.get_catch_by_id(catch_id) @@ -127,11 +146,12 @@ func request_local_sale(catch_ids: Array[StringName]) -> String: var request: Dictionary = { "request_id": request_id, "session_id": _session.get_session_id(), - "buyer_id": str(_buyer.id) if _buyer != null else "", + "buyer_id": str(buyer.id), "catches": evidence, } _pending_local_request_id = request_id _pending_local_catch_ids = catch_ids.duplicate() + _pending_local_buyer_id = buyer.id local_sale_pending.emit(request_id) if _session.is_host(): _handle_sale_request(_session.get_local_peer_id(), request) @@ -182,27 +202,23 @@ func _handle_sale_request(peer_id: int, data: Dictionary) -> void: request_id, "A sale is already pending." )) return - if not _is_buyer_available_for_peer(peer_id): - var avatar: Player = _spawn_service.get_avatar(peer_id) - var message: String = ( - "The buyer is unavailable." - if avatar == null or _pelican_landmark == null - else "Move closer to the buyer." - ) - _record_and_send(peer_id, _rejected_result(request_id, message)) - return - if _buyer == null or not _buyer.is_valid(): + var buyer_id := StringName(str(data["buyer_id"])) + var buyer: FishBuyerProfileType = _buyers.get(buyer_id) + if buyer == null or not buyer.is_valid(): _record_and_send(peer_id, _rejected_result( request_id, "The buyer is unavailable." )) return - if StringName(str(data["buyer_id"])) != _buyer.id: + if ( + buyer_id == MAIN_SHOP_BUYER_ID + and not _is_shop_available_for_peer(peer_id) + ): _record_and_send(peer_id, _rejected_result( - request_id, "The buyer is unavailable." + request_id, "Move closer to the fishing shop." )) return var result: Dictionary = _build_authoritative_result( - peer_id, request_id, data["catches"] + peer_id, request_id, data["catches"], buyer ) _pending_by_peer[peer_id] = request_id _record_and_send(peer_id, result) @@ -212,8 +228,9 @@ func _build_authoritative_result( peer_id: int, request_id: String, evidence_values: Array, + buyer: FishBuyerProfileType, ) -> Dictionary: - if _fish_catalog == null or _buyer == null: + if _fish_catalog == null or buyer == null: return _rejected_result( request_id, "The buyer is unavailable." ) @@ -245,7 +262,7 @@ func _build_authoritative_result( return _rejected_result( request_id, "Favorite catches cannot be sold." ) - var offer: int = _buyer.get_offer(decoded.sale_value) + var offer: int = buyer.get_offer(decoded.sale_value) if ( offer < 0 or base_value > 9223372036854775807 - decoded.sale_value @@ -262,6 +279,7 @@ func _build_authoritative_result( "request_id": request_id, "session_id": _session.get_session_id(), "target_peer_id": peer_id, + "buyer_id": str(buyer.id), "accepted": true, "catch_ids": catch_ids, "payout": payout, @@ -338,6 +356,12 @@ func _apply_sale_result(data: Dictionary) -> void: return if str(data["request_id"]) != _pending_local_request_id: return + var result_buyer_id := StringName(str( + data.get("buyer_id", _pending_local_buyer_id) + )) + if result_buyer_id != _pending_local_buyer_id: + _fail_local_apply(data, "Sale could not be completed.") + return if not bool(data["accepted"]): _received_results[result_id] = true _bound_dictionary(_received_results) @@ -359,8 +383,12 @@ func _apply_sale_result(data: Dictionary) -> void: if _reservations != null and _reservations.is_fish_reserved(catch_id): _fail_local_apply(data, "Reserved in a letter.") return + var buyer: FishBuyerProfileType = _buyers.get(_pending_local_buyer_id) + if buyer == null or not buyer.is_valid(): + _fail_local_apply(data, "The buyer is unavailable.") + return var preview: FishSaleResultType = _sale_service.preview_batch( - catch_ids, _buyer + catch_ids, buyer ) if not preview.is_success(): var message: String = ( @@ -380,7 +408,7 @@ func _apply_sale_result(data: Dictionary) -> void: var sequence_snapshot: int = _inventory.get_next_catch_sequence() var wallet_snapshot: int = _wallet.get_balance() var local_result: FishSaleResultType = _sale_service.sell_batch( - catch_ids, _buyer + catch_ids, buyer ) if not local_result.is_success() or not _save_manager.save_if_dirty(): _inventory.replace_all_catches( @@ -420,6 +448,7 @@ func _finish_local_sale( ) -> void: _pending_local_request_id = "" _pending_local_catch_ids.clear() + _pending_local_buyer_id = StringName() local_sale_finished.emit( request_id, accepted, message, catch_ids, payout ) @@ -494,33 +523,6 @@ func _handle_acknowledgement( _pending_by_peer.erase(peer_id) -func _is_buyer_available_for_peer(peer_id: int) -> bool: - if ( - _session == null - or not _session.is_host() - or not _session.is_gameplay_session_active() - or _spawn_service == null - or _pelican_landmark == null - or not is_instance_valid(_pelican_landmark) - ): - return false - var avatar: Player = _spawn_service.get_avatar(peer_id) - if ( - avatar == null - or avatar.is_water_recovery_active() - or ( - _network_fishing != null - and _network_fishing.has_peer_attempt(peer_id) - ) - ): - return false - return ( - avatar.global_position.distance_to( - _pelican_landmark.global_position - ) <= PELICAN_INTERACTION_RANGE - ) - - func _on_peer_removed(peer_id: int) -> void: _request_ledgers.erase(peer_id) _pending_by_peer.erase(peer_id) @@ -530,6 +532,28 @@ func _on_peer_removed(peer_id: int) -> void: _acknowledged_results.erase(result_id) +func _is_shop_available_for_peer(peer_id: int) -> bool: + if ( + _session == null + or not _session.is_host() + or not _session.is_gameplay_session_active() + or _spawn_service == null + or _shop_interaction == null + or not is_instance_valid(_shop_interaction) + ): + return false + var avatar: Player = _spawn_service.get_avatar(peer_id) + return ( + avatar != null + and not avatar.is_water_recovery_active() + and ( + _network_fishing == null + or not _network_fishing.has_peer_attempt(peer_id) + ) + and _shop_interaction.is_avatar_in_range(avatar) + ) + + func _on_session_state_changed(state: NetworkSession.State) -> void: if state in [ NetworkSession.State.INACTIVE, @@ -558,6 +582,7 @@ func _clear_session_state() -> void: _received_results.clear() _pending_local_request_id = "" _pending_local_catch_ids.clear() + _pending_local_buyer_id = StringName() func _bound_dictionary(values: Dictionary) -> void: diff --git a/tests/economy_regression_validation.gd b/tests/economy_regression_validation.gd index ee44e20..2df0479 100644 --- a/tests/economy_regression_validation.gd +++ b/tests/economy_regression_validation.gd @@ -47,26 +47,22 @@ func _run() -> void: main.get_node("%PlayerAssetReservationService") as PlayerAssetReservationService ) - var pelican := ( - main.get("_test_world") as TestWorld - ).get_pelican_convenience_landmark() assert(player != null) assert(catalog != null and catalog.candidates.size() == 8) assert(sale_service != null) assert(shop_service != null) assert(session != null and session.is_host()) assert(reservations != null) - assert(pelican != null) sale_service.local_sale_finished.connect(_on_sale_finished) shop_service.local_purchase_finished.connect(_on_shop_finished) - player.global_position = pelican.global_position await process_frame _test_each_species(player, catalog, sale_service) _test_multi_sale(player, catalog, sale_service) _test_reservations(player, catalog, sale_service, reservations) - _test_rejection_cleanup(player, catalog, sale_service, pelican) - await _test_player_menu_sale(main, player, catalog, sale_service, pelican) + _test_anywhere_sale(player, catalog, sale_service) + await _test_player_menu_sale(main, player, catalog, sale_service) + await _test_host_shop_sale(main, player, catalog, sale_service) assert(session.set_host_open(true)) assert(session.state == NetworkSession.State.OPEN_HOST) @@ -76,6 +72,9 @@ func _run() -> void: assert(session.set_host_open(false)) await _test_host_shop_purchase(main, player, shop_service) + await _test_fishing_shop_sale_ui( + main, player, catalog, sale_service, reservations + ) assert(not sale_service.is_local_sale_pending()) assert(not shop_service.is_local_purchase_pending()) @@ -111,14 +110,9 @@ func _run_multiplayer_host() -> void: assert(remote_peer_id > 1) var spawn_service := main.get("_player_spawn_service") as PlayerSpawnService var remote_avatar := spawn_service.get_avatar(remote_peer_id) - var pelican := ( - main.get("_test_world") as TestWorld - ).get_pelican_convenience_landmark() - assert(remote_avatar != null and pelican != null) - # The client cannot teleport its authoritative avatar. Place the host-owned - # test avatar at each interaction so this fixture validates the remote - # transaction path without conflating it with the movement suite. - remote_avatar.global_position = pelican.global_position + assert(remote_avatar != null) + # Pelican sales are available from the Cooler anywhere. The host-owned + # avatar only moves when the later Fishing Shop purchase needs proximity. await create_timer(5.0).timeout var interaction := main.get("_shop_interaction") as FishingShopInteraction assert(interaction != null) @@ -156,10 +150,6 @@ func _run_multiplayer_client() -> void: var shop_service := main.get_node("%NetworkShopService") as NetworkShopService sale_service.local_sale_finished.connect(_on_sale_finished) shop_service.local_purchase_finished.connect(_on_shop_finished) - var pelican := ( - main.get("_test_world") as TestWorld - ).get_pelican_convenience_landmark() - player.global_position = pelican.global_position var catch_ids: Array[StringName] = [] for fish: FishData in catalog.candidates: var fish_catch := _make_catch(fish) @@ -182,6 +172,28 @@ func _run_multiplayer_client() -> void: var interaction := main.get("_shop_interaction") as FishingShopInteraction player.global_position = interaction.global_position await create_timer(5.5).timeout + var shop_catch := _make_catch(catalog.candidates.front()) + player.inventory.add_catch(shop_catch) + var shop_sale_balance_before: int = player.wallet.get_balance() + _sale_result.clear() + assert(not sale_service.request_local_sale( + [shop_catch.catch_id], + NetworkSaleService.MAIN_SHOP_BUYER_ID, + ).is_empty()) + var shop_sale_deadline: int = Time.get_ticks_msec() + 10000 + while Time.get_ticks_msec() < shop_sale_deadline: + await process_frame + if not _sale_result.is_empty(): + break + print("Client shop sale result: ", _sale_result) + assert(not _sale_result.is_empty() and bool(_sale_result[1])) + assert(not player.inventory.contains_catch_id(shop_catch.catch_id)) + assert( + player.wallet.get_balance() + == shop_sale_balance_before + shop_catch.sale_value + ) + assert(not sale_service.is_local_sale_pending()) + assert(player.wallet.credit(100)) _shop_result.clear() assert(not shop_service.request_supply(&"coffee").is_empty()) @@ -277,34 +289,90 @@ func _test_reservations( ) -func _test_rejection_cleanup( +func _test_anywhere_sale( player: Player, catalog: FishPool, sale_service: NetworkSaleService, - pelican: Node3D, ) -> void: var fish_catch := _make_catch(catalog.candidates[5]) player.inventory.add_catch(fish_catch) - player.global_position = pelican.global_position + Vector3(20.0, 0.0, 0.0) - _assert_sale(player, sale_service, [fish_catch.catch_id], false) - assert(player.inventory.contains_catch_id(fish_catch.catch_id)) - assert(not sale_service.is_local_sale_pending()) - player.global_position = pelican.global_position + # The spawn point is intentionally far from the Pelican landmark. Discounted + # Pelican sales are a convenience action and do not require proximity. + player.global_position = Vector3(0.0, 3.95, 13.0) _assert_sale(player, sale_service, [fish_catch.catch_id], true) +func _test_host_shop_sale( + main: Node, + player: Player, + catalog: FishPool, + sale_service: NetworkSaleService, +) -> void: + var interaction := main.get("_shop_interaction") as FishingShopInteraction + assert(interaction != null) + var out_of_range_catch := _make_catch(catalog.candidates[6]) + player.inventory.add_catch(out_of_range_catch) + player.global_position = Vector3(0.0, 3.95, 13.0) + for _frame: int in 4: + await physics_frame + _assert_sale( + player, + sale_service, + [out_of_range_catch.catch_id], + false, + true, + NetworkSaleService.MAIN_SHOP_BUYER_ID, + ) + player.global_position = interaction.global_position + for _frame: int in 4: + await physics_frame + assert(interaction.is_avatar_in_range(player)) + var balance_before: int = player.wallet.get_balance() + _assert_sale( + player, + sale_service, + [out_of_range_catch.catch_id], + true, + true, + NetworkSaleService.MAIN_SHOP_BUYER_ID, + ) + assert( + player.wallet.get_balance() + == balance_before + out_of_range_catch.sale_value + ) + var species_ids: Array[StringName] = [] + var expected_payout: int = 0 + for fish: FishData in catalog.candidates: + var fish_catch := _make_catch(fish) + player.inventory.add_catch(fish_catch) + species_ids.append(fish_catch.catch_id) + expected_payout += fish_catch.sale_value + var species_balance_before: int = player.wallet.get_balance() + _assert_sale( + player, + sale_service, + species_ids, + true, + true, + NetworkSaleService.MAIN_SHOP_BUYER_ID, + ) + assert( + player.wallet.get_balance() + == species_balance_before + expected_payout + ) + + func _test_player_menu_sale( main: Node, player: Player, catalog: FishPool, sale_service: NetworkSaleService, - pelican: Node3D, ) -> void: - player.global_position = pelican.global_position var game_ui := main.get_node("%GameUI") as GameUI var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu var fish_catch := _make_catch(catalog.candidates[6]) player.inventory.add_catch(fish_catch) + player.global_position = Vector3(0.0, 3.95, 13.0) player_menu.open_menu() await create_timer(2.2).timeout player_menu.call("_on_catch_card_pressed", fish_catch.catch_id) @@ -312,15 +380,23 @@ func _test_player_menu_sale( var sell_action := player_menu.get_node("%SellBubble") as Button var confirmation := player_menu.get_node("%SaleConfirmation") as Control var confirm_button := player_menu.get_node("%ConfirmSaleButton") as Button + var ui_viewport := main.get_node( + "UIPresentation/UIViewport" + ) as SubViewport assert(sell_action.visible and not sell_action.disabled) assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP) assert(sell_action.focus_mode == Control.FOCUS_ALL) - sell_action.pressed.emit() + assert(ui_viewport != null) + await _activate_focused_button(sell_action, ui_viewport) await process_frame assert(confirmation.visible) assert(confirm_button.visible and not confirm_button.disabled) + assert( + confirmation.z_index + > (player_menu.get_node("%CoolerOuterWall") as Control).z_index + ) _sale_result.clear() - confirm_button.pressed.emit() + await _activate_focused_button(confirm_button, ui_viewport) await process_frame assert(not _sale_result.is_empty() and bool(_sale_result[1])) assert(not player.inventory.contains_catch_id(fish_catch.catch_id)) @@ -339,6 +415,21 @@ func _test_player_menu_sale( await create_timer(2.2).timeout +func _activate_focused_button( + button: Button, + ui_viewport: SubViewport, +) -> void: + button.grab_focus() + await process_frame + assert(button.has_focus()) + for is_pressed: bool in [true, false]: + var accept := InputEventAction.new() + accept.action = &"ui_accept" + accept.pressed = is_pressed + ui_viewport.push_input(accept, false) + await process_frame + + func _test_host_shop_purchase( main: Node, player: Player, @@ -359,16 +450,123 @@ func _test_host_shop_purchase( assert(not shop_service.is_local_purchase_pending()) +func _test_fishing_shop_sale_ui( + main: Node, + player: Player, + catalog: FishPool, + sale_service: NetworkSaleService, + reservations: PlayerAssetReservationService, +) -> void: + var interaction := main.get("_shop_interaction") as FishingShopInteraction + var game_ui := main.get_node("%GameUI") as GameUI + var shop := game_ui.get_node("%FishingShop") as FishingShop + var player_menu := game_ui.get_node("%PlayerMenu") as PlayerMenu + var shop_backdrop := main.get_node("%ShopBackdrop") as ColorRect + var ui_viewport := main.get_node( + "UIPresentation/UIViewport" + ) as SubViewport + assert( + interaction != null + and shop != null + and player_menu != null + and shop_backdrop != null + and ui_viewport != null + ) + player.global_position = interaction.global_position + for _frame: int in 4: + await physics_frame + var fish_catch := _make_catch(catalog.candidates[7]) + var reserved_catch := _make_catch(catalog.candidates[6]) + player.inventory.add_catch(fish_catch) + player.inventory.add_catch(reserved_catch) + var reservation_id := "shop-ui-reservation" + assert(reservations.reserve(reservation_id, { + "type": PlayerAssetReservationService.AttachmentType.FISH, + "catch_id": str(reserved_catch.catch_id), + "catch": reserved_catch.to_network_dict(), + })) + var balance_before: int = player.wallet.get_balance() + assert(shop.open_shop()) + await process_frame + assert(shop_backdrop.visible) + assert(shop_backdrop.material is ShaderMaterial) + assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer")) + var shop_panel := shop.get_node("%ShopPanel") as PanelContainer + assert(shop_panel != null and shop_panel.size == Vector2(980.0, 600.0)) + var shop_panel_style := shop_panel.get_theme_stylebox("panel") as StyleBoxFlat + assert(shop_panel_style != null) + assert(shop_panel_style.corner_radius_top_left == 24) + var buy_mode := shop.get_node("%BuyModeButton") as Button + var sell_mode := shop.get_node("%SellModeButton") as Button + assert(buy_mode.visible and sell_mode.visible) + await _activate_focused_button(sell_mode, ui_viewport) + await process_frame + assert(shop.visible and not player_menu.visible) + assert(not shop.has_node("ShopPanel/Margin/Layout/Body/FishSales")) + assert((shop.get_node("%ShopCoolerPage") as Control).visible) + assert(not (shop.get_node("%ShopPanel") as Control).visible) + var mounted_cooler := player_menu.get("_cooler_page") as Control + assert(mounted_cooler != null and mounted_cooler.visible) + assert( + mounted_cooler.get_parent() == shop.get_node("%ShopCoolerMount") + ) + var cooler_outer_wall := player_menu.get("_cooler_outer_wall") as Control + var water_surface := player_menu.get("_cooler_water_surface") as ColorRect + assert(cooler_outer_wall != null and cooler_outer_wall.visible) + assert(water_surface.visible and water_surface.material is ShaderMaterial) + assert( + StringName( + (player_menu.get("_sale_buyer_override") as FishBuyerProfile).id + ) == NetworkSaleService.MAIN_SHOP_BUYER_ID + ) + var fish_nodes: Dictionary = player_menu.get("_fish_nodes") + var fish_button := fish_nodes.get(fish_catch.catch_id) as Button + var reserved_button := fish_nodes.get(reserved_catch.catch_id) as Button + assert(fish_button != null and fish_button.visible) + assert(reserved_button != null and reserved_button.visible) + await _activate_focused_button(fish_button, ui_viewport) + var sell_button := player_menu.get("_sell_bubble") as Button + assert(sell_button.visible and not sell_button.disabled) + await _activate_focused_button(sell_button, ui_viewport) + await process_frame + var confirmation := player_menu.get("_sale_confirmation") as Control + var confirm_button := player_menu.get("_confirm_sale_button") as Button + assert(confirmation.visible) + assert( + confirmation.z_index + > cooler_outer_wall.z_index + ) + _sale_result.clear() + await _activate_focused_button(confirm_button, ui_viewport) + await process_frame + assert(not _sale_result.is_empty() and bool(_sale_result[1])) + assert(not player.inventory.contains_catch_id(fish_catch.catch_id)) + assert(player.wallet.get_balance() == balance_before + fish_catch.sale_value) + assert(not sale_service.is_local_sale_pending()) + assert(reservations.release(reservation_id)) + var back_button := shop.get_node("%BackToShopButton") as Button + await _activate_focused_button(back_button, ui_viewport) + await process_frame + assert(shop.visible and (shop.get_node("%ShopPanel") as Control).visible) + assert(not (shop.get_node("%ShopCoolerPage") as Control).visible) + assert(not player_menu.is_shop_cooler_mounted()) + shop.close_shop() + await process_frame + + func _assert_sale( player: Player, sale_service: NetworkSaleService, catch_ids: Array[StringName], expected_success: bool, expect_request_id: bool = true, + buyer_id: StringName = NetworkSaleService.PELICAN_BUYER_ID, ) -> void: var balance_before: int = player.wallet.get_balance() _sale_result.clear() - var request_id: String = sale_service.request_local_sale(catch_ids) + var request_id: String = sale_service.request_local_sale( + catch_ids, buyer_id + ) assert((not request_id.is_empty()) == expect_request_id) assert(not _sale_result.is_empty()) assert(bool(_sale_result[1]) == expected_success) diff --git a/tests/fish_catalog_content_validation.gd b/tests/fish_catalog_content_validation.gd index c0c4dc1..5972da2 100644 --- a/tests/fish_catalog_content_validation.gd +++ b/tests/fish_catalog_content_validation.gd @@ -5,6 +5,7 @@ const CollectionLogType = preload("res://collection/collection_log.gd") const FishDataType = preload("res://fish/fish_data.gd") const FishInventoryType = preload("res://inventory/fish_inventory.gd") const FishPoolType = preload("res://fish/fish_pool.gd") +const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd") const FishSelectorType = preload("res://fish/fish_selector.gd") const FishingContextType = preload("res://fishing/fishing_context.gd") const NetworkSaleServiceType = preload( @@ -227,7 +228,10 @@ func _validate_catches_and_authoritative_sale() -> void: root.add_child(sale_service) sale_service.set("_session", session) sale_service.set("_fish_catalog", Catalog) - sale_service.set("_buyer", PelicanBuyer) + var sale_buyers: Dictionary[StringName, FishBuyerProfileType] = { + PelicanBuyer.id: PelicanBuyer, + } + sale_service.set("_buyers", sale_buyers) for index: int in Catalog.candidates.size(): var fish: FishDataType = Catalog.candidates[index] @@ -257,6 +261,7 @@ func _validate_catches_and_authoritative_sale() -> void: 1, "catalog_sale_%d" % index, [loaded.to_network_dict()], + PelicanBuyer, ) assert(bool(sale_result.get("accepted", false))) assert(int(sale_result.get("base_value", -1)) == loaded.sale_value) diff --git a/tests/inventory_notepad_art_validation.gd b/tests/inventory_notepad_art_validation.gd new file mode 100644 index 0000000..cbe3414 --- /dev/null +++ b/tests/inventory_notepad_art_validation.gd @@ -0,0 +1,273 @@ +extends SceneTree + +const PlayerMenuScene = preload("res://ui/player_menu.tscn") +const UIReferencePresentationType = preload( + "res://ui/ui_reference_presentation.gd" +) +const EXPECTED_HOST_SIZE := Vector2(278.0, 484.0) +const EXPECTED_ART_SIZE := Vector2(306.28125, 580.8) +const EXPECTED_ART_POSITION := Vector2(-4.140625, -28.4) +const INVENTORY_PANEL_RECT := Rect2(54.0, 166.0, 882.0, 484.0) +const NOTEPAD_HOST_RECT := Rect2(952.0, 166.0, 278.0, 484.0) +const NOTEPAD_ART_RECT := Rect2( + NOTEPAD_HOST_RECT.position + EXPECTED_ART_POSITION, + EXPECTED_ART_SIZE, +) +const HOTBAR_BUBBLE_FIELD_RECT := Rect2(245.0, 608.0, 790.0, 104.0) +const TARGET_SIZES: Array[Vector2] = [ + Vector2(1280.0, 720.0), + Vector2(1920.0, 1080.0), + Vector2(2560.0, 1440.0), + Vector2(3840.0, 2160.0), + Vector2(640.0, 480.0), + Vector2(1024.0, 768.0), + Vector2(1729.0, 973.0), + Vector2(2560.0, 1080.0), + Vector2(3440.0, 1440.0), +] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + _print_runtime_parameters() + var presentation_stage := Control.new() + presentation_stage.name = "CanonicalUIStage" + _apply_stage_layout(presentation_stage, Vector2(root.size)) + root.add_child(presentation_stage) + var player_menu := PlayerMenuScene.instantiate() as PlayerMenu + presentation_stage.add_child(player_menu) + await process_frame + var notepads: Array[Node] = player_menu.find_children( + "*", "InventoryNotepad", true, false + ) + assert(notepads.size() == 3) + for node: Node in notepads: + var notepad := node as InventoryNotepad + assert(notepad != null) + assert(notepad.size.is_equal_approx(EXPECTED_HOST_SIZE)) + assert(notepad.mouse_filter == Control.MOUSE_FILTER_PASS) + var art_rect: Rect2 = notepad.get_art_rect() + assert(art_rect.size.is_equal_approx(EXPECTED_ART_SIZE)) + assert(art_rect.position.is_equal_approx(EXPECTED_ART_POSITION)) + assert(art_rect.get_center().is_equal_approx( + EXPECTED_HOST_SIZE * 0.5 + + InventoryNotepad.NOTEPAD_ART_OFFSET + )) + _validate_shared_inventory_geometry(player_menu) + _validate_inventory_layering(player_menu) + _validate_resolution_matrix() + await _capture_inventory_pages(player_menu) + print("Inventory notepad artwork validation: PASS") + presentation_stage.queue_free() + await process_frame + quit() + + +func _print_runtime_parameters() -> void: + var window: Window = root + var screen_index: int = window.current_screen + print( + ( + "Runtime UI: window=%s screen=%s screen_scale=%.3f dpi=%d " + + "content_size=%s content_mode=%d content_aspect=%d " + + "content_factor=%.3f" + ) + % [ + window.size, + DisplayServer.screen_get_size(screen_index), + DisplayServer.screen_get_scale(screen_index), + DisplayServer.screen_get_dpi(screen_index), + window.content_scale_size, + window.content_scale_mode, + window.content_scale_aspect, + window.content_scale_factor, + ] + ) + + +func _apply_stage_layout(stage: Control, display_size: Vector2) -> void: + stage.set_anchors_preset(Control.PRESET_TOP_LEFT) + stage.position = UIReferencePresentationType.get_offset(display_size) + stage.size = UIReferencePresentationType.REFERENCE_SIZE + stage.scale = Vector2.ONE * UIReferencePresentationType.get_scale( + display_size + ) + + +func _validate_shared_inventory_geometry(player_menu: PlayerMenu) -> void: + for node_name: StringName in [ + &"DetailConstellation", + &"BagDetailConstellation", + &"TackleDetailPanel", + ]: + var host := player_menu.get_node("%%%s" % node_name) as Control + assert(host != null) + assert(host.position.is_equal_approx(Vector2(952.0, 166.0))) + assert(host.size.is_equal_approx(EXPECTED_HOST_SIZE)) + + +func _validate_inventory_layering(player_menu: PlayerMenu) -> void: + var bag_filters := player_menu.get_node("%BagFilterTabs") as Control + var bag_panel := player_menu.get_node("%BagOuterWall") as Control + var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control + var cooler_panel := player_menu.get_node("%CoolerOuterWall") as Control + var tackle_panel := player_menu.get_node("%TackleMainPanel") as Control + assert(bag_filters != null) + assert(bag_panel != null) + assert(sale_confirmation != null) + assert(cooler_panel != null) + assert(tackle_panel != null) + assert(bag_filters.z_index > bag_panel.z_index) + assert(sale_confirmation.z_index > cooler_panel.z_index) + assert(sale_confirmation.z_index > tackle_panel.z_index) + # The contextual Hotbar uses z=90 while the Player Menu is open. + assert(sale_confirmation.z_index > 90) + assert(sale_confirmation.position.is_equal_approx(Vector2(380.0, 265.0))) + assert(sale_confirmation.size.is_equal_approx(Vector2(520.0, 190.0))) + var confirmation_style := sale_confirmation.get_theme_stylebox( + "panel" + ) as StyleBoxFlat + assert(confirmation_style != null) + assert( + confirmation_style.bg_color.is_equal_approx( + UtilityPageStyle.OCEAN_PANEL_DEEP + ) + ) + + +func _validate_resolution_matrix() -> void: + for target_size: Vector2 in TARGET_SIZES: + var stage_rect: Rect2 = UIReferencePresentationType.get_rect( + target_size + ) + var stage_scale: float = UIReferencePresentationType.get_scale( + target_size + ) + assert(is_equal_approx( + stage_rect.size.x / stage_rect.size.y, + 1280.0 / 720.0, + )) + assert(stage_rect.get_center().is_equal_approx(target_size * 0.5)) + _validate_transformed_rect( + "Inventory panel", INVENTORY_PANEL_RECT, stage_rect, stage_scale + ) + _validate_transformed_rect( + "Notepad host", NOTEPAD_HOST_RECT, stage_rect, stage_scale + ) + _validate_transformed_rect( + "Notepad art", NOTEPAD_ART_RECT, stage_rect, stage_scale + ) + _validate_transformed_rect( + "Hotbar", HOTBAR_BUBBLE_FIELD_RECT, stage_rect, stage_scale + ) + var screen_host: Rect2 = _to_screen_rect( + NOTEPAD_HOST_RECT, stage_rect.position, stage_scale + ) + var screen_art: Rect2 = _to_screen_rect( + NOTEPAD_ART_RECT, stage_rect.position, stage_scale + ) + assert(is_equal_approx( + screen_art.size.y, + screen_host.size.y * InventoryNotepad.NOTEPAD_CANONICAL_OVERSCAN, + )) + assert(screen_art.get_center().is_equal_approx( + screen_host.get_center() + + InventoryNotepad.NOTEPAD_ART_OFFSET * stage_scale + )) + print( + ( + "UI geometry: window=%s stage=%s scale=%.6f " + + "inventory=%s notepad=%s art=%s hotbar=%s" + ) + % [ + target_size, + stage_rect, + stage_scale, + _to_screen_rect( + INVENTORY_PANEL_RECT, + stage_rect.position, + stage_scale, + ), + screen_host, + screen_art, + _to_screen_rect( + HOTBAR_BUBBLE_FIELD_RECT, + stage_rect.position, + stage_scale, + ), + ] + ) + + +func _validate_transformed_rect( + label: String, + canonical_rect: Rect2, + stage_rect: Rect2, + stage_scale: float, +) -> void: + var screen_rect: Rect2 = _to_screen_rect( + canonical_rect, stage_rect.position, stage_scale + ) + assert( + screen_rect.position.is_equal_approx( + stage_rect.position + canonical_rect.position * stage_scale + ), + "%s position does not use the single stage transform" % label, + ) + assert( + screen_rect.size.is_equal_approx(canonical_rect.size * stage_scale), + "%s size does not use the single stage transform" % label, + ) + + +func _to_screen_rect( + canonical_rect: Rect2, + stage_offset: Vector2, + stage_scale: float, +) -> Rect2: + return Rect2( + stage_offset + canonical_rect.position * stage_scale, + canonical_rect.size * stage_scale, + ) + + +func _capture_inventory_pages(player_menu: PlayerMenu) -> void: + if not OS.has_environment("NETFISHING_NOTEPAD_CAPTURE"): + return + player_menu.visible = true + var sections: Array[PlayerMenu.Section] = [ + PlayerMenu.Section.COOLER, + PlayerMenu.Section.TACKLE_BOX, + PlayerMenu.Section.BAG, + ] + var suffixes: Array[String] = ["cooler", "tackle", "equipment"] + for index: int in sections.size(): + player_menu.call("_show_section_immediate", sections[index]) + await process_frame + await process_frame + await _save_capture(suffixes[index]) + player_menu.call("_show_section_immediate", PlayerMenu.Section.COOLER) + var sale_confirmation := player_menu.get_node("%SaleConfirmation") as Control + var confirmation_message := player_menu.get_node( + "%ConfirmationMessage" + ) as Label + assert(sale_confirmation != null) + assert(confirmation_message != null) + confirmation_message.text = "sell selected fish?" + sale_confirmation.visible = true + await process_frame + await process_frame + await _save_capture("sale-confirmation") + sale_confirmation.visible = false + + +func _save_capture(suffix: String) -> void: + var image: Image = root.get_texture().get_image() + var path: String = "%s-%s.png" % [ + OS.get_environment("NETFISHING_NOTEPAD_CAPTURE"), + suffix, + ] + assert(image.save_png(path) == OK) diff --git a/tests/inventory_notepad_art_validation.gd.uid b/tests/inventory_notepad_art_validation.gd.uid new file mode 100644 index 0000000..59a9b8b --- /dev/null +++ b/tests/inventory_notepad_art_validation.gd.uid @@ -0,0 +1 @@ +uid://np6jfuf8tej8 diff --git a/tests/ui_scaling_runtime_validation.gd b/tests/ui_scaling_runtime_validation.gd new file mode 100644 index 0000000..7fbcead --- /dev/null +++ b/tests/ui_scaling_runtime_validation.gd @@ -0,0 +1,195 @@ +extends SceneTree + +const UIPixelationPresenterType = preload( + "res://ui/ui_pixelation_presenter.gd" +) +const UIReferencePresentationType = preload( + "res://ui/ui_reference_presentation.gd" +) +const GameUIScene := preload("res://ui/game_ui.tscn") +const TARGET_SIZES: Array[Vector2i] = [ + Vector2i(1280, 720), + Vector2i(1920, 1080), + Vector2i(2560, 1440), + Vector2i(3840, 2160), + Vector2i(640, 480), + Vector2i(1024, 768), + Vector2i(1729, 973), + Vector2i(2560, 1080), + Vector2i(3440, 1440), +] + + +func _initialize() -> void: + call_deferred("_run") + + +func _run() -> void: + var original_size: Vector2i = root.size + var presenter := UIPixelationPresenterType.new() + presenter.name = "UIPresentation" + presenter.stretch = true + var ui_viewport := SubViewport.new() + ui_viewport.name = "UIViewport" + var game_ui := GameUIScene.instantiate() as GameUI + var ui_root := game_ui.get_node("UIRoot") as Control + var canonical_stage := game_ui.get_node( + "UIRoot/CanonicalStage" + ) as Control + var chat_ui := game_ui.get_node("UIRoot/ChatUI") as ChatUI + var player_menu := game_ui.get_node( + "UIRoot/CanonicalStage/PlayerMenu" + ) as PlayerMenu + var hotbar := game_ui.get_node( + "UIRoot/CanonicalStage/Hotbar" + ) as HotbarUI + var screen_fade := game_ui.get_node("UIRoot/ScreenFade") as Control + var title_screen := game_ui.get_node("UIRoot/TitleScreen") as TitleScreen + var title_content_stage := title_screen.get_node( + "ResponsiveTitleStage" + ) as Control + var decorative_fish_layer := title_screen.get_node( + "DecorativeFishLayer" + ) as Control + var decorative_bubble_layer := title_screen.get_node( + "DecorativeBubbleLayer" + ) as Control + ui_viewport.add_child(game_ui) + presenter.add_child(ui_viewport) + root.add_child(presenter) + await process_frame + chat_ui.set_available(true) + print( + "UI viewport embedding: root=%s subviewport=%s" + % [root.gui_embed_subwindows, ui_viewport.gui_embed_subwindows] + ) + + for target_size: Vector2i in TARGET_SIZES: + root.size = target_size + await process_frame + await process_frame + var actual_size: Vector2i = root.size + var display_size := Vector2(actual_size) + var expected_scale: float = UIReferencePresentationType.get_scale( + display_size + ) + var expected_stage_offset: Vector2 = UIReferencePresentationType.get_offset( + display_size + ) + assert(presenter.position.is_equal_approx(Vector2.ZERO)) + assert(ui_root.position.is_equal_approx(Vector2.ZERO)) + var expected_visible_size: Vector2 = ( + UIReferencePresentationType.get_visible_reference_size(display_size) + ) + assert(ui_root.size.is_equal_approx(expected_visible_size)) + assert(canonical_stage.size.is_equal_approx( + UIReferencePresentationType.REFERENCE_SIZE + )) + assert(canonical_stage.position.is_equal_approx( + UIReferencePresentationType.get_stage_position(display_size) + )) + assert(chat_ui.size.is_equal_approx(expected_visible_size)) + assert(screen_fade.size.is_equal_approx(expected_visible_size)) + assert(player_menu.position.is_equal_approx(Vector2.ZERO)) + assert(player_menu.size.is_equal_approx( + UIReferencePresentationType.REFERENCE_SIZE + )) + assert(hotbar.position.is_equal_approx(Vector2.ZERO)) + assert(hotbar.size.is_equal_approx( + UIReferencePresentationType.REFERENCE_SIZE + )) + assert(title_screen.size.is_equal_approx(expected_visible_size)) + assert(title_content_stage.size.is_equal_approx( + UIReferencePresentationType.REFERENCE_SIZE + )) + assert(title_content_stage.position.is_equal_approx( + canonical_stage.position + )) + assert(decorative_fish_layer.size.is_equal_approx( + expected_visible_size + )) + assert(decorative_bubble_layer.size.is_equal_approx( + expected_visible_size + )) + var left_to_right_bounds: Vector2 = ( + TitleScreen.get_decorative_fish_crossing_bounds( + expected_visible_size.x, 160.0, 1.0 + ) + ) + var right_to_left_bounds: Vector2 = ( + TitleScreen.get_decorative_fish_crossing_bounds( + expected_visible_size.x, 160.0, -1.0 + ) + ) + assert(left_to_right_bounds.x < -160.0) + assert(left_to_right_bounds.y > expected_visible_size.x) + assert(right_to_left_bounds.x > expected_visible_size.x) + assert(right_to_left_bounds.y < -160.0) + assert(is_equal_approx(presenter.scale.x, presenter.scale.y)) + assert(is_equal_approx(ui_root.scale.x, ui_root.scale.y)) + var effective_scale: float = presenter.scale.x * ui_root.scale.x + assert(is_equal_approx(effective_scale, expected_scale)) + assert(( + canonical_stage.position * effective_scale + ).distance_to(expected_stage_offset) <= 0.01) + var expected_render_height: int = mini( + roundi(UIReferencePresentationType.REFERENCE_SIZE.y * expected_scale), + PlayerSettings.get_ui_render_height( + PlayerSettings.DEFAULT_UI_PIXEL_SIZE, + roundi(UIReferencePresentationType.REFERENCE_SIZE.y), + ), + ) + var expected_render_scale: float = ( + float(expected_render_height) + / UIReferencePresentationType.REFERENCE_SIZE.y + ) + var expected_viewport_size := Vector2i( + roundi(expected_visible_size.x * expected_render_scale), + roundi(expected_visible_size.y * expected_render_scale), + ) + assert(presenter.get_ui_viewport_size() == expected_viewport_size) + assert(presenter.size.is_equal_approx(Vector2(expected_viewport_size))) + var chat_panel := chat_ui.get_node("ChatPanel") as Control + assert(chat_panel != null) + assert(chat_panel.visible) + assert(is_equal_approx(chat_panel.position.x, 0.0)) + assert(is_equal_approx( + (chat_panel.position.y + chat_panel.size.y) * effective_scale, + display_size.y - ChatUI.BOTTOM_MARGIN * effective_scale, + )) + print( + ( + "UI resize: requested=%s actual=%s viewport=%s container_scale=%.6f " + + "root_scale=%.6f effective_scale=%.6f stage_offset=%s " + + "chat_x=%.3f" + ) + % [ + target_size, + actual_size, + presenter.get_ui_viewport_size(), + presenter.scale.x, + ui_root.scale.x, + effective_scale, + expected_stage_offset, + chat_panel.position.x * effective_scale, + ] + ) + if OS.has_environment("NETFISHING_UI_SCALE_CAPTURE"): + await process_frame + var capture: Image = root.get_texture().get_image() + var capture_path: String = ( + "%s-requested-%dx%d-actual-%dx%d.png" + ) % [ + OS.get_environment("NETFISHING_UI_SCALE_CAPTURE"), + target_size.x, + target_size.y, + actual_size.x, + actual_size.y, + ] + assert(capture.save_png(capture_path) == OK) + + presenter.queue_free() + root.size = original_size + await process_frame + print("UI scaling runtime validation: PASS") + quit() diff --git a/tests/ui_scaling_runtime_validation.gd.uid b/tests/ui_scaling_runtime_validation.gd.uid new file mode 100644 index 0000000..0b6079a --- /dev/null +++ b/tests/ui_scaling_runtime_validation.gd.uid @@ -0,0 +1 @@ +uid://d1x8t4mf2kqv6 diff --git a/ui/chat_ui.gd b/ui/chat_ui.gd index f67727a..481d04a 100644 --- a/ui/chat_ui.gd +++ b/ui/chat_ui.gd @@ -60,6 +60,7 @@ var _send_pending: bool = false var _pending_send_body: String = "" var _prior_movement: bool = true var _prior_camera: bool = true +var _output_scale: float = 1.0 func _ready() -> void: @@ -563,6 +564,10 @@ func _set_presentation_state( _refresh_visibility() +func set_output_scale(output_scale: float) -> void: + _output_scale = maxf(output_scale, 0.001) + + func _refresh_handle_labels(state: PresentationState) -> void: var collapsed := state == PresentationState.COLLAPSED var height_state := _visible_state_before_collapse if collapsed else state @@ -632,7 +637,9 @@ func _update_speech() -> void: if camera.is_position_behind(world_position): bubble.hide() continue - var screen_position := camera.unproject_position(world_position) + var screen_position := ( + camera.unproject_position(world_position) / _output_scale + ) var viewport_size := get_viewport_rect().size if ( screen_position.x < -80.0 diff --git a/ui/components/inventory_notepad.gd b/ui/components/inventory_notepad.gd index de5e119..16be9f9 100644 --- a/ui/components/inventory_notepad.gd +++ b/ui/components/inventory_notepad.gd @@ -8,7 +8,8 @@ const CONTENT_MARGIN_RIGHT := 18.0 const CONTENT_MARGIN_BOTTOM := 16.0 const HANDWRITTEN_FONT: Font = preload("res://ui/fonts/seattle_avenue.otf") const NOTEPAD_TEXTURE: Texture2D = preload("res://art/ui/ui_notepad.png") -const NOTEPAD_ART_SCALE := 1.1 +const NOTEPAD_CANONICAL_OVERSCAN: float = 1.2 +const NOTEPAD_ART_OFFSET := Vector2(10.0, 20.0) @export var title_text := "notes" @export var inset_content := true @@ -34,17 +35,7 @@ static func apply_handwritten_to(root: Control) -> void: func _draw() -> void: - var texture_size := NOTEPAD_TEXTURE.get_size() - var uniform_scale := minf( - size.x / texture_size.x, - size.y / texture_size.y, - ) * NOTEPAD_ART_SCALE - var rendered_size := texture_size * uniform_scale - var rendered_rect := Rect2( - (size - rendered_size) * 0.5, - rendered_size, - ) - draw_texture_rect(NOTEPAD_TEXTURE, rendered_rect, false) + draw_texture_rect(NOTEPAD_TEXTURE, get_art_rect(), false) var font: Font = HANDWRITTEN_FONT draw_string( @@ -58,6 +49,34 @@ func _draw() -> void: ) +func get_art_rect() -> Rect2: + return get_art_rect_for_host(size) + + +static func get_art_rect_for_host(host_size: Vector2) -> Rect2: + # Source pixels establish the artwork aspect ratio and sampling quality. The + # canonical host rect, not the source dimensions, owns its displayed size. + var source_size: Vector2 = NOTEPAD_TEXTURE.get_size() + if ( + host_size.x <= 0.0 + or host_size.y <= 0.0 + or source_size.x <= 0.0 + or source_size.y <= 0.0 + ): + return Rect2() + var fit_scale: float = minf( + host_size.x / source_size.x, + host_size.y / source_size.y, + ) + var rendered_size: Vector2 = ( + source_size * fit_scale * NOTEPAD_CANONICAL_OVERSCAN + ) + return Rect2( + (host_size - rendered_size) * 0.5 + NOTEPAD_ART_OFFSET, + rendered_size, + ) + + static func make_layout_style(with_content_margins: bool = true) -> StyleBoxFlat: var paper := StyleBoxFlat.new() paper.bg_color = Color.TRANSPARENT diff --git a/ui/fishing_shop.gd b/ui/fishing_shop.gd index 3b469c4..ba97e6d 100644 --- a/ui/fishing_shop.gd +++ b/ui/fishing_shop.gd @@ -4,10 +4,6 @@ extends Control const INPUT_OWNER: StringName = &"fishing_shop" const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop" const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd") -const FishCatchType = preload("res://fish/fish_catch.gd") -const FishInventoryType = preload("res://inventory/fish_inventory.gd") -const FishSaleResultType = preload("res://economy/fish_sale_result.gd") -const FishSaleServiceType = preload("res://economy/fish_sale_service.gd") const PlayerWalletType = preload("res://economy/player_wallet.gd") const FishingShopStockType = preload("res://economy/fishing_shop_stock.gd") const ItemCatalogType = preload("res://items/item_catalog.gd") @@ -24,11 +20,12 @@ const PlayerCoolerCapacityType = preload( const ShopInteractionType = preload( "res://world/fishing_shop_interaction.gd" ) -const FishBatchSelectionType = preload( - "res://ui/fish_batch_selection.gd" -) +const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") signal menu_visibility_changed(is_open: bool) +signal sell_fish_requested +signal shop_cooler_return_requested +signal shop_cooler_confirmation_cancel_requested enum CloseReason { USER, @@ -39,17 +36,12 @@ enum CloseReason { } @onready var _wallet_label: Label = %WalletLabel -@onready var _fish_sales: VBoxContainer = ( - $ShopPanel/Margin/Layout/Body/FishSales -) -@onready var _fish_list: ItemList = %FishList -@onready var _sales_title: Label = %SalesTitle -@onready var _fish_empty: Label = %FishEmpty -@onready var _fish_texture: TextureRect = %FishTexture -@onready var _fish_name: Label = %FishName -@onready var _fish_details: Label = %FishDetails -@onready var _selection_summary: Label = %SelectionSummary -@onready var _sell_button: Button = %SellButton +@onready var _shop_panel: PanelContainer = %ShopPanel +@onready var _shop_cooler_page: Control = %ShopCoolerPage +@onready var _shop_cooler_mount: Control = %ShopCoolerMount +@onready var _back_to_shop_button: Button = %BackToShopButton +@onready var _buy_mode_button: Button = %BuyModeButton +@onready var _sell_mode_button: Button = %SellModeButton @onready var _feedback: Label = %Feedback @onready var _reel_level: Label = %ReelLevel @onready var _reel_effect: Label = %ReelEffect @@ -64,15 +56,9 @@ enum CloseReason { @onready var _cooler_effect: Label = %CoolerEffect @onready var _cooler_cost: Label = %CoolerCost @onready var _cooler_purchase: Button = %CoolerPurchase -@onready var _confirmation: PanelContainer = %SaleConfirmation -@onready var _confirmation_text: Label = %ConfirmationText -@onready var _confirm_sale: Button = %ConfirmSale -@onready var _cancel_sale: Button = %CancelSale var _player: PlayerType -var _inventory: FishInventoryType var _wallet: PlayerWalletType -var _sale_service: FishSaleServiceType var _buyer: FishBuyerProfileType var _upgrades: PlayerFishingUpgradesType var _fishing_spot: FishingSpotType @@ -80,42 +66,93 @@ var _interaction: ShopInteractionType var _bag: PlayerBagType var _item_catalog: ItemCatalogType var _cooler_capacity: PlayerCoolerCapacityType -var _network_session: NetworkSession var _network_shop: NetworkShopService -var _fish_selection := FishBatchSelectionType.new() -var _confirmation_catch_ids: Array[StringName] = [] -var _confirmation_generation: int = -1 var _prior_movement_enabled: bool = true var _prior_camera_enabled: bool = true var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE var _snapshot_stored: bool = false var _mouse_snapshot_stored: bool = false -var _generation: int = 0 -var _selection_input_generation: int = 0 var _transaction_in_progress: bool = false var _closing: bool = false +var _cooler_page_active: bool = false +var _cooler_modal_open: bool = false func _ready() -> void: - # Catch selling belongs to the Pelican action in the Player Menu. Keep the - # legacy shop sales controls out of the active interface. - _fish_sales.hide() + UtilityPageStyleType.apply_page(self) + _apply_shop_styles() %CloseButton.pressed.connect(close_shop) - _fish_list.item_selected.connect(_on_fish_selected) - _fish_list.item_clicked.connect(_on_fish_clicked) - _sell_button.pressed.connect(_open_sale_confirmation) - _confirm_sale.pressed.connect(_on_confirm_sale) - _cancel_sale.pressed.connect(_close_sale_confirmation) + _buy_mode_button.pressed.connect(_focus_buy_page) + _sell_mode_button.pressed.connect(_request_shop_cooler) + _back_to_shop_button.pressed.connect(shop_cooler_return_requested.emit) _reel_purchase.pressed.connect(_purchase_reel_speed) _barrier_purchase.pressed.connect(_purchase_barrier_power) _cooler_purchase.pressed.connect(_purchase_cooler_capacity) +func _request_shop_cooler() -> void: + if _transaction_in_progress: + _feedback.text = "Finish the current transaction first." + return + if not _is_transaction_context_valid(): + _feedback.text = "The fishing shop is no longer available." + return + sell_fish_requested.emit() + + +func _focus_buy_page() -> void: + _feedback.text = "" + if _supplies_list.get_child_count() > 0: + var first_supply := _supplies_list.get_child(0) as Button + if first_supply != null and not first_supply.disabled: + first_supply.grab_focus() + return + _reel_purchase.grab_focus() + + +func _apply_shop_styles() -> void: + var shop_panel := $ShopPanel as PanelContainer + var shop_panel_style := UtilityPageStyleType.panel_style() + shop_panel_style.set_corner_radius_all(24) + shop_panel_style.content_margin_left = 0.0 + shop_panel_style.content_margin_top = 0.0 + shop_panel_style.content_margin_right = 0.0 + shop_panel_style.content_margin_bottom = 0.0 + shop_panel.add_theme_stylebox_override( + "panel", shop_panel_style + ) + for panel: PanelContainer in [ + $ShopPanel/Margin/Layout/Body/Upgrades/ReelCard, + $ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard, + $ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard, + ]: + panel.add_theme_stylebox_override( + "panel", UtilityPageStyleType.row_style() + ) + for button: BaseButton in [ + %CloseButton, + _buy_mode_button, + _sell_mode_button, + _reel_purchase, + _barrier_purchase, + _cooler_purchase, + _back_to_shop_button, + ]: + UtilityPageStyleType.apply_ocean_button(button) + _buy_mode_button.add_theme_stylebox_override( + "normal", + UtilityPageStyleType.ocean_button_style( + UtilityPageStyleType.OCEAN_SELECTED + ), + ) + _feedback.add_theme_color_override( + "font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY + ) + + func setup( player: PlayerType, - inventory: FishInventoryType, wallet: PlayerWalletType, - sale_service: FishSaleServiceType, buyer: FishBuyerProfileType, upgrades: PlayerFishingUpgradesType, fishing_spot: FishingSpotType, @@ -123,13 +160,10 @@ func setup( bag: PlayerBagType, item_catalog: ItemCatalogType, cooler_capacity: PlayerCoolerCapacityType, - network_session: NetworkSession, network_shop: NetworkShopService, ) -> void: _player = player - _inventory = inventory _wallet = wallet - _sale_service = sale_service _buyer = buyer _upgrades = upgrades _fishing_spot = fishing_spot @@ -137,7 +171,6 @@ func setup( _bag = bag _item_catalog = item_catalog _cooler_capacity = cooler_capacity - _network_session = network_session _network_shop = network_shop if ( _network_shop != null @@ -151,9 +184,6 @@ func setup( _network_shop.local_purchase_finished.connect( _on_network_purchase_finished ) - _fish_selection.clear() - if not _inventory.catches_changed.is_connected(_on_inventory_changed): - _inventory.catches_changed.connect(_on_inventory_changed) if not _wallet.balance_changed.is_connected(_on_wallet_changed): _wallet.balance_changed.connect(_on_wallet_changed) if not _upgrades.upgrades_changed.is_connected(_on_upgrades_changed): @@ -180,7 +210,6 @@ func open_shop() -> bool: or _buyer.id != MAIN_SHOP_BUYER_ID ): return false - _generation += 1 _closing = false _transaction_in_progress = ( _network_shop != null @@ -196,10 +225,10 @@ func open_shop() -> bool: _fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true) Input.mouse_mode = Input.MOUSE_MODE_VISIBLE _feedback.text = "" - _close_sale_confirmation() + deactivate_shop_cooler_page() show() _refresh_all() - %CloseButton.grab_focus() + _buy_mode_button.grab_focus() menu_visibility_changed.emit(true) return true @@ -207,13 +236,42 @@ func open_shop() -> bool: func consume_escape() -> bool: if not visible: return false - if _confirmation.visible: - _close_sale_confirmation() - else: - close_shop() + if _cooler_page_active: + if _cooler_modal_open: + shop_cooler_confirmation_cancel_requested.emit() + else: + shop_cooler_return_requested.emit() + return true + close_shop() return true +func get_shop_cooler_mount() -> Control: + return _shop_cooler_mount + + +func activate_shop_cooler_page() -> void: + _cooler_page_active = true + _shop_panel.hide() + _shop_cooler_page.show() + _back_to_shop_button.disabled = false + + +func deactivate_shop_cooler_page() -> void: + _cooler_page_active = false + _cooler_modal_open = false + _back_to_shop_button.disabled = false + _shop_cooler_page.hide() + _shop_panel.show() + if visible: + _buy_mode_button.grab_focus() + + +func set_shop_cooler_modal_open(is_open: bool) -> void: + _cooler_modal_open = is_open + _back_to_shop_button.disabled = is_open + + func close_shop( reason: CloseReason = CloseReason.USER, restore_controls: bool = true, @@ -221,10 +279,7 @@ func close_shop( if not visible: return _closing = true - _generation += 1 _transaction_in_progress = false - _close_sale_confirmation() - _fish_selection.clear() hide() get_viewport().gui_release_focus() if _fishing_spot != null and is_instance_valid(_fishing_spot): @@ -241,6 +296,7 @@ func close_shop( _apply_mouse_close_policy(reason) _closing = false menu_visibility_changed.emit(false) + deactivate_shop_cooler_page() func close_for_range_exit() -> void: @@ -268,8 +324,6 @@ func _refresh_all() -> void: if not is_node_ready(): return _refresh_wallet() - _refresh_fish_list() - _refresh_selected_fish() _refresh_upgrades() _refresh_supplies() _refresh_cooler_capacity() @@ -283,144 +337,6 @@ func _refresh_wallet() -> void: ) -func _refresh_fish_list() -> void: - _fish_list.clear() - var catches: Array[FishCatchType] = ( - _inventory.get_all_catches() if _inventory != null else [] - ) - _sales_title.text = "cooler %d / %d • full base value" % [ - catches.size(), - _cooler_capacity.get_capacity() if _cooler_capacity != null else 0, - ] - catches.sort_custom( - func(a: FishCatchType, b: FishCatchType) -> bool: - return a.catch_sequence > b.catch_sequence - ) - var visible_ids: Array[StringName] = [] - for fish_catch: FishCatchType in catches: - if fish_catch != null and fish_catch.is_valid(): - visible_ids.append(fish_catch.catch_id) - _fish_selection.set_visible_order(visible_ids) - for fish_catch: FishCatchType in catches: - if fish_catch == null or not fish_catch.is_valid(): - continue - var marker: String = "" - if _fish_selection.is_selected(fish_catch.catch_id): - marker += "✓ " - if fish_catch.catch_id == _fish_selection.get_focused_id(): - marker += "◆ " - if fish_catch.is_favorited: - marker += "★ " - var index: int = _fish_list.add_item( - "%s%s — %.2f lb" - % [marker, fish_catch.fish.display_name, fish_catch.weight_lb], - fish_catch.fish.display_texture - ) - _fish_list.set_item_metadata(index, String(fish_catch.catch_id)) - _fish_list.set_item_tooltip( - index, - "%s • %s" - % [ - fish_catch.fish.get_rarity_name(), - "favorited" if fish_catch.is_favorited else "available", - ] - ) - _fish_empty.visible = _fish_list.item_count == 0 - _fish_list.deselect_all() - for index: int in range(_fish_list.item_count): - var catch_id := StringName(str(_fish_list.get_item_metadata(index))) - if _fish_selection.is_selected(catch_id): - _fish_list.select(index, false) - - -func _refresh_selected_fish() -> void: - var fish_catch: FishCatchType = _get_selected_catch() - _fish_texture.texture = ( - fish_catch.fish.display_texture if fish_catch != null else null - ) - _fish_name.text = ( - fish_catch.fish.display_name if fish_catch != null else "select a fish" - ) - if fish_catch == null: - _fish_details.text = "choose one individual fish from your cooler." - _update_sale_summary() - return - var individual_preview: FishSaleResultType = ( - _sale_service.preview_batch([fish_catch.catch_id], _buyer) - if _sale_service != null - else null - ) - var offer: int = ( - individual_preview.payout if individual_preview != null else -1 - ) - _fish_details.text = ( - "%.2f lb • %s\nbase value: $%d\nmain-shop offer: $%d%s" - % [ - fish_catch.weight_lb, - fish_catch.fish.get_rarity_name(), - fish_catch.sale_value, - offer, - "\nfavorited fish cannot be sold." - if fish_catch.is_favorited - else "", - ] - ) - _update_sale_summary() - - -func _update_sale_summary() -> void: - var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() - var selected_count: int = selected_ids.size() - _sell_button.text = ( - "sell fish" - if selected_count <= 1 - else "sell %d fish" % selected_count - ) - if selected_count == 0: - _selection_summary.text = "no fish selected" - _sell_button.disabled = true - return - if _network_session != null and _network_session.is_joined_client(): - _selection_summary.text = ( - "1 fish selected" - if selected_count == 1 - else "%d fish selected" % selected_count - ) - _selection_summary.text += ( - "\nSell catches to the nearby pelicans." - ) - _sell_button.disabled = true - return - var preview: FishSaleResultType = ( - _sale_service.preview_batch(selected_ids, _buyer) - if _sale_service != null - else null - ) - _selection_summary.text = ( - "1 fish selected" - if selected_count == 1 - else "%d fish selected" % selected_count - ) - if ( - preview != null - and ( - preview.is_success() - or preview.status == FishSaleResultType.Status.FAVORITED - ) - ): - _selection_summary.text += "\ntotal offer: $%d" % preview.payout - if preview != null and preview.status == FishSaleResultType.Status.FAVORITED: - _selection_summary.text += ( - "\nfavorited fish must be removed from the selection." - ) - _sell_button.disabled = ( - preview == null - or not preview.is_success() - or _transaction_in_progress - or _closing - ) - - func _refresh_upgrades() -> void: if _upgrades == null: return @@ -516,6 +432,7 @@ func _refresh_supplies() -> void: if _network_shop == null or not _network_shop.can_request_purchase() else item.description ) + UtilityPageStyleType.apply_ocean_button(button) button.pressed.connect(_purchase_supply.bind(item_id)) _supplies_list.add_child(button) @@ -549,142 +466,6 @@ func _refresh_cooler_capacity() -> void: _cooler_purchase.text = "purchase" -func _on_fish_selected(index: int) -> void: - if index < 0 or index >= _fish_list.item_count: - return - var input_generation: int = _selection_input_generation - call_deferred( - "_apply_keyboard_fish_selection", - index, - input_generation - ) - - -func _apply_keyboard_fish_selection( - index: int, - input_generation: int, -) -> void: - if ( - input_generation != _selection_input_generation - or index < 0 - or index >= _fish_list.item_count - ): - return - var catch_id := StringName(str(_fish_list.get_item_metadata(index))) - _fish_selection.select_only(catch_id) - _feedback.text = "" - _refresh_fish_list() - _refresh_selected_fish() - - -func _on_fish_clicked( - index: int, - _position: Vector2, - mouse_button_index: int, -) -> void: - if ( - mouse_button_index != MOUSE_BUTTON_LEFT - or index < 0 - or index >= _fish_list.item_count - ): - return - _selection_input_generation += 1 - var catch_id := StringName(str(_fish_list.get_item_metadata(index))) - _fish_selection.apply_click( - catch_id, - Input.is_key_pressed(KEY_CTRL), - Input.is_key_pressed(KEY_SHIFT) - ) - _feedback.text = "" - _refresh_fish_list() - _refresh_selected_fish() - - -func _open_sale_confirmation() -> void: - if _network_session != null and _network_session.is_joined_client(): - _feedback.text = "Sell catches to the nearby pelicans." - return - var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() - if not _is_transaction_context_valid() or selected_ids.is_empty(): - _feedback.text = "the fish selection is no longer available." - _refresh_all() - return - var preview: FishSaleResultType = _sale_service.preview_batch( - selected_ids, - _buyer - ) - if not preview.is_success(): - _feedback.text = ( - "favorited fish cannot be sold. " - + "remove them from the selection first." - if preview.status == FishSaleResultType.Status.FAVORITED - else preview.get_message() - ) - return - _confirmation_catch_ids = selected_ids.duplicate() - _confirmation_generation = _generation - _confirmation_text.text = ( - "sell %d fish to the fishing shop for $%d?" - % [preview.fish_count, preview.payout] - ) - _confirmation.show() - _cancel_sale.grab_focus() - - -func _close_sale_confirmation() -> void: - _confirmation_catch_ids.clear() - _confirmation_generation = -1 - _confirmation.hide() - - -func _on_confirm_sale() -> void: - if ( - _transaction_in_progress - or ( - _network_session != null - and _network_session.is_joined_client() - ) - ): - return - var transaction_generation: int = _generation - var catch_ids: Array[StringName] = _confirmation_catch_ids.duplicate() - var confirmation_generation: int = _confirmation_generation - _close_sale_confirmation() - if ( - catch_ids.is_empty() - or not _is_transaction_context_valid() - or transaction_generation != _generation - or confirmation_generation != transaction_generation - or _buyer.id != MAIN_SHOP_BUYER_ID - ): - _feedback.text = "unable to complete sale." - return - _transaction_in_progress = true - var result: FishSaleResultType = _sale_service.sell_batch( - catch_ids, - _buyer - ) - _transaction_in_progress = false - if ( - transaction_generation != _generation - or not visible - ): - return - if result.is_success(): - _feedback.text = ( - "fish sold for $%d." - if result.fish_count == 1 - else "%d fish sold for $%d." % [ - result.fish_count, - result.payout, - ] - ) - _fish_selection.remove_ids(catch_ids) - else: - _feedback.text = result.get_message() - _refresh_all() - - func _purchase_reel_speed() -> void: _purchase_upgrade(true) @@ -777,13 +558,6 @@ func _on_network_purchase_finished( _refresh_all() -func _get_selected_catch() -> FishCatchType: - var focused_id: StringName = _fish_selection.get_focused_id() - if _inventory == null or focused_id.is_empty(): - return null - return _inventory.get_catch_by_id(focused_id) - - func _is_transaction_context_valid() -> bool: return ( visible @@ -798,27 +572,6 @@ func _is_transaction_context_valid() -> bool: ) -func _on_inventory_changed() -> void: - _refresh_fish_list() - if not _confirmation_catch_ids.is_empty(): - var preview: FishSaleResultType = _sale_service.preview_batch( - _confirmation_catch_ids, - _buyer - ) - if ( - _confirmation_generation != _generation - or not preview.is_success() - ): - _close_sale_confirmation() - _feedback.text = ( - "favorited fish cannot be sold. " - + "remove them from the selection first." - if preview.status == FishSaleResultType.Status.FAVORITED - else preview.get_message() - ) - _refresh_selected_fish() - - func _on_wallet_changed(_balance: int, _delta: int) -> void: _refresh_wallet() _refresh_upgrades() @@ -839,7 +592,6 @@ func _on_bag_changed() -> void: func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void: _refresh_cooler_capacity() - _refresh_fish_list() func _apply_mouse_close_policy(reason: CloseReason) -> void: diff --git a/ui/fishing_shop.tscn b/ui/fishing_shop.tscn index 793b8e2..f4dd2a3 100644 --- a/ui/fishing_shop.tscn +++ b/ui/fishing_shop.tscn @@ -19,9 +19,10 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 theme = ExtResource("2_theme") +theme_override_font_sizes/font_size = 20 script = ExtResource("1_script") -[node name="Dimmer" type="ColorRect" parent="."] +[node name="InputBlocker" type="Control" parent="."] layout_mode = 1 anchors_preset = 15 anchor_right = 1.0 @@ -29,28 +30,60 @@ anchor_bottom = 1.0 grow_horizontal = 2 grow_vertical = 2 mouse_filter = 0 -color = Color(0.015, 0.02, 0.03, 0.64) + +[node name="ShopCoolerPage" type="Control" parent="."] +unique_name_in_owner = true +visible = false +z_index = 90 +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="ShopCoolerMount" type="Control" parent="ShopCoolerPage"] +unique_name_in_owner = true +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="BackToShopButton" type="Button" parent="ShopCoolerPage"] +unique_name_in_owner = true +z_index = 110 +layout_mode = 0 +offset_left = 1052.0 +offset_top = 54.0 +offset_right = 1218.0 +offset_bottom = 104.0 +text = "back to shop" [node name="ShopPanel" type="PanelContainer" parent="."] +unique_name_in_owner = true layout_mode = 1 anchors_preset = 8 anchor_left = 0.5 anchor_top = 0.5 anchor_right = 0.5 anchor_bottom = 0.5 -offset_left = -560.0 -offset_top = -320.0 -offset_right = 560.0 -offset_bottom = 320.0 +offset_left = -490.0 +offset_top = -300.0 +offset_right = 490.0 +offset_bottom = 300.0 grow_horizontal = 2 grow_vertical = 2 [node name="Margin" type="MarginContainer" parent="ShopPanel"] layout_mode = 2 theme_override_constants/margin_left = 16 -theme_override_constants/margin_top = 14 +theme_override_constants/margin_top = 12 theme_override_constants/margin_right = 16 -theme_override_constants/margin_bottom = 14 +theme_override_constants/margin_bottom = 12 [node name="Layout" type="VBoxContainer" parent="ShopPanel/Margin"] layout_mode = 2 @@ -64,7 +97,7 @@ theme_override_constants/separation = 8 layout_mode = 2 size_flags_horizontal = 3 text = "fishing shop" -theme_override_font_sizes/font_size = 36 +theme_override_font_sizes/font_size = 32 theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) [node name="CoinIcon" type="TextureRect" parent="ShopPanel/Margin/Layout/Header"] @@ -84,7 +117,24 @@ text = "wallet: $0" unique_name_in_owner = true layout_mode = 2 text = "close" -custom_minimum_size = Vector2(110, 58) +custom_minimum_size = Vector2(104, 48) + +[node name="ModeTabs" type="HBoxContainer" parent="ShopPanel/Margin/Layout"] +layout_mode = 2 +alignment = 1 +theme_override_constants/separation = 10 + +[node name="BuyModeButton" type="Button" parent="ShopPanel/Margin/Layout/ModeTabs"] +unique_name_in_owner = true +custom_minimum_size = Vector2(180, 42) +layout_mode = 2 +text = "buy gear & supplies" + +[node name="SellModeButton" type="Button" parent="ShopPanel/Margin/Layout/ModeTabs"] +unique_name_in_owner = true +custom_minimum_size = Vector2(180, 42) +layout_mode = 2 +text = "sell fish" [node name="Feedback" type="Label" parent="ShopPanel/Margin/Layout"] unique_name_in_owner = true @@ -98,123 +148,27 @@ layout_mode = 2 size_flags_vertical = 3 theme_override_constants/separation = 12 -[node name="FishSales" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] -custom_minimum_size = Vector2(420, 0) -layout_mode = 2 -size_flags_horizontal = 3 -theme_override_constants/separation = 6 - -[node name="SalesTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales"] -unique_name_in_owner = true -layout_mode = 2 -text = "sell individual cooler fish • full base value" -theme_override_font_sizes/font_size = 26 - -[node name="SalesBody" type="HSplitContainer" parent="ShopPanel/Margin/Layout/Body/FishSales"] -layout_mode = 2 -size_flags_vertical = 3 -split_offset = 220 -theme_override_constants/separation = 8 - -[node name="ListPanel" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"] -custom_minimum_size = Vector2(205, 0) -layout_mode = 2 - -[node name="ListMargin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel"] -layout_mode = 2 -theme_override_constants/margin_left = 7 -theme_override_constants/margin_top = 7 -theme_override_constants/margin_right = 7 -theme_override_constants/margin_bottom = 7 - -[node name="ListStack" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin"] -layout_mode = 2 - -[node name="FishEmpty" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"] -unique_name_in_owner = true -layout_mode = 2 -text = "your cooler is empty." -horizontal_alignment = 1 - -[node name="FishList" type="ItemList" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/ListPanel/ListMargin/ListStack"] -unique_name_in_owner = true -layout_mode = 2 -size_flags_vertical = 3 -fixed_icon_size = Vector2i(48, 36) -icon_mode = 1 -select_mode = 1 -same_column_width = true - -[node name="FishDetail" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody"] -custom_minimum_size = Vector2(185, 0) -layout_mode = 2 - -[node name="DetailMargin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail"] -layout_mode = 2 -theme_override_constants/margin_left = 9 -theme_override_constants/margin_top = 9 -theme_override_constants/margin_right = 9 -theme_override_constants/margin_bottom = 9 - -[node name="DetailStack" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin"] -layout_mode = 2 -theme_override_constants/separation = 6 - -[node name="FishTexture" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] -unique_name_in_owner = true -custom_minimum_size = Vector2(160, 90) -layout_mode = 2 -expand_mode = 1 -stretch_mode = 5 -texture_filter = 1 - -[node name="FishName" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -text = "select a fish" -horizontal_alignment = 1 -theme_override_font_sizes/font_size = 29 - -[node name="FishDetails" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -text = "choose one individual fish from your cooler." -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="SelectionSummary" type="Label" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -text = "no fish selected" -horizontal_alignment = 1 -autowrap_mode = 2 -theme_override_colors/font_color = Color(0.208, 0.725, 0.78, 1) - -[node name="SellButton" type="Button" parent="ShopPanel/Margin/Layout/Body/FishSales/SalesBody/FishDetail/DetailMargin/DetailStack"] -unique_name_in_owner = true -layout_mode = 2 -disabled = true -text = "sell selected" - [node name="Upgrades" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] custom_minimum_size = Vector2(270, 0) layout_mode = 2 +size_flags_horizontal = 3 theme_override_constants/separation = 8 [node name="UpgradeTitle" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades"] layout_mode = 2 text = "fishing upgrades" -theme_override_font_sizes/font_size = 26 +theme_override_font_sizes/font_size = 23 [node name="Supplies" type="VBoxContainer" parent="ShopPanel/Margin/Layout/Body"] custom_minimum_size = Vector2(210, 0) layout_mode = 2 +size_flags_horizontal = 3 theme_override_constants/separation = 6 [node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"] layout_mode = 2 text = "supplies" -theme_override_font_sizes/font_size = 26 +theme_override_font_sizes/font_size = 23 [node name="Scroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Supplies"] layout_mode = 2 @@ -233,16 +187,16 @@ layout_mode = 2 [node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard"] layout_mode = 2 theme_override_constants/margin_left = 9 -theme_override_constants/margin_top = 9 +theme_override_constants/margin_top = 7 theme_override_constants/margin_right = 9 -theme_override_constants/margin_bottom = 9 +theme_override_constants/margin_bottom = 7 [node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin"] layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"] -custom_minimum_size = Vector2(48, 48) +custom_minimum_size = Vector2(44, 44) layout_mode = 2 texture = ExtResource("3_reel") expand_mode = 1 @@ -256,22 +210,25 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] layout_mode = 2 text = "reel speed" -theme_override_font_sizes/font_size = 26 +theme_override_font_sizes/font_size = 22 [node name="ReelLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "level 0" +theme_override_font_sizes/font_size = 18 [node name="ReelEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "1.00× → 1.10×" +theme_override_font_sizes/font_size = 18 [node name="ReelCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "$50" +theme_override_font_sizes/font_size = 18 [node name="ReelPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/ReelCard/Margin/Row"] unique_name_in_owner = true @@ -284,16 +241,16 @@ layout_mode = 2 [node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard"] layout_mode = 2 theme_override_constants/margin_left = 9 -theme_override_constants/margin_top = 9 +theme_override_constants/margin_top = 7 theme_override_constants/margin_right = 9 -theme_override_constants/margin_bottom = 9 +theme_override_constants/margin_bottom = 7 [node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin"] layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"] -custom_minimum_size = Vector2(48, 48) +custom_minimum_size = Vector2(44, 44) layout_mode = 2 texture = ExtResource("4_barrier") expand_mode = 1 @@ -307,22 +264,25 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] layout_mode = 2 text = "barrier power" -theme_override_font_sizes/font_size = 26 +theme_override_font_sizes/font_size = 22 [node name="BarrierLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "level 0" +theme_override_font_sizes/font_size = 18 [node name="BarrierEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "1 damage → 2 damage" +theme_override_font_sizes/font_size = 18 [node name="BarrierCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "$100" +theme_override_font_sizes/font_size = 18 [node name="BarrierPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/BarrierCard/Margin/Row"] unique_name_in_owner = true @@ -335,16 +295,16 @@ layout_mode = 2 [node name="Margin" type="MarginContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard"] layout_mode = 2 theme_override_constants/margin_left = 9 -theme_override_constants/margin_top = 9 +theme_override_constants/margin_top = 7 theme_override_constants/margin_right = 9 -theme_override_constants/margin_bottom = 9 +theme_override_constants/margin_bottom = 7 [node name="Row" type="HBoxContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin"] layout_mode = 2 theme_override_constants/separation = 9 [node name="Icon" type="TextureRect" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] -custom_minimum_size = Vector2(48, 48) +custom_minimum_size = Vector2(44, 44) layout_mode = 2 texture = ExtResource("6_cooler") expand_mode = 1 @@ -358,74 +318,27 @@ size_flags_horizontal = 3 [node name="Name" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] layout_mode = 2 text = "cooler capacity" -theme_override_font_sizes/font_size = 26 +theme_override_font_sizes/font_size = 22 [node name="CoolerLevel" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "level 0" +theme_override_font_sizes/font_size = 18 [node name="CoolerEffect" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "12 → 18 fish" +theme_override_font_sizes/font_size = 18 [node name="CoolerCost" type="Label" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row/Data"] unique_name_in_owner = true layout_mode = 2 text = "$75" +theme_override_font_sizes/font_size = 18 [node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/CoolerCard/Margin/Row"] unique_name_in_owner = true layout_mode = 2 text = "purchase" - -[node name="SaleConfirmation" type="PanelContainer" parent="."] -unique_name_in_owner = true -visible = false -z_index = 10 -layout_mode = 1 -anchors_preset = 8 -anchor_left = 0.5 -anchor_top = 0.5 -anchor_right = 0.5 -anchor_bottom = 0.5 -offset_left = -320.0 -offset_top = -150.0 -offset_right = 320.0 -offset_bottom = 150.0 -grow_horizontal = 2 -grow_vertical = 2 - -[node name="Margin" type="MarginContainer" parent="SaleConfirmation"] -layout_mode = 2 -theme_override_constants/margin_left = 18 -theme_override_constants/margin_top = 16 -theme_override_constants/margin_right = 18 -theme_override_constants/margin_bottom = 16 - -[node name="Stack" type="VBoxContainer" parent="SaleConfirmation/Margin"] -layout_mode = 2 -theme_override_constants/separation = 12 - -[node name="ConfirmationText" type="Label" parent="SaleConfirmation/Margin/Stack"] -unique_name_in_owner = true -layout_mode = 2 -autowrap_mode = 2 -horizontal_alignment = 1 - -[node name="Buttons" type="HBoxContainer" parent="SaleConfirmation/Margin/Stack"] -layout_mode = 2 -alignment = 1 -theme_override_constants/separation = 8 - -[node name="ConfirmSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"] -unique_name_in_owner = true -layout_mode = 2 -text = "sell fish" -theme_type_variation = &"DangerButton" - -[node name="CancelSale" type="Button" parent="SaleConfirmation/Margin/Stack/Buttons"] -unique_name_in_owner = true -layout_mode = 2 -text = "cancel" diff --git a/ui/game_ui.gd b/ui/game_ui.gd index af0c4b4..eef3fd9 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -41,6 +41,7 @@ signal crisp_reset_focus_requested signal interactive_pointer_ui_changed(is_open: bool) signal passive_pointer_ui_changed(is_enabled: bool) signal player_menu_backdrop_visibility_changed(is_visible: bool) +signal shop_backdrop_visibility_changed(is_visible: bool) @onready var _status_label: Label = %StatusLabel @onready var _gameplay_transient_hud: Control = %GameplayTransientHUD @@ -65,7 +66,7 @@ signal player_menu_backdrop_visibility_changed(is_visible: bool) $UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel ) @onready var _pause_settings_panel: SettingsPanelType = ( - $UIRoot/PauseMenu/ResponsivePauseStage/PausePresentationScaleRoot/SettingsCenter/SettingsPanel + $UIRoot/CanonicalStage/PauseMenu/ResponsivePauseStage/PausePresentationScaleRoot/SettingsCenter/SettingsPanel ) var _showcase_active: bool = false @@ -77,6 +78,8 @@ var _shop_open: bool = false var _chat_input_open: bool = false var _player_menu_hotbar_visible: bool = false var _item_effects: PlayerItemEffectsType +var _main_shop_buyer: FishBuyerProfileType +var _shop_interaction: ShopInteractionType func _ready() -> void: @@ -151,6 +154,9 @@ func setup( _player_menu.inventory_hotbar_context_changed.connect( _on_inventory_hotbar_context_changed ) + _player_menu.shop_cooler_modal_changed.connect( + _on_shop_cooler_modal_changed + ) _player_menu.setup( player, inventory, @@ -174,9 +180,7 @@ func setup( _hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot) _fishing_shop.setup( player, - inventory, wallet, - sale_service, main_shop_buyer, fishing_upgrades, fishing_spot, @@ -184,10 +188,18 @@ func setup( bag, item_catalog, cooler_capacity, - network_session, - network_shop_service + network_shop_service, ) _fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed) + _fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested) + _fishing_shop.shop_cooler_return_requested.connect( + _on_shop_cooler_return_requested + ) + _fishing_shop.shop_cooler_confirmation_cancel_requested.connect( + _player_menu.cancel_shop_cooler_confirmation + ) + _main_shop_buyer = main_shop_buyer + _shop_interaction = shop_interaction func setup_data_and_identity( @@ -554,6 +566,9 @@ func _on_player_menu_exit_started() -> void: func _on_shop_visibility_changed(is_open: bool) -> void: _shop_open = is_open + shop_backdrop_visibility_changed.emit(is_open) + if not is_open and _player_menu.is_shop_cooler_mounted(): + _player_menu.unmount_shop_cooler() _refresh_chat_availability() _refresh_hotbar_visibility() _hotbar_ui.set_gameplay_input_enabled( @@ -567,6 +582,33 @@ func _on_shop_visibility_changed(is_open: bool) -> void: _emit_interactive_pointer_ui_changed() +func _on_shop_sell_fish_requested() -> void: + if ( + not _fishing_shop.visible + or _main_shop_buyer == null + or not _main_shop_buyer.is_valid() + or _shop_interaction == null + or not _shop_interaction.is_local_player_in_range() + or _fishing_spot == null + or not _fishing_spot.is_ready_for_shop_transaction() + ): + return + if _player_menu.mount_shop_cooler( + _fishing_shop.get_shop_cooler_mount(), + _main_shop_buyer, + ): + _fishing_shop.activate_shop_cooler_page() + + +func _on_shop_cooler_return_requested() -> void: + _player_menu.unmount_shop_cooler() + _fishing_shop.deactivate_shop_cooler_page() + + +func _on_shop_cooler_modal_changed(is_open: bool) -> void: + _fishing_shop.set_shop_cooler_modal_open(is_open) + + func _on_inventory_hotbar_context_changed(show_hotbar: bool) -> void: var context_changed: bool = _player_menu_hotbar_visible != show_hotbar _player_menu_hotbar_visible = show_hotbar diff --git a/ui/game_ui.tscn b/ui/game_ui.tscn index 2b2089a..17e14a7 100644 --- a/ui/game_ui.tscn +++ b/ui/game_ui.tscn @@ -41,7 +41,11 @@ script = ExtResource("1_ui") unique_name_in_owner = true mouse_filter = 2 -[node name="GameplayTransientHUD" type="Control" parent="UIRoot"] +[node name="CanonicalStage" type="Control" parent="UIRoot"] +unique_name_in_owner = true +mouse_filter = 2 + +[node name="GameplayTransientHUD" type="Control" parent="UIRoot/CanonicalStage"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -51,7 +55,7 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="FishingPanel" type="PanelContainer" parent="UIRoot/GameplayTransientHUD"] +[node name="FishingPanel" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 60 @@ -69,33 +73,33 @@ grow_vertical = 0 mouse_filter = 2 theme = ExtResource("3_theme") -[node name="MarginContainer" type="MarginContainer" parent="UIRoot/GameplayTransientHUD/FishingPanel"] +[node name="MarginContainer" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel"] theme_override_constants/margin_left = 14 theme_override_constants/margin_top = 8 theme_override_constants/margin_right = 14 theme_override_constants/margin_bottom = 8 -[node name="VBoxContainer" type="VBoxContainer" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer"] +[node name="VBoxContainer" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer"] theme_override_constants/separation = 4 -[node name="StatusLabel" type="Label" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="StatusLabel" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false text = "" horizontal_alignment = 1 theme_override_font_sizes/font_size = 16 -[node name="ShowcaseDetails" type="Label" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="ShowcaseDetails" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false horizontal_alignment = 1 -[node name="CatchTrack" type="Control" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="CatchTrack" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false custom_minimum_size = Vector2(0, 20) -[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -109,7 +113,7 @@ theme_override_styles/fill = SubResource("StyleBox_catch_fill") value = 0.0 show_percentage = false -[node name="RedChaseProgress" type="ProgressBar" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="RedChaseProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -123,7 +127,7 @@ theme_override_styles/fill = SubResource("StyleBox_chase_fill") value = 0.0 show_percentage = false -[node name="BarrierMarkers" type="Control" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] +[node name="BarrierMarkers" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer/CatchTrack"] unique_name_in_owner = true layout_mode = 1 anchors_preset = 15 @@ -133,23 +137,23 @@ grow_horizontal = 2 grow_vertical = 2 mouse_filter = 2 -[node name="BarrierSummary" type="Label" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="BarrierSummary" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false horizontal_alignment = 1 -[node name="BarrierHealth" type="Label" parent="UIRoot/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] +[node name="BarrierHealth" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/FishingPanel/MarginContainer/VBoxContainer"] unique_name_in_owner = true visible = false horizontal_alignment = 1 -[node name="PlayerMenu" parent="UIRoot" instance=ExtResource("2_menu")] +[node name="PlayerMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("2_menu")] unique_name_in_owner = true -[node name="Hotbar" parent="UIRoot" instance=ExtResource("7_hotbar")] +[node name="Hotbar" parent="UIRoot/CanonicalStage" instance=ExtResource("7_hotbar")] unique_name_in_owner = true -[node name="ShopPrompt" type="PanelContainer" parent="UIRoot/GameplayTransientHUD"] +[node name="ShopPrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 55 @@ -167,22 +171,22 @@ grow_vertical = 0 mouse_filter = 2 theme = ExtResource("3_theme") -[node name="Margin" type="MarginContainer" parent="UIRoot/GameplayTransientHUD/ShopPrompt"] +[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt"] layout_mode = 2 theme_override_constants/margin_left = 12 theme_override_constants/margin_top = 7 theme_override_constants/margin_right = 12 theme_override_constants/margin_bottom = 7 -[node name="Label" type="Label" parent="UIRoot/GameplayTransientHUD/ShopPrompt/Margin"] +[node name="Label" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/ShopPrompt/Margin"] layout_mode = 2 text = "press e to open fishing shop" horizontal_alignment = 1 -[node name="FishingShop" parent="UIRoot" instance=ExtResource("8_shop")] +[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")] unique_name_in_owner = true -[node name="EffectStatus" type="Label" parent="UIRoot/GameplayTransientHUD"] +[node name="EffectStatus" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD"] unique_name_in_owner = true visible = false z_index = 54 @@ -223,5 +227,5 @@ script = ExtResource("4_fade") [node name="TitleScreen" parent="UIRoot" instance=ExtResource("5_title")] unique_name_in_owner = true -[node name="PauseMenu" parent="UIRoot" instance=ExtResource("6_pause")] +[node name="PauseMenu" parent="UIRoot/CanonicalStage" instance=ExtResource("6_pause")] unique_name_in_owner = true diff --git a/ui/pause_menu.gd b/ui/pause_menu.gd index 15621e9..f13003e 100644 --- a/ui/pause_menu.gd +++ b/ui/pause_menu.gd @@ -6,7 +6,6 @@ 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 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( @@ -48,7 +47,6 @@ enum CloseReason { @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 ) @@ -75,8 +73,6 @@ 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 _pending_join_endpoint: String = "" @@ -98,7 +94,6 @@ func _ready() -> void: ) _join_game_page.join_requested.connect(_on_join_game_requested) _join_game_page.back_requested.connect(_close_join_game) - _dim_background.color.a = 0.0 _confirmation_page.hide_page() resized.connect(_update_responsive_pause_stage) call_deferred("_update_responsive_pause_stage") @@ -144,7 +139,6 @@ func open_menu() -> void: _action_in_progress = false _root_page.hide_page() show() - _fade_backdrop(true) _update_responsive_pause_stage() _begin_root_entry(false) menu_visibility_changed.emit(true) @@ -159,7 +153,6 @@ func resume() -> void: or _confirmation_action != ConfirmationAction.NONE ): return - _fade_backdrop(false) _begin_root_exit(_finish_user_close) @@ -268,7 +261,6 @@ func _finish_open_settings() -> void: _settings_panel.open_panel( _settings_manager, SettingsPanelType.PresentationMode.GAMEPLAY_MODAL, - true, true ) @@ -493,9 +485,6 @@ func _finish_close(reason: CloseReason, restore_controls: bool) -> void: _closing_menu = false _root_transition_generation += 1 _root_transition_active = false - _cancel_backdrop_fade() - _dim_background.color.a = 0.0 - _dim_background.hide() _settings_panel.hide() _join_game_page.close_page() _root_page.hide_page() @@ -519,45 +508,6 @@ 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 _update_responsive_pause_stage() -> void: if not is_node_ready(): return diff --git a/ui/pause_menu.tscn b/ui/pause_menu.tscn index 4bdaa9c..2477f23 100644 --- a/ui/pause_menu.tscn +++ b/ui/pause_menu.tscn @@ -25,17 +25,6 @@ mouse_filter = 0 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 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 0 -color = Color(0.025, 0.12, 0.19, 0.68) - [node name="TransitionBubbleLayer" type="Control" parent="."] unique_name_in_owner = true z_index = 1 diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 821823d..0687e9d 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -79,10 +79,12 @@ const COOLER_RARITY_UNCOMMON := Color("64c87c") const COOLER_RARITY_RARE := Color("6098dd") const COOLER_RARITY_EPIC := Color("a979cf") const COOLER_RARITY_LEGENDARY := Color("db78a7") +const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop" signal menu_visibility_changed(is_open: bool) signal inventory_hotbar_context_changed(show_hotbar: bool) signal menu_exit_started +signal shop_cooler_modal_changed(is_open: bool) enum Section { COOLER, @@ -131,6 +133,7 @@ const INVENTORY_HEADER_INSET := Vector2(24.0, 20.0) const INVENTORY_HEADER_HEIGHT := 40.0 const INVENTORY_MAIN_CORNER_RADIUS: int = 58 const INVENTORY_INNER_CORNER_RADIUS: int = 45 +const SALE_CONFIRMATION_SIZE := Vector2(520.0, 190.0) @onready var _navigation_cluster: BubbleClusterType = %NavigationCluster @onready var _presentation_scale_root: Control = %PlayerMenuPresentationScaleRoot @@ -276,6 +279,12 @@ var _reservations: PlayerAssetReservationService var _network_profile_service: NetworkProfileService var _network_player_list: NetworkPlayerListService var _default_buyer: FishBuyerProfileType +var _sale_buyer_override: FishBuyerProfileType +var _shop_cooler_context_active: bool = false +var _cooler_original_parent: Node +var _cooler_original_index: int = -1 +var _confirmation_original_parent: Node +var _confirmation_original_index: int = -1 var _catalog: FishPoolType var _fishing_spot: FishingSpotType var _bag: PlayerBagType @@ -336,6 +345,10 @@ var _logbook_page_tween: Tween func _ready() -> void: _configure_inventory_transition_group() + _cooler_original_parent = _cooler_page.get_parent() + _cooler_original_index = _cooler_page.get_index() + _confirmation_original_parent = _sale_confirmation.get_parent() + _confirmation_original_index = _sale_confirmation.get_index() _inventory_sub_tabs.move_child(_tackle_sub_tab, 1) _inventory_sub_tabs.move_child(_bag_sub_tab, 2) _inventory_tab.pressed.connect(_show_last_inventory_section) @@ -652,6 +665,7 @@ func consume_escape() -> bool: func open_menu() -> void: if ( visible + or _shop_cooler_context_active or _player == null or _fishing_spot == null or not _fishing_spot.can_open_player_menu() @@ -678,6 +692,99 @@ func open_menu() -> void: menu_visibility_changed.emit(true) +func mount_shop_cooler( + host: Control, + buyer: FishBuyerProfileType, +) -> bool: + if ( + visible + or _shop_cooler_context_active + or host == null + or not is_instance_valid(host) + or buyer == null + or not buyer.is_valid() + or buyer.id != MAIN_SHOP_BUYER_ID + ): + return false + _menu_generation += 1 + _cancel_page_tween() + _transitioning = false + _page_transitioning = false + _sale_buyer_override = buyer + _shop_cooler_context_active = true + _current_section = Section.COOLER + _last_inventory_section = Section.COOLER + _cooler_page.reparent(host, false) + _sale_confirmation.reparent(host, false) + _cooler_page.position = Vector2.ZERO + _cooler_page.size = DESKTOP_REFERENCE_SIZE + _cooler_page.scale = Vector2.ONE + _cooler_page.modulate = Color.WHITE + _cooler_page.visible = true + _sale_confirmation.visible = false + _update_shell_layout() + _refresh_inventory() + _set_content_interactive(true) + _update_cooler_water_mask() + set_process(true) + call_deferred("_focus_shop_cooler") + return true + + +func unmount_shop_cooler() -> void: + if not _shop_cooler_context_active: + return + _close_sale_confirmation() + _set_content_interactive(false) + _cooler_page.visible = false + _cooler_page.reparent(_cooler_original_parent, false) + _cooler_original_parent.move_child( + _cooler_page, + mini(_cooler_original_index, _cooler_original_parent.get_child_count() - 1), + ) + _sale_confirmation.reparent(_confirmation_original_parent, false) + _confirmation_original_parent.move_child( + _sale_confirmation, + mini( + _confirmation_original_index, + _confirmation_original_parent.get_child_count() - 1, + ), + ) + _sale_buyer_override = null + _shop_cooler_context_active = false + _menu_generation += 1 + if not visible: + set_process(false) + + +func is_shop_cooler_mounted() -> bool: + return _shop_cooler_context_active + + +func cancel_shop_cooler_confirmation() -> void: + if _shop_cooler_context_active and _sale_confirmation.visible: + _close_sale_confirmation() + + +func _focus_shop_cooler() -> void: + if not _shop_cooler_context_active or not _cooler_page.is_visible_in_tree(): + return + var focused_node := _fish_nodes.get( + _fish_selection.get_focused_id() + ) as CoolerFishSpriteType + if focused_node != null and focused_node.focus_mode != Control.FOCUS_NONE: + focused_node.grab_focus() + return + for fish_catch: FishCatchType in _sorted_catches: + var fish_node := _fish_nodes.get( + fish_catch.catch_id + ) as CoolerFishSpriteType + if fish_node != null and fish_node.focus_mode != Control.FOCUS_NONE: + fish_node.grab_focus() + return + _cooler_sort_option.grab_focus() + + func close_menu( reason: CloseReason = CloseReason.USER, restore_controls: bool = true, @@ -886,6 +993,9 @@ func _show_last_inventory_section() -> void: func _focus_current_section() -> void: + if _shop_cooler_context_active: + _focus_shop_cooler() + return if _current_section == Section.COOLER: _cooler_sub_tab.grab_focus() elif _current_section == Section.BAG: @@ -903,12 +1013,15 @@ func _focus_current_section() -> void: func _process(delta: float) -> void: - if visible: + if visible or _shop_cooler_context_active: _motion_elapsed += delta - _navigation_cluster.advance_motion(delta) - _apply_navigation_selection_presentation() + if visible: + _navigation_cluster.advance_motion(delta) + _apply_navigation_selection_presentation() for fish_node: CoolerFishSpriteType in _fish_nodes.values(): fish_node.advance_presentation(delta, _motion_elapsed) + if not visible: + return var bag_motion_enabled: bool = ( not _bag_drag_active and not get_viewport().gui_is_dragging() ) @@ -1040,6 +1153,20 @@ func _apply_inventory_styles() -> void: "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY, ) + _sale_confirmation.add_theme_stylebox_override( + "panel", + UtilityPageStyle.panel_style(), + ) + _confirmation_message.add_theme_font_override( + "font", + UtilityPageStyle.TuffyFont, + ) + _confirmation_message.add_theme_color_override( + "font_color", + UtilityPageStyle.OCEAN_TEXT_PRIMARY, + ) + UtilityPageStyle.apply_ocean_button(_confirm_sale_button) + UtilityPageStyle.apply_ocean_button(_cancel_sale_button) func _set_bag_view(view: BagView) -> void: @@ -1531,11 +1658,9 @@ func _update_shell_layout() -> void: _players_page.set_anchors_preset(Control.PRESET_TOP_LEFT) _players_page.position = Vector2.ZERO _players_page.size = reference_size + _sale_confirmation.size = SALE_CONFIRMATION_SIZE _sale_confirmation.position = ( - Vector2(70.0, 128.0) if compact else Vector2(320.0, 200.0) - ) - _sale_confirmation.size = ( - Vector2(500.0, 250.0) if compact else Vector2(640.0, 320.0) + (reference_size - SALE_CONFIRMATION_SIZE) * 0.5 ) _cooler_host.custom_minimum_size = Vector2( 520.0 if compact else 800.0, @@ -2592,11 +2717,18 @@ func _configure_cooler_fish_focus() -> void: controls.append(fish_node) var columns: int = 3 if _compact_layout else 9 if controls.is_empty(): - _cooler_sub_tab.focus_neighbor_bottom = NodePath() + if not _shop_cooler_context_active: + _cooler_sub_tab.focus_neighbor_bottom = NodePath() return - _cooler_sub_tab.focus_neighbor_bottom = _cooler_sub_tab.get_path_to( - controls.front() + var top_control: Control = ( + _cooler_sort_option + if _shop_cooler_context_active + else _cooler_sub_tab ) + if not _shop_cooler_context_active: + _cooler_sub_tab.focus_neighbor_bottom = _cooler_sub_tab.get_path_to( + controls.front() + ) for index: int in controls.size(): var control: CoolerFishSpriteType = controls[index] control.focus_neighbor_left = control.get_path_to( @@ -2606,7 +2738,7 @@ func _configure_cooler_fish_focus() -> void: controls[mini(index + 1, controls.size() - 1)] ) control.focus_neighbor_top = ( - control.get_path_to(_cooler_sub_tab) + control.get_path_to(top_control) if index < columns else control.get_path_to(controls[index - columns]) ) @@ -2776,9 +2908,10 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: _cooler_detail_texture.texture = fish_catch.fish.display_texture _cooler_detail_texture.visible = fish_catch.fish.display_texture != null _cooler_detail_name.text = fish_catch.fish.display_name + var active_buyer: FishBuyerProfileType = _get_active_sale_buyer() var individual_preview: FishSaleResultType = ( - _sale_service.preview_batch([fish_catch.catch_id], _default_buyer) - if _sale_service != null + _sale_service.preview_batch([fish_catch.catch_id], active_buyer) + if _sale_service != null and active_buyer != null else null ) var buyer_offer: int = ( @@ -2793,8 +2926,8 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: ): _cooler_detail_name.text += " • reserved in mail" _cooler_weight_unit.text = "lb • %s" % fish_catch.fish.get_rarity_name() - if buyer_offer >= 0 and _default_buyer != null: - _cooler_offer_label.text = "%s offer" % _default_buyer.display_name + if buyer_offer >= 0 and active_buyer != null: + _cooler_offer_label.text = _get_offer_label(active_buyer) _cooler_offer_value.text = "$%d" % buyer_offer else: _cooler_offer_label.text = "buyer unavailable" @@ -2831,6 +2964,11 @@ func _update_inventory_detail(fish_catch: FishCatchType) -> void: func _update_sale_summary() -> void: + var active_buyer: FishBuyerProfileType = _get_active_sale_buyer() + var buyer_id: StringName = ( + active_buyer.id if active_buyer != null else StringName() + ) + var offer_label: String = _get_offer_label(active_buyer) var selected_ids: Array[StringName] = _fish_selection.get_selected_ids() var selected_count: int = selected_ids.size() _sell_button.text = ( @@ -2842,7 +2980,7 @@ func _update_sale_summary() -> void: if selected_count == 0: _selection_summary.text = "no fish selected" _selection_status.set_content("selected", "none") - _offer_status.set_content("pelican offer", "—") + _offer_status.set_content(offer_label, "—") _sell_button.text = "sell fish" _sell_button.disabled = true _sell_bubble.disabled = true @@ -2866,7 +3004,7 @@ func _update_sale_summary() -> void: else "%d fish selected" % selected_count ) _selection_status.set_content("selected", str(selected_count)) - _offer_status.set_content("pelican offer", "pending") + _offer_status.set_content(offer_label, "pending") _sell_button.disabled = true _sell_bubble.disabled = true _sale_unavailable.text = "Selling…" @@ -2874,7 +3012,8 @@ func _update_sale_summary() -> void: return if ( _network_sale_service == null - or not _network_sale_service.can_request_sale() + or buyer_id.is_empty() + or not _network_sale_service.can_request_sale(buyer_id) ): _selection_summary.text = ( "1 fish selected" @@ -2882,7 +3021,7 @@ func _update_sale_summary() -> void: else "%d fish selected" % selected_count ) _selection_status.set_content("selected", str(selected_count)) - _offer_status.set_content("pelican offer", "unavailable") + _offer_status.set_content(offer_label, "unavailable") _sell_button.disabled = true _sell_bubble.disabled = true _sell_bubble.persistent_mark = false @@ -2893,8 +3032,8 @@ func _update_sale_summary() -> void: _sale_unavailable.visible = true return var preview: FishSaleResultType = ( - _sale_service.preview_batch(selected_ids, _default_buyer) - if _sale_service != null + _sale_service.preview_batch(selected_ids, active_buyer) + if _sale_service != null and active_buyer != null else null ) var count_text: String = ( @@ -2907,21 +3046,21 @@ func _update_sale_summary() -> void: if ( preview != null and preview.payout >= 0 - and _default_buyer != null + and active_buyer != null and ( preview.is_success() or preview.status == FishSaleResultType.Status.FAVORITED ) ): _selection_summary.text += "\n%s total offer: $%d" % [ - _default_buyer.display_name, + active_buyer.display_name, preview.payout, ] _set_cooler_selection_summary(selected_count, preview.payout) - _offer_status.set_content("pelican offer", "$%d" % preview.payout) + _offer_status.set_content(offer_label, "$%d" % preview.payout) else: _set_cooler_selection_summary(selected_count, -1) - _offer_status.set_content("pelican offer", "unavailable") + _offer_status.set_content(offer_label, "unavailable") _sell_button.disabled = preview == null or not preview.is_success() _sell_bubble.disabled = _sell_button.disabled _sell_bubble.persistent_mark = false @@ -2962,6 +3101,10 @@ func _on_favorite_pressed() -> void: func _on_sell_pressed() -> void: + var active_buyer: FishBuyerProfileType = _get_active_sale_buyer() + var buyer_id: StringName = ( + active_buyer.id if active_buyer != null else StringName() + ) if ( _sale_in_progress or ( @@ -2973,7 +3116,8 @@ func _on_sell_pressed() -> void: return if ( _network_sale_service == null - or not _network_sale_service.can_request_sale() + or buyer_id.is_empty() + or not _network_sale_service.can_request_sale(buyer_id) ): _transaction_feedback.text = ( "Selling is not supported by this server." @@ -2983,13 +3127,13 @@ func _on_sell_pressed() -> void: if ( _inventory == null or _sale_service == null - or _default_buyer == null + or active_buyer == null or selected_ids.is_empty() ): return var preview: FishSaleResultType = _sale_service.preview_batch( selected_ids, - _default_buyer + active_buyer ) if not preview.is_success(): _transaction_feedback.text = ( @@ -3001,8 +3145,8 @@ func _on_sell_pressed() -> void: _refresh_inventory() return _confirmation_catch_ids = selected_ids.duplicate() - _confirmation_buyer = _default_buyer - _confirmation_buyer_id = _default_buyer.id + _confirmation_buyer = active_buyer + _confirmation_buyer_id = active_buyer.id _confirmation_generation = _menu_generation var fish_label: String = "fish" _confirmation_message.text = ( @@ -3010,12 +3154,14 @@ func _on_sell_pressed() -> void: % [ preview.fish_count, fish_label, - _get_buyer_display_group(_default_buyer), + _get_buyer_display_group(active_buyer), preview.payout, preview.base_value, ] ) _sale_confirmation.visible = true + if _shop_cooler_context_active: + shop_cooler_modal_changed.emit(true) _set_shell_interactive(false) for button: Button in [_confirm_sale_button, _cancel_sale_button]: button.focus_mode = Control.FOCUS_ALL @@ -3028,7 +3174,9 @@ func _on_confirm_sale_pressed() -> void: if ( _sale_in_progress or _network_sale_service == null - or not _network_sale_service.can_request_sale() + or not _network_sale_service.can_request_sale( + _confirmation_buyer_id + ) or _sale_service == null or _confirmation_catch_ids.is_empty() or _confirmation_buyer == null @@ -3040,11 +3188,13 @@ func _on_confirm_sale_pressed() -> void: var requested_catch_ids: Array[StringName] = ( _confirmation_catch_ids.duplicate() ) + var requested_buyer_id: StringName = _confirmation_buyer_id var transaction_generation: int = _menu_generation _confirm_sale_button.disabled = true _close_sale_confirmation() var request_id: String = _network_sale_service.request_local_sale( - requested_catch_ids + requested_catch_ids, + requested_buyer_id, ) if request_id.is_empty(): _sale_in_progress = false @@ -3052,7 +3202,7 @@ func _on_confirm_sale_pressed() -> void: if ( _network_sale_service.is_local_sale_pending() and transaction_generation == _menu_generation - and visible + and (visible or _shop_cooler_context_active) ): _transaction_feedback.text = "Selling…" @@ -3066,7 +3216,7 @@ func _can_use_shared_world_actions() -> bool: func _on_network_sale_pending(_request_id: String) -> void: _sale_in_progress = true - if visible: + if visible or _shop_cooler_context_active: _transaction_feedback.text = "Selling…" _update_sale_summary() @@ -3081,19 +3231,26 @@ func _on_network_sale_finished( _sale_in_progress = false if accepted: _fish_selection.remove_ids(catch_ids) - if not visible: + if not visible and not _shop_cooler_context_active: return - _transaction_feedback.text = ( + _refresh_all() + var feedback_message: String = ( "Sale complete. $%d received." % payout if accepted else message ) - _refresh_all() + _transaction_feedback.text = feedback_message + if not accepted: + _sale_unavailable.text = feedback_message + _sale_unavailable.visible = true if _current_section == Section.COOLER: call_deferred("_restore_inventory_tab_focus") func _restore_inventory_tab_focus() -> void: + if _shop_cooler_context_active: + _focus_shop_cooler() + return if ( visible and _current_section == Section.COOLER @@ -3110,6 +3267,8 @@ func _close_sale_confirmation() -> void: _confirmation_buyer_id = StringName() _confirmation_generation = -1 _sale_confirmation.visible = false + if was_visible and _shop_cooler_context_active: + shop_cooler_modal_changed.emit(false) _confirmation_message.text = "" _confirm_sale_button.disabled = false for button: Button in [_confirm_sale_button, _cancel_sale_button]: @@ -3117,7 +3276,7 @@ func _close_sale_confirmation() -> void: button.mouse_filter = Control.MOUSE_FILTER_IGNORE if ( was_visible - and visible + and (visible or _shop_cooler_context_active) and not _transitioning and not _page_transitioning ): @@ -3171,6 +3330,22 @@ func _get_buyer_display_group( return "buyer" +func _get_active_sale_buyer() -> FishBuyerProfileType: + if _sale_buyer_override != null and _sale_buyer_override.is_valid(): + return _sale_buyer_override + return _default_buyer + + +func _get_offer_label(buyer: FishBuyerProfileType) -> String: + if buyer == null: + return "buyer offer" + if buyer.id == MAIN_SHOP_BUYER_ID: + return "shop offer" + if buyer.id == NetworkSaleService.PELICAN_BUYER_ID: + return "pelican offer" + return "buyer offer" + + func _refresh_logbook() -> void: if not is_node_ready(): return diff --git a/ui/player_menu.tscn b/ui/player_menu.tscn index d648ad0..cd0171b 100644 --- a/ui/player_menu.tscn +++ b/ui/player_menu.tscn @@ -487,7 +487,7 @@ mouse_filter = 1 [node name="BagFilterTabs" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"] unique_name_in_owner = true -z_index = 5 +z_index = 55 layout_mode = 0 offset_left = 122.0 offset_top = 166.0 @@ -1330,12 +1330,12 @@ columns = 4 [node name="SaleConfirmation" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true visible = false -z_index = 40 +z_index = 120 layout_mode = 0 -offset_left = 320.0 -offset_top = 200.0 -offset_right = 960.0 -offset_bottom = 520.0 +offset_left = 380.0 +offset_top = 265.0 +offset_right = 840.0 +offset_bottom = 455.0 [node name="ConfirmationMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation"] layout_mode = 2 @@ -1347,29 +1347,31 @@ theme_override_constants/margin_bottom = 20 [node name="ConfirmationLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin"] layout_mode = 2 theme_override_constants/separation = 12 +alignment = 1 [node name="ConfirmationMessage" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"] unique_name_in_owner = true layout_mode = 2 +theme_override_font_sizes/font_size = 22 autowrap_mode = 2 horizontal_alignment = 1 [node name="ConfirmationButtons" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout"] layout_mode = 2 +theme_override_constants/separation = 10 alignment = 1 [node name="ConfirmSaleButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 text = "confirm" -theme_type_variation = &"DangerButton" -custom_minimum_size = Vector2(170, 64) +custom_minimum_size = Vector2(140, 44) [node name="CancelSaleButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/SaleConfirmation/ConfirmationMargin/ConfirmationLayout/ConfirmationButtons"] unique_name_in_owner = true layout_mode = 2 text = "cancel" -custom_minimum_size = Vector2(170, 64) +custom_minimum_size = Vector2(140, 44) [node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"] unique_name_in_owner = true diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index 417f833..2e329a6 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -32,7 +32,6 @@ enum PresentationMode { GAMEPLAY_MODAL, } -@onready var _backdrop: ColorRect = $Backdrop @onready var _root_page: SettingsBubblePage = %RootPage @onready var _display_page: SettingsBubblePage = %DisplayPage @onready var _controls_page: SettingsBubblePage = %ControlsPage @@ -210,15 +209,10 @@ 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 _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 = "" diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index ad84849..595d055 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -15,16 +15,6 @@ anchors_preset = 0 theme = ExtResource("2_theme") script = ExtResource("1_panel") -[node name="Backdrop" type="ColorRect" parent="."] -layout_mode = 1 -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -mouse_filter = 2 -color = Color(0.025, 0.085, 0.115, 0.72) - [node name="SettingsFeedback" type="Label" parent="."] unique_name_in_owner = true layout_mode = 1 diff --git a/ui/title_screen.gd b/ui/title_screen.gd index b4fa53e..d950da8 100644 --- a/ui/title_screen.gd +++ b/ui/title_screen.gd @@ -1541,12 +1541,12 @@ func _spawn_decorative_fish(generation: int) -> void: 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 + var horizontal_bounds: Vector2 = get_decorative_fish_crossing_bounds( + layer_size.x, + presentation_size.x, + direction, ) + fish_control.position = Vector2(horizontal_bounds.x, base_y) _decorative_fish_layer.add_child(fish_control) _decorative_fish = fish_control _decorative_fish_tween = create_tween() @@ -1586,17 +1586,14 @@ func _update_decorative_fish( 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 horizontal_bounds: Vector2 = get_decorative_fish_crossing_bounds( + _decorative_fish_layer.size.x, + fish_control.size.x, + direction, ) - 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( + horizontal_bounds.x, horizontal_bounds.y, progress ) - fish_control.position.x = lerpf(start_x, end_x, progress) var bobbed_y: float = ( base_y + sin(progress * crossing_duration * bob_speed) * bob_amplitude @@ -1609,6 +1606,20 @@ func _update_decorative_fish( fish_control.position = _snap_world_preview(fish_control.position) +static func get_decorative_fish_crossing_bounds( + layer_width: float, + fish_width: float, + direction: float, +) -> Vector2: + var outside_left: float = -fish_width - FISH_EDGE_MARGIN + var outside_right: float = layer_width + FISH_EDGE_MARGIN + return ( + Vector2(outside_left, outside_right) + if direction > 0.0 + else Vector2(outside_right, outside_left) + ) + + func _on_decorative_fish_finished( fish_control: TextureRect, generation: int, diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index ec41217..18481c3 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -13,6 +13,13 @@ signal effective_pixel_size_changed( @onready var _ui_viewport: SubViewport = $UIViewport @onready var _ui_root: Control = $UIViewport/GameUI/UIRoot +@onready var _canonical_stage: Control = ( + $UIViewport/GameUI/UIRoot/CanonicalStage +) +@onready var _chat_ui: ChatUI = $UIViewport/GameUI/UIRoot/ChatUI +@onready var _title_content_stage: Control = ( + $UIViewport/GameUI/UIRoot/TitleScreen/ResponsiveTitleStage +) var _requested_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE var _effective_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE @@ -94,21 +101,29 @@ func _resize_presentation() -> void: float(render_height) / UIReferencePresentationType.REFERENCE_SIZE.y ) + var visible_reference_size: Vector2 = ( + UIReferencePresentationType.get_visible_reference_size(display_size) + ) var viewport_size := Vector2i( - roundi( - UIReferencePresentationType.REFERENCE_SIZE.x - * render_scale - ), - render_height, + roundi(visible_reference_size.x * render_scale), + roundi(visible_reference_size.y * render_scale), ) set_anchors_preset(Control.PRESET_TOP_LEFT) - position = UIReferencePresentationType.get_offset(display_size) + position = Vector2.ZERO size = Vector2(viewport_size) scale = Vector2.ONE * (reference_scale / render_scale) stretch_shrink = 1 _ui_root.position = Vector2.ZERO _ui_root.scale = Vector2.ONE * render_scale - _ui_root.size = UIReferencePresentationType.REFERENCE_SIZE + _ui_root.size = visible_reference_size + _canonical_stage.position = ( + UIReferencePresentationType.get_stage_position(display_size) + ) + _canonical_stage.size = UIReferencePresentationType.REFERENCE_SIZE + _title_content_stage.set_anchors_preset(Control.PRESET_TOP_LEFT) + _title_content_stage.position = _canonical_stage.position + _title_content_stage.size = UIReferencePresentationType.REFERENCE_SIZE + _chat_ui.set_output_scale(reference_scale) if ( previous_effective_size != _effective_pixel_size or _requested_pixel_size != _effective_pixel_size diff --git a/ui/ui_reference_presentation.gd b/ui/ui_reference_presentation.gd index 2b0dc5a..e9710a3 100644 --- a/ui/ui_reference_presentation.gd +++ b/ui/ui_reference_presentation.gd @@ -1,6 +1,15 @@ class_name UIReferencePresentation extends RefCounted +# All functional UI is authored in this coordinate system and receives one +# centered, uniform transform at presentation time. Texture source dimensions +# describe sampling quality and aspect ratio; a canonical Control rect owns +# every texture's on-screen layout size. +# +# Edge-docked overlays use the same canonical units and uniform scale, but may +# use the extra logical canvas outside the centered stage on non-16:9 windows. +# This keeps deliberate window-edge attachment without letting individual +# controls calculate monitor- or DPI-specific geometry. const REFERENCE_SIZE: Vector2 = Vector2(1280.0, 720.0) @@ -18,6 +27,14 @@ static func get_offset(display_size: Vector2) -> Vector2: ) * 0.5 +static func get_visible_reference_size(display_size: Vector2) -> Vector2: + return display_size / get_scale(display_size) + + +static func get_stage_position(display_size: Vector2) -> Vector2: + return (get_visible_reference_size(display_size) - REFERENCE_SIZE) * 0.5 + + static func get_rect(display_size: Vector2) -> Rect2: var presentation_scale: float = get_scale(display_size) return Rect2( diff --git a/world/interactables/pelican_buyer_world.tscn b/world/interactables/pelican_buyer_world.tscn index 445010a..434f526 100644 --- a/world/interactables/pelican_buyer_world.tscn +++ b/world/interactables/pelican_buyer_world.tscn @@ -50,7 +50,7 @@ no_depth_test = true [node name="ConvenienceLabel" type="Label3D" parent="Visual"] position = Vector3(0, 1.65, -1.15) -text = "sell nearby • 0.25x" +text = "sell anytime • 0.25x" font_size = 20 outline_size = 6 billboard = 1