netfishing/ui/mail_page.gd

621 lines
19 KiB
GDScript
Raw 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
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)
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 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)
paper.add_theme_stylebox_override("panel", UtilityPageStyle.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)
_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 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"])))
UtilityPageStyle.apply_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()
_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()
func _style_controls(root: Node) -> void:
for node: Node in root.find_children("*", "", true, false):
if node is BaseButton:
UtilityPageStyle.apply_button(node)
elif node is LineEdit:
UtilityPageStyle.apply_line_edit(node)
elif node is Label:
node.add_theme_color_override(
"font_color", UtilityPageStyle.INK
)
elif node is TextEdit:
node.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
node.add_theme_color_override(
"font_color", UtilityPageStyle.LIGHT_TEXT
)
node.add_theme_color_override(
"font_placeholder_color",
Color(0.82, 0.80, 0.71, 0.84)
)