Add gathering, unified inventory, and real-time world

This commit is contained in:
Alexander Sellite 2026-08-18 18:19:36 -04:00
parent 173371e6fc
commit fa57edca83
113 changed files with 6700 additions and 1307 deletions

View file

@ -193,7 +193,6 @@ func setup(
_service.local_message_confirmed.connect(_on_local_message_confirmed)
_service.history_replaced.connect(_on_history)
_service.send_rejected.connect(_on_rejected)
_service.world_command_finished.connect(_on_world_command_finished)
_session.peer_removed.connect(_on_peer_removed)
_entry.text = _settings.current_settings.chat_draft
_entry.caret_column = _entry.text.length()
@ -835,8 +834,6 @@ func _send() -> void:
if body.strip_edges().is_empty():
close_chat()
return
if _handle_chat_command(body):
return
_send_pending = true
_pending_send_body = NetworkChatProtocol.sanitize_body(body)
_entry.editable = false
@ -852,85 +849,6 @@ func _send() -> void:
_set_status("Sending…")
func _handle_chat_command(body: String) -> bool:
var command_text := body.strip_edges()
if not command_text.begins_with("/"):
return false
var parts: PackedStringArray = command_text.split(" ", false)
var command := String(parts[0]).trim_prefix("/").to_lower()
match command:
"time":
_handle_time_command(parts)
"weather":
_handle_weather_command(parts)
"":
_show_command_error("Enter a command after /.")
_:
_show_command_error("Unknown command: /%s" % command)
_entry.clear()
_flush_draft()
return true
func _show_command_error(message: String) -> void:
_set_status(message)
close_chat(true)
func _handle_time_command(parts: PackedStringArray) -> void:
if parts.size() != 2:
_show_command_error("Usage: /time [dawn, day, dusk, night]")
return
if _service == null or _world_time == null:
_show_command_error("World time is unavailable.")
return
var phase_name := String(parts[1]).to_lower()
if phase_name not in ["dawn", "day", "dusk", "night"]:
_show_command_error("Usage: /time [dawn, day, dusk, night]")
return
if not _service.request_world_time_change(phase_name):
_show_command_error(
"Only the host or an operator can change world time."
)
return
close_chat()
func _handle_weather_command(parts: PackedStringArray) -> void:
if parts.size() != 2:
_show_command_error(
"Usage: /weather [clear, cloudy, rainy, foggy]"
)
return
if _service == null or _world_weather == null:
_show_command_error("World weather is unavailable.")
return
var weather_name := String(parts[1]).to_lower()
match weather_name:
"clear", "sunny":
weather_name = "clear"
"cloudy", "rainy", "foggy":
pass
_:
_show_command_error(
"Usage: /weather [clear, cloudy, rainy, foggy]"
)
return
if not _service.request_world_weather_change(weather_name):
_show_command_error(
"Only the host or an operator can change world weather."
)
return
close_chat()
func _on_world_command_finished(success: bool, message: String) -> void:
if success:
_set_status("")
elif not message.is_empty():
_set_status(message)
func _on_local_message_confirmed(message: Dictionary) -> void:
if (
not _send_pending

View file

@ -0,0 +1,159 @@
class_name GeneralInventoryGrid
extends Control
signal entry_selected(kind: int, identity: StringName)
signal slot_activated(slot: GeneralInventorySlot)
signal context_requested(slot: GeneralInventorySlot)
signal context_changed(slot: GeneralInventorySlot, text: String, active: bool)
const DEFAULT_SLOT_SIZE := Vector2(52.0, 52.0)
const DEFAULT_SLOT_SEPARATION: int = 5
const COLUMNS: int = PlayerInventoryLayout.INVENTORY_COLUMNS
var _layout: PlayerInventoryLayout
var _bag: PlayerBag
var _fish_inventory: FishInventory
var _hotbar: PlayerHotbar
var _item_catalog: ItemCatalog
var _container: int = PlayerInventoryLayout.InventoryContainer.INVENTORY
var _grid: GridContainer
var _slots: Array[GeneralInventorySlot] = []
var _slot_size: Vector2 = DEFAULT_SLOT_SIZE
var _slot_separation: int = DEFAULT_SLOT_SEPARATION
func _ready() -> void:
_grid = GridContainer.new()
_grid.columns = COLUMNS
_apply_grid_separation()
add_child(_grid)
func set_slot_presentation(slot_size: Vector2, separation: int) -> void:
_slot_size = Vector2(maxf(1.0, slot_size.x), maxf(1.0, slot_size.y))
_slot_separation = maxi(0, separation)
if _grid != null:
_apply_grid_separation()
if is_node_ready() and _layout != null:
var active_capacity := (
_layout.get_inventory_capacity()
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
else _layout.get_storage_capacity()
)
_rebuild(
PlayerInventoryLayout.MAX_INVENTORY_SLOT_COUNT
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
else PlayerInventoryLayout.MAX_STORAGE_SLOT_COUNT,
active_capacity,
)
func setup(
layout: PlayerInventoryLayout,
bag: PlayerBag,
fish_inventory: FishInventory,
hotbar: PlayerHotbar,
item_catalog: ItemCatalog,
container: int,
) -> void:
_layout = layout
_bag = bag
_fish_inventory = fish_inventory
_hotbar = hotbar
_item_catalog = item_catalog
_container = container
if _layout != null and not _layout.layout_changed.is_connected(refresh):
_layout.layout_changed.connect(refresh)
refresh()
func refresh() -> void:
if not is_node_ready() or _layout == null:
return
var active_capacity := (
_layout.get_inventory_capacity()
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
else _layout.get_storage_capacity()
)
var visible_capacity := (
PlayerInventoryLayout.MAX_INVENTORY_SLOT_COUNT
if _container == PlayerInventoryLayout.InventoryContainer.INVENTORY
else PlayerInventoryLayout.MAX_STORAGE_SLOT_COUNT
)
if _slots.size() != visible_capacity:
_rebuild(visible_capacity, active_capacity)
else:
for index: int in _slots.size():
_slots[index].set_locked(index >= active_capacity)
for slot: GeneralInventorySlot in _slots:
slot.refresh()
func get_slots() -> Array[GeneralInventorySlot]:
var active: Array[GeneralInventorySlot] = []
for slot: GeneralInventorySlot in _slots:
if not slot.disabled:
active.append(slot)
return active
func get_first_occupied_slot() -> GeneralInventorySlot:
for slot: GeneralInventorySlot in _slots:
if not slot.entry_identity.is_empty():
return slot
return _slots.front() if not _slots.is_empty() else null
func _rebuild(visible_capacity: int, active_capacity: int) -> void:
for child: Node in _grid.get_children():
child.queue_free()
_slots.clear()
for slot_index: int in visible_capacity:
var slot := GeneralInventorySlot.new()
slot.custom_minimum_size = _slot_size
slot.entry_selected.connect(
func(kind: int, identity: StringName) -> void:
entry_selected.emit(kind, identity)
)
slot.pressed.connect(func() -> void: slot_activated.emit(slot))
slot.context_requested.connect(
func(source: GeneralInventorySlot) -> void:
context_requested.emit(source)
)
slot.context_changed.connect(
func(
source: GeneralInventorySlot,
text: String,
active: bool,
) -> void:
context_changed.emit(source, text, active)
)
_grid.add_child(slot)
slot.set_presentation_size(_slot_size)
slot.configure(
slot_index,
_container,
slot_index >= active_capacity,
_layout,
_bag,
_fish_inventory,
_hotbar,
_item_catalog,
)
_slots.append(slot)
custom_minimum_size = Vector2(
float(COLUMNS) * _slot_size.x
+ float(COLUMNS - 1) * _slot_separation,
ceilf(float(visible_capacity) / float(COLUMNS)) * _slot_size.y
+ maxf(
0.0,
ceilf(float(visible_capacity) / float(COLUMNS)) - 1.0,
) * _slot_separation,
)
size = custom_minimum_size
ControllerFocusNavigation.configure_spatial_neighbors(_slots)
func _apply_grid_separation() -> void:
_grid.add_theme_constant_override("h_separation", _slot_separation)
_grid.add_theme_constant_override("v_separation", _slot_separation)

View file

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

View file

@ -0,0 +1,346 @@
class_name GeneralInventorySlot
extends Button
signal entry_selected(kind: int, identity: StringName)
signal entry_moved
signal context_requested(slot: GeneralInventorySlot)
signal context_changed(slot: GeneralInventorySlot, text: String, active: bool)
const ItemDataType = preload("res://items/item_data.gd")
const LOCK_ICON: Texture2D = preload("res://ui/icons/pictograms/lock_light.png")
var slot_index: int = -1
var container: int = -1
var entry_kind: int = -1
var entry_identity: StringName
var _locked: bool = false
var _layout: PlayerInventoryLayout
var _bag: PlayerBag
var _fish_inventory: FishInventory
var _hotbar: PlayerHotbar
var _item_catalog: ItemCatalog
var _icon: TextureRect
var _quantity: Label
var _presentation_size := Vector2(52.0, 52.0)
var _context_text: String = ""
var _context_hovered: bool = false
var _context_focused: bool = false
var _staged: bool = false
func _ready() -> void:
focus_mode = Control.FOCUS_ALL
mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
pressed.connect(_on_pressed)
focus_entered.connect(_set_context_focused.bind(true))
focus_exited.connect(_set_context_focused.bind(false))
mouse_entered.connect(_set_context_hovered.bind(true))
mouse_exited.connect(_set_context_hovered.bind(false))
gui_input.connect(_on_gui_input)
_icon = TextureRect.new()
_icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_icon)
_quantity = Label.new()
_quantity.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_quantity.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_quantity.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_quantity.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
_quantity.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_quantity)
_apply_presentation()
func set_presentation_size(presentation_size: Vector2) -> void:
_presentation_size = presentation_size
custom_minimum_size = presentation_size
if is_node_ready():
_apply_presentation()
func configure(
new_slot_index: int,
new_container: int,
is_locked: bool,
layout: PlayerInventoryLayout,
bag: PlayerBag,
fish_inventory: FishInventory,
hotbar: PlayerHotbar,
item_catalog: ItemCatalog,
) -> void:
slot_index = new_slot_index
container = new_container
_locked = is_locked
_layout = layout
_bag = bag
_fish_inventory = fish_inventory
_hotbar = hotbar
_item_catalog = item_catalog
name = "InventorySlot%d" % slot_index
refresh()
func set_locked(is_locked: bool) -> void:
if _locked == is_locked:
return
_locked = is_locked
refresh()
func refresh() -> void:
call_deferred("_update_context_presence")
entry_kind = -1
entry_identity = StringName()
_context_text = ""
_icon.texture = null
_quantity.text = ""
disabled = _locked
_apply_icon_geometry()
if _locked:
_icon.texture = LOCK_ICON
_icon.modulate = Color(UtilityPageStyle.OCEAN_DISABLED, 0.18)
var storage_slot := (
container == PlayerInventoryLayout.InventoryContainer.STORAGE
)
_context_text = (
"locked storage slot" if storage_slot else "locked backpack slot"
)
tooltip_text = ""
accessibility_name = "%s %d" % [_context_text, slot_index + 1]
return
_icon.modulate = Color.WHITE
_context_text = "empty slot"
tooltip_text = ""
accessibility_name = "empty %s slot %d" % [
"storage"
if container == PlayerInventoryLayout.InventoryContainer.STORAGE
else "inventory",
slot_index + 1,
]
if _layout == null:
return
var entry := _layout.get_entry_at(container, slot_index)
if entry.is_empty():
return
entry_kind = int(entry.get("kind", -1))
entry_identity = StringName(str(entry.get("identity", "")))
if entry_kind == PlayerInventoryLayout.EntryKind.ITEM:
var item: ItemDataType = (
_item_catalog.get_item_by_id(entry_identity)
if _item_catalog != null else null
)
if item == null:
return
_icon.texture = item.icon
var quantity := _bag.get_quantity(entry_identity) if _bag != null else 0
_quantity.text = "×%d" % quantity if quantity > 1 else ""
_context_text = _item_context_text(item, quantity)
accessibility_name = "%s, slot %d" % [item.display_name, slot_index + 1]
elif entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
var fish_catch = (
_fish_inventory.get_catch_by_id(entry_identity)
if _fish_inventory != null else null
)
if fish_catch == null:
return
_icon.texture = fish_catch.fish.display_texture
_context_text = _catch_context_text(fish_catch)
var catch_name: String = FishQuality.qualified_name(
fish_catch.fish.display_name, fish_catch.quality
)
accessibility_name = "%s, slot %d" % [catch_name, slot_index + 1]
func _on_pressed() -> void:
if _locked:
return
entry_selected.emit(entry_kind, entry_identity)
func _on_gui_input(event: InputEvent) -> void:
var mouse_event := event as InputEventMouseButton
if (
mouse_event != null
and mouse_event.button_index == MOUSE_BUTTON_RIGHT
and mouse_event.pressed
and not _locked
and not entry_identity.is_empty()
):
context_requested.emit(self)
accept_event()
func _set_context_hovered(active: bool) -> void:
_context_hovered = active
_update_context_presence()
func _set_context_focused(active: bool) -> void:
_context_focused = active
_update_context_presence()
func _update_context_presence() -> void:
if _locked or entry_identity.is_empty():
context_changed.emit(self, "", false)
return
var active: bool = _context_hovered or _context_focused
context_changed.emit(self, _context_text if active else "", active)
func set_move_source(active: bool) -> void:
modulate = Color(1.0, 1.0, 1.0, 0.42) if active else Color.WHITE
func set_staged(active: bool) -> void:
if _staged == active:
return
_staged = active
_apply_style()
func _item_context_text(item: ItemDataType, quantity: int) -> String:
var lines: Array[String] = [item.display_name]
lines.append("%s • quantity %d" % [item.get_category_name(), quantity])
if item.equippable:
lines.append("equippable")
elif item.usable:
lines.append("usable")
if not item.description.strip_edges().is_empty():
lines.append(item.description.strip_edges())
return "\n".join(lines)
func _catch_context_text(fish_catch: FishCatch) -> String:
var catch_name: String = FishQuality.qualified_name(
fish_catch.fish.display_name,
fish_catch.quality,
)
return "%s\n%0.2f lb • value %d\n%s" % [
catch_name,
fish_catch.weight_lb,
fish_catch.sale_value,
fish_catch.fish.logbook_fact,
]
func _get_drag_data(_at_position: Vector2) -> Variant:
if _locked or entry_identity.is_empty() or _icon.texture == null:
return null
var preview := TextureRect.new()
preview.custom_minimum_size = Vector2(56.0, 56.0)
preview.texture = _icon.texture
preview.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
preview.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
preview.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
preview.mouse_filter = Control.MOUSE_FILTER_IGNORE
set_drag_preview(preview)
if entry_kind == PlayerInventoryLayout.EntryKind.CATCH:
return {
"kind": "cooler_fish",
"catch_id": String(entry_identity),
}
return {
"kind": "bag_item",
"item_id": String(entry_identity),
}
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
if _locked or typeof(data) != TYPE_DICTIONARY or _layout == null:
return false
var kind := str((data as Dictionary).get("kind", ""))
return kind in ["bag_item", "cooler_fish", "hotbar_slot"]
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if not _can_drop_data(Vector2.ZERO, data):
return
var payload := data as Dictionary
var payload_kind := str(payload.get("kind", ""))
var moved: bool = false
if payload_kind == "hotbar_slot":
moved = (
_hotbar != null
and _hotbar.move_slot_to_container(
int(payload.get("slot_index", -1)),
container,
slot_index,
)
)
else:
var kind := (
PlayerInventoryLayout.EntryKind.CATCH
if payload_kind == "cooler_fish"
else PlayerInventoryLayout.EntryKind.ITEM
)
var identity := StringName(str(
payload.get(
"catch_id" if payload_kind == "cooler_fish" else "item_id",
"",
)
))
moved = _layout.move_entry(kind, identity, container, slot_index)
if moved:
entry_moved.emit()
func _apply_style() -> void:
var radius: int = roundi(minf(_presentation_size.x, _presentation_size.y) * 0.5)
var normal := UtilityPageStyle.rounded_style(
Color(
UtilityPageStyle.OCEAN_SELECTED
if _staged else UtilityPageStyle.OCEAN_FIELD,
0.92 if _staged else 0.88,
),
radius,
)
var hover := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_SELECTED, 0.92), radius
)
var locked := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_FIELD, 0.38), radius
)
for state: StringName in [&"normal", &"pressed"]:
add_theme_stylebox_override(state, normal)
add_theme_stylebox_override("disabled", locked)
for state: StringName in [&"hover", &"focus"]:
add_theme_stylebox_override(state, hover)
func _apply_presentation() -> void:
_apply_icon_geometry()
var is_large: bool = _presentation_size.x >= 70.0
var quantity_width: float = 42.0 if is_large else 34.0
var quantity_height: float = 20.0 if is_large else 17.0
_quantity.position = Vector2(
-quantity_width - (8.0 if is_large else 5.0),
-quantity_height - (6.0 if is_large else 4.0),
)
_quantity.size = Vector2(quantity_width, quantity_height)
_quantity.add_theme_font_size_override("font_size", 14 if is_large else 11)
_apply_style()
func _apply_icon_geometry() -> void:
var is_large: bool = _presentation_size.x >= 70.0
var margin: float
if _locked:
var lock_size: float = 24.0 if is_large else 18.0
margin = maxf(
(_presentation_size.x - lock_size) * 0.5,
0.0,
)
else:
margin = 10.0 if is_large else 7.0
_icon.offset_left = margin
_icon.offset_top = margin
_icon.offset_right = -margin
_icon.offset_bottom = -margin

View file

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

View file

@ -6,7 +6,10 @@ const CONTENT_MARGIN_LEFT := 18.0
const CONTENT_MARGIN_TOP := 98.0
const CONTENT_MARGIN_RIGHT := 18.0
const CONTENT_MARGIN_BOTTOM := 16.0
# Retained for the Logbook's separate handwritten presentation. Inventory
# notepads deliberately prioritize legibility with Tuffy.
const HANDWRITTEN_FONT: Font = preload("res://ui/fonts/seattle_avenue.otf")
const NOTEPAD_FONT: Font = preload("res://ui/fonts/Tuffy_Bold.otf")
const NOTEPAD_TEXTURE: Texture2D = preload("res://art/ui/ui_notepad.png")
const NOTEPAD_CANONICAL_OVERSCAN: float = 1.2
const NOTEPAD_ART_OFFSET := Vector2(10.0, 20.0)
@ -24,11 +27,11 @@ func _ready() -> void:
static func apply_handwritten_to(root: Control) -> void:
root.add_theme_font_override("font", HANDWRITTEN_FONT)
root.add_theme_font_override("font", NOTEPAD_FONT)
for descendant: Node in root.find_children("*", "Control", true, false):
var control := descendant as Control
control.add_theme_font_override(
"font", HANDWRITTEN_FONT
"font", NOTEPAD_FONT
)
if control is Label:
control.add_theme_color_override("font_color", INK_COLOR)
@ -37,7 +40,7 @@ static func apply_handwritten_to(root: Control) -> void:
func _draw() -> void:
draw_texture_rect(NOTEPAD_TEXTURE, get_art_rect(), false)
var font: Font = HANDWRITTEN_FONT
var font: Font = NOTEPAD_FONT
draw_string(
font,
Vector2(20.0, 58.0),

View file

@ -0,0 +1,71 @@
class_name ShopSaleTraySlot
extends Button
signal remove_requested(key: String)
signal drop_requested(payload: Dictionary)
var entry_key: String = ""
var _icon: TextureRect
var _quantity: Label
func _ready() -> void:
custom_minimum_size = GeneralInventoryGrid.DEFAULT_SLOT_SIZE
focus_mode = Control.FOCUS_ALL
texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
pressed.connect(func() -> void: remove_requested.emit(entry_key))
_icon = TextureRect.new()
_icon.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_icon.offset_left = 6.0
_icon.offset_top = 4.0
_icon.offset_right = -6.0
_icon.offset_bottom = -4.0
_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
_icon.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
_icon.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_icon)
_quantity = Label.new()
_quantity.set_anchors_preset(Control.PRESET_BOTTOM_RIGHT)
_quantity.position = Vector2(-34.0, -20.0)
_quantity.size = Vector2(30.0, 16.0)
_quantity.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
_quantity.mouse_filter = Control.MOUSE_FILTER_IGNORE
add_child(_quantity)
var normal := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_FIELD, 0.96), 26
)
var hover := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_SELECTED, 0.96), 26
)
for state: StringName in [&"normal", &"pressed", &"disabled"]:
add_theme_stylebox_override(state, normal)
for state: StringName in [&"hover", &"focus"]:
add_theme_stylebox_override(state, hover)
func configure(
key: String,
icon: Texture2D,
label: String,
quantity: int = 1,
) -> void:
entry_key = key
_icon.texture = icon
_quantity.text = "×%d" % quantity if quantity > 1 else ""
tooltip_text = "%s · select to remove" % label
accessibility_name = tooltip_text
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
return (
typeof(data) == TYPE_DICTIONARY
and str((data as Dictionary).get("kind", "")) in [
"bag_item", "cooler_fish"
]
)
func _drop_data(_at_position: Vector2, data: Variant) -> void:
if _can_drop_data(Vector2.ZERO, data):
drop_requested.emit((data as Dictionary).duplicate(true))

View file

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

View file

@ -104,7 +104,7 @@ const SHOP_SECTION_LABELS: Array[String] = [
"Snacks",
"Equipment",
"Art Supplies",
"Sell Fish",
"Sell",
]
const SUPPLY_ICON_GRID_COLUMNS: int = 9
const SUPPLY_ICON_TILE_SIZE := Vector2(72.0, 72.0)
@ -154,6 +154,9 @@ const ROD_PRICE_ICON_SIZE: float = 22.0
@onready var _cooler_cost: CurrencyAmount = %CoolerCost
@onready var _cooler_price_bubble: PanelContainer = %CoolerPriceBubble
@onready var _cooler_purchase: Button = %CoolerPurchase
@onready var _backpack_cost: CurrencyAmount = %BackpackCost
@onready var _backpack_price_bubble: PanelContainer = %BackpackPriceBubble
@onready var _backpack_purchase: Button = %BackpackPurchase
var _player: PlayerType
var _wallet: PlayerWalletType
@ -162,10 +165,16 @@ var _upgrades: PlayerFishingUpgradesType
var _fishing_spot: FishingSpotType
var _interaction: ShopInteractionType
var _bag: PlayerBagType
var _inventory: FishInventory
var _hotbar: PlayerHotbar
var _inventory_layout: PlayerInventoryLayout
var _item_catalog: ItemCatalogType
var _cooler_capacity: PlayerCoolerCapacityType
var _art_unlocks: PlayerArtUnlocksType
var _network_shop: NetworkShopService
var _network_sale: NetworkSaleService
var _reservations: PlayerAssetReservationService
var _sell_inventory: ShopSellInventory
var _prior_movement_enabled: bool = true
var _prior_camera_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
@ -191,6 +200,7 @@ func _ready() -> void:
_reel_purchase.pressed.connect(_purchase_reel_speed)
_barrier_purchase.pressed.connect(_purchase_barrier_power)
_cooler_purchase.pressed.connect(_purchase_cooler_capacity)
_backpack_purchase.pressed.connect(_purchase_backpack_capacity)
func setup_controller_mapping(
@ -327,7 +337,9 @@ func _request_shop_cooler() -> bool:
if not _is_transaction_context_valid():
_set_feedback("The fishing shop is no longer available.")
return false
sell_fish_requested.emit()
activate_shop_cooler_page()
if _sell_inventory != null:
_sell_inventory.activate()
return _cooler_page_active
@ -339,6 +351,7 @@ func _focus_shop_section() -> void:
_reel_purchase,
_barrier_purchase,
_cooler_purchase,
_backpack_purchase,
]:
if not upgrade_button.disabled:
upgrade_button.grab_focus()
@ -403,7 +416,7 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
_update_shop_tab_selection()
return
if _cooler_page_active:
shop_cooler_return_requested.emit()
deactivate_shop_cooler_page()
_shop_section = section_index as ShopSection
var showing_upgrades: bool = _shop_section == ShopSection.UPGRADES
_upgrades_content.visible = showing_upgrades
@ -494,6 +507,7 @@ func _apply_shop_styles() -> void:
_reel_price_bubble,
_barrier_price_bubble,
_cooler_price_bubble,
_backpack_price_bubble,
]:
var price_style := UtilityPageStyleType.rounded_style(
UtilityPageStyleType.OCEAN_FIELD,
@ -510,6 +524,7 @@ func _apply_shop_styles() -> void:
_reel_cost,
_barrier_cost,
_cooler_cost,
_backpack_cost,
]:
var amount_label := price.get_amount_label()
amount_label.add_theme_color_override(
@ -521,6 +536,7 @@ func _apply_shop_styles() -> void:
_reel_purchase,
_barrier_purchase,
_cooler_purchase,
_backpack_purchase,
]:
UtilityPageStyleType.apply_ocean_button(button)
_feedback.add_theme_color_override(
@ -536,10 +552,15 @@ func setup(
fishing_spot: FishingSpotType,
interaction: ShopInteractionType,
bag: PlayerBagType,
inventory: FishInventory,
hotbar: PlayerHotbar,
inventory_layout: PlayerInventoryLayout,
item_catalog: ItemCatalogType,
cooler_capacity: PlayerCoolerCapacityType,
art_unlocks: PlayerArtUnlocksType,
network_shop: NetworkShopService,
network_sale: NetworkSaleService,
reservations: PlayerAssetReservationService,
) -> void:
_player = player
_wallet = wallet
@ -548,10 +569,28 @@ func setup(
_fishing_spot = fishing_spot
_interaction = interaction
_bag = bag
_inventory = inventory
_hotbar = hotbar
_inventory_layout = inventory_layout
_item_catalog = item_catalog
_cooler_capacity = cooler_capacity
_art_unlocks = art_unlocks
_network_shop = network_shop
_network_sale = network_sale
_reservations = reservations
_sell_inventory = ShopSellInventory.new()
_sell_inventory.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
_shop_cooler_mount.add_child(_sell_inventory)
_sell_inventory.setup(
_inventory_layout,
_bag,
_inventory,
_hotbar,
_item_catalog,
_buyer,
_reservations,
_network_sale,
)
if (
_network_shop != null
and not _network_shop.local_purchase_pending.is_connected(
@ -574,6 +613,12 @@ func setup(
_on_cooler_capacity_changed
):
_cooler_capacity.capacity_changed.connect(_on_cooler_capacity_changed)
if not _inventory_layout.backpack_capacity_changed.is_connected(
_on_backpack_capacity_changed
):
_inventory_layout.backpack_capacity_changed.connect(
_on_backpack_capacity_changed
)
if not _art_unlocks.unlocks_changed.is_connected(_on_art_unlocks_changed):
_art_unlocks.unlocks_changed.connect(_on_art_unlocks_changed)
_refresh_all()
@ -611,6 +656,8 @@ func open_shop() -> bool:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_set_feedback("")
deactivate_shop_cooler_page()
if _sell_inventory != null:
_sell_inventory.clear_staged()
show()
_shop_tab_bar.show()
_controller_zone = ControllerZone.TABS
@ -780,6 +827,7 @@ func _refresh_all() -> void:
_refresh_upgrades()
_refresh_supplies()
_refresh_cooler_capacity()
_refresh_backpack_capacity()
func _refresh_wallet() -> void:
@ -1515,25 +1563,60 @@ func _refresh_cooler_capacity() -> void:
)
var cooler_effect: String
if cost < 0:
cooler_effect = "%d fish · maximum level" % (
cooler_effect = "%d slots · maximum level" % (
_cooler_capacity.get_capacity()
)
_cooler_price_bubble.hide()
else:
_cooler_price_bubble.show()
cooler_effect = "%d%d fish" % [
cooler_effect = "%d%d slots" % [
_cooler_capacity.get_capacity(),
next_capacity,
]
_cooler_cost.set_amount(cost)
_cooler_purchase.tooltip_text = _upgrade_tooltip(
"cooler capacity",
"storage capacity",
level,
cooler_effect,
)
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
func _refresh_backpack_capacity() -> void:
if _inventory_layout == null:
return
var level := _inventory_layout.get_backpack_level()
var cost := _inventory_layout.get_next_backpack_cost()
var next_capacity := _inventory_layout.get_next_inventory_capacity()
_backpack_purchase.disabled = (
cost < 0
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_backpack_purchase()
or not _inventory_layout.can_purchase_backpack(_wallet)
)
var effect: String
if cost < 0:
effect = "%d slots · maximum level" % (
_inventory_layout.get_inventory_capacity()
)
_backpack_price_bubble.hide()
else:
_backpack_price_bubble.show()
effect = "%d%d slots" % [
_inventory_layout.get_inventory_capacity(),
next_capacity,
]
_backpack_cost.set_amount(cost)
_backpack_purchase.tooltip_text = _upgrade_tooltip(
"backpack capacity",
level,
effect,
)
_backpack_purchase.accessibility_name = _backpack_purchase.tooltip_text
func _purchase_reel_speed() -> void:
_purchase_upgrade(true)
@ -1555,7 +1638,7 @@ func _purchase_supply(item_id: StringName) -> void:
item_id, owned, item.max_stack
)
if quantity <= 0 or not _bag.can_add_item(item_id, quantity):
_set_feedback("Your Bag is full.")
_set_feedback("Your inventory is full.")
return
var total_cost: int = FishingShopStockType.get_purchase_cost(
item_id, quantity, _bag.is_bait_unlocked(item_id)
@ -1617,6 +1700,23 @@ func _purchase_cooler_capacity() -> void:
_network_shop.request_cooler_capacity_upgrade()
func _purchase_backpack_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_set_feedback("unable to complete purchase.")
return
var cost := _inventory_layout.get_next_backpack_cost()
if cost < 0:
_set_feedback("Upgrade is already at maximum.")
return
if not _wallet.can_afford(cost):
_set_feedback("Insufficient funds.")
return
if _network_shop == null:
_set_feedback("Purchase could not be completed.")
return
_network_shop.request_backpack_capacity_upgrade()
func _purchase_upgrade(is_reel_speed: bool) -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_set_feedback("unable to complete purchase.")
@ -1721,6 +1821,7 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void:
_refresh_upgrades()
_refresh_supplies()
_refresh_cooler_capacity()
_refresh_backpack_capacity()
func _on_upgrades_changed(
@ -1738,6 +1839,10 @@ func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
_refresh_cooler_capacity()
func _on_backpack_capacity_changed(_level: int, _capacity: int) -> void:
_refresh_backpack_capacity()
func _on_art_unlocks_changed(_unlock_mask: int) -> void:
_refresh_supplies()

View file

@ -1,11 +1,10 @@
[gd_scene load_steps=9 format=3]
[gd_scene load_steps=8 format=3]
[ext_resource type="Script" path="res://ui/fishing_shop.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[ext_resource type="Texture2D" path="res://items/icons/shop/64_speed_plus.png" id="3_reel"]
[ext_resource type="Texture2D" path="res://items/icons/shop/64_power_plus.png" id="4_barrier"]
[ext_resource type="Texture2D" path="res://items/icons/shop/32_currency.png" id="5_coin"]
[ext_resource type="Texture2D" path="res://items/icons/equipment/64_cooler_plus.png" id="6_cooler"]
[ext_resource type="Texture2D" path="res://ui/icons/pictograms/x_light.png" id="7_close"]
[ext_resource type="PackedScene" path="res://ui/components/currency_amount.tscn" id="8_currency"]
@ -269,10 +268,10 @@ mouse_filter = 2
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "cooler capacity"
icon = ExtResource("6_cooler")
tooltip_text = "storage capacity"
icon = ExtResource("7_close")
expand_icon = true
icon_max_width = 128
icon_max_width = 92
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
unique_name_in_owner = true
@ -288,3 +287,32 @@ unique_name_in_owner = true
layout_mode = 2
amount = 75
icon_size = 24.0
[node name="BackpackTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
custom_minimum_size = Vector2(144, 168)
layout_mode = 2
mouse_filter = 2
[node name="BackpackPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile"]
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "backpack capacity"
icon = ExtResource("7_close")
expand_icon = true
icon_max_width = 92
[node name="BackpackPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile"]
unique_name_in_owner = true
z_index = 2
offset_left = 14.0
offset_top = 128.0
offset_right = 130.0
offset_bottom = 160.0
mouse_filter = 2
[node name="BackpackCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/BackpackTile/BackpackPriceBubble" instance=ExtResource("8_currency")]
unique_name_in_owner = true
layout_mode = 2
amount = 1500
icon_size = 24.0

View file

@ -27,6 +27,10 @@ const PlayerFishingUpgradesType = preload(
const ShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
const PlayerStorageType = preload("res://ui/player_storage.gd")
const PlayerStorageInteractionType = preload(
"res://world/player_storage_interaction.gd"
)
const PlayerItemEffectsType = preload(
"res://progression/player_item_effects.gd"
)
@ -134,6 +138,8 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
@onready var _pause_menu: PauseMenuType = %PauseMenu
@onready var _hotbar_ui: HotbarUIType = %Hotbar
@onready var _fishing_shop: FishingShopType = %FishingShop
@onready var _player_storage: PlayerStorageType = %PlayerStorage
@onready var _storage_prompt: PanelContainer = %StoragePrompt
@onready var _shop_prompt: Control = %ShopPrompt
@onready var _shop_prompt_bubble: PanelContainer = %ShopPromptBubble
@onready var _shop_prompt_message: Label = %ShopPromptMessage
@ -174,10 +180,12 @@ var _gameplay_hud_hidden: bool = false
var _fishing_spot: FishingSpotType
var _system_menu_open: bool = false
var _shop_open: bool = false
var _storage_open: bool = false
var _chat_input_open: bool = false
var _player_menu_hotbar_visible: bool = false
var _main_shop_buyer: FishBuyerProfileType
var _shop_interaction: ShopInteractionType
var _storage_interaction: PlayerStorageInteractionType
var _surface_drawing: NetworkSurfaceDrawingService
var _surface_drawing_hotbar_selected: bool = false
var _experience: PlayerExperienceType
@ -294,10 +302,12 @@ func setup(
fishing_spot: FishingSpotType,
bag: PlayerBagType,
hotbar: PlayerHotbarType,
inventory_layout: PlayerInventoryLayout,
item_catalog: ItemCatalogType,
main_shop_buyer: FishBuyerProfileType,
fishing_upgrades: PlayerFishingUpgradesType,
shop_interaction: ShopInteractionType,
storage_interaction: PlayerStorageInteractionType,
item_effects: PlayerItemEffectsType,
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
@ -398,6 +408,7 @@ func setup(
fishing_spot,
bag,
hotbar,
inventory_layout,
item_catalog,
cooler_capacity,
network_session,
@ -430,10 +441,28 @@ func setup(
fishing_spot,
shop_interaction,
bag,
inventory,
hotbar,
inventory_layout,
item_catalog,
cooler_capacity,
art_unlocks,
network_shop_service,
network_sale_service,
reservations,
)
_player_storage.setup(
player,
fishing_spot,
storage_interaction,
inventory_layout,
bag,
inventory,
hotbar,
item_catalog,
)
_player_storage.menu_visibility_changed.connect(
_on_storage_visibility_changed
)
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
@ -446,6 +475,7 @@ func setup(
)
_main_shop_buyer = main_shop_buyer
_shop_interaction = shop_interaction
_storage_interaction = storage_interaction
_surface_drawing = surface_drawing
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
set_edge_docks(
@ -505,6 +535,7 @@ func _input(event: InputEvent) -> void:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
and not _chat_input_open
and not _showcase_active
and not _virtual_mouse_active
@ -552,6 +583,7 @@ func _can_use_character_call() -> bool:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
and not _chat_input_open
and not _showcase_active
and not _virtual_mouse_active
@ -565,8 +597,16 @@ func _character_call_yields_to_world_interaction(
) -> bool:
return (
event.is_action_pressed("interact")
and _shop_interaction != null
and _shop_interaction.is_local_player_in_range()
and (
(
_shop_interaction != null
and _shop_interaction.is_local_player_in_range()
)
or (
_storage_interaction != null
and _storage_interaction.is_local_player_in_range()
)
)
and _fishing_spot != null
and _fishing_spot.can_open_fishing_shop()
and not _fishing_shop.visible
@ -614,6 +654,7 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool:
or _system_menu_open
or _player_menu_open
or _shop_open
or _storage_open
):
return false
var use_mapping: bool = (
@ -825,6 +866,7 @@ func _can_start_virtual_mouse() -> bool:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
and not _chat_input_open
and _player != null
and _fishing_spot != null
@ -1182,6 +1224,7 @@ func _can_surface_drawing_be_active() -> bool:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
and not _chat_input_open
and not _showcase_active
and _fishing_spot != null
@ -1460,6 +1503,7 @@ func _refresh_active_bait_indicator_visibility() -> void:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
@ -1494,8 +1538,10 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
_surface_drawing.deactivate()
close_player_menu_for_session_end()
_fishing_shop.close_for_session_end()
_player_storage.close_for_session_end()
_fishing_panel.visible = false
_shop_prompt.hide()
_storage_prompt.hide()
_hotbar_ui.set_presentation_visible(false, false)
_hotbar_ui.set_gameplay_input_enabled(false)
else:
@ -1526,6 +1572,7 @@ func _can_toggle_gameplay_hud() -> bool:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
and not _chat_input_open
and not _showcase_active
and not _emote_radial_menu.is_open()
@ -1540,6 +1587,7 @@ func _refresh_gameplay_hud_visibility() -> void:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
_gameplay_transient_hud.visible = show_world_hud
_experience_presentation.visible = (
@ -1558,11 +1606,13 @@ func set_system_menu_open(is_open: bool) -> void:
and not is_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
_hotbar_ui.set_drag_enabled(_player_menu_open and not is_open)
if is_open:
_hotbar_ui.set_drag_enabled(false)
_shop_prompt.hide()
_storage_prompt.hide()
_emit_interactive_pointer_ui_changed()
@ -1570,6 +1620,53 @@ func get_fishing_shop() -> FishingShopType:
return _fishing_shop
func get_player_storage() -> PlayerStorageType:
return _player_storage
func set_storage_prompt_visible(
requested_visible: bool,
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
) -> void:
if not _storage_prompt.has_meta(&"styled"):
_storage_prompt.set_meta(&"styled", true)
var style := UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_PANEL_MID, 0.96), 12
)
style.anti_aliasing = false
_storage_prompt.add_theme_stylebox_override("panel", style)
_storage_prompt.visible = (
requested_visible
and _gameplay_ui_enabled
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
if _storage_prompt.visible and world_anchor.is_finite():
_position_storage_prompt(world_anchor)
func _position_storage_prompt(world_anchor: Vector3) -> void:
if _player == null:
_storage_prompt.hide()
return
var camera := _player.get_gameplay_camera()
if camera == null or camera.is_position_behind(world_anchor):
_storage_prompt.hide()
return
var camera_size := camera.get_viewport().get_visible_rect().size
var ui_size := _canonical_stage.size
if camera_size.x <= 0.0 or camera_size.y <= 0.0:
_storage_prompt.hide()
return
var point := camera.unproject_position(world_anchor) * ui_size / camera_size
_storage_prompt.position = Vector2(
clampf(point.x - _storage_prompt.size.x * 0.5, 8.0, ui_size.x - _storage_prompt.size.x - 8.0),
clampf(point.y - _storage_prompt.size.y - 12.0, 8.0, ui_size.y - _storage_prompt.size.y - 8.0),
)
func set_shop_prompt_visible(
requested_visible: bool,
world_anchor: Vector3 = Vector3(0.0, INF, 0.0),
@ -1581,6 +1678,7 @@ func set_shop_prompt_visible(
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
if _shop_prompt.visible:
if world_anchor.is_finite():
@ -2216,9 +2314,30 @@ func _on_shop_visibility_changed(is_open: bool) -> void:
and not is_open
and not _system_menu_open
and not _player_menu_open
and not _storage_open
)
if is_open:
_shop_prompt.hide()
_storage_prompt.hide()
_emit_interactive_pointer_ui_changed()
func _on_storage_visibility_changed(is_open: bool) -> void:
_storage_open = is_open
_refresh_surface_drawing_activation()
_refresh_gameplay_hud_visibility()
_refresh_chat_availability()
_refresh_hotbar_visibility()
_hotbar_ui.set_gameplay_input_enabled(
_gameplay_ui_enabled
and not is_open
and not _system_menu_open
and not _player_menu_open
and not _shop_open
)
if is_open:
_shop_prompt.hide()
_storage_prompt.hide()
_emit_interactive_pointer_ui_changed()
@ -2338,6 +2457,7 @@ func _refresh_hotbar_visibility() -> void:
)
and not _system_menu_open
and not _shop_open
and not _storage_open
and (
not _player_menu_open
or _player_menu_hotbar_visible
@ -2349,7 +2469,11 @@ func _refresh_hotbar_visibility() -> void:
func _emit_interactive_pointer_ui_changed() -> void:
_refresh_active_bait_indicator_visibility()
interactive_pointer_ui_changed.emit(
_system_menu_open or _player_menu_open or _shop_open or _chat_input_open
_system_menu_open
or _player_menu_open
or _shop_open
or _storage_open
or _chat_input_open
)
@ -2368,6 +2492,7 @@ func _refresh_chat_availability() -> void:
and not _system_menu_open
and not _player_menu_open
and not _shop_open
and not _storage_open
)
_chat_ui.set_available(chat_available)
_chat_ui.set_hud_hidden(_gameplay_hud_hidden and not _chat_input_open)

View file

@ -13,6 +13,7 @@
[ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"]
[ext_resource type="PackedScene" path="res://ui/quick_radial_menu.tscn" id="12_quick"]
[ext_resource type="Script" path="res://ui/controller_virtual_cursor.gd" id="13_cursor"]
[ext_resource type="PackedScene" path="res://ui/player_storage.tscn" id="14_storage"]
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.032, 0.118, 0.15, 1)
@ -471,6 +472,32 @@ polygon = PackedVector2Array(-8, 0, 8, 0, 0, 10)
[node name="FishingShop" parent="UIRoot/CanonicalStage" instance=ExtResource("8_shop")]
unique_name_in_owner = true
[node name="PlayerStorage" parent="UIRoot/CanonicalStage" instance=ExtResource("14_storage")]
unique_name_in_owner = true
[node name="StoragePrompt" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
visible = false
z_index = 56
offset_right = 220.0
offset_bottom = 50.0
mouse_filter = 2
theme = ExtResource("3_theme")
[node name="Margin" type="MarginContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt"]
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="Message" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/StoragePrompt/Margin"]
layout_mode = 2
text = "E open storage"
horizontal_alignment = 1
vertical_alignment = 1
theme_override_font_sizes/font_size = 18
[node name="ChatUI" type="Control" parent="UIRoot"]
unique_name_in_owner = true
layout_mode = 1

View file

@ -19,7 +19,9 @@ const DESKTOP_REFERENCE_SIZE := Vector2(1280.0, 720.0)
const COMPACT_REFERENCE_SIZE := Vector2(640.0, 480.0)
const HOTBAR_PRESENTATION_SCALE: float = 0.80
const HOTBAR_CANONICAL_POSITION := Vector2.ZERO
const HOTBAR_MENU_POSITION := Vector2(-136.0, -90.0)
# In inventory context the row intentionally straddles the panel's bottom
# edge, visually extending the storage layout into its nine quick-access slots.
const HOTBAR_MENU_POSITION := Vector2(0.0, -30.0)
const HOTBAR_GAMEPLAY_Z_INDEX: int = 35
# PlayerMenu is z=30 and its authored inventory panels are relative z=50.
const HOTBAR_MENU_Z_INDEX: int = 90

View file

@ -34,7 +34,7 @@ static func category_label(category: Category) -> String:
Category.SHELLFISH:
return "Shellfish"
_:
return "Misc"
return "Insects"
static func empty_state(category: Category) -> String:

View file

@ -18,6 +18,13 @@ const FishQualityType = preload("res://fish/fish_quality.gd")
const CurrencyPresentationType = preload(
"res://ui/currency_presentation.gd"
)
const INBOX_ENTRY_WIDTH: float = 742.0
const ATTACHMENT_COLUMN_X: float = 516.0
const ATTACHMENT_COLUMN_WIDTH: float = 266.0
const AMOUNT_FIELD_X_WITH_CURRENCY: float = 594.0
const AMOUNT_FIELD_X_PLAIN: float = 570.0
const AMOUNT_FIELD_WIDTH_WITH_CURRENCY: float = 130.0
const AMOUNT_FIELD_WIDTH_PLAIN: float = 154.0
var _service: NetworkMailService
var _reservations: PlayerAssetReservationService
@ -434,7 +441,7 @@ func _build_ui() -> void:
root.add_child(page)
_status = Label.new()
_status.position = Vector2(18, 408)
_status.size = Vector2(1024, 28)
_status.size = Vector2(758, 28)
_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_status.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
@ -449,19 +456,19 @@ func _build_inbox() -> Control:
var title := Label.new()
title.text = "mail"
title.position = Vector2(12, 0)
title.size = Vector2(500, 42)
title.size = Vector2(300, 42)
title.add_theme_font_size_override("font_size", 30)
page.add_child(title)
_send_mail_button = Button.new()
_send_mail_button.text = "send mail"
_send_mail_button.position = Vector2(820, 0)
_send_mail_button.size = Vector2(150, 48)
_send_mail_button.position = Vector2(642, 0)
_send_mail_button.size = Vector2(140, 48)
_send_mail_button.pressed.connect(_show_compose)
page.add_child(_send_mail_button)
_archive_view_button = Button.new()
_archive_view_button.text = "archive"
_archive_view_button.position = Vector2(654, 0)
_archive_view_button.size = Vector2(154, 48)
_archive_view_button.position = Vector2(490, 0)
_archive_view_button.size = Vector2(140, 48)
_archive_view_button.pressed.connect(func() -> void:
_showing_archive = not _showing_archive
_archive_view_button.text = "inbox" if _showing_archive else "archive"
@ -470,16 +477,16 @@ func _build_inbox() -> Control:
page.add_child(_archive_view_button)
var scroll := ScrollContainer.new()
scroll.position = Vector2(12, 62)
scroll.size = Vector2(1036, 334)
scroll.size = Vector2(770, 334)
page.add_child(scroll)
_inbox_list = VBoxContainer.new()
_inbox_list.custom_minimum_size = Vector2(1008, 0)
_inbox_list.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 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)
_empty_label.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 80)
_inbox_list.add_child(_empty_label)
return page
@ -494,43 +501,43 @@ func _build_compose() -> Control:
page.add_child(title)
_greeting = OptionButton.new()
_greeting.position = Vector2(12, 48)
_greeting.size = Vector2(180, 46)
_greeting.size = Vector2(140, 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)
_recipient.position = Vector2(164, 48)
_recipient.size = Vector2(328, 46)
page.add_child(_recipient)
_body = TextEdit.new()
_body.position = Vector2(12, 106)
_body.size = Vector2(650, 218)
_body.size = Vector2(480, 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)
_salutation.size = Vector2(180, 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)
_signature.position = Vector2(204, 342)
_signature.size = Vector2(288, 36)
page.add_child(_signature)
_attachment_kind = OptionButton.new()
_attachment_kind.position = Vector2(688, 48)
_attachment_kind.size = Vector2(280, 46)
_attachment_kind.position = Vector2(ATTACHMENT_COLUMN_X, 48)
_attachment_kind.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 46)
for label: String in ["No gift", "Currency", "Fish", "Item"]:
_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.position = Vector2(ATTACHMENT_COLUMN_X, 106)
_attachment_choice.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 46)
_attachment_choice.item_selected.connect(
func(_index: int) -> void:
_update_attachment_amount_limit()
@ -539,16 +546,16 @@ func _build_compose() -> Control:
page.add_child(_attachment_choice)
_coin_available_heading = Label.new()
_coin_available_heading.text = "Available"
_coin_available_heading.position = Vector2(688, 158)
_coin_available_heading.position = Vector2(ATTACHMENT_COLUMN_X, 158)
_coin_available_heading.size = Vector2(80, 28)
page.add_child(_coin_available_heading)
_coin_available = CurrencyPresentationType.instantiate_amount(0, 18.0)
_coin_available.position = Vector2(770, 158)
_coin_available.size = Vector2(198, 28)
_coin_available.position = Vector2(598, 158)
_coin_available.size = Vector2(184, 28)
_coin_available.alignment = BoxContainer.ALIGNMENT_BEGIN
page.add_child(_coin_available)
_attachment_amount_currency_icon = TextureRect.new()
_attachment_amount_currency_icon.position = Vector2(746, 204)
_attachment_amount_currency_icon.position = Vector2(570, 204)
_attachment_amount_currency_icon.size = Vector2(18, 18)
_attachment_amount_currency_icon.texture = preload(
"res://items/icons/shop/32_currency.png"
@ -563,27 +570,27 @@ func _build_compose() -> Control:
_attachment_amount_currency_icon.mouse_filter = Control.MOUSE_FILTER_IGNORE
page.add_child(_attachment_amount_currency_icon)
_attachment_amount = LineEdit.new()
_attachment_amount.position = Vector2(770, 190)
_attachment_amount.size = Vector2(140, 46)
_attachment_amount.position = Vector2(AMOUNT_FIELD_X_WITH_CURRENCY, 190)
_attachment_amount.size = Vector2(AMOUNT_FIELD_WIDTH_WITH_CURRENCY, 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.position = Vector2(ATTACHMENT_COLUMN_X, 190)
_amount_minus.size = Vector2(44, 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.position = Vector2(734, 190)
_amount_plus.size = Vector2(48, 46)
_amount_plus.pressed.connect(_step_attachment_amount.bind(1))
page.add_child(_amount_plus)
_attachment_summary = RichTextLabel.new()
_attachment_summary.position = Vector2(688, 246)
_attachment_summary.size = Vector2(280, 78)
_attachment_summary.position = Vector2(ATTACHMENT_COLUMN_X, 246)
_attachment_summary.size = Vector2(ATTACHMENT_COLUMN_WIDTH, 78)
_attachment_summary.bbcode_enabled = true
_attachment_summary.fit_content = true
_attachment_summary.scroll_active = false
@ -591,14 +598,14 @@ func _build_compose() -> Control:
page.add_child(_attachment_summary)
_compose_cancel = Button.new()
_compose_cancel.text = "cancel"
_compose_cancel.position = Vector2(688, 342)
_compose_cancel.size = Vector2(126, 48)
_compose_cancel.position = Vector2(ATTACHMENT_COLUMN_X, 342)
_compose_cancel.size = Vector2(122, 48)
_compose_cancel.pressed.connect(_show_inbox)
page.add_child(_compose_cancel)
_send_button = Button.new()
_send_button.text = "send letter"
_send_button.position = Vector2(826, 342)
_send_button.size = Vector2(142, 48)
_send_button.position = Vector2(646, 342)
_send_button.size = Vector2(136, 48)
_send_button.pressed.connect(_send)
page.add_child(_send_button)
_recipient.item_selected.connect(func(_i: int) -> void:
@ -613,12 +620,12 @@ 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.size = Vector2(500, 350)
_letter_text.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_letter_text.vertical_alignment = VERTICAL_ALIGNMENT_TOP
page.add_child(_letter_text)
_letter_gift = RichTextLabel.new()
_letter_gift.position = Vector2(730, 46)
_letter_gift.position = Vector2(552, 46)
_letter_gift.size = Vector2(230, 180)
_letter_gift.bbcode_enabled = true
_letter_gift.fit_content = true
@ -627,7 +634,7 @@ func _build_letter() -> Control:
page.add_child(_letter_gift)
_accept = Button.new()
_accept.text = "accept gift"
_accept.position = Vector2(730, 250)
_accept.position = Vector2(552, 250)
_accept.size = Vector2(230, 48)
_accept.pressed.connect(func() -> void:
_service.accept_gift(_current_mail_id)
@ -635,7 +642,7 @@ func _build_letter() -> Control:
page.add_child(_accept)
_decline = Button.new()
_decline.text = "decline gift"
_decline.position = Vector2(730, 308)
_decline.position = Vector2(552, 308)
_decline.size = Vector2(230, 48)
_decline.pressed.connect(func() -> void:
_service.decline_gift(_current_mail_id)
@ -649,13 +656,13 @@ func _build_letter() -> Control:
page.add_child(_letter_close)
_archive = Button.new()
_archive.text = "archive"
_archive.position = Vector2(550, 370)
_archive.position = Vector2(466, 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.position = Vector2(626, 370)
_delete.size = Vector2(150, 48)
_delete.pressed.connect(_delete_current)
page.add_child(_delete)
@ -751,7 +758,7 @@ func _refresh_inbox() -> void:
empty.text = (
"No archived letters." if _showing_archive else "No letters yet."
)
empty.custom_minimum_size = Vector2(1008, 80)
empty.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 80)
empty.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_inbox_list.add_child(empty)
call_deferred("_refresh_controller_navigation")
@ -779,7 +786,7 @@ func _refresh_inbox() -> void:
first_line.left(72),
" · gift enclosed" if gift else "",
]
button.custom_minimum_size = Vector2(1008, 54)
button.custom_minimum_size = Vector2(INBOX_ENTRY_WIDTH, 54)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.pressed.connect(_open_letter.bind(str(letter["mail_id"])))
UtilityPageStyle.apply_ocean_button(button)
@ -826,8 +833,14 @@ func _refresh_attachment_choices(_index: int) -> void:
_coin_available_heading.visible = currency_selected
_coin_available.visible = currency_selected
_attachment_amount_currency_icon.visible = currency_selected
_attachment_amount.position.x = 770.0 if currency_selected else 746.0
_attachment_amount.size.x = 140.0 if currency_selected else 164.0
_attachment_amount.position.x = (
AMOUNT_FIELD_X_WITH_CURRENCY
if currency_selected else AMOUNT_FIELD_X_PLAIN
)
_attachment_amount.size.x = (
AMOUNT_FIELD_WIDTH_WITH_CURRENCY
if currency_selected else AMOUNT_FIELD_WIDTH_PLAIN
)
match _attachment_kind.selected:
0:
_attachment_choice.add_item("No attachment")

File diff suppressed because it is too large Load diff

View file

@ -72,15 +72,16 @@ unique_name_in_owner = true
visible = false
z_index = 40
layout_mode = 0
offset_left = 150.0
offset_left = 513.0
offset_top = 136.0
offset_right = 664.0
offset_right = 767.0
offset_bottom = 174.0
mouse_filter = 1
theme_override_constants/separation = 6
[node name="CoolerSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(124, 38)
layout_mode = 2
toggle_mode = true
@ -94,12 +95,13 @@ custom_minimum_size = Vector2(124, 38)
layout_mode = 2
toggle_mode = true
button_group = null
text = "Equipment"
text = "Inventory"
script = ExtResource("23_organizer_tab")
palette_index = 2
[node name="ItemsSubTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/InventorySubTabs"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(124, 38)
layout_mode = 2
toggle_mode = true
@ -494,10 +496,10 @@ mouse_filter = 1
unique_name_in_owner = true
z_index = 50
layout_mode = 0
offset_left = 122.0
offset_top = 132.0
offset_right = 1018.0
offset_bottom = 574.0
offset_left = 199.0
offset_top = 166.0
offset_right = 1081.0
offset_bottom = 650.0
[node name="BagOuterMargin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall"]
layout_mode = 2
@ -524,7 +526,7 @@ horizontal_scroll_mode = 0
[node name="BagHost" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagOuterWall/BagOuterMargin/BagInnerLiner/BagInnerMargin/BagScroll"]
unique_name_in_owner = true
custom_minimum_size = Vector2(788, 358)
custom_minimum_size = Vector2(810, 420)
layout_mode = 2
mouse_filter = 1
@ -553,14 +555,26 @@ text = "your bag is empty"
horizontal_alignment = 1
vertical_alignment = 1
[node name="BagModalBlocker" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
unique_name_in_owner = true
visible = false
z_index = 110
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
[node name="BagDetailConstellation" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage"]
unique_name_in_owner = true
visible = false
layout_mode = 0
offset_left = 984.0
offset_top = 266.0
offset_right = 1256.0
offset_bottom = 570.0
offset_left = 501.0
offset_top = 166.0
offset_right = 779.0
offset_bottom = 650.0
mouse_filter = 2
[node name="BagDetailBubble" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation"]
@ -572,7 +586,7 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("22_inventory_notepad")
title_text = "bag notes"
title_text = "inventory notes"
[node name="BagDetailLayout" type="VBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble"]
layout_mode = 2
@ -601,6 +615,25 @@ theme_override_font_sizes/font_size = 15
horizontal_alignment = 1
autowrap_mode = 2
[node name="BagDetailActions" type="HBoxContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 6
alignment = 1
[node name="BagFavoriteButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout/BagDetailActions" instance=ExtResource("12_ink_action")]
unique_name_in_owner = true
custom_minimum_size = Vector2(104, 50)
layout_mode = 2
text = "favorite"
allow_persistent_mark = true
[node name="BagSellButton" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/BagPage/BagDetailConstellation/BagDetailBubble/BagDetailLayout/BagDetailActions" instance=ExtResource("12_ink_action")]
unique_name_in_owner = true
custom_minimum_size = Vector2(104, 50)
layout_mode = 2
text = "sell fish"
[node name="TackleBoxPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true
visible = false
@ -613,10 +646,10 @@ mouse_filter = 1
unique_name_in_owner = true
z_index = 50
layout_mode = 0
offset_left = 54.0
offset_left = 199.0
offset_top = 166.0
offset_right = 936.0
offset_bottom = 602.0
offset_right = 1081.0
offset_bottom = 650.0
[node name="Margin" type="MarginContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel"]
layout_mode = 2
@ -667,7 +700,7 @@ layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 28
theme_override_constants/v_separation = 12
columns = 3
columns = 4
[node name="BaitEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/BaitColumn/BaitBody"]
unique_name_in_owner = true
@ -720,7 +753,7 @@ layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/h_separation = 28
theme_override_constants/v_separation = 12
columns = 3
columns = 4
[node name="LureEmpty" type="Label" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage/TackleMainPanel/Margin/Layout/TackleColumns/LureColumn/LureBody"]
unique_name_in_owner = true
@ -735,13 +768,26 @@ text = "No lures collected."
horizontal_alignment = 1
vertical_alignment = 1
[node name="TackleModalBlocker" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
unique_name_in_owner = true
visible = false
z_index = 110
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 0
[node name="TackleDetailPanel" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/TackleBoxPage"]
unique_name_in_owner = true
visible = false
layout_mode = 0
offset_left = 952.0
offset_left = 501.0
offset_top = 166.0
offset_right = 1230.0
offset_bottom = 602.0
offset_right = 779.0
offset_bottom = 650.0
script = ExtResource("22_inventory_notepad")
title_text = "tackle box notes"
@ -861,9 +907,9 @@ custom_minimum_size = Vector2(140, 44)
[node name="NavigationCluster" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
unique_name_in_owner = true
layout_mode = 0
offset_left = 330.0
offset_left = 388.0
offset_top = 44.0
offset_right = 1170.0
offset_right = 1228.0
offset_bottom = 144.0
mouse_filter = 1
script = ExtResource("5_cluster")

317
ui/player_storage.gd Normal file
View file

@ -0,0 +1,317 @@
class_name PlayerStorage
extends Control
const INPUT_OWNER: StringName = &"player_storage"
signal menu_visibility_changed(is_open: bool)
var _player: Player
var _fishing_spot: FishingSpot
var _interaction: PlayerStorageInteraction
var _layout: PlayerInventoryLayout
var _inventory_grid: GeneralInventoryGrid
var _storage_grid: GeneralInventoryGrid
var _inventory_count: Label
var _storage_count: Label
var _feedback: Label
var _prior_movement_enabled: bool = true
var _prior_camera_enabled: bool = true
var _prior_mouse_mode: Input.MouseMode = Input.MOUSE_MODE_VISIBLE
func _ready() -> void:
UtilityPageStyle.apply_page(self)
_build_ui()
hide()
func setup(
player: Player,
fishing_spot: FishingSpot,
interaction: PlayerStorageInteraction,
layout: PlayerInventoryLayout,
bag: PlayerBag,
fish_inventory: FishInventory,
hotbar: PlayerHotbar,
item_catalog: ItemCatalog,
) -> void:
_player = player
_fishing_spot = fishing_spot
_interaction = interaction
_layout = layout
_inventory_grid.setup(
layout,
bag,
fish_inventory,
hotbar,
item_catalog,
PlayerInventoryLayout.InventoryContainer.INVENTORY,
)
_storage_grid.setup(
layout,
bag,
fish_inventory,
hotbar,
item_catalog,
PlayerInventoryLayout.InventoryContainer.STORAGE,
)
_inventory_grid.entry_selected.connect(
_on_entry_selected.bind(
PlayerInventoryLayout.InventoryContainer.STORAGE
)
)
_storage_grid.entry_selected.connect(
_on_entry_selected.bind(
PlayerInventoryLayout.InventoryContainer.INVENTORY
)
)
if _layout != null and not _layout.layout_changed.is_connected(_refresh):
_layout.layout_changed.connect(_refresh)
_refresh()
func open_storage() -> bool:
if (
visible
or _player == null
or _interaction == null
or not _interaction.is_local_player_in_range()
or _fishing_spot == null
or not _fishing_spot.can_open_fishing_shop()
):
return false
_prior_movement_enabled = _player.is_movement_enabled()
_prior_camera_enabled = _player.is_camera_input_enabled()
_prior_mouse_mode = Input.mouse_mode
_player.set_movement_enabled(false)
_player.set_camera_input_enabled(false)
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, true)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_feedback.text = "select an item to move it · drag to choose a slot"
_refresh()
show()
call_deferred("_focus_first_slot")
menu_visibility_changed.emit(true)
return true
func close_storage(restore_controls: bool = true) -> void:
if not visible:
return
var viewport := get_viewport()
if viewport != null:
viewport.gui_release_focus()
hide()
if _fishing_spot != null:
_fishing_spot.set_local_menu_input_suppressed(INPUT_OWNER, false)
if restore_controls and _player != null:
_player.set_movement_enabled(_prior_movement_enabled)
_player.set_camera_input_enabled(_prior_camera_enabled)
Input.mouse_mode = _prior_mouse_mode
menu_visibility_changed.emit(false)
func close_for_range_exit() -> void:
close_storage()
func close_for_water_recovery() -> void:
close_storage(false)
func close_for_session_end() -> void:
close_storage(false)
func consume_escape() -> bool:
if not visible:
return false
close_storage()
return true
func _unhandled_input(event: InputEvent) -> void:
if not visible or not event.is_action_pressed("ui_cancel"):
return
close_storage()
get_viewport().set_input_as_handled()
func _on_entry_selected(
kind: int,
identity: StringName,
target_container: int,
) -> void:
if kind < 0 or identity.is_empty() or _layout == null:
return
if _layout.move_entry_to_first_free(kind, identity, target_container):
_feedback.text = (
"moved to storage"
if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE
else "moved to inventory"
)
else:
_feedback.text = (
"storage is full"
if target_container == PlayerInventoryLayout.InventoryContainer.STORAGE
else "inventory is full"
)
func _refresh() -> void:
if _layout == null:
return
_inventory_grid.refresh()
_storage_grid.refresh()
_inventory_count.text = "%d / %d" % [
_layout.get_inventory_count(),
_layout.get_inventory_capacity(),
]
_storage_count.text = "%d / %d" % [
_layout.get_storage_count(),
_layout.get_storage_capacity(),
]
call_deferred("_connect_grid_focus")
func _focus_first_slot() -> void:
var slot := _inventory_grid.get_first_occupied_slot()
if slot == null:
slot = _storage_grid.get_first_occupied_slot()
if slot != null:
slot.grab_focus()
func _connect_grid_focus() -> void:
var inventory_slots := _inventory_grid.get_slots()
var storage_slots := _storage_grid.get_slots()
if inventory_slots.is_empty() or storage_slots.is_empty():
return
var row_count := mini(
ceili(
float(inventory_slots.size())
/ float(PlayerInventoryLayout.INVENTORY_COLUMNS)
),
ceili(
float(storage_slots.size())
/ float(PlayerInventoryLayout.INVENTORY_COLUMNS)
),
)
for row: int in row_count:
var inventory_index := mini(
row * PlayerInventoryLayout.INVENTORY_COLUMNS
+ PlayerInventoryLayout.INVENTORY_COLUMNS - 1,
inventory_slots.size() - 1,
)
var storage_index := mini(
row * PlayerInventoryLayout.INVENTORY_COLUMNS,
storage_slots.size() - 1,
)
inventory_slots[inventory_index].focus_neighbor_right = (
inventory_slots[inventory_index].get_path_to(storage_slots[storage_index])
)
storage_slots[storage_index].focus_neighbor_left = (
storage_slots[storage_index].get_path_to(inventory_slots[inventory_index])
)
func _build_ui() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
z_index = 82
mouse_filter = Control.MOUSE_FILTER_STOP
var blocker := ColorRect.new()
blocker.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
blocker.color = Color(0.02, 0.08, 0.10, 0.55)
blocker.mouse_filter = Control.MOUSE_FILTER_STOP
add_child(blocker)
var panel := PanelContainer.new()
panel.set_anchors_preset(Control.PRESET_CENTER)
panel.position = Vector2(-580.0, -310.0)
panel.size = Vector2(1160.0, 620.0)
panel.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_DEEP, 22),
)
add_child(panel)
var outer := MarginContainer.new()
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
outer.add_theme_constant_override(side, 20)
panel.add_child(outer)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 12)
outer.add_child(layout)
var header := HBoxContainer.new()
layout.add_child(header)
var title := Label.new()
title.text = "private storage"
title.add_theme_font_size_override("font_size", 32)
title.add_theme_color_override("font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
var close := Button.new()
close.text = "×"
close.tooltip_text = "Close storage"
close.custom_minimum_size = Vector2(48.0, 48.0)
close.flat = true
close.add_theme_font_size_override("font_size", 30)
close.pressed.connect(close_storage)
header.add_child(close)
var columns := HBoxContainer.new()
columns.size_flags_vertical = Control.SIZE_EXPAND_FILL
columns.add_theme_constant_override("separation", 18)
layout.add_child(columns)
var inventory_column := _build_column(columns, "inventory")
_inventory_count = inventory_column["count"]
_inventory_grid = GeneralInventoryGrid.new()
(inventory_column["scroll"] as ScrollContainer).add_child(_inventory_grid)
var storage_column := _build_column(columns, "storage")
_storage_count = storage_column["count"]
_storage_grid = GeneralInventoryGrid.new()
(storage_column["scroll"] as ScrollContainer).add_child(_storage_grid)
_feedback = Label.new()
_feedback.text = "select an item to move it · drag to choose a slot"
_feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_feedback.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
layout.add_child(_feedback)
func _build_column(parent: HBoxContainer, title_text: String) -> Dictionary:
var panel := PanelContainer.new()
panel.custom_minimum_size = Vector2(550.0, 0.0)
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_MID, 16),
)
parent.add_child(panel)
var margin := MarginContainer.new()
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
margin.add_theme_constant_override(side, 12)
panel.add_child(margin)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 8)
margin.add_child(column)
var header := HBoxContainer.new()
column.add_child(header)
var title := Label.new()
title.text = title_text
title.add_theme_font_size_override("font_size", 22)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
var count := Label.new()
count.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
count.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
header.add_child(count)
var scroll := ScrollContainer.new()
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
column.add_child(scroll)
return {"count": count, "scroll": scroll}

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

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

14
ui/player_storage.tscn Normal file
View file

@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/player_storage.gd" id="1_script"]
[node name="PlayerStorage" type="Control"]
unique_name_in_owner = true
visible = false
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_script")

View file

@ -191,7 +191,10 @@ func _build() -> void:
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
root.add_child(scroll)
_list = VBoxContainer.new()
_list.custom_minimum_size = Vector2(1032, 0)
_list.custom_minimum_size = Vector2(
UtilityPageStyle.LAPTOP_CONTENT_SIZE.x,
0.0,
)
_list.add_theme_constant_override("separation", 7)
scroll.add_child(_list)
_status = Label.new()
@ -207,18 +210,22 @@ func _build_host_settings(root: VBoxContainer) -> void:
"panel", UtilityPageStyle.row_style(false)
)
root.add_child(_host_settings_panel)
var row := HBoxContainer.new()
row.custom_minimum_size.y = 58.0
row.add_theme_constant_override("separation", 10)
_host_settings_panel.add_child(row)
var settings := VBoxContainer.new()
settings.custom_minimum_size.y = 104.0
settings.add_theme_constant_override("separation", 8)
_host_settings_panel.add_child(settings)
var identity_row := HBoxContainer.new()
identity_row.add_theme_constant_override("separation", 10)
settings.add_child(identity_row)
var label := Label.new()
label.text = "room name"
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
row.add_child(label)
identity_row.add_child(label)
_room_name_edit = LineEdit.new()
_room_name_edit.custom_minimum_size = Vector2(300.0, 42.0)
_room_name_edit.custom_minimum_size = Vector2(0.0, 42.0)
_room_name_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH
_room_name_edit.placeholder_text = "Player Name's Server"
UtilityPageStyle.apply_ocean_line_edit(_room_name_edit)
@ -226,11 +233,14 @@ func _build_host_settings(root: VBoxContainer) -> void:
func(_value: String) -> void: _commit_room_name()
)
_room_name_edit.focus_exited.connect(_commit_room_name)
row.add_child(_room_name_edit)
_online_toggle = _build_host_toggle("online", row)
identity_row.add_child(_room_name_edit)
var access_row := HBoxContainer.new()
access_row.add_theme_constant_override("separation", 10)
settings.add_child(access_row)
_online_toggle = _build_host_toggle("online", access_row)
_online_state_label = _online_toggle.get_node("StateBadge/State") as Label
_online_toggle.pressed.connect(_on_online_pressed)
_discoverable_toggle = _build_host_toggle("discovery", row)
_discoverable_toggle = _build_host_toggle("discovery", access_row)
_discoverable_state_label = (
_discoverable_toggle.get_node("StateBadge/State") as Label
)
@ -244,12 +254,12 @@ func _build_host_settings(root: VBoxContainer) -> void:
_host_discovery_status.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
row.add_child(_host_discovery_status)
access_row.add_child(_host_discovery_status)
func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button:
var button := Button.new()
button.custom_minimum_size = Vector2(150.0, 46.0)
button.custom_minimum_size = Vector2(140.0, 46.0)
button.text = label_text
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.clip_contents = false
@ -448,7 +458,8 @@ func _build_active_rows() -> void:
for entry: PlayerListEntry in entries:
var row := _make_row()
var identity := Label.new()
identity.custom_minimum_size.x = 430
identity.custom_minimum_size.x = 250
identity.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var markers: Array[String] = []
if entry.is_host:
markers.append("host")
@ -470,7 +481,7 @@ func _build_active_rows() -> void:
)
row.add_child(identity)
var ping := Label.new()
ping.custom_minimum_size.x = 80
ping.custom_minimum_size.x = 60
ping.text = (
"Local" if entry.is_host
else "%d ms" % entry.ping_to_host_ms
@ -484,31 +495,31 @@ func _build_active_rows() -> void:
mute.text = "unmute" if entry.muted else "mute"
mute.disabled = entry.is_local_player
mute.pressed.connect(_toggle_mute.bind(entry))
UtilityPageStyle.apply_ocean_button(mute)
UtilityPageStyle.apply_compact_ocean_button(mute)
row.add_child(mute)
var block := Button.new()
block.text = "block"
block.disabled = entry.is_local_player
block.pressed.connect(_confirm_block.bind(entry))
UtilityPageStyle.apply_ocean_button(block)
UtilityPageStyle.apply_compact_ocean_button(block)
row.add_child(block)
if entry.can_manage_operator:
var operator := Button.new()
operator.text = "deop" if entry.is_operator else "op"
operator.pressed.connect(_confirm_operator.bind(entry))
UtilityPageStyle.apply_ocean_button(operator)
UtilityPageStyle.apply_compact_ocean_button(operator)
row.add_child(operator)
var kick := Button.new()
kick.text = "kick"
kick.disabled = not entry.can_kick
kick.pressed.connect(_confirm_kick.bind(entry))
UtilityPageStyle.apply_ocean_button(kick)
UtilityPageStyle.apply_compact_ocean_button(kick)
row.add_child(kick)
var ban := Button.new()
ban.text = "ban"
ban.disabled = not entry.can_ban
ban.pressed.connect(_confirm_ban.bind(entry))
UtilityPageStyle.apply_ocean_button(ban)
UtilityPageStyle.apply_compact_ocean_button(ban)
row.add_child(ban)
@ -516,7 +527,8 @@ func _build_session_artwork_controls() -> void:
var counts: Vector2i = _service.get_session_artwork_counts()
var row := _make_row()
var label := Label.new()
label.custom_minimum_size.x = 830
label.custom_minimum_size.x = 600
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.text = "session artwork · %d layers · %d painted pixels" % [
counts.x, counts.y,
]
@ -534,7 +546,7 @@ func _build_session_artwork_controls() -> void:
_service.reset_session_artwork,
)
)
UtilityPageStyle.apply_ocean_button(reset)
UtilityPageStyle.apply_compact_ocean_button(reset)
row.add_child(reset)
@ -546,7 +558,8 @@ func _build_relationship_rows() -> void:
for record: Dictionary in records:
var row := _make_row()
var label := Label.new()
label.custom_minimum_size.x = 690
label.custom_minimum_size.x = 560
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var fingerprint := str(record["fingerprint"])
label.text = "%s · %s %s" % [
str(record.get("last_known_display_name", "Player")),
@ -564,7 +577,7 @@ func _build_relationship_rows() -> void:
unblock.pressed.connect(func() -> void:
_service.set_blocked(fingerprint, str(record["last_known_display_name"]), false)
)
UtilityPageStyle.apply_ocean_button(unblock)
UtilityPageStyle.apply_compact_ocean_button(unblock)
row.add_child(unblock)
var unmute := Button.new()
unmute.text = "unmute"
@ -572,7 +585,7 @@ func _build_relationship_rows() -> void:
unmute.pressed.connect(func() -> void:
_service.set_muted(fingerprint, str(record["last_known_display_name"]), false)
)
UtilityPageStyle.apply_ocean_button(unmute)
UtilityPageStyle.apply_compact_ocean_button(unmute)
row.add_child(unmute)
@ -585,7 +598,8 @@ func _build_ban_rows() -> void:
var row := _make_row()
var fingerprint := str(record["target_fingerprint"])
var label := Label.new()
label.custom_minimum_size.x = 830
label.custom_minimum_size.x = 600
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
label.text = "%s · %s banned %s" % [
str(record.get("last_known_display_name", "Player")),
NetworkIdentityCrypto.compact_suffix(fingerprint),
@ -599,7 +613,7 @@ func _build_ban_rows() -> void:
var unban := Button.new()
unban.text = "unban"
unban.pressed.connect(_confirm_unban.bind(fingerprint))
UtilityPageStyle.apply_ocean_button(unban)
UtilityPageStyle.apply_compact_ocean_button(unban)
row.add_child(unban)

View file

@ -3,13 +3,16 @@ extends Control
const CHECK_DEBOUNCE_SECONDS: float = 0.4
const APPEARANCE_PREVIEW_INTERVAL_SECONDS: float = 0.08
const OPTION_GRID_COLUMNS: int = 6
const FUR_PALETTE_GRID_COLUMNS: int = 10
const FUR_PATTERN_GRID_COLUMNS: int = 4
const OPTION_GRID_COLUMNS: int = 3
const FUR_PALETTE_GRID_COLUMNS: int = 6
const FUR_PATTERN_GRID_COLUMNS: int = 2
const FUR_CHANNEL_GRID_COLUMNS: int = 2
const FUR_PALETTE_SWATCH_SIZE: float = 38.0
const FUR_CHANNEL_SWATCH_SIZE: float = 22.0
const FUR_PART_TAB_WIDTH: float = 58.0
const FUR_COLOR_PICKER_POPUP_SIZE: Vector2i = Vector2i(720, 560)
const FUR_COLOR_PICKER_SV_SIZE: Vector2i = Vector2i(520, 300)
const VOICE_OPTION_BUTTON_SIZE: Vector2 = Vector2(68.0, 32.0)
const FUR_SECTION_PATTERNS: String = "patterns"
const FUR_SECTION_COLORS: String = "colors"
const FEATURE_DRAWER_ANIMATION_SECONDS: float = 0.16
@ -722,7 +725,7 @@ func _build_ui() -> void:
_category_list.add_theme_constant_override("separation", 5)
category_scroll.add_child(_category_list)
_option_list = VBoxContainer.new()
_option_list.custom_minimum_size = Vector2(360, 0)
_option_list.custom_minimum_size = Vector2(326, 0)
_option_list.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_option_list.size_flags_vertical = Control.SIZE_EXPAND_FILL
_option_list.add_theme_constant_override("separation", 5)
@ -732,13 +735,13 @@ func _build_ui() -> void:
body_spacer.size_flags_horizontal = Control.SIZE_SHRINK_CENTER
body.add_child(body_spacer)
var preview_stack := VBoxContainer.new()
preview_stack.custom_minimum_size.x = 260.0
preview_stack.custom_minimum_size.x = 240.0
preview_stack.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_stack.alignment = BoxContainer.ALIGNMENT_CENTER
preview_stack.add_theme_constant_override("separation", 7)
body.add_child(preview_stack)
var preview_frame := PanelContainer.new()
preview_frame.custom_minimum_size = Vector2(260.0, 0.0)
preview_frame.custom_minimum_size = Vector2(240.0, 0.0)
preview_frame.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_frame.add_theme_stylebox_override(
"panel", UtilityPageStyle.rounded_style(
@ -752,7 +755,7 @@ func _build_ui() -> void:
preview_layer.mouse_filter = Control.MOUSE_FILTER_PASS
preview_frame.add_child(preview_layer)
_preview = preload("res://ui/profile_preview.tscn").instantiate()
_preview.custom_minimum_size = Vector2(260.0, 0.0)
_preview.custom_minimum_size = Vector2(240.0, 0.0)
_preview.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_preview.size_flags_vertical = Control.SIZE_EXPAND_FILL
preview_layer.add_child(_preview)
@ -911,7 +914,7 @@ func _build_voice_options() -> void:
var settings_grid := GridContainer.new()
settings_grid.name = "VoiceSettingsGrid"
settings_grid.columns = 2
settings_grid.add_theme_constant_override("h_separation", 10)
settings_grid.add_theme_constant_override("h_separation", 8)
settings_grid.add_theme_constant_override("v_separation", 8)
settings_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_option_list.add_child(settings_grid)
@ -943,7 +946,7 @@ func _build_voice_options() -> void:
sample_set_button.text = str(option.get("label", sample_set_id))
sample_set_button.toggle_mode = true
sample_set_button.button_group = sample_set_group
sample_set_button.custom_minimum_size = Vector2(108.0, 32.0)
sample_set_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
sample_set_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
sample_set_button.button_pressed = (
_draft_sample_set_id == sample_set_id
@ -951,7 +954,7 @@ func _build_voice_options() -> void:
sample_set_button.pressed.connect(
_select_sample_set_option.bind(sample_set_id)
)
UtilityPageStyle.apply_compact_ocean_button(sample_set_button)
_apply_voice_option_button(sample_set_button)
sample_set_grid.add_child(sample_set_button)
var pitch_title := Label.new()
pitch_title.text = "pitch"
@ -976,11 +979,11 @@ func _build_voice_options() -> void:
button.text = str(option.get("label", option_id))
button.toggle_mode = true
button.button_group = pitch_group
button.custom_minimum_size = Vector2(108.0, 32.0)
button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.button_pressed = _draft_voice_id == option_id
button.pressed.connect(_select_voice_option.bind(option_id))
UtilityPageStyle.apply_compact_ocean_button(button)
_apply_voice_option_button(button)
grid.add_child(button)
var speed_title := Label.new()
speed_title.text = "playback speed"
@ -1008,13 +1011,13 @@ func _build_voice_options() -> void:
)
speed_button.toggle_mode = true
speed_button.button_group = speed_group
speed_button.custom_minimum_size = Vector2(108.0, 32.0)
speed_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
speed_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
speed_button.button_pressed = _draft_speech_speed_id == speed_id
speed_button.pressed.connect(
_select_speech_speed_option.bind(speed_id)
)
UtilityPageStyle.apply_compact_ocean_button(speed_button)
_apply_voice_option_button(speed_button)
speed_grid.add_child(speed_button)
var call_title := Label.new()
call_title.text = "call (G)"
@ -1039,14 +1042,29 @@ func _build_voice_options() -> void:
call_button.text = str(option.get("label", call_id))
call_button.toggle_mode = true
call_button.button_group = call_group
call_button.custom_minimum_size = Vector2(108.0, 32.0)
call_button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
call_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
call_button.button_pressed = _draft_call_id == call_id
call_button.pressed.connect(_select_call_option.bind(call_id))
UtilityPageStyle.apply_compact_ocean_button(call_button)
_apply_voice_option_button(call_button)
call_grid.add_child(call_button)
func _apply_voice_option_button(button: Button) -> void:
UtilityPageStyle.apply_compact_ocean_button(button)
button.custom_minimum_size = VOICE_OPTION_BUTTON_SIZE
button.add_theme_font_size_override("font_size", 12)
for state: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var style := button.get_theme_stylebox(state).duplicate() as StyleBoxFlat
if style == null:
continue
style.content_margin_left = 6.0
style.content_margin_right = 6.0
button.add_theme_stylebox_override(state, style)
func _select_sample_set_option(sample_set_id: String) -> void:
if not VoiceProfilesType.is_valid_sample_set(sample_set_id):
return
@ -1473,7 +1491,7 @@ func _build_fur_pattern_options() -> void:
var part_tabs := HBoxContainer.new()
part_tabs.name = "FurPatternPartTabs"
part_tabs.custom_minimum_size.y = 38.0
part_tabs.add_theme_constant_override("separation", 4)
part_tabs.add_theme_constant_override("separation", 2)
part_tab_margin.add_child(part_tabs)
for style_index: int in range(
CharacterCustomizationCatalog.FUR_STYLE_IDS.size()
@ -1486,7 +1504,7 @@ func _build_fur_pattern_options() -> void:
style_field
)
part_tab.palette_index = mini(style_index, 2)
part_tab.custom_minimum_size = Vector2(90.0, 38.0)
part_tab.custom_minimum_size = Vector2(FUR_PART_TAB_WIDTH, 38.0)
part_tab.focus_mode = Control.FOCUS_ALL
part_tab.mouse_filter = Control.MOUSE_FILTER_STOP
part_tab.pressed.connect(_select_fur_pattern_part.bind(style_field))
@ -1586,7 +1604,7 @@ func _build_fur_color_channels(options: Array) -> void:
var channel_grid := GridContainer.new()
channel_grid.name = "FurColorChannelGrid"
channel_grid.columns = CharacterCustomizationCatalog.FUR_COLOR_IDS.size()
channel_grid.columns = FUR_CHANNEL_GRID_COLUMNS
channel_grid.size_flags_horizontal = Control.SIZE_EXPAND_FILL
channel_grid.add_theme_constant_override("h_separation", 10)
channel_grid.add_theme_constant_override("v_separation", 8)

366
ui/shop_sell_inventory.gd Normal file
View file

@ -0,0 +1,366 @@
class_name ShopSellInventory
extends Control
const MAIN_SHOP_BUYER_ID: StringName = &"main_fishing_shop"
var _layout: PlayerInventoryLayout
var _bag: PlayerBag
var _fish_inventory: FishInventory
var _item_catalog: ItemCatalog
var _buyer: FishBuyerProfile
var _reservations: PlayerAssetReservationService
var _network_sale: NetworkSaleService
var _inventory_grid: GeneralInventoryGrid
var _tray_grid: GridContainer
var _total_label: Label
var _feedback: Label
var _sell_button: Button
var _staged: Dictionary[String, Dictionary] = {}
func _ready() -> void:
UtilityPageStyle.apply_page(self)
_build_ui()
func setup(
layout: PlayerInventoryLayout,
bag: PlayerBag,
fish_inventory: FishInventory,
hotbar: PlayerHotbar,
item_catalog: ItemCatalog,
buyer: FishBuyerProfile,
reservations: PlayerAssetReservationService,
network_sale: NetworkSaleService,
) -> void:
_layout = layout
_bag = bag
_fish_inventory = fish_inventory
_item_catalog = item_catalog
_buyer = buyer
_reservations = reservations
_network_sale = network_sale
_inventory_grid.setup(
layout,
bag,
fish_inventory,
hotbar,
item_catalog,
PlayerInventoryLayout.InventoryContainer.INVENTORY,
)
_inventory_grid.entry_selected.connect(_on_inventory_entry_selected)
if _layout != null:
_layout.layout_changed.connect(_refresh)
if _network_sale != null:
_network_sale.local_sale_pending.connect(_on_sale_pending)
_network_sale.local_sale_finished.connect(_on_sale_finished)
_refresh()
func activate() -> void:
_feedback.text = "select or drag items into the sell tray"
_refresh()
var slot := _inventory_grid.get_first_occupied_slot()
if slot != null:
slot.grab_focus()
func clear_staged() -> void:
_staged.clear()
_refresh_tray()
func _on_inventory_entry_selected(kind: int, identity: StringName) -> void:
_stage(kind, identity)
func _stage(kind: int, identity: StringName) -> void:
if identity.is_empty():
return
var key := (
PlayerInventoryLayout.catch_key(identity)
if kind == PlayerInventoryLayout.EntryKind.CATCH
else PlayerInventoryLayout.item_key(identity)
)
if _staged.has(key):
_staged.erase(key)
_refresh_tray()
return
if kind == PlayerInventoryLayout.EntryKind.CATCH:
var fish_catch := _fish_inventory.get_catch_by_id(identity)
if fish_catch == null:
_feedback.text = "that catch is no longer available"
return
if fish_catch.is_favorited:
_feedback.text = "favorite catches cannot be sold"
return
if _reservations != null and _reservations.is_fish_reserved(identity):
_feedback.text = "reserved in a letter"
return
_staged[key] = {"kind": kind, "identity": identity, "quantity": 1}
else:
var item := _item_catalog.get_item_by_id(identity)
if not ItemResalePolicy.is_sellable(item):
_feedback.text = "equipment cannot be sold"
return
var available := _bag.get_quantity(identity)
if _reservations != null:
available = _reservations.get_available_item_quantity(identity)
if available < 1:
_feedback.text = "that item is reserved"
return
_staged[key] = {
"kind": kind,
"identity": identity,
"quantity": available,
}
_feedback.text = ""
_refresh_tray()
func _on_drop_payload(payload: Dictionary) -> void:
var payload_kind := str(payload.get("kind", ""))
_stage(
PlayerInventoryLayout.EntryKind.CATCH
if payload_kind == "cooler_fish"
else PlayerInventoryLayout.EntryKind.ITEM,
StringName(str(payload.get(
"catch_id" if payload_kind == "cooler_fish" else "item_id", ""
))),
)
func _on_remove_requested(key: String) -> void:
_staged.erase(key)
_refresh_tray()
func _submit_sale() -> void:
if _staged.is_empty() or _network_sale == null:
return
var catch_ids: Array[StringName] = []
var items: Dictionary[StringName, int] = {}
for record: Dictionary in _staged.values():
var identity := StringName(str(record.get("identity", "")))
if int(record.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
catch_ids.append(identity)
else:
items[identity] = int(record.get("quantity", 0))
if _network_sale.request_local_mixed_sale(
catch_ids, items, MAIN_SHOP_BUYER_ID
).is_empty():
_feedback.text = "sale could not be started"
func _on_sale_pending(_request_id: String) -> void:
_sell_button.disabled = true
_feedback.text = "selling…"
func _on_sale_finished(
_request_id: String,
accepted: bool,
message: String,
_catch_ids: Array[StringName],
_payout: int,
) -> void:
if accepted:
_staged.clear()
_feedback.text = message.to_lower()
_refresh()
func _refresh() -> void:
_validate_staged()
_inventory_grid.refresh()
_refresh_tray()
func _validate_staged() -> void:
for key: String in _staged.keys():
var record: Dictionary = _staged[key]
var identity := StringName(str(record.get("identity", "")))
if int(record.get("kind", -1)) == PlayerInventoryLayout.EntryKind.CATCH:
if (
_fish_inventory.get_catch_by_id(identity) == null
or not _layout.is_catch_in_inventory(identity)
):
_staged.erase(key)
elif (
_bag.get_quantity(identity) < int(record.get("quantity", 0))
or not _layout.is_item_in_inventory(identity)
):
_staged.erase(key)
func _refresh_tray() -> void:
_refresh_inventory_staged_highlights()
for child: Node in _tray_grid.get_children():
child.queue_free()
var keys: Array[String] = []
keys.assign(_staged.keys())
keys.sort()
for key: String in keys:
var record: Dictionary = _staged[key]
var identity := StringName(str(record["identity"]))
var icon: Texture2D
var label: String
if int(record["kind"]) == PlayerInventoryLayout.EntryKind.CATCH:
var fish_catch := _fish_inventory.get_catch_by_id(identity)
if fish_catch == null:
continue
icon = fish_catch.fish.display_texture
label = fish_catch.fish.display_name
else:
var item := _item_catalog.get_item_by_id(identity)
if item == null:
continue
icon = item.icon
label = item.display_name
var slot := ShopSaleTraySlot.new()
_tray_grid.add_child(slot)
slot.configure(key, icon, label, int(record.get("quantity", 1)))
slot.remove_requested.connect(_on_remove_requested)
slot.drop_requested.connect(_on_drop_payload)
var drop_slot := ShopSaleTraySlot.new()
_tray_grid.add_child(drop_slot)
drop_slot.configure("", null, "drop an item here")
drop_slot.disabled = false
drop_slot.drop_requested.connect(_on_drop_payload)
var total := _calculate_total()
_total_label.text = str(total)
_sell_button.disabled = (
_staged.is_empty()
or total < 0
or _network_sale == null
or _network_sale.is_local_sale_pending()
)
call_deferred("_configure_focus")
func _refresh_inventory_staged_highlights() -> void:
if _inventory_grid == null:
return
for slot: GeneralInventorySlot in _inventory_grid.get_slots():
var key: String = ""
if not slot.entry_identity.is_empty():
key = (
PlayerInventoryLayout.catch_key(slot.entry_identity)
if slot.entry_kind == PlayerInventoryLayout.EntryKind.CATCH
else PlayerInventoryLayout.item_key(slot.entry_identity)
)
slot.set_staged(not key.is_empty() and _staged.has(key))
func _configure_focus() -> void:
var controls: Array[Control] = []
controls.assign(_inventory_grid.get_slots())
for child: Node in _tray_grid.get_children():
var control := child as Control
if control != null and control.focus_mode != Control.FOCUS_NONE:
controls.append(control)
controls.append(_sell_button)
ControllerFocusNavigation.configure_spatial_neighbors(controls)
func _calculate_total() -> int:
var total: int = 0
for record: Dictionary in _staged.values():
var identity := StringName(str(record["identity"]))
if int(record["kind"]) == PlayerInventoryLayout.EntryKind.CATCH:
var fish_catch := _fish_inventory.get_catch_by_id(identity)
if fish_catch == null:
return -1
total += _buyer.get_quality_offer(
fish_catch.fish.get_sale_value_for_weight(fish_catch.weight_lb),
fish_catch.quality,
)
else:
var item := _item_catalog.get_item_by_id(identity)
var unit_value := ItemResalePolicy.get_unit_value(item)
if unit_value < 0:
return -1
total += unit_value * int(record.get("quantity", 0))
return total
func _build_ui() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
var outer := MarginContainer.new()
outer.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
outer.add_theme_constant_override("margin_left", 68)
outer.add_theme_constant_override("margin_top", 92)
outer.add_theme_constant_override("margin_right", 68)
outer.add_theme_constant_override("margin_bottom", 44)
add_child(outer)
var columns := HBoxContainer.new()
columns.add_theme_constant_override("separation", 20)
outer.add_child(columns)
var source := _build_panel(columns, "inventory")
_inventory_grid = GeneralInventoryGrid.new()
(source as VBoxContainer).add_child(_inventory_grid)
var tray := _build_panel(columns, "sell tray")
var tray_scroll := ScrollContainer.new()
tray_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
tray_scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
(tray as VBoxContainer).add_child(tray_scroll)
_tray_grid = GridContainer.new()
_tray_grid.columns = PlayerInventoryLayout.INVENTORY_COLUMNS
_tray_grid.add_theme_constant_override(
"h_separation", GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
)
_tray_grid.add_theme_constant_override(
"v_separation", GeneralInventoryGrid.DEFAULT_SLOT_SEPARATION
)
tray_scroll.add_child(_tray_grid)
_feedback = Label.new()
_feedback.text = "select or drag items into the sell tray"
_feedback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_feedback.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
(tray as VBoxContainer).add_child(_feedback)
var total_row := HBoxContainer.new()
(tray as VBoxContainer).add_child(total_row)
var total_title := Label.new()
total_title.text = "total"
total_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
total_row.add_child(total_title)
var coin := TextureRect.new()
coin.custom_minimum_size = Vector2(24.0, 24.0)
coin.texture = preload("res://items/icons/shop/32_currency.png")
coin.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
coin.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
coin.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
total_row.add_child(coin)
_total_label = Label.new()
_total_label.text = "0"
total_row.add_child(_total_label)
_sell_button = Button.new()
_sell_button.text = "sell"
UtilityPageStyle.apply_ocean_button(_sell_button)
_sell_button.pressed.connect(_submit_sale)
(tray as VBoxContainer).add_child(_sell_button)
func _build_panel(parent: HBoxContainer, title_text: String) -> VBoxContainer:
var panel := PanelContainer.new()
panel.size_flags_horizontal = Control.SIZE_EXPAND_FILL
panel.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(UtilityPageStyle.OCEAN_PANEL_MID, 16),
)
parent.add_child(panel)
var margin := MarginContainer.new()
for side: StringName in [&"margin_left", &"margin_top", &"margin_right", &"margin_bottom"]:
margin.add_theme_constant_override(side, 12)
panel.add_child(margin)
var column := VBoxContainer.new()
column.add_theme_constant_override("separation", 8)
margin.add_child(column)
var title := Label.new()
title.text = title_text
title.add_theme_font_size_override("font_size", 22)
column.add_child(title)
return column

View file

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

View file

@ -26,7 +26,8 @@ const GREEN: Color = Color("31594d")
const LIGHT_TEXT: Color = Color("f5eed9")
const DISABLED_TEXT: Color = Color(0.33, 0.36, 0.35, 0.72)
const MOTION_TWEEN_META: StringName = &"utility_page_motion_tween"
const LAPTOP_RECT: Rect2 = Rect2(66.0, 132.0, 1148.0, 520.0)
const LAPTOP_RECT: Rect2 = Rect2(199.0, 132.0, 882.0, 520.0)
const LAPTOP_CONTENT_SIZE: Vector2 = Vector2(794.0, 442.0)
const SUPPLY_BADGE_MIN_WIDTH: float = 46.0
const SUPPLY_BADGE_HEIGHT: float = 24.0
const SUPPLY_BADGE_EDGE_MARGIN: float = 3.0