Add starter RV progression and fishing feedback

This commit is contained in:
Alexander Sellite 2026-08-31 16:03:55 -04:00
parent eed361681a
commit d8bf59bca0
47 changed files with 2268 additions and 467 deletions

View file

@ -0,0 +1,10 @@
shader_type canvas_item;
uniform float intensity : hint_range(0.0, 1.0) = 0.0;
void fragment() {
vec2 centered = UV * 2.0 - 1.0;
centered.x *= 0.56;
float edge = smoothstep(0.42, 1.02, length(centered));
COLOR = vec4(0.16, 0.018, 0.025, edge * intensity * 0.72);
}

View file

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

View file

@ -15,9 +15,6 @@ const PlayerFishingUpgradesType = preload(
"res://progression/player_fishing_upgrades.gd"
)
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"
)
@ -154,9 +151,6 @@ const ROD_PRICE_ICON_SIZE: float = 22.0
@onready var _barrier_price_bubble: PanelContainer = %BarrierPriceBubble
@onready var _barrier_purchase: Button = %BarrierPurchase
@onready var _supplies_list: VBoxContainer = %SuppliesList
@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
@ -172,7 +166,6 @@ 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
@ -202,7 +195,6 @@ func _ready() -> void:
%CloseButton.pressed.connect(close_shop)
_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)
@ -353,7 +345,6 @@ func _focus_shop_section() -> void:
for upgrade_button: Button in [
_reel_purchase,
_barrier_purchase,
_cooler_purchase,
_backpack_purchase,
]:
if not upgrade_button.disabled:
@ -509,7 +500,6 @@ func _apply_shop_styles() -> void:
for panel: PanelContainer in [
_reel_price_bubble,
_barrier_price_bubble,
_cooler_price_bubble,
_backpack_price_bubble,
]:
var price_style := UtilityPageStyleType.rounded_style(
@ -526,7 +516,6 @@ func _apply_shop_styles() -> void:
for price: CurrencyAmount in [
_reel_cost,
_barrier_cost,
_cooler_cost,
_backpack_cost,
]:
var amount_label := price.get_amount_label()
@ -538,7 +527,6 @@ func _apply_shop_styles() -> void:
%CloseButton,
_reel_purchase,
_barrier_purchase,
_cooler_purchase,
_backpack_purchase,
]:
UtilityPageStyleType.apply_ocean_button(button)
@ -559,7 +547,6 @@ func setup(
hotbar: PlayerHotbar,
inventory_layout: PlayerInventoryLayout,
item_catalog: ItemCatalogType,
cooler_capacity: PlayerCoolerCapacityType,
art_unlocks: PlayerArtUnlocksType,
network_shop: NetworkShopService,
network_sale: NetworkSaleService,
@ -576,7 +563,6 @@ func setup(
_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
@ -612,10 +598,6 @@ func setup(
_upgrades.upgrades_changed.connect(_on_upgrades_changed)
if not _bag.contents_changed.is_connected(_on_bag_changed):
_bag.contents_changed.connect(_on_bag_changed)
if not _cooler_capacity.capacity_changed.is_connected(
_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
):
@ -836,7 +818,6 @@ func _refresh_all() -> void:
_refresh_wallet()
_refresh_upgrades()
_refresh_supplies()
_refresh_cooler_capacity()
_refresh_backpack_capacity()
@ -1587,41 +1568,6 @@ func _add_supply_icon_tile(
return tile_host
func _refresh_cooler_capacity() -> void:
if _cooler_capacity == null:
return
var level: int = _cooler_capacity.get_level()
var cost: int = _cooler_capacity.get_next_cost()
var next_capacity: int = _cooler_capacity.get_next_capacity()
_cooler_purchase.disabled = (
cost < 0
or _transaction_in_progress
or _closing
or _network_shop == null
or not _network_shop.can_request_purchase()
or not _cooler_capacity.can_purchase(_wallet)
)
var cooler_effect: String
if cost < 0:
cooler_effect = "%d slots · maximum level" % (
_cooler_capacity.get_capacity()
)
_cooler_price_bubble.hide()
else:
_cooler_price_bubble.show()
cooler_effect = "%d%d slots" % [
_cooler_capacity.get_capacity(),
next_capacity,
]
_cooler_cost.set_amount(cost)
_cooler_purchase.tooltip_text = _upgrade_tooltip(
"storage capacity",
level,
cooler_effect,
)
_cooler_purchase.accessibility_name = _cooler_purchase.tooltip_text
func _refresh_backpack_capacity() -> void:
if _inventory_layout == null:
return
@ -1723,23 +1669,6 @@ func _purchase_art_upgrade(product_id: StringName) -> void:
_network_shop.request_art_upgrade(product_id)
func _purchase_cooler_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_set_feedback("unable to complete purchase.")
return
var cost: int = _cooler_capacity.get_next_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_cooler_capacity_upgrade()
func _purchase_backpack_capacity() -> void:
if _transaction_in_progress or not _is_transaction_context_valid():
_set_feedback("unable to complete purchase.")
@ -1860,7 +1789,6 @@ func _on_wallet_changed(_balance: int, _delta: int) -> void:
_refresh_wallet()
_refresh_upgrades()
_refresh_supplies()
_refresh_cooler_capacity()
_refresh_backpack_capacity()
@ -1875,10 +1803,6 @@ func _on_bag_changed() -> void:
_refresh_supplies()
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()

View file

@ -257,35 +257,6 @@ layout_mode = 2
amount = 100
icon_size = 24.0
[node name="CoolerTile" type="Control" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid"]
custom_minimum_size = Vector2(144, 168)
layout_mode = 2
mouse_filter = 2
[node name="CoolerPurchase" type="Button" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
unique_name_in_owner = true
offset_right = 144.0
offset_bottom = 144.0
tooltip_text = "storage capacity"
icon = ExtResource("7_close")
expand_icon = true
icon_max_width = 92
[node name="CoolerPriceBubble" type="PanelContainer" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile"]
unique_name_in_owner = true
z_index = 2
offset_left = 22.0
offset_top = 128.0
offset_right = 122.0
offset_bottom = 160.0
mouse_filter = 2
[node name="CoolerCost" parent="ShopPanel/Margin/Layout/Body/Upgrades/UpgradeScroll/UpgradeGrid/CoolerTile/CoolerPriceBubble" instance=ExtResource("8_currency")]
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

View file

@ -21,6 +21,7 @@ const PauseMenuType = preload("res://ui/pause_menu.gd")
const NetworkSessionType = preload("res://network/network_session.gd")
const FishingShopType = preload("res://ui/fishing_shop.gd")
const DecorShopType = preload("res://ui/decor_shop.gd")
const RVUpgradeShopType = preload("res://ui/rv_upgrade_shop.gd")
const SettingsPanelType = preload("res://ui/settings_panel.gd")
const PlayerFishingUpgradesType = preload(
"res://progression/player_fishing_upgrades.gd"
@ -31,6 +32,9 @@ const ShopInteractionType = preload(
const DecorShopInteractionType = preload(
"res://world/decor_shop_interaction.gd"
)
const RVUpgradeInteractionType = preload(
"res://world/rv_upgrade_interaction.gd"
)
const PlayerStorageType = preload("res://ui/player_storage.gd")
const HomeDecorToolbarType = preload("res://ui/home_decor_toolbar.gd")
const PlayerStorageInteractionType = preload(
@ -129,6 +133,7 @@ const FISHING_PANEL_SHOWCASE_BOTTOM_OFFSET: float = -104.0
@onready var _barrier_health: Label = %BarrierHealth
@onready var _showcase_details: Label = %ShowcaseDetails
@onready var _fishing_panel: PanelContainer = %FishingPanel
@onready var _fishing_danger_vignette: ColorRect = %FishingDangerVignette
@onready var _experience_panel: PanelContainer = %ExperienceProgressPanel
@onready var _experience_level_label: Label = %ExperienceLevelLabel
@onready var _experience_award_label: Label = %ExperienceAwardLabel
@ -144,6 +149,8 @@ const SHOP_ANIMALESE_VOICE_ID: String = "natural"
const SHOP_ANIMALESE_BASE_PITCH: float = 1.08
const SHOP_SPEECH_CHARACTERS_PER_SECOND: float = 28.0
const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
const FISHING_DANGER_START_GAP: float = 0.15
const FISHING_DANGER_FULL_GAP: float = 0.025
@onready var _canonical_stage: Control = %CanonicalStage
@ -155,6 +162,7 @@ const SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS: int = 5000
@onready var _hotbar_ui: HotbarUIType = %Hotbar
@onready var _fishing_shop: FishingShopType = %FishingShop
@onready var _decor_shop: DecorShopType = %DecorShop
@onready var _rv_upgrade_shop: RVUpgradeShopType = %RVUpgradeShop
@onready var _player_storage: PlayerStorageType = %PlayerStorage
@onready var _home_decor_toolbar: HomeDecorToolbarType = %HomeDecorToolbar
@onready var _rv_storage_button: Button = %RVStorageButton
@ -208,6 +216,7 @@ var _player_menu_hotbar_visible: bool = false
var _main_shop_buyer: FishBuyerProfileType
var _shop_interaction: ShopInteractionType
var _decor_shop_interaction: DecorShopInteractionType
var _rv_upgrade_shop_interaction: RVUpgradeInteractionType
var _storage_interaction: PlayerStorageInteractionType
var _surface_drawing: NetworkSurfaceDrawingService
var _surface_drawing_hotbar_selected: bool = false
@ -235,6 +244,12 @@ var _controller_text_entry_is_open: Callable
var _controller_text_entry_close: Callable
var _restore_chat_keyboard_after_fishing: bool = false
var _rv_storage_button_requested_visible: bool = false
var _previous_barrier_health := PackedInt32Array()
var _barrier_feedback_initialized: bool = false
var _fishing_danger_target: float = 0.0
var _fishing_danger_intensity: float = 0.0
var _fishing_danger_pulse_time: float = 0.0
var _catch_track_shake_tween: Tween
func _ready() -> void:
@ -363,6 +378,7 @@ func setup(
shop_interaction: ShopInteractionType,
storage_interaction: PlayerStorageInteractionType,
decor_shop_interaction: DecorShopInteractionType,
rv_upgrade_shop_interaction: RVUpgradeInteractionType,
item_effects: PlayerItemEffectsType,
cooler_capacity: PlayerCoolerCapacityType,
network_session: NetworkSessionType,
@ -506,7 +522,6 @@ func setup(
hotbar,
inventory_layout,
item_catalog,
cooler_capacity,
art_unlocks,
network_shop_service,
network_sale_service,
@ -521,6 +536,14 @@ func setup(
home_state,
network_shop_service,
)
_rv_upgrade_shop.setup(
player,
wallet,
fishing_spot,
rv_upgrade_shop_interaction,
home_state,
network_shop_service,
)
_player_storage.setup(
player,
fishing_spot,
@ -538,6 +561,8 @@ func setup(
_fishing_shop.menu_exit_started.connect(_on_shop_exit_started)
_decor_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_decor_shop.menu_exit_started.connect(_on_shop_exit_started)
_rv_upgrade_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
_rv_upgrade_shop.menu_exit_started.connect(_on_shop_exit_started)
_fishing_shop.sell_fish_requested.connect(_on_shop_sell_fish_requested)
_fishing_shop.shop_cooler_return_requested.connect(
_on_shop_cooler_return_requested
@ -548,6 +573,7 @@ func setup(
_main_shop_buyer = main_shop_buyer
_shop_interaction = shop_interaction
_decor_shop_interaction = decor_shop_interaction
_rv_upgrade_shop_interaction = rv_upgrade_shop_interaction
_storage_interaction = storage_interaction
_surface_drawing = surface_drawing
if (
@ -571,13 +597,16 @@ func set_world_interactions(
shop_interaction: ShopInteractionType,
storage_interaction: PlayerStorageInteractionType,
decor_shop_interaction: DecorShopInteractionType,
rv_upgrade_shop_interaction: RVUpgradeInteractionType,
) -> void:
_shop_interaction = shop_interaction
_storage_interaction = storage_interaction
_decor_shop_interaction = decor_shop_interaction
_rv_upgrade_shop_interaction = rv_upgrade_shop_interaction
_fishing_shop.set_world_interaction(shop_interaction)
_player_storage.set_world_interaction(storage_interaction)
_decor_shop.set_world_interaction(decor_shop_interaction)
_rv_upgrade_shop.set_world_interaction(rv_upgrade_shop_interaction)
func _prioritize_surface_drawing_pointer_input() -> void:
@ -1539,6 +1568,13 @@ func _process(delta: float) -> void:
_update_virtual_mouse(delta)
_update_controller_menu_scroll(delta)
_update_experience_bubble_position()
_update_fishing_danger_vignette(delta)
_try_start_shop_npc_speech()
if _player != null:
_player.set_fishing_reel_camera_active(
_fishing_spot != null
and _fishing_spot.is_reel_input_active()
)
func _update_controller_menu_scroll(delta: float) -> void:
@ -1793,6 +1829,7 @@ func set_gameplay_ui_enabled(enabled: bool) -> void:
close_player_menu_for_session_end()
_fishing_shop.close_for_session_end()
_decor_shop.close_for_session_end()
_rv_upgrade_shop.close_for_session_end()
_player_storage.close_for_session_end()
_fishing_panel.visible = false
_shop_prompt.hide()
@ -1933,6 +1970,10 @@ func get_decor_shop() -> DecorShopType:
return _decor_shop
func get_rv_upgrade_shop() -> RVUpgradeShopType:
return _rv_upgrade_shop
func get_player_storage() -> PlayerStorageType:
return _player_storage
@ -2027,8 +2068,19 @@ func set_shop_prompt_visible(
_position_shop_prompt(world_anchor)
func set_shop_npc_player_in_range(in_range: bool) -> void:
if _shop_npc_player_in_range == in_range:
func set_shop_npc_player_in_range(
in_range: bool,
dialogue_text: String = "",
) -> void:
var dialogue_changed: bool = (
not dialogue_text.is_empty()
and _shop_prompt_message.text != dialogue_text
)
if dialogue_changed:
_shop_prompt_message.text = dialogue_text
_shop_prompt_message.visible_characters = -1
_shop_npc_spoken_for_current_visit = false
if _shop_npc_player_in_range == in_range and not dialogue_changed:
return
_shop_npc_player_in_range = in_range
if not in_range:
@ -2044,10 +2096,10 @@ func _try_start_shop_npc_speech() -> void:
or not _gameplay_ui_enabled
):
return
_shop_npc_spoken_for_current_visit = true
var now_msec: int = Time.get_ticks_msec()
if now_msec < _shop_npc_next_speech_msec:
return
_shop_npc_spoken_for_current_visit = true
_shop_npc_next_speech_msec = (
now_msec + SHOP_NPC_SPEECH_COOLDOWN_MILLISECONDS
)
@ -2310,6 +2362,12 @@ func _on_catch_display_changed(
and _fishing_spot != null
and _fishing_spot.is_fighting()
)
_update_fishing_feedback(
progress,
chase_progress,
barrier_health,
encounter_visible,
)
_green_catch_progress.value = progress * 100.0
_red_chase_progress.value = maxf(chase_progress, 0.0) * 100.0
_catch_track.visible = encounter_visible
@ -2346,6 +2404,93 @@ func _on_catch_display_changed(
_refresh_fishing_panel_visibility()
func _update_fishing_feedback(
progress: float,
chase_progress: float,
barrier_health: PackedInt32Array,
encounter_visible: bool,
) -> void:
if not encounter_visible:
_previous_barrier_health.clear()
_barrier_feedback_initialized = false
_fishing_danger_target = 0.0
return
if _barrier_feedback_initialized:
var comparison_count: int = mini(
_previous_barrier_health.size(),
barrier_health.size(),
)
var barrier_hit: bool = false
var barrier_broken: bool = false
for barrier_index: int in range(comparison_count):
if (
barrier_health[barrier_index]
>= _previous_barrier_health[barrier_index]
):
continue
barrier_hit = true
barrier_broken = (
barrier_broken
or barrier_health[barrier_index] <= 0
)
if barrier_hit:
_play_barrier_feedback(barrier_broken)
_previous_barrier_health = barrier_health.duplicate()
_barrier_feedback_initialized = true
var chase_gap: float = progress - maxf(chase_progress, 0.0)
_fishing_danger_target = clampf(
inverse_lerp(
FISHING_DANGER_START_GAP,
FISHING_DANGER_FULL_GAP,
chase_gap,
),
0.0,
1.0,
)
func _play_barrier_feedback(barrier_broken: bool) -> void:
if _player != null:
_player.play_fishing_barrier_camera_feedback(barrier_broken)
if _catch_track_shake_tween != null:
_catch_track_shake_tween.kill()
_catch_track.rotation = 0.0
var tilt: float = 0.018 if barrier_broken else 0.008
var beat: float = 0.038 if barrier_broken else 0.028
_catch_track_shake_tween = create_tween()
_catch_track_shake_tween.tween_property(
_catch_track, "rotation", -tilt, beat
)
_catch_track_shake_tween.tween_property(
_catch_track, "rotation", tilt, beat
)
_catch_track_shake_tween.tween_property(
_catch_track, "rotation", -tilt * 0.45, beat
)
_catch_track_shake_tween.tween_property(
_catch_track, "rotation", 0.0, beat
)
func _update_fishing_danger_vignette(delta: float) -> void:
_fishing_danger_pulse_time += delta
_fishing_danger_intensity = lerpf(
_fishing_danger_intensity,
_fishing_danger_target,
1.0 - exp(-8.0 * delta),
)
var pulse: float = (
0.88 + sin(_fishing_danger_pulse_time * 5.5) * 0.12
)
var shown_intensity: float = _fishing_danger_intensity * pulse
_fishing_danger_vignette.visible = shown_intensity > 0.01
var vignette_material := (
_fishing_danger_vignette.material as ShaderMaterial
)
if vignette_material != null:
vignette_material.set_shader_parameter("intensity", shown_intensity)
func _update_barrier_markers(
positions: PackedFloat32Array,
health: PackedInt32Array,

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=24 format=3]
[gd_scene load_steps=27 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"]
@ -17,6 +17,11 @@
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="15_social_prompt"]
[ext_resource type="PackedScene" path="res://ui/decor_shop.tscn" id="16_decor_shop"]
[ext_resource type="PackedScene" path="res://ui/home_decor_toolbar.tscn" id="17_decor_toolbar"]
[ext_resource type="PackedScene" path="res://ui/rv_upgrade_shop.tscn" id="18_rv_upgrade_shop"]
[ext_resource type="Shader" path="res://ui/fishing_danger_vignette.gdshader" id="19_fishing_vignette"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_fishing_vignette"]
shader = ExtResource("19_fishing_vignette")
[sub_resource type="StyleBoxFlat" id="StyleBox_chase_background"]
bg_color = Color(0.032, 0.118, 0.15, 1)
@ -86,6 +91,20 @@ grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="FishingDangerVignette" type="ColorRect" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
visible = false
z_index = 50
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
material = SubResource("ShaderMaterial_fishing_vignette")
color = Color(1, 1, 1, 1)
[node name="ActiveBaitIndicator" type="Control" parent="UIRoot/CanonicalStage/GameplayTransientHUD"]
unique_name_in_owner = true
z_index = 54
@ -312,6 +331,7 @@ offset_right = 420.0
offset_bottom = -124.0
grow_horizontal = 2
grow_vertical = 0
pivot_offset = Vector2(420, 18)
mouse_filter = 2
[node name="GreenCatchProgress" type="ProgressBar" parent="UIRoot/CanonicalStage/GameplayTransientHUD/CatchTrack"]
@ -440,7 +460,7 @@ theme_override_constants/margin_bottom = 8
unique_name_in_owner = true
layout_mode = 2
theme_override_font_sizes/font_size = 18
text = "Hey, if you've got any fish I've got cash! OwO"
text = "Listen, you may suck at fishing but I've got some things that might help."
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
@ -477,6 +497,9 @@ unique_name_in_owner = true
[node name="DecorShop" parent="UIRoot/CanonicalStage" instance=ExtResource("16_decor_shop")]
unique_name_in_owner = true
[node name="RVUpgradeShop" parent="UIRoot/CanonicalStage" instance=ExtResource("18_rv_upgrade_shop")]
unique_name_in_owner = true
[node name="HomeDecorToolbar" parent="UIRoot/CanonicalStage" instance=ExtResource("17_decor_toolbar")]
unique_name_in_owner = true

View file

@ -10,7 +10,10 @@ var _mode_button: Button
var _selection_label: Label
var _rotate_button: Button
var _store_button: Button
var _clear_button: Button
var _mode: PlayerHomeState.PlacementMode = PlayerHomeState.PlacementMode.GRID
var _interactive: bool = true
var _selection_active: bool = false
func _ready() -> void:
@ -22,31 +25,33 @@ func set_active(active: bool) -> void:
if visible == active:
return
visible = active
if active and _mode_button != null:
_mode_button.call_deferred("grab_focus")
func set_interactive(interactive: bool) -> void:
_interactive = interactive
_refresh_button_interactivity()
func set_mode(mode: PlayerHomeState.PlacementMode) -> void:
_mode = mode
if _mode_button != null:
_mode_button.text = (
"placement: grid"
"grid"
if mode == PlayerHomeState.PlacementMode.GRID
else "placement: free"
else "free"
)
_mode_button.tooltip_text = "placement mode: %s" % _mode_button.text
func set_selection(instance_id: String, display_name: String = "") -> void:
var selected: bool = not instance_id.is_empty()
_selection_active = selected
if _selection_label != null:
_selection_label.text = (
"selected: %s" % display_name
if selected else "click decor to select; drag to move"
if selected else "use held decor to place; click or drag to edit"
)
if _rotate_button != null:
_rotate_button.disabled = not selected
if _store_button != null:
_store_button.disabled = not selected
_refresh_button_interactivity()
func report_status(message: String) -> void:
@ -55,65 +60,92 @@ func report_status(message: String) -> void:
func _build_ui() -> void:
custom_minimum_size = Vector2(330.0, 0.0)
position = Vector2(925.0, 540.0)
z_index = 58
set_anchors_preset(Control.PRESET_TOP_RIGHT)
offset_left = -500.0
offset_top = 0.0
offset_right = 0.0
offset_bottom = 106.0
custom_minimum_size = Vector2(500.0, 0.0)
z_index = 85
mouse_filter = Control.MOUSE_FILTER_STOP
add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(
Color(UtilityPageStyle.OCEAN_PANEL_DEEP, 0.96), 16
),
)
var panel_style: StyleBoxFlat = UtilityPageStyle.panel_style()
panel_style.set_corner_radius_all(10)
panel_style.content_margin_left = 6.0
panel_style.content_margin_top = 6.0
panel_style.content_margin_right = 6.0
panel_style.content_margin_bottom = 6.0
add_theme_stylebox_override("panel", panel_style)
var margin := MarginContainer.new()
for side: StringName in [
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
]:
margin.add_theme_constant_override(side, 12)
margin.add_theme_constant_override(side, 6)
add_child(margin)
var layout := VBoxContainer.new()
layout.add_theme_constant_override("separation", 8)
layout.add_theme_constant_override("separation", 5)
margin.add_child(layout)
var title := Label.new()
title.text = "RV decor"
title.add_theme_font_size_override("font_size", 24)
layout.add_child(title)
_selection_label = Label.new()
_selection_label.text = "click decor to select; drag to move"
_selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_selection_label.add_theme_font_size_override("font_size", 17)
layout.add_child(_selection_label)
var row := HBoxContainer.new()
row.add_theme_constant_override("separation", 8)
row.add_theme_constant_override("separation", 5)
layout.add_child(row)
_mode_button = Button.new()
_mode_button.text = "placement: grid"
_mode_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_button(_mode_button)
var title := Label.new()
title.text = "decor"
title.custom_minimum_size = Vector2(70.0, 46.0)
title.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
title.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
title.add_theme_font_size_override("font_size", 19)
row.add_child(title)
_mode_button = _add_tool_button(row, "grid", "switch placement mode", 82.0)
_mode_button.pressed.connect(_toggle_mode)
row.add_child(_mode_button)
_rotate_button = Button.new()
_rotate_button.text = "rotate"
_rotate_button.disabled = true
UtilityPageStyle.apply_ocean_button(_rotate_button)
_rotate_button = _add_tool_button(row, "rotate", "rotate selected decor", 92.0)
_rotate_button.pressed.connect(rotate_requested.emit)
row.add_child(_rotate_button)
var action_row := HBoxContainer.new()
action_row.add_theme_constant_override("separation", 8)
layout.add_child(action_row)
_store_button = Button.new()
_store_button.text = "pack up"
_store_button.disabled = true
_store_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_button(_store_button)
_store_button = _add_tool_button(row, "pack", "pack selected decor", 82.0)
_store_button.pressed.connect(store_requested.emit)
action_row.add_child(_store_button)
var cancel_button := Button.new()
cancel_button.text = "clear selection"
cancel_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_button(cancel_button)
cancel_button.pressed.connect(cancel_requested.emit)
action_row.add_child(cancel_button)
_clear_button = _add_tool_button(row, "clear", "clear selection", 82.0)
_clear_button.pressed.connect(cancel_requested.emit)
_selection_label = Label.new()
_selection_label.text = "use held decor to place; click or drag to edit"
_selection_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_selection_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_selection_label.add_theme_font_size_override("font_size", 15)
layout.add_child(_selection_label)
_refresh_button_interactivity()
func _add_tool_button(
parent: Container,
button_text: String,
button_tooltip: String,
minimum_width: float,
) -> Button:
var button := Button.new()
button.text = button_text
button.tooltip_text = button_tooltip
button.accessibility_name = button_tooltip
button.custom_minimum_size = Vector2(minimum_width, 46.0)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
UtilityPageStyle.apply_ocean_button(button)
for style_name: StringName in [
&"normal", &"hover", &"pressed", &"focus", &"disabled",
]:
var existing: StyleBox = button.get_theme_stylebox(style_name)
var rounded := existing.duplicate() as StyleBoxFlat
if rounded == null:
continue
rounded.set_corner_radius_all(20)
button.add_theme_stylebox_override(style_name, rounded)
parent.add_child(button)
return button
func _refresh_button_interactivity() -> void:
if _mode_button != null:
_mode_button.disabled = not _interactive
if _rotate_button != null:
_rotate_button.disabled = not _interactive or not _selection_active
if _store_button != null:
_store_button.disabled = not _interactive or not _selection_active
if _clear_button != null:
_clear_button.disabled = not _interactive or not _selection_active
func _toggle_mode() -> void:

View file

@ -2575,7 +2575,10 @@ func _on_tackle_button_gui_input(
call_deferred("_release_pointer_focus", button)
if (
mouse_event == null
or mouse_event.button_index != MOUSE_BUTTON_RIGHT
or mouse_event.button_index not in [
MOUSE_BUTTON_LEFT,
MOUSE_BUTTON_RIGHT,
]
or not mouse_event.pressed
):
return

View file

@ -105,7 +105,7 @@ func _open_storage(
):
return false
_home_access = home_access
_title.text = "RV storage" if home_access else "private storage"
_title.text = "home storage" if home_access else "private storage"
_decorate_button.visible = home_access and allow_decorating
_prior_movement_enabled = _player.is_movement_enabled()
_prior_camera_enabled = _player.is_camera_input_enabled()
@ -290,8 +290,8 @@ func _build_ui() -> void:
_title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(_title)
_decorate_button = Button.new()
_decorate_button.text = "decorate RV"
_decorate_button.tooltip_text = "Place decor inside your RV"
_decorate_button.text = "decorate home"
_decorate_button.tooltip_text = "Place decor inside your home"
_decorate_button.custom_minimum_size = Vector2(160.0, 48.0)
UtilityPageStyle.apply_ocean_button(_decorate_button)
_decorate_button.hide()

330
ui/rv_upgrade_shop.gd Normal file
View file

@ -0,0 +1,330 @@
class_name RVUpgradeShop
extends Control
const INPUT_OWNER: StringName = &"rv_upgrade_shop"
const UtilityPageStyleType = preload("res://ui/utility_page_style.gd")
const CurrencyPresentationType = preload("res://ui/currency_presentation.gd")
signal menu_visibility_changed(is_open: bool)
signal menu_exit_started
var _player: Player
var _wallet: PlayerWallet
var _fishing_spot: FishingSpot
var _interaction: RVUpgradeInteraction
var _home_state: PlayerHomeState
var _network_shop: NetworkShopService
var _wallet_amount: CurrencyAmount
var _price_amount: CurrencyAmount
var _home_label: Label
var _storage_label: Label
var _feedback: Label
var _upgrade_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: RVUpgradeInteraction,
home_state: PlayerHomeState,
network_shop: NetworkShopService,
) -> void:
_player = player
_wallet = wallet
_fishing_spot = fishing_spot
_interaction = interaction
_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 _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: RVUpgradeInteraction) -> 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()
_player.set_movement_enabled(false)
_player.set_camera_input_enabled(false)
_prior_mouse_mode = _restorable_mouse_mode(Input.mouse_mode)
_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()
_upgrade_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_upgrade() -> void:
if _network_shop == null or _transaction_pending:
return
var request_id: String = _network_shop.request_home_upgrade()
if request_id.is_empty():
_refresh()
func _on_purchase_pending(_request_id: String) -> void:
_transaction_pending = true
_feedback.text = "upgrading…"
_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.HOME_UPGRADE
or product_id != NetworkShopService.HOME_UPGRADE_PRODUCT_ID
):
return
_transaction_pending = false
_feedback.text = "starter RV unlocked" if accepted else message.to_lower()
_refresh()
func _on_wallet_changed(_balance: int, _delta: int) -> void:
_refresh()
func _refresh() -> void:
if _wallet_amount == null:
return
var balance: int = _wallet.get_balance() if _wallet != null else 0
var tier: int = _home_state.get_tier() if _home_state != null else 0
var current_capacity: int = (
_home_state.get_storage_capacity() if _home_state != null else 9
)
var next_capacity: int = (
_home_state.get_next_storage_capacity() if _home_state != null else -1
)
var cost: int = (
_home_state.get_next_upgrade_cost() if _home_state != null else -1
)
_wallet_amount.set_amount(balance)
_price_amount.set_amount(maxi(cost, 0))
if tier >= PlayerHomeState.MAX_TIER:
_home_label.text = "starter RV"
_storage_label.text = "storage · %d slots" % current_capacity
_upgrade_button.text = "unlocked"
_upgrade_button.disabled = true
_price_amount.visible = false
else:
_home_label.text = "tent → starter RV"
_storage_label.text = "storage · %d%d slots" % [
current_capacity, next_capacity,
]
_upgrade_button.text = "upgrade"
_upgrade_button.disabled = (
_transaction_pending
or _wallet == null
or not _wallet.can_afford(cost)
or _network_shop == null
or not _network_shop.can_request_home_upgrade()
)
_price_amount.visible = true
func _restorable_mouse_mode(mouse_mode: Input.MouseMode) -> Input.MouseMode:
return (
Input.MOUSE_MODE_VISIBLE
if mouse_mode == Input.MOUSE_MODE_CAPTURED
else mouse_mode
)
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(-390.0, -225.0)
panel.size = Vector2(780.0, 450.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 = "home upgrades"
title.add_theme_font_size_override("font_size", 38)
title.size_flags_horizontal = Control.SIZE_EXPAND_FILL
header.add_child(title)
_wallet_amount = CurrencyPresentationType.instantiate_amount(0, 24.0)
header.add_child(_wallet_amount)
var offer := PanelContainer.new()
offer.size_flags_vertical = Control.SIZE_EXPAND_FILL
offer.add_theme_stylebox_override(
"panel",
UtilityPageStyleType.rounded_style(
UtilityPageStyleType.OCEAN_FIELD, 18
),
)
layout.add_child(offer)
var offer_margin := MarginContainer.new()
for side: StringName in [
&"margin_left", &"margin_top", &"margin_right", &"margin_bottom",
]:
offer_margin.add_theme_constant_override(side, 28)
offer.add_child(offer_margin)
var details := VBoxContainer.new()
details.alignment = BoxContainer.ALIGNMENT_CENTER
details.add_theme_constant_override("separation", 14)
offer_margin.add_child(details)
_home_label = Label.new()
_home_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_home_label.add_theme_font_size_override("font_size", 34)
details.add_child(_home_label)
var description := Label.new()
description.text = (
"A weatherproof home with a larger interior and built-in storage."
)
description.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
description.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
description.add_theme_font_size_override("font_size", 20)
details.add_child(description)
_storage_label = Label.new()
_storage_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_storage_label.add_theme_font_size_override("font_size", 22)
details.add_child(_storage_label)
var purchase_row := HBoxContainer.new()
purchase_row.alignment = BoxContainer.ALIGNMENT_CENTER
purchase_row.add_theme_constant_override("separation", 10)
details.add_child(purchase_row)
_upgrade_button = Button.new()
_upgrade_button.custom_minimum_size = Vector2(220.0, 60.0)
UtilityPageStyleType.apply_ocean_button(_upgrade_button)
_upgrade_button.pressed.connect(_purchase_upgrade)
purchase_row.add_child(_upgrade_button)
_price_amount = CurrencyPresentationType.instantiate_amount(0, 24.0)
purchase_row.add_child(_price_amount)
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)
_upgrade_button.focus_neighbor_bottom = _upgrade_button.get_path_to(
_close_button
)
_close_button.focus_neighbor_top = _close_button.get_path_to(
_upgrade_button
)

View file

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

14
ui/rv_upgrade_shop.tscn Normal file
View file

@ -0,0 +1,14 @@
[gd_scene load_steps=2 format=3]
[ext_resource type="Script" path="res://ui/rv_upgrade_shop.gd" id="1_script"]
[node name="RVUpgradeShop" 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")