Add Art Kit painting upgrades

This commit is contained in:
Alexander Sellite 2026-08-02 00:45:07 -04:00
parent 0edbfabdb1
commit 1406a0abc7
34 changed files with 1530 additions and 165 deletions

View file

@ -17,6 +17,10 @@ const PlayerType = preload("res://player/player.gd")
const PlayerCoolerCapacityType = preload(
"res://progression/player_cooler_capacity.gd"
)
const PlayerArtUnlocksType = preload(
"res://progression/player_art_unlocks.gd"
)
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const ShopInteractionType = preload(
"res://world/fishing_shop_interaction.gd"
)
@ -66,6 +70,7 @@ var _interaction: ShopInteractionType
var _bag: PlayerBagType
var _item_catalog: ItemCatalogType
var _cooler_capacity: PlayerCoolerCapacityType
var _art_unlocks: PlayerArtUnlocksType
var _network_shop: NetworkShopService
var _prior_movement_enabled: bool = true
var _prior_camera_enabled: bool = true
@ -102,10 +107,10 @@ func _request_shop_cooler() -> void:
func _focus_buy_page() -> void:
_feedback.text = ""
if _supplies_list.get_child_count() > 0:
var first_supply := _supplies_list.get_child(0) as Button
if first_supply != null and not first_supply.disabled:
first_supply.grab_focus()
for child: Node in _supplies_list.get_children():
var stock_button := child as Button
if stock_button != null and not stock_button.disabled:
stock_button.grab_focus()
return
_reel_purchase.grab_focus()
@ -160,6 +165,7 @@ func setup(
bag: PlayerBagType,
item_catalog: ItemCatalogType,
cooler_capacity: PlayerCoolerCapacityType,
art_unlocks: PlayerArtUnlocksType,
network_shop: NetworkShopService,
) -> void:
_player = player
@ -171,6 +177,7 @@ func setup(
_bag = bag
_item_catalog = item_catalog
_cooler_capacity = cooler_capacity
_art_unlocks = art_unlocks
_network_shop = network_shop
if (
_network_shop != null
@ -194,6 +201,8 @@ func setup(
_on_cooler_capacity_changed
):
_cooler_capacity.capacity_changed.connect(_on_cooler_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()
@ -402,6 +411,7 @@ func _refresh_supplies() -> void:
for child: Node in _supplies_list.get_children():
_supplies_list.remove_child(child)
child.queue_free()
_add_stock_section("supplies")
for item_id: StringName in FishingShopStockType.get_stock_item_ids():
var item: ItemDataType = _item_catalog.get_item_by_id(item_id)
if item == null:
@ -435,6 +445,92 @@ func _refresh_supplies() -> void:
UtilityPageStyleType.apply_ocean_button(button)
button.pressed.connect(_purchase_supply.bind(item_id))
_supplies_list.add_child(button)
_add_stock_section("art kit")
_add_art_kit_button()
_add_stock_section("markers")
for product_id: StringName in ArtShopStockType.MARKER_PRODUCTS:
_add_art_upgrade_button(product_id)
_add_stock_section("brushes")
for product_id: StringName in ArtShopStockType.BRUSH_PRODUCTS:
_add_art_upgrade_button(product_id)
_add_stock_section("grids")
for product_id: StringName in ArtShopStockType.GRID_PRODUCTS:
_add_art_upgrade_button(product_id)
func _add_stock_section(title: String) -> void:
var label := Label.new()
label.text = title
label.add_theme_font_size_override("font_size", 21)
label.add_theme_color_override(
"font_color", UtilityPageStyleType.OCEAN_TEXT_SECONDARY
)
_supplies_list.add_child(label)
func _add_art_kit_button() -> void:
var item: ItemDataType = _item_catalog.get_item_by_id(
ArtShopStockType.ART_KIT_ITEM_ID
)
if item == null:
return
var owned: bool = _bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0
var button: Button = _make_stock_button(
"%s\n$%d%s" % [
item.display_name,
ArtShopStockType.ART_KIT_PRICE,
"owned" if owned else "not owned",
],
item.description,
)
button.icon = item.icon
button.expand_icon = true
button.disabled = (
owned
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_art_purchase()
or not _bag.can_add_item(ArtShopStockType.ART_KIT_ITEM_ID, 1)
or not _wallet.can_afford(ArtShopStockType.ART_KIT_PRICE)
)
button.pressed.connect(_purchase_art_kit)
func _add_art_upgrade_button(product_id: StringName) -> void:
var kit_owned: bool = (
_bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID) > 0
)
var unlocked: bool = _art_unlocks.owns_product(product_id)
var button: Button = _make_stock_button(
"%s\n$%d%s" % [
ArtShopStockType.get_display_name(product_id),
ArtShopStockType.UPGRADE_PRICE,
"unlocked" if unlocked else "locked",
],
ArtShopStockType.get_description(product_id),
)
button.disabled = (
not kit_owned
or unlocked
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_art_purchase()
or not _wallet.can_afford(ArtShopStockType.UPGRADE_PRICE)
)
button.pressed.connect(_purchase_art_upgrade.bind(product_id))
func _make_stock_button(button_text: String, tooltip: String) -> Button:
var button := Button.new()
button.custom_minimum_size = Vector2(195, 54)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
button.text = button_text
button.tooltip_text = tooltip
UtilityPageStyleType.apply_ocean_button(button)
_supplies_list.add_child(button)
return button
func _refresh_cooler_capacity() -> void:
@ -490,6 +586,20 @@ func _purchase_supply(item_id: StringName) -> void:
_network_shop.request_supply(item_id)
func _purchase_art_kit() -> void:
if _network_shop == null or not _is_transaction_context_valid():
_feedback.text = "Purchase could not be completed."
return
_network_shop.request_art_kit()
func _purchase_art_upgrade(product_id: StringName) -> void:
if _network_shop == null or not _is_transaction_context_valid():
_feedback.text = "Purchase could not be completed."
return
_network_shop.request_art_upgrade(product_id)
func _purchase_cooler_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_feedback.text = "unable to complete purchase."
@ -594,6 +704,10 @@ func _on_cooler_capacity_changed(_level: int, _capacity: int) -> void:
_refresh_cooler_capacity()
func _on_art_unlocks_changed(_unlock_mask: int) -> void:
_refresh_supplies()
func _apply_mouse_close_policy(reason: CloseReason) -> void:
if not _mouse_snapshot_stored:
return

View file

@ -167,7 +167,7 @@ theme_override_constants/separation = 6
[node name="Title" type="Label" parent="ShopPanel/Margin/Layout/Body/Supplies"]
layout_mode = 2
text = "supplies"
text = "shop stock"
theme_override_font_sizes/font_size = 23
[node name="Scroll" type="ScrollContainer" parent="ShopPanel/Margin/Layout/Body/Supplies"]

View file

@ -33,6 +33,9 @@ const PlayerCoolerCapacityType = preload(
)
const ChatUIType = preload("res://ui/chat_ui.gd")
const EmoteRadialMenuType = preload("res://ui/emote_radial_menu.gd")
const SurfaceDrawingToolbarType = preload(
"res://ui/surface_drawing_toolbar.gd"
)
const PlayerSettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
@ -65,10 +68,9 @@ signal shop_backdrop_visibility_changed(is_visible: bool)
@onready var _effect_status: Label = %EffectStatus
@onready var _chat_ui: ChatUIType = %ChatUI
@onready var _emote_radial_menu: EmoteRadialMenuType = %EmoteRadialMenu
@onready var _marker_hud: PanelContainer = %MarkerHUD
@onready var _marker_swatch: ColorRect = %MarkerSwatch
@onready var _marker_summary: Label = %MarkerSummary
@onready var _marker_help: Label = %MarkerHelp
@onready var _surface_drawing_toolbar: SurfaceDrawingToolbarType = (
%SurfaceDrawingToolbar
)
@onready var _title_settings_panel: SettingsPanelType = (
$UIRoot/TitleScreen/ResponsiveTitleStage/TitlePresentationScaleRoot/SettingsPanel
)
@ -142,6 +144,7 @@ func setup(
network_player_list: NetworkPlayerListService,
settings_manager: PlayerSettingsManagerType,
surface_drawing: NetworkSurfaceDrawingService,
art_unlocks: PlayerArtUnlocks,
) -> void:
_player = player
_fishing_spot = fishing_spot
@ -159,6 +162,7 @@ func setup(
fishing_spot.status_changed.connect(_on_fishing_status_changed)
fishing_spot.catch_display_changed.connect(_on_catch_display_changed)
fishing_spot.showcase_changed.connect(_on_showcase_changed)
fishing_spot.art_ui_toggle_requested.connect(_toggle_surface_drawing)
_player_menu.menu_visibility_changed.connect(
_on_player_menu_visibility_changed
)
@ -200,6 +204,7 @@ func setup(
bag,
item_catalog,
cooler_capacity,
art_unlocks,
network_shop_service,
)
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
@ -213,13 +218,15 @@ func setup(
_main_shop_buyer = main_shop_buyer
_shop_interaction = shop_interaction
_surface_drawing = surface_drawing
if _surface_drawing != null:
_surface_drawing.hud_state_changed.connect(
_on_surface_drawing_hud_state_changed
)
_surface_drawing_toolbar.setup(_surface_drawing, art_unlocks)
func _input(event: InputEvent) -> void:
if (
_surface_drawing_toolbar != null
and _surface_drawing_toolbar.owns_pointer_event(event)
):
return
var drawing_can_open: bool = (
_gameplay_ui_enabled
and not _system_menu_open
@ -255,6 +262,15 @@ func _on_emote_selected(emote_id: StringName) -> void:
_player.toggle_sitting()
func _toggle_surface_drawing() -> void:
if _surface_drawing == null:
return
if _surface_drawing.is_active():
_surface_drawing.deactivate()
elif _surface_drawing.can_activate():
_surface_drawing.activate()
func setup_data_and_identity(
data_root: PlayerDataRoot,
identity_backups: IdentityBackupService,
@ -719,35 +735,6 @@ func _on_chat_text_entry_ownership_changed(active: bool) -> void:
_emit_interactive_pointer_ui_changed()
func _on_surface_drawing_hud_state_changed(
is_active: bool,
mode_name: String,
color_name: String,
color_value: Color,
brush_size: int,
status: String,
) -> void:
_marker_hud.visible = is_active and _gameplay_ui_enabled
_marker_swatch.visible = mode_name != "place grid"
_marker_swatch.color = color_value
_marker_summary.text = (
"place shared grid"
if mode_name == "place grid"
else "marker • %s • brush %d" % [
color_name.to_lower(), brush_size,
]
)
_marker_help.text = (
status
if not status.is_empty()
else (
"click place/restore • shift hide • ctrl shift finish • shift scroll zoom"
if mode_name == "place grid"
else "click draw • shift click erase • ctrl z undo • shift scroll zoom"
)
)
func _refresh_chat_availability() -> void:
if _chat_ui == null:
return

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=15 format=3]
[gd_scene load_steps=16 format=3]
[ext_resource type="Script" path="res://ui/game_ui.gd" id="1_ui"]
[ext_resource type="PackedScene" path="res://ui/player_menu.tscn" id="2_menu"]
@ -10,6 +10,7 @@
[ext_resource type="PackedScene" path="res://ui/fishing_shop.tscn" id="8_shop"]
[ext_resource type="Script" path="res://ui/chat_ui.gd" id="9_chat"]
[ext_resource type="PackedScene" path="res://ui/emote_radial_menu.tscn" id="10_emote"]
[ext_resource type="PackedScene" path="res://ui/surface_drawing_toolbar.tscn" id="11_drawing_toolbar"]
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.032, 0.118, 0.15, 1)
@ -34,17 +35,6 @@ corner_radius_bottom_left = 12
[sub_resource type="StyleBoxFlat" id="StyleBox_transparent"]
bg_color = Color(0, 0, 0, 0)
[sub_resource type="StyleBoxFlat" id="StyleBox_marker_panel"]
bg_color = Color(0.051, 0.173, 0.227, 0.96)
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12
content_margin_left = 14.0
content_margin_top = 9.0
content_margin_right = 14.0
content_margin_bottom = 9.0
[node name="GameUI" type="CanvasLayer"]
script = ExtResource("1_ui")
@ -56,6 +46,9 @@ mouse_filter = 2
unique_name_in_owner = true
mouse_filter = 2
[node name="SurfaceDrawingToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("11_drawing_toolbar")]
unique_name_in_owner = true
[node name="GameplayTransientHUD" type="Control" parent="UIRoot/CanonicalStage"]
unique_name_in_owner = true
layout_mode = 1
@ -265,51 +258,6 @@ theme_override_colors/font_shadow_color = Color(0, 0, 0, 0.9)
theme_override_constants/shadow_offset_x = 2
theme_override_constants/shadow_offset_y = 2
[node name="MarkerHUD" type="PanelContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
visible = false
z_index = 58
anchors_preset = 10
anchor_left = 1.0
anchor_right = 1.0
offset_left = -510.0
offset_top = 18.0
offset_right = -18.0
offset_bottom = 92.0
grow_horizontal = 0
mouse_filter = 2
theme = ExtResource("3_theme")
theme_override_styles/panel = SubResource("StyleBox_marker_panel")
[node name="Layout" type="HBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/MarkerHUD"]
layout_mode = 2
theme_override_constants/separation = 12
[node name="MarkerSwatch" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD/MarkerHUD/Layout"]
unique_name_in_owner = true
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
mouse_filter = 2
color = Color(0.960784, 0.933333, 0.85098, 1)
[node name="Text" type="VBoxContainer" parent="UIRoot/CanonicalStage/GameplayTransientHUD/MarkerHUD/Layout"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="MarkerSummary" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/MarkerHUD/Layout/Text"]
unique_name_in_owner = true
layout_mode = 2
text = "marker • chalk white • brush 1"
theme_override_font_sizes/font_size = 18
[node name="MarkerHelp" type="Label" parent="UIRoot/CanonicalStage/GameplayTransientHUD/MarkerHUD/Layout/Text"]
unique_name_in_owner = true
layout_mode = 2
text = "click draw • shift erase • ctrl z undo • shift scroll zoom"
theme_override_colors/font_color = Color(0.623529, 0.811765, 0.823529, 1)
theme_override_font_sizes/font_size = 13
[node name="ChatUI" type="Control" parent="UIRoot"]
unique_name_in_owner = true
layout_mode = 1

View file

@ -0,0 +1,195 @@
class_name SurfaceDrawingToolbar
extends PanelContainer
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
@onready var _mode_button: Button = %ModeButton
@onready var _brush_option: OptionButton = %BrushOption
@onready var _grid_option: OptionButton = %GridOption
@onready var _color_list: VBoxContainer = %ColorList
var _service: NetworkSurfaceDrawingService
var _unlocks: PlayerArtUnlocks
var _color_buttons: Dictionary[StringName, Button] = {}
func _ready() -> void:
UtilityPageStyleType.apply_page(self)
var panel: StyleBoxFlat = UtilityPageStyleType.panel_style()
panel.content_margin_left = 10.0
panel.content_margin_top = 10.0
panel.content_margin_right = 10.0
panel.content_margin_bottom = 10.0
add_theme_stylebox_override("panel", panel)
UtilityPageStyleType.apply_ocean_button(_mode_button)
UtilityPageStyleType.apply_ocean_button(_brush_option)
UtilityPageStyleType.apply_ocean_button(_grid_option)
_mode_button.custom_minimum_size = Vector2(48, 48)
_make_mode_button_round()
_mode_button.pressed.connect(_toggle_mode)
_brush_option.item_selected.connect(_select_brush)
_grid_option.item_selected.connect(_select_grid)
_build_options()
hide()
func _make_mode_button_round() -> void:
for style_name: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var existing: StyleBox = _mode_button.get_theme_stylebox(style_name)
var style := existing.duplicate() as StyleBoxFlat
if style == null:
continue
style.set_corner_radius_all(24)
_mode_button.add_theme_stylebox_override(style_name, style)
func setup(
service: NetworkSurfaceDrawingService,
unlocks: PlayerArtUnlocks,
) -> void:
_service = service
_unlocks = unlocks
if _service != null and not _service.hud_state_changed.is_connected(
_on_service_state_changed
):
_service.hud_state_changed.connect(_on_service_state_changed)
if _unlocks != null and not _unlocks.unlocks_changed.is_connected(
_on_unlocks_changed
):
_unlocks.unlocks_changed.connect(_on_unlocks_changed)
_refresh_unlocks()
func owns_pointer_event(event: InputEvent) -> bool:
if not visible:
return false
if _brush_option.get_popup().visible or _grid_option.get_popup().visible:
return true
if event is InputEventMouse:
return get_global_rect().has_point((event as InputEventMouse).position)
return false
func _build_options() -> void:
_brush_option.clear()
for brush_size: int in PlayerArtUnlocks.BRUSH_SIZES:
_brush_option.add_item("%d×" % brush_size, brush_size)
_grid_option.clear()
for grid_size: int in PlayerArtUnlocks.GRID_SIZES:
_grid_option.add_item("%d×" % grid_size, grid_size)
for child: Node in _color_list.get_children():
child.queue_free()
_color_buttons.clear()
for color_id: StringName in SurfaceDrawingPalette.get_color_ids():
var button := Button.new()
button.custom_minimum_size = Vector2(34, 34)
button.focus_mode = Control.FOCUS_ALL
button.tooltip_text = SurfaceDrawingPalette.get_display_name(color_id)
button.pressed.connect(_select_color.bind(color_id))
_color_list.add_child(button)
_color_buttons[color_id] = button
_apply_color_button_style(button, color_id, false)
func _refresh_unlocks() -> void:
if not is_node_ready() or _unlocks == null:
return
var brush_popup: PopupMenu = _brush_option.get_popup()
for index: int in range(_brush_option.item_count):
var brush_size: int = _brush_option.get_item_id(index)
brush_popup.set_item_disabled(
index, not _unlocks.is_brush_size_unlocked(brush_size)
)
var grid_popup: PopupMenu = _grid_option.get_popup()
for index: int in range(_grid_option.item_count):
var grid_size: int = _grid_option.get_item_id(index)
grid_popup.set_item_disabled(
index, not _unlocks.is_grid_size_unlocked(grid_size)
)
for color_id: StringName in _color_buttons:
var button: Button = _color_buttons[color_id]
button.disabled = not _unlocks.is_color_unlocked(color_id)
_apply_color_button_style(
button,
color_id,
_service != null and _service.get_color_id() == color_id,
)
func _apply_color_button_style(
button: Button,
color_id: StringName,
selected: bool,
) -> void:
var color: Color = (
SurfaceDrawingPalette.get_color(color_id)
if not button.disabled
else UtilityPageStyleType.OCEAN_DISABLED
)
for style_name: StringName in [&"normal", &"hover", &"pressed", &"focus"]:
var style := StyleBoxFlat.new()
style.bg_color = color.lightened(0.12) if style_name == &"hover" else color
style.set_border_width_all(3 if selected else 0)
style.border_color = UtilityPageStyleType.OCEAN_TEXT_PRIMARY
style.set_corner_radius_all(17)
button.add_theme_stylebox_override(style_name, style)
var disabled_style := StyleBoxFlat.new()
disabled_style.bg_color = UtilityPageStyleType.OCEAN_DISABLED
disabled_style.set_corner_radius_all(17)
button.add_theme_stylebox_override("disabled", disabled_style)
func _toggle_mode() -> void:
if _service != null:
_service.set_placement_mode(not _service.is_placement_mode())
func _select_brush(index: int) -> void:
if _service != null:
_service.set_brush_size(_brush_option.get_item_id(index))
func _select_grid(index: int) -> void:
if _service != null:
_service.set_grid_size(_grid_option.get_item_id(index))
func _select_color(color_id: StringName) -> void:
if _service != null:
_service.set_color_id(color_id)
func _on_unlocks_changed(_unlock_mask: int) -> void:
_refresh_unlocks()
func _on_service_state_changed(
is_active: bool,
mode_name: String,
_color_name: String,
_color_value: Color,
brush_size: int,
grid_size: int,
_status: String,
) -> void:
visible = is_active
if not is_active:
return
_mode_button.text = "" if mode_name == "place grid" else ""
_mode_button.tooltip_text = (
"Switch to marker mode"
if mode_name == "place grid"
else "Switch to grid mode"
)
_select_option_by_id(_brush_option, brush_size)
_select_option_by_id(_grid_option, grid_size)
_refresh_unlocks()
func _select_option_by_id(option: OptionButton, item_id: int) -> void:
for index: int in range(option.item_count):
if option.get_item_id(index) == item_id:
option.select(index)
return

View file

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

View file

@ -0,0 +1,57 @@
[gd_scene load_steps=3 format=3]
[ext_resource type="Script" path="res://ui/surface_drawing_toolbar.gd" id="1_script"]
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
[node name="SurfaceDrawingToolbar" type="PanelContainer"]
visible = false
z_index = 85
layout_mode = 0
offset_left = 18.0
offset_top = 18.0
offset_right = 284.0
offset_bottom = 340.0
mouse_filter = 0
theme = ExtResource("2_theme")
script = ExtResource("1_script")
[node name="Layout" type="HBoxContainer" parent="."]
layout_mode = 2
theme_override_constants/separation = 10
[node name="ColorList" type="VBoxContainer" parent="Layout"]
unique_name_in_owner = true
layout_mode = 2
theme_override_constants/separation = 5
[node name="Tools" type="VBoxContainer" parent="Layout"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 8
[node name="Top" type="HBoxContainer" parent="Layout/Tools"]
layout_mode = 2
theme_override_constants/separation = 6
[node name="ModeButton" type="Button" parent="Layout/Tools/Top"]
unique_name_in_owner = true
layout_mode = 2
text = "▦"
[node name="BrushOption" type="OptionButton" parent="Layout/Tools/Top"]
unique_name_in_owner = true
custom_minimum_size = Vector2(80, 48)
layout_mode = 2
tooltip_text = "brush size"
[node name="GridOption" type="OptionButton" parent="Layout/Tools/Top"]
unique_name_in_owner = true
custom_minimum_size = Vector2(92, 48)
layout_mode = 2
tooltip_text = "grid size"
[node name="Help" type="Label" parent="Layout/Tools"]
layout_mode = 2
text = "click place/draw\nshift click hide/erase\nctrl z undo\nshift scroll zoom"
theme_override_colors/font_color = Color(0.623529, 0.811765, 0.823529, 1)
theme_override_font_sizes/font_size = 14