class_name DecorShop extends Control const INPUT_OWNER: StringName = &"decor_shop" const DecorCatalogType = preload("res://homes/decor_catalog.gd") const UtilityPageStyleType = preload("res://ui/utility_page_style.gd") signal menu_visibility_changed(is_open: bool) signal menu_exit_started var _player: Player var _wallet: PlayerWallet var _fishing_spot: FishingSpot var _interaction: DecorShopInteraction var _bag: PlayerBag var _home_state: PlayerHomeState var _network_shop: NetworkShopService var _wallet_label: Label var _owned_label: Label var _feedback: Label var _buy_button: Button var _close_button: Button var _transaction_pending: bool = false 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: UtilityPageStyleType.apply_page(self) _build_ui() hide() func setup( player: Player, wallet: PlayerWallet, fishing_spot: FishingSpot, interaction: DecorShopInteraction, bag: PlayerBag, home_state: PlayerHomeState, network_shop: NetworkShopService, ) -> void: _player = player _wallet = wallet _fishing_spot = fishing_spot _interaction = interaction _bag = bag _home_state = home_state _network_shop = network_shop if not _wallet.balance_changed.is_connected(_on_wallet_changed): _wallet.balance_changed.connect(_on_wallet_changed) if not _bag.contents_changed.is_connected(_refresh): _bag.contents_changed.connect(_refresh) if not _home_state.changed.is_connected(_refresh): _home_state.changed.connect(_refresh) if ( _network_shop != null and not _network_shop.local_purchase_pending.is_connected( _on_purchase_pending ) ): _network_shop.local_purchase_pending.connect(_on_purchase_pending) _network_shop.local_purchase_finished.connect(_on_purchase_finished) _refresh() func set_world_interaction(interaction: DecorShopInteraction) -> void: _interaction = interaction func open_shop() -> bool: if ( visible or _player == null or _wallet == null or _fishing_spot == null or _interaction == null or not _interaction.is_local_player_in_range() 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 _transaction_pending = ( _network_shop != null and _network_shop.is_local_purchase_pending() ) _feedback.text = "" _refresh() show() _buy_button.call_deferred("grab_focus") menu_visibility_changed.emit(true) return true func close_shop(restore_controls: bool = true) -> void: if not visible: return menu_exit_started.emit() get_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 consume_escape() -> bool: if not visible: return false close_shop() return true func close_for_range_exit() -> void: close_shop() func close_for_water_recovery() -> void: close_shop(false) func close_for_session_end() -> void: close_shop(false) func _unhandled_input(event: InputEvent) -> void: if not visible or not event.is_action_pressed("ui_cancel"): return close_shop() get_viewport().set_input_as_handled() func _purchase_cube() -> void: if _network_shop == null or _transaction_pending: return var request_id: String = _network_shop.request_decor( DecorCatalogType.BASIC_CUBE_ID ) if request_id.is_empty(): _refresh() func _on_purchase_pending(_request_id: String) -> void: _transaction_pending = true _feedback.text = "purchasing…" _refresh() func _on_purchase_finished( _request_id: String, accepted: bool, message: String, product_id: StringName, category: int, _quantity: int, total_cost: int, ) -> void: if ( category != NetworkShopProtocol.ProductCategory.DECOR or product_id != DecorCatalogType.BASIC_CUBE_ID ): return _transaction_pending = false _feedback.text = ( "basic cube purchased · %d fishcoin spent" % total_cost if accepted else message.to_lower() ) _refresh() func _on_wallet_changed(_balance: int, _delta: int) -> void: _refresh() func _refresh() -> void: if _wallet_label == null: return var balance: int = _wallet.get_balance() if _wallet != null else 0 var owned: int = ( _home_state.get_owned_count(DecorCatalogType.BASIC_CUBE_ID) if _home_state != null else 0 ) var carried: int = ( _bag.get_quantity(DecorCatalogType.BASIC_CUBE_ID) if _bag != null else 0 ) _wallet_label.text = "%d fishcoins" % balance _owned_label.text = "owned %d · packed %d" % [owned, carried] _buy_button.disabled = ( _transaction_pending or _wallet == null or not _wallet.can_afford( DecorCatalogType.get_price(DecorCatalogType.BASIC_CUBE_ID) ) or _bag == null or not _bag.can_add_item(DecorCatalogType.BASIC_CUBE_ID, 1) or owned >= DecorCatalogType.MAX_OWNED_PER_PRODUCT ) func _build_ui() -> void: set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) z_index = 83 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(-440.0, -250.0) panel.size = Vector2(880.0, 500.0) panel.add_theme_stylebox_override( "panel", UtilityPageStyleType.rounded_style( UtilityPageStyleType.OCEAN_PANEL_DEEP, 24 ), ) 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, 28) panel.add_child(margin) var layout := VBoxContainer.new() layout.add_theme_constant_override("separation", 18) margin.add_child(layout) var header := HBoxContainer.new() header.add_theme_constant_override("separation", 12) layout.add_child(header) var title := Label.new() title.text = "decor" title.add_theme_font_size_override("font_size", 38) title.size_flags_horizontal = Control.SIZE_EXPAND_FILL header.add_child(title) _wallet_label = Label.new() _wallet_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT _wallet_label.add_theme_font_size_override("font_size", 22) header.add_child(_wallet_label) var item_panel := PanelContainer.new() item_panel.size_flags_vertical = Control.SIZE_EXPAND_FILL item_panel.add_theme_stylebox_override( "panel", UtilityPageStyleType.rounded_style( UtilityPageStyleType.OCEAN_FIELD, 18 ), ) layout.add_child(item_panel) var item_margin := MarginContainer.new() for side: StringName in [ &"margin_left", &"margin_top", &"margin_right", &"margin_bottom", ]: item_margin.add_theme_constant_override(side, 24) item_panel.add_child(item_margin) var item_row := HBoxContainer.new() item_row.add_theme_constant_override("separation", 28) item_margin.add_child(item_row) var preview := Panel.new() preview.custom_minimum_size = Vector2(150.0, 150.0) preview.add_theme_stylebox_override( "panel", UtilityPageStyleType.rounded_style(Color("b77a55"), 12), ) item_row.add_child(preview) var details := VBoxContainer.new() details.size_flags_horizontal = Control.SIZE_EXPAND_FILL details.add_theme_constant_override("separation", 10) item_row.add_child(details) var item_name := Label.new() item_name.text = DecorCatalogType.get_display_name( DecorCatalogType.BASIC_CUBE_ID ) item_name.add_theme_font_size_override("font_size", 32) details.add_child(item_name) var description := Label.new() description.text = DecorCatalogType.get_description( DecorCatalogType.BASIC_CUBE_ID ) description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART description.add_theme_font_size_override("font_size", 20) description.size_flags_vertical = Control.SIZE_EXPAND_FILL details.add_child(description) _owned_label = Label.new() _owned_label.add_theme_font_size_override("font_size", 18) details.add_child(_owned_label) _buy_button = Button.new() _buy_button.text = "buy · 1 fishcoin" _buy_button.custom_minimum_size = Vector2(260.0, 58.0) UtilityPageStyleType.apply_ocean_button(_buy_button) _buy_button.pressed.connect(_purchase_cube) details.add_child(_buy_button) var footer := HBoxContainer.new() footer.add_theme_constant_override("separation", 12) layout.add_child(footer) _feedback = Label.new() _feedback.size_flags_horizontal = Control.SIZE_EXPAND_FILL _feedback.add_theme_font_size_override("font_size", 18) footer.add_child(_feedback) _close_button = Button.new() _close_button.text = "close" _close_button.custom_minimum_size = Vector2(150.0, 50.0) UtilityPageStyleType.apply_ocean_button(_close_button) _close_button.pressed.connect(close_shop) footer.add_child(_close_button) _buy_button.focus_neighbor_bottom = _buy_button.get_path_to(_close_button) _close_button.focus_neighbor_top = _close_button.get_path_to(_buy_button)