Improve UI scaling and fishing commerce

This commit is contained in:
Alexander Sellite 2026-07-31 21:20:23 -04:00
parent 2de96dd7bb
commit 6b8c3eb2f7
29 changed files with 1515 additions and 817 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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