Add fish selling, buyer profiles, and player wallet

This commit is contained in:
Alexander Sellite 2026-07-25 16:27:07 -04:00
parent fb04615600
commit d2fe367891
25 changed files with 738 additions and 5 deletions

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" load_steps=2 format=3]
[ext_resource type="Script" path="res://economy/fish_buyer_profile.gd" id="1_profile"]
[resource]
script = ExtResource("1_profile")
id = &"main_fishing_shop"
display_name = "Main Fishing Shop"
animal_name_singular = "shopkeeper"
animal_name_plural = "shopkeepers"
payout_multiplier = 1.0

View file

@ -0,0 +1,12 @@
[gd_resource type="Resource" load_steps=2 format=3]
[ext_resource type="Script" path="res://economy/fish_buyer_profile.gd" id="1_profile"]
[resource]
script = ExtResource("1_profile")
id = &"pelicans"
display_name = "Pelicans"
animal_name_singular = "pelican"
animal_name_plural = "pelicans"
payout_multiplier = 0.7
sale_message_template = "You sold your {fish} to the pelicans for ${payout}."

View file

@ -0,0 +1,11 @@
[gd_resource type="Resource" load_steps=2 format=3]
[ext_resource type="Script" path="res://economy/fish_buyer_profile.gd" id="1_profile"]
[resource]
script = ExtResource("1_profile")
id = &"trading_post"
display_name = "Trading Post"
animal_name_singular = "otter"
animal_name_plural = "otters"
payout_multiplier = 0.85

View file

@ -0,0 +1,46 @@
class_name FishBuyerProfile
extends Resource
@export var id: StringName
@export var display_name: String
@export var animal_name_singular: String
@export var animal_name_plural: String
@export_range(0.0, 10.0, 0.01) var payout_multiplier: float = 1.0
@export_multiline var sale_message_template: String
func is_valid() -> bool:
return (
not id.is_empty()
and not display_name.is_empty()
and payout_multiplier >= 0.0
and is_finite(payout_multiplier)
)
func get_offer(base_value: int) -> int:
if not is_valid() or base_value < 0:
return -1
var offer: int = roundi(float(base_value) * payout_multiplier)
return maxi(offer, 0)
func get_sale_message(
fish_name: String,
payout: int,
) -> String:
if not sale_message_template.is_empty():
return sale_message_template.format(
{
"fish": fish_name,
"payout": payout,
}
)
var buyer_name: String = animal_name_plural
if buyer_name.is_empty():
buyer_name = display_name
return "You sold your %s to the %s for $%d." % [
fish_name,
buyer_name,
payout,
]

View file

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

View file

@ -0,0 +1,45 @@
class_name FishSaleResult
extends RefCounted
enum Status {
SUCCESS,
NOT_FOUND,
FAVORITED,
INVALID_VALUE,
INVALID_BUYER,
INVALID_OFFER,
TRANSACTION_FAILED,
}
var success: bool = false
var status: Status = Status.TRANSACTION_FAILED
var catch_id: StringName
var fish_name: String = ""
var buyer_id: StringName
var buyer_display_name: String = ""
var buyer_animal_name: String = ""
var base_value: int = 0
var payout: int = 0
var sale_message: String = ""
func is_success() -> bool:
return success and status == Status.SUCCESS
func get_message() -> String:
match status:
Status.SUCCESS:
return sale_message
Status.NOT_FOUND:
return "Fish no longer exists."
Status.FAVORITED:
return "Favorited fish cannot be sold."
Status.INVALID_VALUE:
return "Invalid sale value."
Status.INVALID_BUYER:
return "Buyer is unavailable."
Status.INVALID_OFFER:
return "Invalid buyer offer."
_:
return "Transaction failed."

View file

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

View file

@ -0,0 +1,98 @@
class_name FishSaleService
extends Node
const FishCatchType = preload("res://fish/fish_catch.gd")
const FishBuyerProfileType = preload("res://economy/fish_buyer_profile.gd")
const FishInventoryType = preload("res://inventory/fish_inventory.gd")
const FishSaleResultType = preload("res://economy/fish_sale_result.gd")
const PlayerWalletType = preload("res://economy/player_wallet.gd")
signal sale_completed(result: FishSaleResultType)
var _inventory: FishInventoryType
var _wallet: PlayerWalletType
func setup(
inventory: FishInventoryType,
wallet: PlayerWalletType,
) -> void:
_inventory = inventory
_wallet = wallet
func can_sell(
catch_id: StringName,
buyer: FishBuyerProfileType,
) -> bool:
return _validate_sale(catch_id, buyer).is_success()
func sell(
catch_id: StringName,
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
var result: FishSaleResultType = _validate_sale(catch_id, buyer)
if not result.is_success():
return result
var removed_catch: FishCatchType = _inventory.remove_catch_by_id(catch_id)
if removed_catch == null:
result.success = false
result.status = FishSaleResultType.Status.NOT_FOUND
return result
if not _wallet.credit(result.payout):
_inventory.add_catch(removed_catch)
result.success = false
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
return result
sale_completed.emit(result)
return result
func _validate_sale(
catch_id: StringName,
buyer: FishBuyerProfileType,
) -> FishSaleResultType:
var result := FishSaleResultType.new()
result.catch_id = catch_id
if (
catch_id.is_empty()
or _inventory == null
or _wallet == null
):
result.status = FishSaleResultType.Status.NOT_FOUND
return result
if buyer == null or not buyer.is_valid():
result.status = FishSaleResultType.Status.INVALID_BUYER
return result
var fish_catch: FishCatchType = _inventory.get_catch_by_id(catch_id)
if fish_catch == null:
result.status = FishSaleResultType.Status.NOT_FOUND
return result
result.fish_name = fish_catch.fish.display_name
result.buyer_id = buyer.id
result.buyer_display_name = buyer.display_name
result.buyer_animal_name = (
buyer.animal_name_plural
if not buyer.animal_name_plural.is_empty()
else buyer.display_name
)
result.base_value = fish_catch.sale_value
result.payout = buyer.get_offer(result.base_value)
if fish_catch.is_favorited:
result.status = FishSaleResultType.Status.FAVORITED
elif fish_catch.sale_value < 0:
result.status = FishSaleResultType.Status.INVALID_VALUE
elif result.payout < 0:
result.status = FishSaleResultType.Status.INVALID_OFFER
elif not _wallet.can_credit(result.payout):
result.status = FishSaleResultType.Status.TRANSACTION_FAILED
else:
result.status = FishSaleResultType.Status.SUCCESS
result.success = true
result.sale_message = buyer.get_sale_message(
result.fish_name,
result.payout
)
return result

View file

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

45
economy/player_wallet.gd Normal file
View file

@ -0,0 +1,45 @@
class_name PlayerWallet
extends Node
signal balance_changed(new_balance: int, delta: int)
@export var current_balance: int = 0
func _ready() -> void:
current_balance = maxi(current_balance, 0)
func get_balance() -> int:
return current_balance
func can_afford(amount: int) -> bool:
return amount >= 0 and current_balance >= amount
func can_credit(amount: int) -> bool:
return (
amount >= 0
and current_balance <= 9223372036854775807 - amount
)
func credit(amount: int) -> bool:
if not can_credit(amount):
return false
if amount == 0:
return true
current_balance += amount
balance_changed.emit(current_balance, amount)
return true
func debit(amount: int) -> bool:
if not can_afford(amount):
return false
if amount == 0:
return true
current_balance -= amount
balance_changed.emit(current_balance, -amount)
return true

View file

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