Add session mail and player gifts

This commit is contained in:
Alexander Sellite 2026-07-29 20:10:44 -04:00
parent 5dda74c9e4
commit e53ad1ac38
19 changed files with 1929 additions and 22 deletions

View file

@ -190,7 +190,7 @@ func _on_message(message: Dictionary) -> void:
}
func _on_history(_messages: Array[Dictionary]) -> void:
func _on_history(_messages: Array) -> void:
if _messages.is_empty():
for peer_id: int in _speech.keys():
_on_peer_removed(peer_id)

View file

@ -116,6 +116,8 @@ func setup(
network_chat_service: NetworkChatService,
network_profile: NetworkProfilePreferences,
spawn_service: PlayerSpawnService,
network_mail_service: NetworkMailService,
reservations: PlayerAssetReservationService,
) -> void:
_fishing_spot = fishing_spot
_item_effects = item_effects
@ -149,7 +151,9 @@ func setup(
item_catalog,
cooler_capacity,
network_session,
network_sale_service
network_sale_service,
network_mail_service,
reservations
)
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
_fishing_shop.setup(

603
ui/mail_page.gd Normal file
View file

@ -0,0 +1,603 @@
class_name MailPage
extends Control
signal focus_requested
const GREETING_LABELS := {
"dear": "Dear",
"to": "To",
"hey": "Hey",
"greetings": "Greetings",
}
const SALUTATION_LABELS := {
"love": "Love",
"from": "From",
"cheers": "Cheers",
"salutations": "Salutations",
"good_luck_have_fun": "Good Luck Have Fun",
}
var _service: NetworkMailService
var _reservations: PlayerAssetReservationService
var _inventory: FishInventory
var _wallet: PlayerWallet
var _bag: PlayerBag
var _catalog: ItemCatalog
var _inbox: Control
var _compose: Control
var _letter: Control
var _inbox_list: VBoxContainer
var _empty_label: Label
var _recipient: OptionButton
var _greeting: OptionButton
var _body: TextEdit
var _salutation: OptionButton
var _signature: Label
var _attachment_kind: OptionButton
var _attachment_choice: OptionButton
var _attachment_amount: SpinBox
var _attachment_summary: Label
var _send_button: Button
var _status: Label
var _letter_text: Label
var _letter_gift: Label
var _accept: Button
var _decline: Button
var _current_mail_id := ""
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_build_ui()
func setup(
service: NetworkMailService,
reservations: PlayerAssetReservationService,
inventory: FishInventory,
wallet: PlayerWallet,
bag: PlayerBag,
catalog: ItemCatalog,
) -> void:
_service = service
_reservations = reservations
_inventory = inventory
_wallet = wallet
_bag = bag
_catalog = catalog
_service.mailbox_changed.connect(_refresh_inbox)
_service.peers_changed.connect(_refresh_recipients)
_service.operation_finished.connect(_on_operation_finished)
_refresh_inbox()
_refresh_recipients()
func activate() -> void:
_show_inbox()
call_deferred("_focus_first")
func deactivate() -> void:
_current_mail_id = ""
_show_inbox()
func consume_escape() -> bool:
if _compose.visible or _letter.visible:
_show_inbox()
return true
return false
func set_interactive(value: bool) -> void:
mouse_filter = Control.MOUSE_FILTER_PASS if value else Control.MOUSE_FILTER_IGNORE
for node: Node in find_children("*", "BaseButton", true, false):
(node as BaseButton).focus_mode = (
Control.FOCUS_ALL if value and node.is_visible_in_tree()
else Control.FOCUS_NONE
)
func _build_ui() -> void:
var paper := PanelContainer.new()
paper.position = Vector2(116, 126)
paper.size = Vector2(1048, 470)
var style := StyleBoxFlat.new()
style.bg_color = Color("f3ecd7")
style.border_color = Color("665a46")
style.set_border_width_all(4)
style.set_corner_radius_all(18)
style.shadow_color = Color(0.05, 0.04, 0.03, 0.42)
style.shadow_size = 7
paper.add_theme_stylebox_override("panel", style)
add_child(paper)
var margin := MarginContainer.new()
margin.add_theme_constant_override("margin_left", 34)
margin.add_theme_constant_override("margin_right", 34)
margin.add_theme_constant_override("margin_top", 28)
margin.add_theme_constant_override("margin_bottom", 28)
paper.add_child(margin)
var root := Control.new()
margin.add_child(root)
_inbox = _build_inbox()
_compose = _build_compose()
_letter = _build_letter()
for page: Control in [_inbox, _compose, _letter]:
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_child(page)
_status = Label.new()
_status.position = Vector2(18, 430)
_status.size = Vector2(900, 28)
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_status.add_theme_color_override("font_color", Color("4a3f31"))
root.add_child(_status)
_show_inbox()
func _build_inbox() -> Control:
var page := Control.new()
var title := Label.new()
title.text = "session mail"
title.position = Vector2(12, 0)
title.size = Vector2(500, 42)
title.add_theme_font_size_override("font_size", 30)
page.add_child(title)
var send := Button.new()
send.text = "send mail"
send.position = Vector2(820, 0)
send.size = Vector2(150, 48)
send.pressed.connect(_show_compose)
page.add_child(send)
var scroll := ScrollContainer.new()
scroll.position = Vector2(12, 62)
scroll.size = Vector2(958, 342)
page.add_child(scroll)
_inbox_list = VBoxContainer.new()
_inbox_list.custom_minimum_size = Vector2(930, 0)
_inbox_list.add_theme_constant_override("separation", 8)
scroll.add_child(_inbox_list)
_empty_label = Label.new()
_empty_label.text = "No letters yet."
_empty_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_empty_label.custom_minimum_size = Vector2(930, 80)
_inbox_list.add_child(_empty_label)
return page
func _build_compose() -> Control:
var page := Control.new()
var title := Label.new()
title.text = "write a letter"
title.position = Vector2(12, 0)
title.size = Vector2(300, 36)
title.add_theme_font_size_override("font_size", 26)
page.add_child(title)
_greeting = OptionButton.new()
_greeting.position = Vector2(12, 48)
_greeting.size = Vector2(180, 46)
for id: String in NetworkMailProtocol.GREETINGS:
_greeting.add_item(GREETING_LABELS[id])
_greeting.set_item_metadata(_greeting.item_count - 1, id)
page.add_child(_greeting)
_recipient = OptionButton.new()
_recipient.position = Vector2(204, 48)
_recipient.size = Vector2(330, 46)
page.add_child(_recipient)
_body = TextEdit.new()
_body.position = Vector2(12, 106)
_body.size = Vector2(650, 218)
_body.placeholder_text = "write your letter…"
_body.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
_body.text_changed.connect(_update_send_state)
page.add_child(_body)
_salutation = OptionButton.new()
_salutation.position = Vector2(12, 336)
_salutation.size = Vector2(250, 46)
for id: String in NetworkMailProtocol.SALUTATIONS:
_salutation.add_item(SALUTATION_LABELS[id])
_salutation.set_item_metadata(_salutation.item_count - 1, id)
page.add_child(_salutation)
_signature = Label.new()
_signature.position = Vector2(274, 342)
_signature.size = Vector2(360, 36)
page.add_child(_signature)
_attachment_kind = OptionButton.new()
_attachment_kind.position = Vector2(688, 48)
_attachment_kind.size = Vector2(280, 46)
for label: String in ["No gift", "Fish coin", "Fish", "Consumable"]:
_attachment_kind.add_item(label)
_attachment_kind.item_selected.connect(_refresh_attachment_choices)
page.add_child(_attachment_kind)
_attachment_choice = OptionButton.new()
_attachment_choice.position = Vector2(688, 106)
_attachment_choice.size = Vector2(280, 46)
_attachment_choice.item_selected.connect(
func(_index: int) -> void:
_update_attachment_amount_limit()
_update_attachment_summary()
)
page.add_child(_attachment_choice)
_attachment_amount = SpinBox.new()
_attachment_amount.position = Vector2(688, 164)
_attachment_amount.size = Vector2(180, 46)
_attachment_amount.min_value = 1
_attachment_amount.max_value = 999
_attachment_amount.value = 1
_attachment_amount.value_changed.connect(
func(_value: float) -> void: _update_attachment_summary()
)
page.add_child(_attachment_amount)
_attachment_summary = Label.new()
_attachment_summary.position = Vector2(688, 224)
_attachment_summary.size = Vector2(280, 100)
_attachment_summary.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_attachment_summary)
var cancel := Button.new()
cancel.text = "cancel"
cancel.position = Vector2(688, 342)
cancel.size = Vector2(126, 48)
cancel.pressed.connect(_show_inbox)
page.add_child(cancel)
_send_button = Button.new()
_send_button.text = "send letter"
_send_button.position = Vector2(826, 342)
_send_button.size = Vector2(142, 48)
_send_button.pressed.connect(_send)
page.add_child(_send_button)
_recipient.item_selected.connect(func(_i: int) -> void: _update_send_state())
_refresh_attachment_choices(0)
return page
func _build_letter() -> Control:
var page := Control.new()
_letter_text = Label.new()
_letter_text.position = Vector2(26, 20)
_letter_text.size = Vector2(680, 350)
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
page.add_child(_letter_text)
_letter_gift = Label.new()
_letter_gift.position = Vector2(730, 46)
_letter_gift.size = Vector2(230, 180)
_letter_gift.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
page.add_child(_letter_gift)
_accept = Button.new()
_accept.text = "accept gift"
_accept.position = Vector2(730, 250)
_accept.size = Vector2(230, 48)
_accept.pressed.connect(func() -> void:
_service.accept_gift(_current_mail_id)
)
page.add_child(_accept)
_decline = Button.new()
_decline.text = "decline gift"
_decline.position = Vector2(730, 308)
_decline.size = Vector2(230, 48)
_decline.pressed.connect(func() -> void:
_service.decline_gift(_current_mail_id)
)
page.add_child(_decline)
var close := Button.new()
close.text = "close"
close.position = Vector2(26, 370)
close.size = Vector2(150, 48)
close.pressed.connect(_show_inbox)
page.add_child(close)
return page
func _show_inbox() -> void:
_inbox.show()
_compose.hide()
_letter.hide()
_current_mail_id = ""
_refresh_inbox()
func _show_compose() -> void:
_inbox.hide()
_compose.show()
_letter.hide()
_body.clear()
_signature.text = _service.get_local_display_name()
_status.text = ""
_refresh_recipients()
_refresh_attachment_choices(_attachment_kind.selected)
_update_send_state()
_greeting.grab_focus()
func _open_letter(mail_id: String) -> void:
var letter := _service.get_letter(mail_id)
if letter.is_empty():
return
_current_mail_id = mail_id
_service.mark_read(mail_id)
_inbox.hide()
_compose.hide()
_letter.show()
_letter_text.text = "%s %s,\n\n%s\n\n%s,\n%s" % [
GREETING_LABELS.get(letter["greeting_id"], "Dear"),
letter.get("recipient_display_name", "you"),
letter["body"],
SALUTATION_LABELS.get(letter["salutation_id"], "From"),
letter["sender_display_name"],
]
_letter_gift.text = _attachment_state_text(letter)
var pending := (
int(letter["attachment"].get("type", 0)) != 0
and int(letter["state"]) in [
NetworkMailProtocol.State.SENT_UNREAD,
NetworkMailProtocol.State.READ,
]
and _service.is_local_recipient(letter)
)
_accept.visible = pending
_decline.visible = pending
if pending:
_accept.grab_focus()
func _refresh_inbox() -> void:
if _service == null or _inbox_list == null:
return
for child: Node in _inbox_list.get_children():
child.queue_free()
var letters := _service.get_local_letters()
if letters.is_empty():
var empty := Label.new()
empty.text = "No letters yet."
empty.custom_minimum_size = Vector2(930, 80)
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_inbox_list.add_child(empty)
return
for letter: Dictionary in letters:
var button := Button.new()
var first_line: String = str(letter["body"]).split("\n", false)[0]
var unread := int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD
var gift := int(letter["attachment"].get("type", 0)) != 0
button.text = "%s%s%s%s" % [
"new · " if unread else "",
letter["sender_display_name"],
first_line.left(72),
" · gift enclosed" if gift else "",
]
button.custom_minimum_size = Vector2(930, 54)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
_inbox_list.add_child(button)
func _refresh_recipients() -> void:
if _service == null or _recipient == null:
return
var selected_peer := (
int(_recipient.get_selected_metadata())
if _recipient.selected >= 0
and _recipient.get_selected_metadata() != null else 0
)
_recipient.clear()
_recipient.add_item("select a player")
_recipient.set_item_metadata(0, 0)
var retained := false
for choice: Dictionary in _service.get_recipient_choices():
_recipient.add_item(choice["label"])
_recipient.set_item_metadata(
_recipient.item_count - 1, choice["peer_id"]
)
if int(choice["peer_id"]) == selected_peer:
_recipient.select(_recipient.item_count - 1)
retained = true
if selected_peer > 0 and not retained:
_recipient.select(0)
if _status != null:
_status.text = "That player is no longer connected."
_update_send_state()
func _refresh_attachment_choices(_index: int) -> void:
if _attachment_choice == null:
return
_attachment_choice.clear()
_attachment_amount.visible = _attachment_kind.selected in [1, 3]
match _attachment_kind.selected:
0:
_attachment_choice.add_item("No attachment")
1:
_attachment_choice.add_item(
"available: %d fish coin"
% _reservations.get_available_fish_coin()
)
_attachment_amount.max_value = maxi(
_reservations.get_available_fish_coin(), 1
)
2:
for fish_catch: FishCatch in _inventory.get_all_catches():
if (
not fish_catch.is_favorited
and not _reservations.is_fish_reserved(fish_catch.catch_id)
):
_attachment_choice.add_item(
"%s%.1f lb" % [
fish_catch.fish.display_name, fish_catch.weight_lb,
]
)
_attachment_choice.set_item_metadata(
_attachment_choice.item_count - 1, str(fish_catch.catch_id)
)
3:
for item: ItemData in _catalog.get_valid_items():
var available := _reservations.get_available_item_quantity(
item.item_id
)
if item.category == ItemData.Category.CONSUMABLE and available > 0:
_attachment_choice.add_item(
"%s ×%d" % [item.display_name, available]
)
_attachment_choice.set_item_metadata(
_attachment_choice.item_count - 1, str(item.item_id)
)
_update_attachment_amount_limit()
_update_attachment_summary()
func _update_attachment_amount_limit() -> void:
if _attachment_kind.selected == 3 and _attachment_choice.selected >= 0:
var item_id := StringName(str(_attachment_choice.get_selected_metadata()))
_attachment_amount.max_value = maxi(
_reservations.get_available_item_quantity(item_id), 1
)
func _make_attachment() -> Dictionary:
match _attachment_kind.selected:
1:
return {
"type": PlayerAssetReservationService.AttachmentType.FISH_COIN,
"amount": int(_attachment_amount.value),
}
2:
if _attachment_choice.selected < 0:
return {}
var catch_id := StringName(str(_attachment_choice.get_selected_metadata()))
var fish_catch := _inventory.get_catch_by_id(catch_id)
if fish_catch == null:
return {}
return {
"type": PlayerAssetReservationService.AttachmentType.FISH,
"catch_id": str(catch_id),
"catch": fish_catch.to_network_dict(),
}
3:
if _attachment_choice.selected < 0:
return {}
return {
"type": PlayerAssetReservationService.AttachmentType.CONSUMABLE,
"item_id": str(_attachment_choice.get_selected_metadata()),
"quantity": int(_attachment_amount.value),
}
return {"type": PlayerAssetReservationService.AttachmentType.NONE}
func _update_attachment_summary() -> void:
if _attachment_summary == null:
return
var attachment := _make_attachment()
_attachment_summary.text = _attachment_text(attachment)
_update_send_state()
func _update_send_state() -> void:
if _send_button == null:
return
var recipient_id := (
int(_recipient.get_selected_metadata())
if _recipient.selected >= 0 and _recipient.get_selected_metadata() != null
else 0
)
var attachment := _make_attachment()
_send_button.disabled = (
recipient_id <= 0
or NetworkMailProtocol.sanitize_body(_body.text).is_empty()
or attachment.is_empty()
or not _reservations.validate_attachment(attachment)
or not _attachment_is_available(attachment)
)
func _attachment_is_available(attachment: Dictionary) -> bool:
match int(attachment.get("type", 0)):
PlayerAssetReservationService.AttachmentType.NONE:
return true
PlayerAssetReservationService.AttachmentType.FISH_COIN:
return (
int(attachment["amount"])
<= _reservations.get_available_fish_coin()
)
PlayerAssetReservationService.AttachmentType.FISH:
return not _reservations.is_fish_reserved(
StringName(str(attachment["catch_id"]))
)
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
return (
int(attachment["quantity"])
<= _reservations.get_available_item_quantity(
StringName(str(attachment["item_id"]))
)
)
return false
func _send() -> void:
var recipient_id := int(_recipient.get_selected_metadata())
var greeting_id := str(_greeting.get_selected_metadata())
var salutation_id := str(_salutation.get_selected_metadata())
if _service.send_letter(
recipient_id, greeting_id, _body.text, salutation_id, _make_attachment()
):
_send_button.disabled = true
_status.text = "Sending letter…"
func _on_operation_finished(success: bool, message: String) -> void:
_status.text = message
if success and _compose.visible:
_show_inbox()
elif _letter.visible and not _current_mail_id.is_empty():
_open_letter(_current_mail_id)
else:
_update_send_state()
func _attachment_state_text(letter: Dictionary) -> String:
var attachment: Dictionary = letter["attachment"]
if int(attachment.get("type", 0)) == 0:
return ""
match int(letter["state"]):
NetworkMailProtocol.State.ACCEPTED:
return "Gift accepted.\n\n%s" % _attachment_text(attachment)
NetworkMailProtocol.State.DECLINED:
return "Gift declined."
NetworkMailProtocol.State.ATTACHMENT_RECALLED, NetworkMailProtocol.State.CANCELLED:
return "Gift returned to sender."
NetworkMailProtocol.State.ACCEPTANCE_PENDING:
return "Transferring gift…"
return "Gift enclosed\n\n%s" % _attachment_text(attachment)
func _attachment_text(attachment: Dictionary) -> String:
match int(attachment.get("type", 0)):
PlayerAssetReservationService.AttachmentType.NONE:
return "No gift attached."
PlayerAssetReservationService.AttachmentType.FISH_COIN:
return "%d fish coin" % int(attachment.get("amount", 0))
PlayerAssetReservationService.AttachmentType.FISH:
var fish_catch := _inventory.get_catch_by_id(
StringName(str(attachment.get("catch_id", "")))
)
if fish_catch != null:
return "%s%.1f lb" % [
fish_catch.fish.display_name, fish_catch.weight_lb,
]
var data: Dictionary = attachment.get("catch", {})
return "%s%.1f lb" % [
str(data.get("fish_id", "fish")).capitalize(),
float(data.get("weight_lb", 0.0)),
]
PlayerAssetReservationService.AttachmentType.CONSUMABLE:
var item := _catalog.get_item_by_id(
StringName(str(attachment.get("item_id", "")))
)
return "%s ×%d" % [
item.display_name if item != null else "Consumable",
int(attachment.get("quantity", 0)),
]
return "Invalid gift."
func _focus_first() -> void:
var buttons := _inbox_list.find_children("*", "Button", true, false)
if not buttons.is_empty():
(buttons.front() as Button).grab_focus()

1
ui/mail_page.gd.uid Normal file
View file

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

View file

@ -72,7 +72,7 @@ const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
const COMPACT_HEIGHT_THRESHOLD: float = 560.0
const NAVIGATION_PRESENTATION_SCALE: float = 0.60
const NAVIGATION_CANONICAL_POSITION := Vector2(463.0, 44.0)
const NAVIGATION_CANONICAL_POSITION := Vector2(424.0, 44.0)
const NAVIGATION_SELECTED_SCALE: float = 1.02
const COOLER_RARITY_COMMON := Color("e8eef0")
const COOLER_RARITY_UNCOMMON := Color("64c87c")
@ -86,6 +86,7 @@ enum Section {
COOLER,
BAG,
LOGBOOK,
MAIL,
}
enum SortMode {
@ -154,6 +155,7 @@ enum CloseReason {
@onready var _bag_sprite_detail_name: Label = %BagSpriteDetailName
@onready var _bag_sprite_detail_data: Label = %BagSpriteDetailData
@onready var _logbook_page: Control = %LogbookPage
@onready var _mail_page: MailPage = %MailPage
@onready var _book_backing: PanelContainer = %BookBacking
@onready var _book_spread: BoxContainer = %BookSpread
@onready var _left_page: PanelContainer = %LeftPage
@ -170,6 +172,8 @@ enum CloseReason {
@onready var _inventory_tab: BubbleButtonType = %InventoryTab
@onready var _bag_tab: BubbleButtonType = %BagTab
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
@onready var _mail_tab: BubbleButtonType = %MailTab
@onready var _mail_unread_badge: Label = %MailUnreadBadge
@onready var _close_button: BubbleButtonType = %CloseButton
@onready var _content_shell: BubbleContentShellType = %MenuPanel
@onready var _content_stage: Control = %Content
@ -228,6 +232,7 @@ var _wallet: PlayerWalletType
var _sale_service: FishSaleServiceType
var _network_session: NetworkSessionType
var _network_sale_service: NetworkSaleService
var _network_mail_service: NetworkMailService
var _default_buyer: FishBuyerProfileType
var _catalog: FishPoolType
var _fishing_spot: FishingSpotType
@ -262,6 +267,7 @@ var _content_rest_position: Vector2 = Vector2.ZERO
var _cooler_rest_position: Vector2 = Vector2.ZERO
var _bag_rest_position: Vector2 = Vector2.ZERO
var _logbook_rest_position: Vector2 = Vector2.ZERO
var _mail_rest_position: Vector2 = Vector2.ZERO
var _page_outgoing_root: Control
var _page_incoming_root: Control
var _page_hosts_shared: bool = false
@ -287,6 +293,7 @@ func _ready() -> void:
_logbook_tab.pressed.connect(
_show_section.bind(Section.LOGBOOK)
)
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
_close_button.pressed.connect(close_menu)
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
_sort_direction.pressed.connect(_on_sort_direction_pressed)
@ -312,6 +319,7 @@ func _ready() -> void:
_inventory_tab,
_bag_tab,
_logbook_tab,
_mail_tab,
_close_button,
])
_configure_navigation_focus()
@ -358,6 +366,8 @@ func setup(
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
network_sale_service: NetworkSaleService,
network_mail_service: NetworkMailService,
reservations: PlayerAssetReservationService,
) -> void:
_player = player
_inventory = inventory
@ -373,6 +383,14 @@ func setup(
_cooler_capacity = cooler_capacity
_network_session = network_session
_network_sale_service = network_sale_service
_network_mail_service = network_mail_service
_mail_page.setup(
network_mail_service, reservations, inventory, wallet, bag, item_catalog
)
_network_mail_service.unread_count_changed.connect(
_on_mail_unread_count_changed
)
_on_mail_unread_count_changed(_network_mail_service.get_unread_count())
if (
_network_sale_service != null
and not _network_sale_service.local_sale_pending.is_connected(
@ -428,6 +446,8 @@ func consume_escape() -> bool:
return true
if _sale_confirmation.visible:
_close_sale_confirmation()
elif _current_section == Section.MAIL and _mail_page.consume_escape():
pass
else:
close_menu()
return true
@ -563,6 +583,7 @@ func _show_section_immediate(section: Section) -> void:
_cooler_page.visible = section == Section.COOLER
_bag_page.visible = section == Section.BAG
_logbook_page.visible = section == Section.LOGBOOK
_mail_page.visible = section == Section.MAIL
_content_shell.visible = false
_inventory_section.visible = false
_bag_section.visible = false
@ -581,9 +602,14 @@ func _show_section_immediate(section: Section) -> void:
_update_bag_detail()
if section != Section.LOGBOOK:
_cancel_logbook_page_transition(true)
if section == Section.MAIL:
_mail_page.activate()
else:
_mail_page.deactivate()
_inventory_tab.button_pressed = section == Section.COOLER
_bag_tab.button_pressed = section == Section.BAG
_logbook_tab.button_pressed = section == Section.LOGBOOK
_mail_tab.button_pressed = section == Section.MAIL
_update_navigation_selection()
_configure_active_page_focus()
@ -593,8 +619,10 @@ func _focus_current_section() -> void:
_inventory_tab.grab_focus()
elif _current_section == Section.BAG:
_bag_tab.grab_focus()
else:
elif _current_section == Section.LOGBOOK:
_logbook_tab.grab_focus()
else:
_mail_tab.grab_focus()
func _process(delta: float) -> void:
@ -621,6 +649,7 @@ func _configure_navigation_focus() -> void:
_inventory_tab,
_bag_tab,
_logbook_tab,
_mail_tab,
_close_button,
]
for index: int in navigation.size():
@ -676,6 +705,8 @@ func _configure_active_page_focus() -> void:
_configure_bag_item_focus()
Section.LOGBOOK:
_configure_logbook_focus()
Section.MAIL:
_mail_page.activate()
func _apply_cooler_control_styles() -> void:
@ -768,6 +799,7 @@ func _apply_navigation_styles() -> void:
_inventory_tab,
_bag_tab,
_logbook_tab,
_mail_tab,
_close_button,
]:
bubble.add_theme_stylebox_override("normal", normal)
@ -797,6 +829,7 @@ func _apply_navigation_selection_presentation() -> void:
_inventory_tab,
_bag_tab,
_logbook_tab,
_mail_tab,
]:
if not bubble.button_pressed:
continue
@ -895,10 +928,16 @@ func _update_navigation_selection() -> void:
_set_navigation_target(_current_section)
func _on_mail_unread_count_changed(count: int) -> void:
_mail_unread_badge.visible = count > 0
_mail_unread_badge.text = "99+" if count > 99 else str(count)
func _set_navigation_target(section: Section) -> void:
_inventory_tab.button_pressed = section == Section.COOLER
_bag_tab.button_pressed = section == Section.BAG
_logbook_tab.button_pressed = section == Section.LOGBOOK
_mail_tab.button_pressed = section == Section.MAIL
func _update_shell_layout() -> void:
@ -915,7 +954,7 @@ func _update_shell_layout() -> void:
_content_shell.size = Vector2(612.0, 286.0) if compact else Vector2(1196.0, 478.0)
_presentation_rest_position = _content_shell.position
var navigation_size := (
Vector2(620.0, 75.0) if compact else Vector2(620.0, 100.0)
Vector2(720.0, 75.0) if compact else Vector2(720.0, 100.0)
)
_navigation_cluster.position = NAVIGATION_CANONICAL_POSITION
_navigation_cluster.size = navigation_size
@ -1084,6 +1123,9 @@ func _update_shell_layout() -> void:
_logbook_page.size = reference_size
_logbook_page.position = Vector2.ZERO
_logbook_rest_position = Vector2.ZERO
_mail_page.size = reference_size
_mail_page.position = Vector2.ZERO
_mail_rest_position = Vector2.ZERO
_book_backing.position = (
Vector2(14.0, 164.0) if compact else Vector2(58.0, 128.0)
)
@ -1144,9 +1186,11 @@ func _update_shell_layout() -> void:
_cooler_page.position = _cooler_rest_position
_bag_page.position = _bag_rest_position
_logbook_page.position = _logbook_rest_position
_mail_page.position = _mail_rest_position
_cooler_page.modulate.a = 1.0
_bag_page.modulate.a = 1.0
_logbook_page.modulate.a = 1.0
_mail_page.modulate.a = 1.0
if not _page_transitioning:
_content_stage.position = _content_rest_position
_layout_cooler_fish(false)
@ -1229,6 +1273,7 @@ func _begin_menu_entry() -> void:
_cooler_page.position = _cooler_rest_position + Vector2.DOWN * start_offset
_bag_page.position = _bag_rest_position + Vector2.DOWN * start_offset
_logbook_page.position = _logbook_rest_position + Vector2.DOWN * start_offset
_mail_page.position = _mail_rest_position + Vector2.DOWN * start_offset
var navigation_rest: Vector2 = _navigation_cluster.position
_navigation_cluster.position = navigation_rest + Vector2.DOWN * start_offset
_presentation_tween = create_tween().set_parallel(true)
@ -1256,6 +1301,12 @@ func _begin_menu_entry() -> void:
_logbook_rest_position,
MENU_ENTER_DURATION
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_presentation_tween.tween_property(
_mail_page,
"position",
_mail_rest_position,
MENU_ENTER_DURATION
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_presentation_tween.tween_property(
_navigation_cluster,
"position",
@ -1316,6 +1367,12 @@ func _begin_menu_exit(reason: CloseReason, restore_controls: bool) -> void:
-_logbook_page.size.y - TRANSITION_SAFE_MARGIN,
MENU_EXIT_DURATION
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_presentation_tween.tween_property(
_mail_page,
"position:y",
-_mail_page.size.y - TRANSITION_SAFE_MARGIN,
MENU_EXIT_DURATION
).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN_OUT)
_presentation_tween.tween_property(
_navigation_cluster,
"position:y",
@ -1377,8 +1434,10 @@ func _get_section_root(section: Section) -> Control:
return _cooler_page
Section.BAG:
return _bag_page
_:
Section.LOGBOOK:
return _logbook_page
_:
return _mail_page
func _get_section_rest_position(section: Section) -> Vector2:
@ -1387,8 +1446,10 @@ func _get_section_rest_position(section: Section) -> Vector2:
return _cooler_rest_position
Section.BAG:
return _bag_rest_position
_:
Section.LOGBOOK:
return _logbook_rest_position
_:
return _mail_rest_position
func _begin_page_transition(section: Section) -> void:
@ -1468,6 +1529,7 @@ func _set_shell_interactive(interactive: bool) -> void:
_inventory_tab,
_bag_tab,
_logbook_tab,
_mail_tab,
_close_button,
]:
bubble.focus_mode = Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
@ -1499,6 +1561,9 @@ func _set_content_interactive(interactive: bool) -> void:
if interactive and _current_section == Section.LOGBOOK
else Control.MOUSE_FILTER_IGNORE
)
_mail_page.set_interactive(
interactive and _current_section == Section.MAIL
)
var cooler_interactive: bool = (
interactive and _current_section == Section.COOLER
)
@ -1569,7 +1634,9 @@ func _cancel_page_tween() -> void:
func _reset_page_transition_visuals() -> void:
for section: Section in [Section.COOLER, Section.BAG, Section.LOGBOOK]:
for section: Section in [
Section.COOLER, Section.BAG, Section.LOGBOOK, Section.MAIL,
]:
var page: Control = _get_section_root(section)
page.position = _get_section_rest_position(section)
page.modulate.a = 1.0

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=19 format=3]
[gd_scene load_steps=20 format=3]
[ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
@ -17,6 +17,7 @@
[ext_resource type="Texture2D" path="res://ui/assets/title/bubbles/bubble1.png" id="15_bubble1"]
[ext_resource type="Texture2D" path="res://ui/assets/title/bubbles/bubble2.png" id="16_bubble2"]
[ext_resource type="Texture2D" path="res://ui/assets/title/bubbles/bubble3.png" id="17_bubble3"]
[ext_resource type="Script" path="res://ui/mail_page.gd" id="18_mail_page"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
@ -574,6 +575,14 @@ offset_right = 1280.0
offset_bottom = 720.0
mouse_filter = 1
[node name="MailPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true
layout_mode = 0
offset_right = 1280.0
offset_bottom = 720.0
mouse_filter = 1
script = ExtResource("18_mail_page")
[node name="BookBacking" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
unique_name_in_owner = true
layout_mode = 0
@ -1197,15 +1206,15 @@ custom_minimum_size = Vector2(170, 64)
[node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 463.0
offset_left = 390.0
offset_top = 44.0
offset_right = 1083.0
offset_right = 1110.0
offset_bottom = 144.0
mouse_filter = 1
script = ExtResource("5_cluster")
profile = ExtResource("4_profile")
desktop_reference_size = Vector2(620, 100)
compact_reference_size = Vector2(620, 100)
desktop_reference_size = Vector2(720, 100)
compact_reference_size = Vector2(720, 100)
[node name="InventoryTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
unique_name_in_owner = true
@ -1267,6 +1276,42 @@ motion_phase = 2.85
deformation_amplitude = 0.012
deformation_period = 6.7
[node name="MailTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
unique_name_in_owner = true
layout_mode = 0
toggle_mode = true
text = "mail"
script = ExtResource("6_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(124, 120)
desktop_anchor = Vector2(500, 60)
compact_anchor = Vector2(501, 73.3333)
compact_minimum_size = Vector2(84, 82)
minimum_font_size = 25
maximum_font_size = 25
horizontal_amplitude = 1.1
vertical_amplitude = 2.2
motion_period = 5.5
motion_phase = 3.55
deformation_amplitude = 0.011
deformation_period = 6.3
[node name="MailUnreadBadge" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster/MailTab"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 86.0
offset_top = 2.0
offset_right = 120.0
offset_bottom = 34.0
mouse_filter = 2
theme_override_colors/font_color = Color(0.12, 0.08, 0.04, 1)
theme_override_colors/font_outline_color = Color(1, 0.88, 0.34, 1)
theme_override_constants/outline_size = 7
theme_override_font_sizes/font_size = 18
text = "1"
horizontal_alignment = 1
vertical_alignment = 1
[node name="CloseButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
unique_name_in_owner = true
layout_mode = 0
@ -1274,8 +1319,8 @@ text = "close"
script = ExtResource("6_bubble")
profile = ExtResource("4_profile")
neutral_size = Vector2(96, 94)
desktop_anchor = Vector2(489, 57)
compact_anchor = Vector2(512.667, 73.3333)
desktop_anchor = Vector2(626, 57)
compact_anchor = Vector2(616, 73.3333)
compact_minimum_size = Vector2(76, 74)
minimum_font_size = 23
maximum_font_size = 23