netfishing/ui/mail_page.gd

757 lines
22 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class_name MailPage
extends Control
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",
}
const FishQualityType = preload("res://fish/fish_quality.gd")
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: LineEdit
var _coin_available: Label
var _amount_minus: Button
var _amount_plus: Button
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 _archive: Button
var _delete: Button
var _current_mail_id := ""
var _showing_archive := false
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
UtilityPageStyle.apply_page(self)
_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()
UtilityPageStyle.animate_in(self)
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 is_composing_letter() -> bool:
return _compose != null and _compose.visible
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 margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self)
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, 408)
_status.size = Vector2(1024, 28)
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_status.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
root.add_child(_status)
_style_controls(root)
_show_inbox()
func _build_inbox() -> Control:
var page := Control.new()
var title := Label.new()
title.text = "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 archive_view := Button.new()
archive_view.text = "archive"
archive_view.position = Vector2(654, 0)
archive_view.size = Vector2(154, 48)
archive_view.pressed.connect(func() -> void:
_showing_archive = not _showing_archive
archive_view.text = "inbox" if _showing_archive else "archive"
_refresh_inbox()
)
page.add_child(archive_view)
var scroll := ScrollContainer.new()
scroll.position = Vector2(12, 62)
scroll.size = Vector2(1036, 334)
page.add_child(scroll)
_inbox_list = VBoxContainer.new()
_inbox_list.custom_minimum_size = Vector2(1008, 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(1008, 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)
_coin_available = Label.new()
_coin_available.position = Vector2(688, 158)
_coin_available.size = Vector2(280, 28)
page.add_child(_coin_available)
_attachment_amount = LineEdit.new()
_attachment_amount.position = Vector2(746, 190)
_attachment_amount.size = Vector2(164, 46)
_attachment_amount.placeholder_text = "0"
_attachment_amount.text = "0"
_attachment_amount.text_changed.connect(_on_attachment_amount_changed)
page.add_child(_attachment_amount)
_amount_minus = Button.new()
_amount_minus.text = ""
_amount_minus.position = Vector2(688, 190)
_amount_minus.size = Vector2(48, 46)
_amount_minus.pressed.connect(_step_attachment_amount.bind(-1))
page.add_child(_amount_minus)
_amount_plus = Button.new()
_amount_plus.text = "+"
_amount_plus.position = Vector2(920, 190)
_amount_plus.size = Vector2(48, 46)
_amount_plus.pressed.connect(_step_attachment_amount.bind(1))
page.add_child(_amount_plus)
_attachment_summary = Label.new()
_attachment_summary.position = Vector2(688, 246)
_attachment_summary.size = Vector2(280, 78)
_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:
_reset_attachment_controls()
_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)
_archive = Button.new()
_archive.text = "archive"
_archive.position = Vector2(550, 370)
_archive.size = Vector2(150, 48)
_archive.pressed.connect(_archive_current)
page.add_child(_archive)
_delete = Button.new()
_delete.text = "delete"
_delete.position = Vector2(710, 370)
_delete.size = Vector2(150, 48)
_delete.pressed.connect(_delete_current)
page.add_child(_delete)
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()
_reset_compose()
_signature.text = _service.get_local_display_name()
_refresh_recipients()
_update_send_state()
_greeting.grab_focus()
func _reset_compose() -> void:
_recipient.select(0)
_greeting.select(0)
_body.clear()
_salutation.select(0)
_status.text = ""
_reset_attachment_controls()
func _reset_attachment_controls() -> void:
_attachment_kind.select(0)
_attachment_choice.clear()
_attachment_choice.add_item("No attachment")
_attachment_amount.text = "0"
_refresh_attachment_choices(0)
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
_archive.text = (
"restore to inbox"
if _service.is_letter_archived(mail_id)
else "archive"
)
_delete.disabled = pending
_delete.tooltip_text = (
"Resolve this gift before deleting the letter." if pending else ""
)
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(_showing_archive)
if letters.is_empty():
var empty := Label.new()
empty.text = (
"No archived letters." if _showing_archive else "No letters yet."
)
empty.custom_minimum_size = Vector2(1008, 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 := (
_service.is_local_recipient(letter)
and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD
)
var gift := int(letter["attachment"].get("type", 0)) != 0
var local_is_sender := (
int(letter["sender_peer_id"]) != 0
and not _service.is_local_recipient(letter)
)
button.text = "%s%s%s%s%s" % [
"new · " if unread else "",
"to " if local_is_sender else "",
(
letter["recipient_display_name"]
if local_is_sender
else letter["sender_display_name"]
),
first_line.left(72),
" · gift enclosed" if gift else "",
]
button.custom_minimum_size = Vector2(1008, 54)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
UtilityPageStyle.apply_ocean_button(button)
_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()
var amount_visible := _attachment_kind.selected in [1, 3]
_attachment_amount.visible = amount_visible
_amount_minus.visible = amount_visible
_amount_plus.visible = amount_visible
_coin_available.visible = _attachment_kind.selected == 1
match _attachment_kind.selected:
0:
_attachment_choice.add_item("No attachment")
1:
_attachment_choice.add_item("Fish coin")
_coin_available.text = "Available: %d fish coin" % (
_reservations.get_available_fish_coin()
)
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" % [
FishQualityType.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
),
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:
_set_attachment_amount(_attachment_amount_value())
func _make_attachment() -> Dictionary:
match _attachment_kind.selected:
1:
return {
"type": PlayerAssetReservationService.AttachmentType.FISH_COIN,
"amount": _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": _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:
_set_attachment_amount(_attachment_amount_value())
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_amount_value() -> int:
return int(_attachment_amount.text) if _attachment_amount.text.is_valid_int() else 0
func _attachment_amount_maximum() -> int:
if _attachment_kind.selected == 1:
return _reservations.get_available_fish_coin()
if _attachment_kind.selected == 3 and _attachment_choice.selected >= 0:
return _reservations.get_available_item_quantity(
StringName(str(_attachment_choice.get_selected_metadata()))
)
return 0
func _set_attachment_amount(value: int) -> void:
var clamped := clampi(value, 0, _attachment_amount_maximum())
var text := str(clamped)
if _attachment_amount.text != text:
_attachment_amount.text = text
_attachment_amount.caret_column = text.length()
_amount_minus.disabled = clamped <= 0
_amount_plus.disabled = clamped >= _attachment_amount_maximum()
_update_attachment_summary()
func _on_attachment_amount_changed(value: String) -> void:
var digits := ""
for character: String in value:
if character >= "0" and character <= "9":
digits += character
_set_attachment_amount(int(digits) if not digits.is_empty() else 0)
func _step_attachment_amount(delta: int) -> void:
_set_attachment_amount(_attachment_amount_value() + delta)
_attachment_amount.grab_focus()
func _archive_current() -> void:
if _current_mail_id.is_empty():
return
var archived := not _service.is_letter_archived(_current_mail_id)
if _service.archive_local_letter(_current_mail_id, archived):
_status.text = "Letter archived." if archived else "Letter restored."
_show_inbox()
func _delete_current() -> void:
if _current_mail_id.is_empty():
return
if _service.delete_local_letter(_current_mail_id):
_status.text = "Letter deleted locally."
_show_inbox()
else:
_status.text = "Resolve the gift before deleting this letter."
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" % [
FishQualityType.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
),
fish_catch.weight_lb,
]
var data: Dictionary = attachment.get("catch", {})
return "%s%.1f lb" % [
FishQualityType.qualified_name(
str(data.get("fish_id", "fish")).capitalize(),
int(data.get("quality", FishQualityType.Tier.BORING)),
),
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()
func _style_controls(root: Node) -> void:
for node: Node in root.find_children("*", "", true, false):
if node is BaseButton:
UtilityPageStyle.apply_ocean_button(node)
elif node is LineEdit:
UtilityPageStyle.apply_ocean_line_edit(node)
elif node is Label:
node.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
elif node is TextEdit:
UtilityPageStyle.apply_text_edit(node)