Add portable RV homes and decor prototype

This commit is contained in:
Alexander Sellite 2026-08-30 21:37:26 -04:00
parent 81decf2b71
commit fe099f47dd
56 changed files with 5459 additions and 112 deletions

View file

@ -8,8 +8,10 @@ const ItemDataType = preload("res://items/item_data.gd")
const FishingRodDataType = preload("res://items/fishing_rod_data.gd")
const OwnedItemType = preload("res://items/owned_item.gd")
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
const DecorCatalogType = preload("res://homes/decor_catalog.gd")
const SHOP_ID: StringName = &"main_fishing_shop"
const FISHING_SHOP_ID: StringName = &"main_fishing_shop"
const DECOR_SHOP_ID: StringName = &"rv_decor_shop"
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
const BARRIER_PRODUCT_ID: StringName = &"barrier_power_upgrade"
const COOLER_PRODUCT_ID: StringName = &"cooler_capacity_upgrade"
@ -31,6 +33,7 @@ var _session: NetworkSession
var _spawn_service: PlayerSpawnService
var _network_fishing: NetworkFishingService
var _interaction: FishingShopInteraction
var _decor_interaction: DecorShopInteraction
var _wallet: PlayerWallet
var _bag: PlayerBag
var _item_catalog: ItemCatalog
@ -47,6 +50,8 @@ var _received_results: Dictionary[String, bool] = {}
var _applied_results: Dictionary[String, bool] = {}
var _pending_local_request: Dictionary = {}
var _reservations: PlayerAssetReservationService
var _home_state: PlayerHomeState
var _network_home: NetworkHomeService
func setup(
@ -63,6 +68,9 @@ func setup(
save_manager: PlayerSaveManager,
reservations: PlayerAssetReservationService,
inventory_layout: PlayerInventoryLayout = null,
home_state: PlayerHomeState = null,
network_home: NetworkHomeService = null,
decor_interaction: DecorShopInteraction = null,
) -> void:
_session = session
_spawn_service = spawn_service
@ -77,6 +85,9 @@ func setup(
_save_manager = save_manager
_reservations = reservations
_inventory_layout = inventory_layout
_home_state = home_state
_network_home = network_home
_decor_interaction = decor_interaction
if not _session.peer_removed.is_connected(_on_peer_removed):
_session.peer_removed.connect(_on_peer_removed)
if not _session.state_changed.is_connected(_on_session_state_changed):
@ -87,6 +98,12 @@ func set_shop_interaction(interaction: FishingShopInteraction) -> void:
_interaction = interaction
func set_decor_shop_interaction(
interaction: DecorShopInteraction,
) -> void:
_decor_interaction = interaction
func can_request_purchase() -> bool:
return (
_session != null
@ -124,6 +141,19 @@ func can_request_backpack_purchase() -> bool:
)
func can_request_decor_purchase() -> bool:
return (
can_request_purchase()
and _home_state != null
and (
_session.is_host()
or _session.supports_server_capability(
NetworkShopProtocol.DECOR_CAPABILITY
)
)
)
func is_local_purchase_pending() -> bool:
return not _pending_local_request.is_empty()
@ -193,6 +223,15 @@ func request_backpack_capacity_upgrade() -> String:
)
func request_decor(product_id: StringName) -> String:
return _request_purchase(
product_id,
NetworkShopProtocol.ProductCategory.DECOR,
1,
_home_state.get_owned_count(product_id) if _home_state != null else 0,
)
func request_art_kit() -> String:
return _request_purchase(
ArtShopStockType.ART_KIT_ITEM_ID,
@ -269,11 +308,25 @@ func _request_purchase(
product_id, category, 0, 0,
)
return ""
if (
category == NetworkShopProtocol.ProductCategory.DECOR
and not can_request_decor_purchase()
):
local_purchase_finished.emit(
"", false, "Decor require a newer server.",
product_id, category, 0, 0,
)
return ""
var request_id: String = _new_id("shop")
var shop_id: StringName = (
DECOR_SHOP_ID
if category == NetworkShopProtocol.ProductCategory.DECOR
else FISHING_SHOP_ID
)
var request: Dictionary = {
"request_id": request_id,
"session_id": _session.get_session_id(),
"shop_id": str(SHOP_ID),
"shop_id": str(shop_id),
"product_id": str(product_id),
"category": category,
"quantity": quantity,
@ -345,16 +398,34 @@ func _handle_purchase_request(peer_id: int, data: Dictionary) -> void:
_rejected_result(data, "A purchase is already pending.")
)
return
if not _is_shop_available_for_peer(peer_id):
var requested_shop_id := StringName(str(data["shop_id"]))
if not _is_shop_available_for_peer(peer_id, requested_shop_id):
var avatar: Player = _spawn_service.get_avatar(peer_id)
var requested_interaction: FishingShopInteraction = (
_decor_interaction
if requested_shop_id == DECOR_SHOP_ID
else _interaction
)
var message: String = (
"The shop is unavailable."
if avatar == null or _interaction == null
if avatar == null or requested_interaction == null
else "Move closer to the shop."
)
_record_and_send(peer_id, _rejected_result(data, message))
return
var result: Dictionary = _build_authoritative_result(peer_id, data)
if (
bool(result.get("accepted", false))
and int(result.get("category", -1))
== NetworkShopProtocol.ProductCategory.DECOR
and _network_home != null
):
_network_home.authorize_decor_purchase(
peer_id,
StringName(str(result["product_id"])),
int(result["resulting_state"]),
str(result["result_id"]),
)
_pending_by_peer[peer_id] = request_id
_record_and_send(peer_id, result)
@ -368,10 +439,24 @@ func _build_authoritative_result(
var quantity: int = request["quantity"]
var wallet_balance: int = request["wallet_balance"]
var current_state: int = request["current_state"]
if (
category == NetworkShopProtocol.ProductCategory.DECOR
and _network_home != null
):
var authoritative_count: int = (
_network_home.get_authoritative_owned_count(peer_id, product_id)
)
if authoritative_count >= 0:
current_state = authoritative_count
var cost: int = -1
var resulting_state: int = current_state
var rejection: String = ""
if StringName(str(request["shop_id"])) != SHOP_ID:
var expected_shop_id: StringName = (
DECOR_SHOP_ID
if category == NetworkShopProtocol.ProductCategory.DECOR
else FISHING_SHOP_ID
)
if StringName(str(request["shop_id"])) != expected_shop_id:
rejection = "The shop is unavailable."
elif quantity < 1:
rejection = "Purchase could not be completed."
@ -545,6 +630,19 @@ func _build_authoritative_result(
resulting_state = PlayerArtUnlocks.resulting_mask(
current_state, product_id
)
NetworkShopProtocol.ProductCategory.DECOR:
cost = DecorCatalogType.get_price(product_id)
if (
not DecorCatalogType.has_product(product_id)
or quantity != 1
or current_state < 0
or current_state
>= DecorCatalogType.MAX_OWNED_PER_PRODUCT
or cost < 0
):
rejection = "Decor is unavailable."
else:
resulting_state = current_state + 1
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
rejection = "Insufficient funds."
if not rejection.is_empty():
@ -667,6 +765,9 @@ func _apply_purchase_result(data: Dictionary) -> void:
if _inventory_layout != null else 0
)
var art_snapshot: int = _art_unlocks.get_unlock_mask()
var home_snapshot: Dictionary = (
_home_state.to_save_data() if _home_state != null else {}
)
var applied: bool = _apply_local_product(data)
if not applied or not _save_manager.save_if_dirty():
_bag.replace_all_items(bag_snapshot)
@ -676,6 +777,8 @@ func _apply_purchase_result(data: Dictionary) -> void:
if _inventory_layout != null:
_inventory_layout.restore_backpack_level(backpack_snapshot)
_art_unlocks.restore_mask(art_snapshot)
if _home_state != null and not home_snapshot.is_empty():
_home_state.restore_from_save_data(home_snapshot)
_wallet.restore_balance(wallet_snapshot)
_save_manager.save_if_dirty()
_fail_local_apply(data, "Purchase could not be completed.")
@ -696,6 +799,7 @@ func _validate_local_result(data: Dictionary) -> String:
or _cooler_capacity == null
or _art_unlocks == null
or _save_manager == null
or _home_state == null
or _wallet.get_balance() != int(data["expected_wallet"])
or str(data["product_id"])
!= str(_pending_local_request.get("product_id", ""))
@ -806,6 +910,20 @@ func _validate_local_result(data: Dictionary) -> String:
or cost != ArtShopStockType.UPGRADE_PRICE
):
return "Purchase could not be completed."
NetworkShopProtocol.ProductCategory.DECOR:
if (
not DecorCatalogType.has_product(product_id)
or _home_state.get_owned_count(product_id) != expected_state
or resulting_state != expected_state + 1
or cost != DecorCatalogType.get_price(product_id)
or expected_state >= DecorCatalogType.MAX_OWNED_PER_PRODUCT
or not _bag.can_add_item(product_id, 1)
):
return (
"Your inventory is full."
if not _bag.can_add_item(product_id, 1)
else "Purchase could not be completed."
)
_:
return "Purchase could not be completed."
return ""
@ -845,6 +963,12 @@ func _apply_local_product(data: Dictionary) -> bool:
return _art_unlocks.purchase_product(
product_id, _wallet, int(data["total_cost"])
)
NetworkShopProtocol.ProductCategory.DECOR:
return (
_wallet.debit(int(data["total_cost"]))
and _bag.add_item(product_id, 1)
and _home_state.add_owned_decor(product_id, 1)
)
return false
@ -921,7 +1045,7 @@ func _handle_acknowledgement(
peer_id: int,
result_id: String,
request_id: String,
_applied: bool,
applied: bool,
message: String,
) -> void:
if (
@ -933,20 +1057,32 @@ func _handle_acknowledgement(
or _result_owners.get(result_id, 0) != peer_id
):
return
if _network_home != null:
_network_home.finalize_decor_purchase(
peer_id, result_id, applied
)
_acknowledged_results[result_id] = true
_bound_dictionary(_acknowledged_results)
if _pending_by_peer.get(peer_id, "") == request_id:
_pending_by_peer.erase(peer_id)
func _is_shop_available_for_peer(peer_id: int) -> bool:
func _is_shop_available_for_peer(
peer_id: int,
shop_id: StringName,
) -> bool:
var interaction: FishingShopInteraction = (
_decor_interaction
if shop_id == DECOR_SHOP_ID
else _interaction
)
if (
_session == null
or not _session.is_host()
or not _session.is_gameplay_session_active()
or _spawn_service == null
or _interaction == null
or not is_instance_valid(_interaction)
or interaction == null
or not is_instance_valid(interaction)
):
return false
var avatar: Player = _spawn_service.get_avatar(peer_id)
@ -957,7 +1093,7 @@ func _is_shop_available_for_peer(peer_id: int) -> bool:
_network_fishing == null
or not _network_fishing.has_peer_attempt(peer_id)
)
and _interaction.is_avatar_in_range(avatar)
and interaction.is_avatar_in_range(avatar)
)