Add Art Kit painting upgrades
This commit is contained in:
parent
0edbfabdb1
commit
1406a0abc7
34 changed files with 1530 additions and 165 deletions
|
|
@ -11,6 +11,7 @@ static func resolve(
|
|||
normal: Vector3,
|
||||
fallback_tangent: Vector3,
|
||||
canvas_states: Array[Dictionary],
|
||||
requested_grid_size: int = SurfaceDrawingProtocol.DEFAULT_GRID_SIZE,
|
||||
) -> Dictionary:
|
||||
var surface_normal: Vector3 = normal.normalized()
|
||||
var surface_tangent: Vector3 = _projected_tangent(
|
||||
|
|
@ -46,16 +47,25 @@ static func resolve(
|
|||
).normalized()
|
||||
var width: float = float(state["width"]) * float(state["cell_size"])
|
||||
var height: float = float(state["height"]) * float(state["cell_size"])
|
||||
var requested_extent: float = (
|
||||
float(requested_grid_size) * SurfaceDrawingProtocol.CELL_SIZE
|
||||
)
|
||||
if width <= 0.0 or height <= 0.0:
|
||||
continue
|
||||
var horizontal_step: int = roundi(relative.dot(anchor_tangent) / width)
|
||||
var vertical_step: int = roundi(relative.dot(anchor_bitangent) / height)
|
||||
var horizontal_spacing: float = (width + requested_extent) * 0.5
|
||||
var vertical_spacing: float = (height + requested_extent) * 0.5
|
||||
var horizontal_step: int = roundi(
|
||||
relative.dot(anchor_tangent) / horizontal_spacing
|
||||
)
|
||||
var vertical_step: int = roundi(
|
||||
relative.dot(anchor_bitangent) / vertical_spacing
|
||||
)
|
||||
if horizontal_step == 0 and vertical_step == 0:
|
||||
continue
|
||||
var candidate: Vector3 = (
|
||||
anchor_origin
|
||||
+ anchor_tangent * float(horizontal_step) * width
|
||||
+ anchor_bitangent * float(vertical_step) * height
|
||||
+ anchor_tangent * float(horizontal_step) * horizontal_spacing
|
||||
+ anchor_bitangent * float(vertical_step) * vertical_spacing
|
||||
)
|
||||
var distance: float = candidate.distance_to(origin)
|
||||
if distance > nearest_distance:
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
class_name SurfaceDrawingProtocol
|
||||
extends RefCounted
|
||||
|
||||
const CAPABILITY: StringName = &"surface_drawing_v1"
|
||||
const CAPABILITY: StringName = &"surface_drawing_v2"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.ITEM_RELIABLE_CHANNEL
|
||||
const GRID_WIDTH: int = 32
|
||||
const GRID_HEIGHT: int = 32
|
||||
const GRID_SIZES: Array[int] = [16, 32, 64, 128]
|
||||
const DEFAULT_GRID_SIZE: int = 16
|
||||
const MAX_GRID_SIZE: int = 128
|
||||
# Compatibility aliases for callers that only need the default dimensions.
|
||||
const GRID_WIDTH: int = DEFAULT_GRID_SIZE
|
||||
const GRID_HEIGHT: int = DEFAULT_GRID_SIZE
|
||||
const CELL_SIZE: float = 0.075
|
||||
const MAX_ACTIVE_CANVASES: int = 24
|
||||
const MAX_CANVASES: int = 48
|
||||
const MAX_SESSION_GRID_CELLS: int = 49152
|
||||
const MAX_EDITS_PER_REQUEST: int = 16
|
||||
const MAX_CANVAS_ID_LENGTH: int = 64
|
||||
const MAX_REQUEST_ID_LENGTH: int = 64
|
||||
|
|
@ -26,9 +31,9 @@ static func validate_canvas_request(data: Variant) -> bool:
|
|||
and _valid_vector(value.get("normal"))
|
||||
and _valid_vector(value.get("tangent"))
|
||||
and typeof(value.get("width")) == TYPE_INT
|
||||
and int(value["width"]) == GRID_WIDTH
|
||||
and int(value["width"]) in GRID_SIZES
|
||||
and typeof(value.get("height")) == TYPE_INT
|
||||
and int(value["height"]) == GRID_HEIGHT
|
||||
and int(value["height"]) == int(value["width"])
|
||||
and typeof(value.get("cell_size")) in [TYPE_FLOAT, TYPE_INT]
|
||||
and is_equal_approx(float(value["cell_size"]), CELL_SIZE)
|
||||
)
|
||||
|
|
@ -44,6 +49,9 @@ static func validate_edit_request(data: Variant) -> bool:
|
|||
or str(value["canvas_id"]).is_empty()
|
||||
or str(value["canvas_id"]).length() > MAX_CANVAS_ID_LENGTH
|
||||
or not _valid_stroke_id(value.get("stroke_id"))
|
||||
or typeof(value.get("brush_size")) != TYPE_INT
|
||||
or int(value["brush_size"]) < 1
|
||||
or int(value["brush_size"]) > 4
|
||||
or typeof(value.get("edits")) != TYPE_ARRAY
|
||||
):
|
||||
return false
|
||||
|
|
@ -90,9 +98,9 @@ static func validate_canvas_state(data: Variant) -> bool:
|
|||
or not _valid_vector(value.get("normal"))
|
||||
or not _valid_vector(value.get("tangent"))
|
||||
or typeof(value.get("width")) != TYPE_INT
|
||||
or int(value["width"]) != GRID_WIDTH
|
||||
or int(value["width"]) not in GRID_SIZES
|
||||
or typeof(value.get("height")) != TYPE_INT
|
||||
or int(value["height"]) != GRID_HEIGHT
|
||||
or int(value["height"]) != int(value["width"])
|
||||
or typeof(value.get("cell_size")) not in [TYPE_FLOAT, TYPE_INT]
|
||||
or not is_equal_approx(float(value["cell_size"]), CELL_SIZE)
|
||||
or typeof(value.get("revision")) != TYPE_INT
|
||||
|
|
@ -108,10 +116,16 @@ static func validate_canvas_state(data: Variant) -> bool:
|
|||
):
|
||||
return false
|
||||
var cells: Array = value["cells"]
|
||||
if cells.size() > GRID_WIDTH * GRID_HEIGHT:
|
||||
var grid_width: int = int(value["width"])
|
||||
var grid_height: int = int(value["height"])
|
||||
if cells.size() > grid_width * grid_height:
|
||||
return false
|
||||
for cell_value: Variant in cells:
|
||||
if not validate_authoritative_cell(cell_value):
|
||||
if (
|
||||
not validate_authoritative_cell(cell_value)
|
||||
or int((cell_value as Dictionary)["x"]) >= grid_width
|
||||
or int((cell_value as Dictionary)["y"]) >= grid_height
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
|
@ -164,10 +178,10 @@ static func validate_cell_edit(data: Variant) -> bool:
|
|||
if (
|
||||
typeof(value.get("x")) != TYPE_INT
|
||||
or int(value["x"]) < 0
|
||||
or int(value["x"]) >= GRID_WIDTH
|
||||
or int(value["x"]) >= MAX_GRID_SIZE
|
||||
or typeof(value.get("y")) != TYPE_INT
|
||||
or int(value["y"]) < 0
|
||||
or int(value["y"]) >= GRID_HEIGHT
|
||||
or int(value["y"]) >= MAX_GRID_SIZE
|
||||
or typeof(value.get("color_id")) not in [TYPE_STRING, TYPE_STRING_NAME]
|
||||
):
|
||||
return false
|
||||
|
|
|
|||
64
economy/art_shop_stock.gd
Normal file
64
economy/art_shop_stock.gd
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
class_name ArtShopStock
|
||||
extends RefCounted
|
||||
|
||||
const ART_KIT_ITEM_ID: StringName = &"art_kit"
|
||||
const ART_KIT_PRICE: int = 1000
|
||||
const UPGRADE_PRICE: int = 50
|
||||
|
||||
const MARKER_PRODUCTS: Array[StringName] = [
|
||||
&"marker_ocean_teal",
|
||||
&"marker_coral",
|
||||
&"marker_sunny",
|
||||
&"marker_leaf",
|
||||
&"marker_blue",
|
||||
&"marker_violet",
|
||||
&"marker_charcoal",
|
||||
]
|
||||
const BRUSH_PRODUCTS: Array[StringName] = [
|
||||
&"brush_2x",
|
||||
&"brush_3x",
|
||||
&"brush_4x",
|
||||
]
|
||||
const GRID_PRODUCTS: Array[StringName] = [
|
||||
&"grid_32x",
|
||||
&"grid_64x",
|
||||
&"grid_128x",
|
||||
]
|
||||
|
||||
|
||||
static func get_price(product_id: StringName) -> int:
|
||||
if product_id == ART_KIT_ITEM_ID:
|
||||
return ART_KIT_PRICE
|
||||
return UPGRADE_PRICE if PlayerArtUnlocks.is_product_id(product_id) else -1
|
||||
|
||||
|
||||
static func get_display_name(product_id: StringName) -> String:
|
||||
if product_id == ART_KIT_ITEM_ID:
|
||||
return "Art Kit"
|
||||
var color_id: StringName = PlayerArtUnlocks.color_id_for_product(product_id)
|
||||
if not color_id.is_empty():
|
||||
return "%s marker" % SurfaceDrawingPalette.get_display_name(color_id)
|
||||
var brush_size: int = PlayerArtUnlocks.brush_size_for_product(product_id)
|
||||
if brush_size > 0:
|
||||
return "%d× brush" % brush_size
|
||||
var grid_size: int = PlayerArtUnlocks.grid_size_for_product(product_id)
|
||||
if grid_size > 0:
|
||||
return "%d×%d grid" % [grid_size, grid_size]
|
||||
return "Unknown art supply"
|
||||
|
||||
|
||||
static func get_description(product_id: StringName) -> String:
|
||||
if product_id == ART_KIT_ITEM_ID:
|
||||
return "Press P in the game world to paint!"
|
||||
var color_id: StringName = PlayerArtUnlocks.color_id_for_product(product_id)
|
||||
if not color_id.is_empty():
|
||||
return "Unlocks this marker color in the Paint UI."
|
||||
var brush_size: int = PlayerArtUnlocks.brush_size_for_product(product_id)
|
||||
if brush_size > 0:
|
||||
return "Unlocks the %d× brush in the Paint UI." % brush_size
|
||||
var grid_size: int = PlayerArtUnlocks.grid_size_for_product(product_id)
|
||||
if grid_size > 0:
|
||||
return "Unlocks the %d×%d grid in the Paint UI." % [
|
||||
grid_size, grid_size,
|
||||
]
|
||||
return ""
|
||||
1
economy/art_shop_stock.gd.uid
Normal file
1
economy/art_shop_stock.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cqagg27unh12c
|
||||
|
|
@ -36,6 +36,7 @@ const FishingSurfaceSampleType = preload(
|
|||
const FishingSurfaceResolverType = preload(
|
||||
"res://fishing/fishing_surface_resolver.gd"
|
||||
)
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
|
||||
signal status_changed(status: String)
|
||||
signal catch_display_changed(
|
||||
|
|
@ -56,6 +57,7 @@ signal showcase_changed(
|
|||
signal bite_activated
|
||||
signal ready_for_equipment_refresh
|
||||
signal fish_showcase_toggle_requested
|
||||
signal art_ui_toggle_requested
|
||||
|
||||
enum FishingState {
|
||||
READY,
|
||||
|
|
@ -467,6 +469,13 @@ func _unhandled_input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var active_item: ItemDataType = _get_active_item()
|
||||
if (
|
||||
active_item != null
|
||||
and active_item.item_id == ArtShopStockType.ART_KIT_ITEM_ID
|
||||
):
|
||||
art_ui_toggle_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
active_item != null
|
||||
and active_item.category == ItemDataType.Category.CONSUMABLE
|
||||
|
|
|
|||
17
items/catalog/art_kit.tres
Normal file
17
items/catalog/art_kit.tres
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[gd_resource type="Resource" script_class="ItemData" load_steps=3 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://items/item_data.gd" id="1_item"]
|
||||
[ext_resource type="Texture2D" path="res://items/icons/placeholder/art_kit.svg" id="2_icon"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_item")
|
||||
item_id = &"art_kit"
|
||||
display_name = "Art Kit"
|
||||
description = "Press P in the game world to paint!"
|
||||
category = 1
|
||||
icon = ExtResource("2_icon")
|
||||
stackable = false
|
||||
max_stack = 1
|
||||
usable = false
|
||||
equippable = true
|
||||
hotbar_allowed = true
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=8 format=3]
|
||||
[gd_resource type="Resource" script_class="ItemCatalog" load_steps=9 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://items/item_catalog.gd" id="1_script"]
|
||||
[ext_resource type="Resource" path="res://items/catalog/basic_fishing_rod.tres" id="2_rod"]
|
||||
|
|
@ -7,7 +7,8 @@
|
|||
[ext_resource type="Resource" path="res://items/catalog/snack.tres" id="5_snack"]
|
||||
[ext_resource type="Resource" path="res://items/catalog/fish_finder.tres" id="6_finder"]
|
||||
[ext_resource type="Resource" path="res://items/catalog/cooler_expansion.tres" id="7_cooler"]
|
||||
[ext_resource type="Resource" path="res://items/catalog/art_kit.tres" id="8_art_kit"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_script")
|
||||
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler")]
|
||||
items = [ExtResource("2_rod"), ExtResource("3_coffee"), ExtResource("4_energy"), ExtResource("5_snack"), ExtResource("6_finder"), ExtResource("7_cooler"), ExtResource("8_art_kit")]
|
||||
|
|
|
|||
7
items/icons/placeholder/art_kit.svg
Normal file
7
items/icons/placeholder/art_kit.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<rect x="7" y="10" width="50" height="44" rx="12" fill="#35b9c7"/>
|
||||
<rect x="12" y="15" width="40" height="34" rx="9" fill="#0d2c3a"/>
|
||||
<circle cx="23" cy="27" r="6" fill="#f5eed9"/>
|
||||
<circle cx="39" cy="27" r="6" fill="#ef5b62"/>
|
||||
<path d="M18 42L30 33L35 38L46 30L50 42Z" fill="#ffd166"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 388 B |
43
items/icons/placeholder/art_kit.svg.import
Normal file
43
items/icons/placeholder/art_kit.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bm0kkevv62mu0"
|
||||
path="res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://items/icons/placeholder/art_kit.svg"
|
||||
dest_files=["res://.godot/imported/art_kit.svg-d4bd09e1da2e2339f7d505ee23d771f8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
14
main/main.gd
14
main/main.gd
|
|
@ -19,6 +19,7 @@ const PlayerSettingsType = preload("res://settings/player_settings.gd")
|
|||
const TitleScreenType = preload("res://ui/title_screen.gd")
|
||||
const PauseMenuType = preload("res://ui/pause_menu.gd")
|
||||
const ItemCatalogType = preload("res://items/item_catalog.gd")
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
const ItemDataType = preload("res://items/item_data.gd")
|
||||
const FishingShopType = preload("res://ui/fishing_shop.gd")
|
||||
const FishingShopInteractionType = preload(
|
||||
|
|
@ -323,7 +324,8 @@ func _initialize_after_data_root() -> void:
|
|||
_player.hotbar,
|
||||
item_catalog,
|
||||
_player.fishing_upgrades,
|
||||
_player.cooler_capacity
|
||||
_player.cooler_capacity,
|
||||
_player.art_unlocks,
|
||||
)
|
||||
_save_manager.set_autosave_enabled(false)
|
||||
_asset_reservations.setup(
|
||||
|
|
@ -364,6 +366,8 @@ func _initialize_after_data_root() -> void:
|
|||
_relationships,
|
||||
_player,
|
||||
_surface_drawings_root,
|
||||
_player.bag,
|
||||
_player.art_unlocks,
|
||||
)
|
||||
_network_player_list.set_surface_drawing_service(
|
||||
_network_surface_drawing
|
||||
|
|
@ -408,6 +412,7 @@ func _initialize_after_data_root() -> void:
|
|||
item_catalog,
|
||||
_player.fishing_upgrades,
|
||||
_player.cooler_capacity,
|
||||
_player.art_unlocks,
|
||||
_save_manager,
|
||||
_asset_reservations
|
||||
)
|
||||
|
|
@ -454,6 +459,7 @@ func _initialize_after_data_root() -> void:
|
|||
_network_player_list,
|
||||
_settings_manager,
|
||||
_network_surface_drawing,
|
||||
_player.art_unlocks,
|
||||
)
|
||||
_game_ui.setup_data_and_identity(
|
||||
_data_root,
|
||||
|
|
@ -1339,6 +1345,12 @@ func _on_active_hotbar_item_changed(
|
|||
and _player.bag.owns_item(item_id)
|
||||
)
|
||||
_player.set_active_item_is_rod(active_is_rod)
|
||||
_player.set_active_art_kit(
|
||||
item.icon if item != null else null,
|
||||
item_id == ArtShopStockType.ART_KIT_ITEM_ID
|
||||
and item != null
|
||||
and _player.bag.owns_item(item_id),
|
||||
)
|
||||
_network_item_use.submit_local_equipped(item_id, active_is_rod or (
|
||||
item != null and _player.bag.owns_item(item_id)
|
||||
))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
class_name NetworkItemUseService
|
||||
extends Node
|
||||
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
|
||||
const MAX_LEDGER_ENTRIES: int = 64
|
||||
|
||||
signal local_item_use_pending(request_id: String)
|
||||
|
|
@ -324,8 +326,12 @@ func _apply_equipped(data: Dictionary) -> void:
|
|||
var peer_id: int = data["owner_peer_id"]
|
||||
var avatar := _spawn_service.get_avatar(peer_id)
|
||||
if avatar != null:
|
||||
avatar.set_active_item_is_rod(
|
||||
int(data["category"]) == ItemData.Category.ROD
|
||||
var item_id := StringName(str(data["item_id"]))
|
||||
var item: ItemData = _catalog.get_item_by_id(item_id)
|
||||
avatar.set_active_item_is_rod(int(data["category"]) == ItemData.Category.ROD)
|
||||
avatar.set_active_art_kit(
|
||||
item.icon if item != null else null,
|
||||
item_id == ArtShopStockType.ART_KIT_ITEM_ID and bool(data["owns_item"]),
|
||||
)
|
||||
equipped_state_changed.emit(
|
||||
peer_id, StringName(str(data["item_id"])), int(data["category"])
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ const ITEM_RELIABLE_CHANNEL: int = 7
|
|||
const CHAT_RELIABLE_CHANNEL: int = 8
|
||||
const MAIL_RELIABLE_CHANNEL: int = 9
|
||||
const ENET_CHANNEL_COUNT: int = 10
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v1"
|
||||
const SURFACE_DRAWING_CAPABILITY: String = "surface_drawing_v2"
|
||||
const ART_SHOP_CAPABILITY: String = "art_shop_v1"
|
||||
|
||||
enum RejectionCode {
|
||||
NONE,
|
||||
|
|
@ -198,6 +199,7 @@ static func make_server_hello(
|
|||
"fishing_v1",
|
||||
"sale_v1",
|
||||
"shop_v1",
|
||||
ART_SHOP_CAPABILITY,
|
||||
"item_use_v1",
|
||||
"equipment_v1",
|
||||
"fish_showcase_v1",
|
||||
|
|
|
|||
|
|
@ -361,6 +361,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
if is_host():
|
||||
return str(capability) in PackedStringArray([
|
||||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||
NetworkProtocol.ART_SHOP_CAPABILITY,
|
||||
"item_use_v1", "equipment_v1", "fish_showcase_v1",
|
||||
NetworkProtocol.SURFACE_DRAWING_CAPABILITY,
|
||||
"chat_v1",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ class_name NetworkShopProtocol
|
|||
extends RefCounted
|
||||
|
||||
const CAPABILITY: StringName = &"shop_v1"
|
||||
const ART_CAPABILITY: StringName = &"art_shop_v1"
|
||||
const RELIABLE_CHANNEL: int = NetworkProtocol.SHOP_RELIABLE_CHANNEL
|
||||
const MAX_ID_LENGTH: int = 96
|
||||
const MAX_MESSAGE_LENGTH: int = 160
|
||||
|
|
@ -13,6 +14,8 @@ enum ProductCategory {
|
|||
REEL_SPEED_UPGRADE,
|
||||
BARRIER_POWER_UPGRADE,
|
||||
COOLER_CAPACITY_UPGRADE,
|
||||
ART_KIT,
|
||||
ART_UPGRADE,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ const FishingShopStockType = preload(
|
|||
)
|
||||
const ItemDataType = preload("res://items/item_data.gd")
|
||||
const OwnedItemType = preload("res://items/owned_item.gd")
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
|
||||
const SHOP_ID: StringName = &"main_fishing_shop"
|
||||
const REEL_PRODUCT_ID: StringName = &"reel_speed_upgrade"
|
||||
|
|
@ -33,6 +34,7 @@ var _bag: PlayerBag
|
|||
var _item_catalog: ItemCatalog
|
||||
var _upgrades: PlayerFishingUpgrades
|
||||
var _cooler_capacity: PlayerCoolerCapacity
|
||||
var _art_unlocks: PlayerArtUnlocks
|
||||
var _save_manager: PlayerSaveManager
|
||||
var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||
var _pending_by_peer: Dictionary[int, String] = {}
|
||||
|
|
@ -54,6 +56,7 @@ func setup(
|
|||
item_catalog: ItemCatalog,
|
||||
upgrades: PlayerFishingUpgrades,
|
||||
cooler_capacity: PlayerCoolerCapacity,
|
||||
art_unlocks: PlayerArtUnlocks,
|
||||
save_manager: PlayerSaveManager,
|
||||
reservations: PlayerAssetReservationService,
|
||||
) -> void:
|
||||
|
|
@ -66,6 +69,7 @@ func setup(
|
|||
_item_catalog = item_catalog
|
||||
_upgrades = upgrades
|
||||
_cooler_capacity = cooler_capacity
|
||||
_art_unlocks = art_unlocks
|
||||
_save_manager = save_manager
|
||||
_reservations = reservations
|
||||
if not _session.peer_removed.is_connected(_on_peer_removed):
|
||||
|
|
@ -87,6 +91,18 @@ func can_request_purchase() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func can_request_art_purchase() -> bool:
|
||||
return (
|
||||
can_request_purchase()
|
||||
and (
|
||||
_session.is_host()
|
||||
or _session.supports_server_capability(
|
||||
NetworkShopProtocol.ART_CAPABILITY
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_local_purchase_pending() -> bool:
|
||||
return not _pending_local_request.is_empty()
|
||||
|
||||
|
|
@ -127,6 +143,39 @@ func request_cooler_capacity_upgrade() -> String:
|
|||
)
|
||||
|
||||
|
||||
func request_art_kit() -> String:
|
||||
return _request_purchase(
|
||||
ArtShopStockType.ART_KIT_ITEM_ID,
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT,
|
||||
1,
|
||||
_bag.get_quantity(ArtShopStockType.ART_KIT_ITEM_ID)
|
||||
if _bag != null else 0,
|
||||
)
|
||||
|
||||
|
||||
func request_art_upgrade(product_id: StringName) -> String:
|
||||
if (
|
||||
_bag == null
|
||||
or not _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
|
||||
):
|
||||
local_purchase_finished.emit(
|
||||
"",
|
||||
false,
|
||||
"Own an Art Kit before buying upgrades.",
|
||||
product_id,
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
return ""
|
||||
return _request_purchase(
|
||||
product_id,
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE,
|
||||
1,
|
||||
_art_unlocks.get_unlock_mask() if _art_unlocks != null else 0,
|
||||
)
|
||||
|
||||
|
||||
func _request_purchase(
|
||||
product_id: StringName,
|
||||
category: int,
|
||||
|
|
@ -149,6 +198,18 @@ func _request_purchase(
|
|||
0
|
||||
)
|
||||
return ""
|
||||
if (
|
||||
category in [
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT,
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE,
|
||||
]
|
||||
and not can_request_art_purchase()
|
||||
):
|
||||
local_purchase_finished.emit(
|
||||
"", false, "Art supplies require a newer server.",
|
||||
product_id, category, 0, 0,
|
||||
)
|
||||
return ""
|
||||
var request_id: String = _new_id("shop")
|
||||
var request: Dictionary = {
|
||||
"request_id": request_id,
|
||||
|
|
@ -316,6 +377,43 @@ func _build_authoritative_result(
|
|||
]
|
||||
)
|
||||
resulting_state = current_state + 1
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
var art_item: ItemDataType = _item_catalog.get_item_by_id(
|
||||
product_id
|
||||
) if _item_catalog != null else null
|
||||
cost = ArtShopStockType.get_price(product_id)
|
||||
if (
|
||||
product_id != ArtShopStockType.ART_KIT_ITEM_ID
|
||||
or art_item == null
|
||||
or not art_item.is_valid()
|
||||
or art_item.category != ItemDataType.Category.TOOL
|
||||
or art_item.stackable
|
||||
or not art_item.equippable
|
||||
or not art_item.hotbar_allowed
|
||||
or current_state != 0
|
||||
):
|
||||
rejection = (
|
||||
"Art Kit already owned."
|
||||
if current_state > 0
|
||||
else "Purchase could not be completed."
|
||||
)
|
||||
else:
|
||||
resulting_state = 1
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE:
|
||||
cost = ArtShopStockType.get_price(product_id)
|
||||
if (
|
||||
not PlayerArtUnlocks.is_product_id(product_id)
|
||||
or current_state < 0
|
||||
or (current_state & ~PlayerArtUnlocks.ALL_UNLOCK_MASK) != 0
|
||||
or PlayerArtUnlocks.resulting_mask(
|
||||
current_state, product_id
|
||||
) == current_state
|
||||
):
|
||||
rejection = "Upgrade is already unlocked."
|
||||
else:
|
||||
resulting_state = PlayerArtUnlocks.resulting_mask(
|
||||
current_state, product_id
|
||||
)
|
||||
if rejection.is_empty() and (cost < 0 or wallet_balance < cost):
|
||||
rejection = "Not enough fish coin."
|
||||
if not rejection.is_empty():
|
||||
|
|
@ -430,11 +528,13 @@ func _apply_purchase_result(data: Dictionary) -> void:
|
|||
var reel_snapshot: int = _upgrades.get_reel_speed_level()
|
||||
var barrier_snapshot: int = _upgrades.get_barrier_power_level()
|
||||
var cooler_snapshot: int = _cooler_capacity.get_level()
|
||||
var art_snapshot: int = _art_unlocks.get_unlock_mask()
|
||||
var applied: bool = _apply_local_product(data)
|
||||
if not applied or not _save_manager.save_if_dirty():
|
||||
_bag.replace_all_items(bag_snapshot)
|
||||
_upgrades.restore_levels(reel_snapshot, barrier_snapshot)
|
||||
_cooler_capacity.restore_level(cooler_snapshot)
|
||||
_art_unlocks.restore_mask(art_snapshot)
|
||||
_wallet.restore_balance(wallet_snapshot)
|
||||
_save_manager.save_if_dirty()
|
||||
_fail_local_apply(data, "Purchase could not be completed.")
|
||||
|
|
@ -453,6 +553,7 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
or _bag == null
|
||||
or _upgrades == null
|
||||
or _cooler_capacity == null
|
||||
or _art_unlocks == null
|
||||
or _save_manager == null
|
||||
or _wallet.get_balance() != int(data["expected_wallet"])
|
||||
or str(data["product_id"])
|
||||
|
|
@ -472,7 +573,11 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
and _reservations.get_available_fish_coin() < cost
|
||||
):
|
||||
return "Reserved in a letter."
|
||||
if int(data["resulting_state"]) != expected_state + 1:
|
||||
var resulting_state: int = int(data["resulting_state"])
|
||||
if (
|
||||
category != NetworkShopProtocol.ProductCategory.ART_UPGRADE
|
||||
and resulting_state != expected_state + 1
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
if not _wallet.can_afford(cost):
|
||||
return "Not enough fish coin."
|
||||
|
|
@ -497,6 +602,24 @@ func _validate_local_result(data: Dictionary) -> String:
|
|||
return "Purchase could not be completed."
|
||||
if _cooler_capacity.get_next_cost() != cost:
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
if (
|
||||
product_id != ArtShopStockType.ART_KIT_ITEM_ID
|
||||
or _bag.get_quantity(product_id) != expected_state
|
||||
or not _bag.can_add_item(product_id, 1)
|
||||
or cost != ArtShopStockType.ART_KIT_PRICE
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE:
|
||||
if (
|
||||
_art_unlocks.get_unlock_mask() != expected_state
|
||||
or not PlayerArtUnlocks.is_product_id(product_id)
|
||||
or resulting_state != PlayerArtUnlocks.resulting_mask(
|
||||
expected_state, product_id
|
||||
)
|
||||
or cost != ArtShopStockType.UPGRADE_PRICE
|
||||
):
|
||||
return "Purchase could not be completed."
|
||||
_:
|
||||
return "Purchase could not be completed."
|
||||
return ""
|
||||
|
|
@ -517,6 +640,15 @@ func _apply_local_product(data: Dictionary) -> bool:
|
|||
return _upgrades.purchase_barrier_power(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.COOLER_CAPACITY_UPGRADE:
|
||||
return _cooler_capacity.purchase(_wallet)
|
||||
NetworkShopProtocol.ProductCategory.ART_KIT:
|
||||
return (
|
||||
_wallet.debit(int(data["total_cost"]))
|
||||
and _bag.add_item(product_id, 1)
|
||||
)
|
||||
NetworkShopProtocol.ProductCategory.ART_UPGRADE:
|
||||
return _art_unlocks.purchase_product(
|
||||
product_id, _wallet, int(data["total_cost"])
|
||||
)
|
||||
return false
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ extends Node
|
|||
const BrushHighlightShader: Shader = preload(
|
||||
"res://drawing/surface_drawing_highlight.gdshader"
|
||||
)
|
||||
const ArtShopStockType = preload("res://economy/art_shop_stock.gd")
|
||||
|
||||
signal hud_state_changed(
|
||||
is_active: bool,
|
||||
|
|
@ -11,6 +12,7 @@ signal hud_state_changed(
|
|||
color_name: String,
|
||||
color_value: Color,
|
||||
brush_size: int,
|
||||
grid_size: int,
|
||||
status: String,
|
||||
)
|
||||
signal session_artwork_changed(canvas_count: int, painted_cell_count: int)
|
||||
|
|
@ -30,6 +32,8 @@ var _session: NetworkSession
|
|||
var _spawn_service: PlayerSpawnService
|
||||
var _relationships: PlayerRelationshipStore
|
||||
var _local_player: Player
|
||||
var _bag: PlayerBag
|
||||
var _art_unlocks: PlayerArtUnlocks
|
||||
var _drawing_root: Node3D
|
||||
var _canvas_states: Dictionary[String, Dictionary] = {}
|
||||
var _canvas_nodes: Dictionary[String, SurfaceDrawingCanvas] = {}
|
||||
|
|
@ -42,6 +46,7 @@ var _placement_preview_material: StandardMaterial3D
|
|||
var _active: bool = false
|
||||
var _placing_grid: bool = false
|
||||
var _brush_size: int = 1
|
||||
var _grid_size: int = SurfaceDrawingProtocol.DEFAULT_GRID_SIZE
|
||||
var _color_ids: Array[StringName] = []
|
||||
var _color_index: int = 0
|
||||
var _painting: bool = false
|
||||
|
|
@ -60,6 +65,7 @@ var _peer_request_times: Dictionary[int, PackedInt64Array] = {}
|
|||
var _peer_request_ids: Dictionary[int, PackedStringArray] = {}
|
||||
var _stroke_history_by_peer: Dictionary[int, Dictionary] = {}
|
||||
var _cell_last_stroke: Dictionary[String, String] = {}
|
||||
var _peer_art_entitlements: Dictionary[int, Dictionary] = {}
|
||||
|
||||
|
||||
func setup(
|
||||
|
|
@ -68,13 +74,17 @@ func setup(
|
|||
relationships: PlayerRelationshipStore,
|
||||
local_player: Player,
|
||||
drawing_root: Node3D,
|
||||
bag: PlayerBag,
|
||||
art_unlocks: PlayerArtUnlocks,
|
||||
) -> void:
|
||||
_session = session
|
||||
_spawn_service = spawn_service
|
||||
_relationships = relationships
|
||||
_local_player = local_player
|
||||
_drawing_root = drawing_root
|
||||
_color_ids = SurfaceDrawingPalette.get_color_ids()
|
||||
_bag = bag
|
||||
_art_unlocks = art_unlocks
|
||||
_refresh_local_unlocks()
|
||||
if _session != null:
|
||||
_session.state_changed.connect(_on_session_state_changed)
|
||||
_session.peer_removed.connect(_on_peer_removed)
|
||||
|
|
@ -82,6 +92,14 @@ func setup(
|
|||
_relationships.relationship_changed.connect(
|
||||
_on_relationship_changed
|
||||
)
|
||||
if _bag != null and not _bag.contents_changed.is_connected(
|
||||
_on_local_art_entitlement_changed
|
||||
):
|
||||
_bag.contents_changed.connect(_on_local_art_entitlement_changed)
|
||||
if _art_unlocks != null and not _art_unlocks.unlocks_changed.is_connected(
|
||||
_on_local_art_unlocks_changed
|
||||
):
|
||||
_art_unlocks.unlocks_changed.connect(_on_local_art_unlocks_changed)
|
||||
_create_brush_preview()
|
||||
_create_placement_preview()
|
||||
set_process(true)
|
||||
|
|
@ -99,7 +117,7 @@ func handle_input(event: InputEvent, can_open: bool) -> bool:
|
|||
if _active:
|
||||
deactivate()
|
||||
return true
|
||||
if can_open and _drawing_available():
|
||||
if can_open and can_activate():
|
||||
activate()
|
||||
return true
|
||||
return false
|
||||
|
|
@ -180,7 +198,7 @@ func handle_input(event: InputEvent, can_open: bool) -> bool:
|
|||
if _camera_look_active:
|
||||
return false
|
||||
_pointer_screen_position = _clamped_pointer_position(
|
||||
_pointer_screen_position + motion_event.screen_relative
|
||||
motion_event.position
|
||||
)
|
||||
_update_aim()
|
||||
return true
|
||||
|
|
@ -188,18 +206,20 @@ func handle_input(event: InputEvent, can_open: bool) -> bool:
|
|||
|
||||
|
||||
func activate() -> void:
|
||||
if _active or not _drawing_available():
|
||||
if _active or not can_activate():
|
||||
return
|
||||
_active = true
|
||||
_placing_grid = false
|
||||
_placing_grid = true
|
||||
_refresh_local_unlocks()
|
||||
_rebuild_placement_preview()
|
||||
_reset_stroke()
|
||||
_pointer_screen_position = get_viewport().get_visible_rect().size * 0.5
|
||||
_prior_mouse_mode = Input.mouse_mode
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
_update_aim()
|
||||
_refresh_stencil_visibility()
|
||||
_emit_hud_state(
|
||||
"r place grid • click draw • shift erase • ctrl z undo • shift scroll zoom"
|
||||
"click to place a shared grid"
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -226,6 +246,55 @@ func is_placement_mode() -> bool:
|
|||
return _placing_grid
|
||||
|
||||
|
||||
func can_activate() -> bool:
|
||||
return _drawing_available() and _owns_art_kit()
|
||||
|
||||
|
||||
func set_placement_mode(enabled: bool) -> void:
|
||||
_set_placement_mode(enabled)
|
||||
|
||||
|
||||
func set_color_id(color_id: StringName) -> bool:
|
||||
var color_index: int = _color_ids.find(color_id)
|
||||
if color_index < 0:
|
||||
return false
|
||||
_color_index = color_index
|
||||
_reset_stroke()
|
||||
_emit_hud_state("")
|
||||
return true
|
||||
|
||||
|
||||
func set_brush_size(value: int) -> bool:
|
||||
if _art_unlocks == null or not _art_unlocks.is_brush_size_unlocked(value):
|
||||
return false
|
||||
_set_brush_size(value)
|
||||
return true
|
||||
|
||||
|
||||
func set_grid_size(value: int) -> bool:
|
||||
if _art_unlocks == null or not _art_unlocks.is_grid_size_unlocked(value):
|
||||
return false
|
||||
if _grid_size == value:
|
||||
return true
|
||||
_grid_size = value
|
||||
_rebuild_placement_preview()
|
||||
_update_previews()
|
||||
_emit_hud_state("")
|
||||
return true
|
||||
|
||||
|
||||
func get_brush_size() -> int:
|
||||
return _brush_size
|
||||
|
||||
|
||||
func get_grid_size() -> int:
|
||||
return _grid_size
|
||||
|
||||
|
||||
func get_color_id() -> StringName:
|
||||
return _current_color_id()
|
||||
|
||||
|
||||
func get_pointer_screen_position() -> Vector2:
|
||||
return _pointer_screen_position
|
||||
|
||||
|
|
@ -236,12 +305,166 @@ func set_unlocked_color_ids(unlocked_ids: Array[StringName]) -> void:
|
|||
_emit_hud_state("")
|
||||
|
||||
|
||||
func _refresh_local_unlocks() -> void:
|
||||
if _art_unlocks == null:
|
||||
_color_ids = [SurfaceDrawingPalette.DEFAULT_COLOR_ID]
|
||||
return
|
||||
var previous_color: StringName = _current_color_id()
|
||||
_color_ids = _art_unlocks.get_unlocked_color_ids()
|
||||
var previous_index: int = _color_ids.find(previous_color)
|
||||
_color_index = previous_index if previous_index >= 0 else 0
|
||||
if not _art_unlocks.is_brush_size_unlocked(_brush_size):
|
||||
_brush_size = PlayerArtUnlocks.BASE_BRUSH_SIZE
|
||||
if not _art_unlocks.is_grid_size_unlocked(_grid_size):
|
||||
_grid_size = PlayerArtUnlocks.BASE_GRID_SIZE
|
||||
|
||||
|
||||
func _owns_art_kit() -> bool:
|
||||
return _bag != null and _bag.owns_item(ArtShopStockType.ART_KIT_ITEM_ID)
|
||||
|
||||
|
||||
func _local_entitlement() -> Dictionary:
|
||||
return {
|
||||
"has_kit": _owns_art_kit(),
|
||||
"unlock_mask": (
|
||||
_art_unlocks.get_unlock_mask() if _art_unlocks != null else 0
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
func _publish_local_entitlement() -> void:
|
||||
if _session == null or not _session.is_gameplay_session_active():
|
||||
return
|
||||
var entitlement: Dictionary = _local_entitlement()
|
||||
if _session.is_host():
|
||||
_peer_art_entitlements[_session.get_local_peer_id()] = entitlement
|
||||
else:
|
||||
submit_art_entitlement.rpc_id(
|
||||
1,
|
||||
_session.get_session_id(),
|
||||
bool(entitlement["has_kit"]),
|
||||
int(entitlement["unlock_mask"]),
|
||||
)
|
||||
|
||||
|
||||
@rpc(
|
||||
"any_peer",
|
||||
"call_remote",
|
||||
"reliable",
|
||||
SurfaceDrawingProtocol.RELIABLE_CHANNEL,
|
||||
)
|
||||
func submit_art_entitlement(
|
||||
session_id: String,
|
||||
has_kit: bool,
|
||||
unlock_mask: int,
|
||||
) -> void:
|
||||
var sender_id: int = multiplayer.get_remote_sender_id()
|
||||
if (
|
||||
_session == null
|
||||
or not _session.is_host()
|
||||
or not _session.is_authenticated_peer(sender_id)
|
||||
or session_id != _session.get_session_id()
|
||||
or unlock_mask < 0
|
||||
or (unlock_mask & ~PlayerArtUnlocks.ALL_UNLOCK_MASK) != 0
|
||||
):
|
||||
return
|
||||
_peer_art_entitlements[sender_id] = {
|
||||
"has_kit": has_kit,
|
||||
"unlock_mask": unlock_mask,
|
||||
}
|
||||
|
||||
|
||||
func _peer_entitlement(peer_id: int) -> Dictionary:
|
||||
if _session != null and peer_id == _session.get_local_peer_id():
|
||||
return _local_entitlement()
|
||||
return Dictionary(_peer_art_entitlements.get(peer_id, {}))
|
||||
|
||||
|
||||
func _peer_owns_art_kit(peer_id: int) -> bool:
|
||||
return bool(_peer_entitlement(peer_id).get("has_kit", false))
|
||||
|
||||
|
||||
func _peer_can_place_grid(peer_id: int, grid_size: int) -> bool:
|
||||
var entitlement: Dictionary = _peer_entitlement(peer_id)
|
||||
return (
|
||||
bool(entitlement.get("has_kit", false))
|
||||
and _mask_unlocks_grid(
|
||||
int(entitlement.get("unlock_mask", 0)), grid_size
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _peer_can_edit(peer_id: int, request: Dictionary) -> bool:
|
||||
var entitlement: Dictionary = _peer_entitlement(peer_id)
|
||||
if not bool(entitlement.get("has_kit", false)):
|
||||
return false
|
||||
var unlock_mask: int = int(entitlement.get("unlock_mask", 0))
|
||||
if not _mask_unlocks_brush(unlock_mask, int(request["brush_size"])):
|
||||
return false
|
||||
for value: Variant in request["edits"]:
|
||||
var edit: Dictionary = value
|
||||
var color_id := StringName(str(edit.get("color_id", "")))
|
||||
if not color_id.is_empty() and not _mask_unlocks_color(
|
||||
unlock_mask, color_id
|
||||
):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
func _mask_unlocks_color(unlock_mask: int, color_id: StringName) -> bool:
|
||||
if color_id == SurfaceDrawingPalette.DEFAULT_COLOR_ID:
|
||||
return true
|
||||
for product_id: StringName in PlayerArtUnlocks.COLOR_PRODUCTS:
|
||||
if (
|
||||
PlayerArtUnlocks.color_id_for_product(product_id) == color_id
|
||||
and _mask_owns_product(unlock_mask, product_id)
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _mask_unlocks_brush(unlock_mask: int, brush_size: int) -> bool:
|
||||
if brush_size == PlayerArtUnlocks.BASE_BRUSH_SIZE:
|
||||
return true
|
||||
for product_id: StringName in PlayerArtUnlocks.BRUSH_PRODUCTS:
|
||||
if (
|
||||
PlayerArtUnlocks.brush_size_for_product(product_id) == brush_size
|
||||
and _mask_owns_product(unlock_mask, product_id)
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _mask_unlocks_grid(unlock_mask: int, grid_size: int) -> bool:
|
||||
if grid_size == PlayerArtUnlocks.BASE_GRID_SIZE:
|
||||
return true
|
||||
for product_id: StringName in PlayerArtUnlocks.GRID_PRODUCTS:
|
||||
if (
|
||||
PlayerArtUnlocks.grid_size_for_product(product_id) == grid_size
|
||||
and _mask_owns_product(unlock_mask, product_id)
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _mask_owns_product(unlock_mask: int, product_id: StringName) -> bool:
|
||||
var bit: int = PlayerArtUnlocks.get_product_bit(product_id)
|
||||
return bit >= 0 and (unlock_mask & (1 << bit)) != 0
|
||||
|
||||
|
||||
func request_canvas_at_surface(
|
||||
origin: Vector3,
|
||||
normal: Vector3,
|
||||
tangent: Vector3,
|
||||
) -> bool:
|
||||
if not _drawing_available() or normal.is_zero_approx() or tangent.is_zero_approx():
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
or _art_unlocks == null
|
||||
or not _art_unlocks.is_grid_size_unlocked(_grid_size)
|
||||
or normal.is_zero_approx()
|
||||
or tangent.is_zero_approx()
|
||||
):
|
||||
return false
|
||||
var data: Dictionary = {
|
||||
"request_id": _new_request_id("canvas"),
|
||||
|
|
@ -249,8 +472,8 @@ func request_canvas_at_surface(
|
|||
"origin": SurfaceDrawingProtocol.vector_to_array(origin),
|
||||
"normal": SurfaceDrawingProtocol.vector_to_array(normal.normalized()),
|
||||
"tangent": SurfaceDrawingProtocol.vector_to_array(tangent.normalized()),
|
||||
"width": SurfaceDrawingProtocol.GRID_WIDTH,
|
||||
"height": SurfaceDrawingProtocol.GRID_HEIGHT,
|
||||
"width": _grid_size,
|
||||
"height": _grid_size,
|
||||
"cell_size": SurfaceDrawingProtocol.CELL_SIZE,
|
||||
}
|
||||
if _session.is_host():
|
||||
|
|
@ -265,7 +488,12 @@ func request_cell_edits(
|
|||
edits: Array[Dictionary],
|
||||
stroke_id: String = "",
|
||||
) -> bool:
|
||||
if not _drawing_available() or canvas_id.is_empty() or edits.is_empty():
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
or canvas_id.is_empty()
|
||||
or edits.is_empty()
|
||||
):
|
||||
return false
|
||||
var resolved_stroke_id: String = stroke_id
|
||||
if resolved_stroke_id.is_empty():
|
||||
|
|
@ -276,6 +504,7 @@ func request_cell_edits(
|
|||
"session_id": _session.get_session_id(),
|
||||
"canvas_id": canvas_id,
|
||||
"stroke_id": resolved_stroke_id,
|
||||
"brush_size": _brush_size,
|
||||
"edits": edits,
|
||||
}
|
||||
if not SurfaceDrawingProtocol.validate_edit_request(data):
|
||||
|
|
@ -292,7 +521,7 @@ func request_guide_visibility(
|
|||
should_be_visible: bool,
|
||||
should_finalize: bool = false,
|
||||
) -> bool:
|
||||
if not _drawing_available() or canvas_id.is_empty():
|
||||
if not _drawing_available() or not _owns_art_kit() or canvas_id.is_empty():
|
||||
return false
|
||||
var data: Dictionary = {
|
||||
"request_id": _new_request_id("guide"),
|
||||
|
|
@ -311,7 +540,11 @@ func request_guide_visibility(
|
|||
|
||||
|
||||
func request_undo_last_stroke() -> bool:
|
||||
if not _drawing_available() or _last_local_stroke_id.is_empty():
|
||||
if (
|
||||
not _drawing_available()
|
||||
or not _owns_art_kit()
|
||||
or _last_local_stroke_id.is_empty()
|
||||
):
|
||||
_emit_hud_state("nothing to undo")
|
||||
return false
|
||||
var data: Dictionary = {
|
||||
|
|
@ -540,6 +773,7 @@ func _resolved_placement() -> Dictionary:
|
|||
normal,
|
||||
_surface_tangent(normal),
|
||||
states,
|
||||
_grid_size,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -562,7 +796,7 @@ func _request_canvas_at_aim() -> void:
|
|||
else:
|
||||
_emit_hud_state("a shared grid already covers this area")
|
||||
return
|
||||
if _overlaps_existing_canvas(origin, normal):
|
||||
if _overlaps_existing_canvas(origin, normal, _grid_size):
|
||||
_emit_hud_state("a shared grid already covers this area")
|
||||
return
|
||||
if request_canvas_at_surface(origin, normal, placement["tangent"]):
|
||||
|
|
@ -586,13 +820,17 @@ func submit_canvas_request(data: Dictionary) -> void:
|
|||
|
||||
|
||||
func _handle_canvas_request(peer_id: int, data: Dictionary) -> void:
|
||||
var requested_size: int = int(data.get("width", 0))
|
||||
if (
|
||||
not _session.is_host()
|
||||
or not SurfaceDrawingProtocol.validate_canvas_request(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
or not _accept_request(peer_id, str(data["request_id"]))
|
||||
or not _peer_can_place_grid(peer_id, requested_size)
|
||||
or _canvas_states.size() >= SurfaceDrawingProtocol.MAX_CANVASES
|
||||
or _active_canvas_count() >= SurfaceDrawingProtocol.MAX_ACTIVE_CANVASES
|
||||
or _allocated_grid_cells() + requested_size * requested_size
|
||||
> SurfaceDrawingProtocol.MAX_SESSION_GRID_CELLS
|
||||
):
|
||||
return
|
||||
var avatar: Player = _spawn_service.get_avatar(peer_id)
|
||||
|
|
@ -616,7 +854,9 @@ func _handle_canvas_request(peer_id: int, data: Dictionary) -> void:
|
|||
var normal: Vector3 = _normalized_or(
|
||||
surface_hit.get("normal", requested_normal), requested_normal
|
||||
)
|
||||
if _overlaps_existing_canvas(surface_hit["position"], normal):
|
||||
if _overlaps_existing_canvas(
|
||||
surface_hit["position"], normal, requested_size
|
||||
):
|
||||
return
|
||||
var tangent: Vector3 = SurfaceDrawingProtocol.array_to_vector(
|
||||
data["tangent"]
|
||||
|
|
@ -639,8 +879,8 @@ func _handle_canvas_request(peer_id: int, data: Dictionary) -> void:
|
|||
),
|
||||
"normal": SurfaceDrawingProtocol.vector_to_array(normal),
|
||||
"tangent": SurfaceDrawingProtocol.vector_to_array(tangent),
|
||||
"width": SurfaceDrawingProtocol.GRID_WIDTH,
|
||||
"height": SurfaceDrawingProtocol.GRID_HEIGHT,
|
||||
"width": requested_size,
|
||||
"height": requested_size,
|
||||
"cell_size": SurfaceDrawingProtocol.CELL_SIZE,
|
||||
"revision": 0,
|
||||
"guide_visible": true,
|
||||
|
|
@ -672,6 +912,7 @@ func _handle_guide_request(peer_id: int, data: Dictionary) -> void:
|
|||
or not SurfaceDrawingProtocol.validate_guide_request(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
or not _accept_request(peer_id, str(data["request_id"]))
|
||||
or not _peer_owns_art_kit(peer_id)
|
||||
):
|
||||
return
|
||||
var canvas_id: String = str(data["canvas_id"])
|
||||
|
|
@ -794,6 +1035,7 @@ func _handle_edit_request(peer_id: int, data: Dictionary) -> void:
|
|||
or not SurfaceDrawingProtocol.validate_edit_request(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
or not _accept_request(peer_id, str(data["request_id"]))
|
||||
or not _peer_can_edit(peer_id, data)
|
||||
):
|
||||
return
|
||||
var canvas_id: String = str(data["canvas_id"])
|
||||
|
|
@ -808,11 +1050,15 @@ func _handle_edit_request(peer_id: int, data: Dictionary) -> void:
|
|||
):
|
||||
return
|
||||
var stroke_id: String = str(data["stroke_id"])
|
||||
var grid_width: int = int(state["width"])
|
||||
var grid_height: int = int(state["height"])
|
||||
var mutations: Dictionary[String, Dictionary] = {}
|
||||
for edit_value: Variant in data["edits"]:
|
||||
var edit: Dictionary = edit_value
|
||||
var x: int = int(edit["x"])
|
||||
var y: int = int(edit["y"])
|
||||
if x >= grid_width or y >= grid_height:
|
||||
continue
|
||||
var cell_position: Vector3 = _state_cell_position(
|
||||
state, x, y
|
||||
)
|
||||
|
|
@ -998,7 +1244,9 @@ func _publish_cell_mutations(
|
|||
var state: Dictionary = _canvas_states.get(canvas_id, {})
|
||||
if state.is_empty():
|
||||
continue
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(state["cells"])
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state["cells"], int(state["width"])
|
||||
)
|
||||
var canvas_mutations: Dictionary = mutations[canvas_id]
|
||||
var cell_keys: Array[int] = []
|
||||
for cell_key_value: Variant in canvas_mutations.keys():
|
||||
|
|
@ -1048,6 +1296,7 @@ func _handle_undo_request(peer_id: int, data: Dictionary) -> void:
|
|||
or not SurfaceDrawingProtocol.validate_undo_request(data)
|
||||
or str(data["session_id"]) != _session.get_session_id()
|
||||
or not _accept_request(peer_id, str(data["request_id"]))
|
||||
or not _peer_owns_art_kit(peer_id)
|
||||
):
|
||||
return
|
||||
var stroke_id: String = str(data["stroke_id"])
|
||||
|
|
@ -1226,10 +1475,18 @@ func _apply_canvas_update(data: Dictionary) -> void:
|
|||
var canvas: SurfaceDrawingCanvas = _canvas_nodes.get(canvas_id)
|
||||
if state.is_empty() or canvas == null or int(data["revision"]) <= int(state["revision"]):
|
||||
return
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(state["cells"])
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state["cells"], int(state["width"])
|
||||
)
|
||||
var grid_width: int = int(state["width"])
|
||||
var grid_height: int = int(state["height"])
|
||||
for edit_value: Variant in data["edits"]:
|
||||
var edit: Dictionary = edit_value
|
||||
var key: int = int(edit["y"]) * int(state["width"]) + int(edit["x"])
|
||||
var x: int = int(edit["x"])
|
||||
var y: int = int(edit["y"])
|
||||
if x < 0 or x >= grid_width or y < 0 or y >= grid_height:
|
||||
return
|
||||
var key: int = y * grid_width + x
|
||||
if str(edit["color_id"]).is_empty():
|
||||
cells.erase(key)
|
||||
else:
|
||||
|
|
@ -1365,7 +1622,11 @@ func _state_cell_position(state: Dictionary, x: int, y: int) -> Vector3:
|
|||
)
|
||||
|
||||
|
||||
func _overlaps_existing_canvas(origin: Vector3, normal: Vector3) -> bool:
|
||||
func _overlaps_existing_canvas(
|
||||
origin: Vector3,
|
||||
normal: Vector3,
|
||||
requested_size: int,
|
||||
) -> bool:
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
if bool(state.get("finalized", false)):
|
||||
continue
|
||||
|
|
@ -1384,8 +1645,16 @@ func _overlaps_existing_canvas(origin: Vector3, normal: Vector3) -> bool:
|
|||
state["tangent"]
|
||||
).normalized()
|
||||
var bitangent: Vector3 = existing_normal.cross(tangent).normalized()
|
||||
var width: float = float(state["width"]) * float(state["cell_size"])
|
||||
var height: float = float(state["height"]) * float(state["cell_size"])
|
||||
var width: float = (
|
||||
(float(state["width"]) + float(requested_size))
|
||||
* float(state["cell_size"])
|
||||
* 0.5
|
||||
)
|
||||
var height: float = (
|
||||
(float(state["height"]) + float(requested_size))
|
||||
* float(state["cell_size"])
|
||||
* 0.5
|
||||
)
|
||||
var clearance: float = float(state["cell_size"]) * 0.5
|
||||
if (
|
||||
absf(relative.dot(tangent)) < width - clearance
|
||||
|
|
@ -1403,6 +1672,13 @@ func _active_canvas_count() -> int:
|
|||
return count
|
||||
|
||||
|
||||
func _allocated_grid_cells() -> int:
|
||||
var count: int = 0
|
||||
for state: Dictionary in _canvas_states.values():
|
||||
count += int(state.get("width", 0)) * int(state.get("height", 0))
|
||||
return count
|
||||
|
||||
|
||||
func _create_brush_preview() -> void:
|
||||
if _drawing_root == null:
|
||||
return
|
||||
|
|
@ -1427,8 +1703,15 @@ func _create_brush_preview() -> void:
|
|||
|
||||
|
||||
func _create_placement_preview() -> void:
|
||||
_rebuild_placement_preview()
|
||||
|
||||
|
||||
func _rebuild_placement_preview() -> void:
|
||||
if _drawing_root == null:
|
||||
return
|
||||
if _placement_preview != null:
|
||||
_placement_preview.queue_free()
|
||||
_placement_preview = null
|
||||
var mesh := ImmediateMesh.new()
|
||||
_placement_preview_material = StandardMaterial3D.new()
|
||||
_placement_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
|
|
@ -1437,22 +1720,22 @@ func _create_placement_preview() -> void:
|
|||
_placement_preview_material.albedo_color = Color(0.46, 0.91, 0.95, 0.72)
|
||||
mesh.surface_begin(Mesh.PRIMITIVE_LINES, _placement_preview_material)
|
||||
var half_width: float = (
|
||||
float(SurfaceDrawingProtocol.GRID_WIDTH)
|
||||
float(_grid_size)
|
||||
* SurfaceDrawingProtocol.CELL_SIZE
|
||||
* 0.5
|
||||
)
|
||||
var half_height: float = (
|
||||
float(SurfaceDrawingProtocol.GRID_HEIGHT)
|
||||
float(_grid_size)
|
||||
* SurfaceDrawingProtocol.CELL_SIZE
|
||||
* 0.5
|
||||
)
|
||||
for x: int in range(SurfaceDrawingProtocol.GRID_WIDTH + 1):
|
||||
for x: int in range(_grid_size + 1):
|
||||
var horizontal: float = (
|
||||
-half_width + float(x) * SurfaceDrawingProtocol.CELL_SIZE
|
||||
)
|
||||
mesh.surface_add_vertex(Vector3(horizontal, -half_height, 0.0))
|
||||
mesh.surface_add_vertex(Vector3(horizontal, half_height, 0.0))
|
||||
for y: int in range(SurfaceDrawingProtocol.GRID_HEIGHT + 1):
|
||||
for y: int in range(_grid_size + 1):
|
||||
var vertical: float = (
|
||||
-half_height + float(y) * SurfaceDrawingProtocol.CELL_SIZE
|
||||
)
|
||||
|
|
@ -1528,7 +1811,12 @@ func _cycle_color(direction: int) -> void:
|
|||
|
||||
|
||||
func _set_brush_size(value: int) -> void:
|
||||
_brush_size = clampi(value, 1, 4)
|
||||
var requested: int = clampi(value, 1, 4)
|
||||
if _art_unlocks != null and not _art_unlocks.is_brush_size_unlocked(
|
||||
requested
|
||||
):
|
||||
return
|
||||
_brush_size = requested
|
||||
_reset_stroke()
|
||||
_emit_hud_state("")
|
||||
|
||||
|
|
@ -1552,6 +1840,7 @@ func _emit_hud_state(status: String) -> void:
|
|||
SurfaceDrawingPalette.get_display_name(_current_color_id()),
|
||||
_current_color(),
|
||||
_brush_size,
|
||||
_grid_size,
|
||||
status,
|
||||
)
|
||||
|
||||
|
|
@ -1582,14 +1871,17 @@ func _new_request_id(prefix: String) -> String:
|
|||
return "%s-%d-%d" % [prefix, Time.get_ticks_msec(), _request_sequence]
|
||||
|
||||
|
||||
func _cells_by_key(values: Array) -> Dictionary[int, Dictionary]:
|
||||
func _cells_by_key(
|
||||
values: Array,
|
||||
grid_width: int,
|
||||
) -> Dictionary[int, Dictionary]:
|
||||
var result: Dictionary[int, Dictionary] = {}
|
||||
for value: Variant in values:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
continue
|
||||
var cell: Dictionary = value
|
||||
var key: int = (
|
||||
int(cell.get("y", -1)) * SurfaceDrawingProtocol.GRID_WIDTH
|
||||
int(cell.get("y", -1)) * grid_width
|
||||
+ int(cell.get("x", -1))
|
||||
)
|
||||
result[key] = cell.duplicate(true)
|
||||
|
|
@ -1597,7 +1889,9 @@ func _cells_by_key(values: Array) -> Dictionary[int, Dictionary]:
|
|||
|
||||
|
||||
func _state_cell(state: Dictionary, x: int, y: int) -> Dictionary:
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(state.get("cells", []))
|
||||
var cells: Dictionary[int, Dictionary] = _cells_by_key(
|
||||
state.get("cells", []), int(state["width"])
|
||||
)
|
||||
return Dictionary(
|
||||
cells.get(_cell_key_for_state(state, x, y), {})
|
||||
).duplicate(true)
|
||||
|
|
@ -1686,6 +1980,20 @@ func _on_peer_removed(peer_id: int) -> void:
|
|||
_peer_request_times.erase(peer_id)
|
||||
_peer_request_ids.erase(peer_id)
|
||||
_stroke_history_by_peer.erase(peer_id)
|
||||
_peer_art_entitlements.erase(peer_id)
|
||||
|
||||
|
||||
func _on_local_art_entitlement_changed() -> void:
|
||||
if _active and not _owns_art_kit():
|
||||
deactivate()
|
||||
_publish_local_entitlement()
|
||||
|
||||
|
||||
func _on_local_art_unlocks_changed(_unlock_mask: int) -> void:
|
||||
_refresh_local_unlocks()
|
||||
_rebuild_placement_preview()
|
||||
_publish_local_entitlement()
|
||||
_emit_hud_state("")
|
||||
|
||||
|
||||
func _on_relationship_changed(_fingerprint: String) -> void:
|
||||
|
|
@ -1695,8 +2003,15 @@ func _on_relationship_changed(_fingerprint: String) -> void:
|
|||
|
||||
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
||||
if state == NetworkSession.State.JOINED_CLIENT:
|
||||
_publish_local_entitlement()
|
||||
request_canvas_snapshot.rpc_id(1, _new_request_id("snapshot"))
|
||||
return
|
||||
if state in [
|
||||
NetworkSession.State.PRIVATE_HOST,
|
||||
NetworkSession.State.OPEN_HOST,
|
||||
]:
|
||||
_publish_local_entitlement()
|
||||
return
|
||||
if state not in [
|
||||
NetworkSession.State.INACTIVE,
|
||||
NetworkSession.State.DISCONNECTING,
|
||||
|
|
@ -1709,6 +2024,7 @@ func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|||
_canvas_sequence = 0
|
||||
_peer_request_times.clear()
|
||||
_peer_request_ids.clear()
|
||||
_peer_art_entitlements.clear()
|
||||
|
||||
|
||||
func _clear_session_artwork_state() -> void:
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ const PlayerItemEffectsType = preload(
|
|||
const PlayerCoolerCapacityType = preload(
|
||||
"res://progression/player_cooler_capacity.gd"
|
||||
)
|
||||
const PlayerArtUnlocksType = preload(
|
||||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
const FishingRodAttachmentScene = preload(
|
||||
"res://player/fishing_rod_attachment.tscn"
|
||||
)
|
||||
|
|
@ -105,11 +108,13 @@ class ShowcaseCameraSnapshot:
|
|||
@onready var fishing_upgrades: PlayerFishingUpgradesType = %FishingUpgrades
|
||||
@onready var item_effects: PlayerItemEffectsType = %ItemEffects
|
||||
@onready var cooler_capacity: PlayerCoolerCapacityType = %CoolerCapacity
|
||||
@onready var art_unlocks: PlayerArtUnlocksType = %ArtUnlocks
|
||||
@onready var _cast_origin: Marker3D = %CastOrigin
|
||||
@onready var _catch_display: Node3D = %CatchDisplay
|
||||
@onready var _catch_sprite: Sprite3D = %CatchSprite
|
||||
@onready var _held_fish_display: Node3D = %HeldFishDisplay
|
||||
@onready var _held_fish_sprite: Sprite3D = %HeldFishSprite
|
||||
@onready var _held_art_kit_sprite: Sprite3D = %HeldArtKitSprite
|
||||
|
||||
var _gravity: float = float(ProjectSettings.get_setting("physics/3d/default_gravity"))
|
||||
var _camera_dragging: bool = false
|
||||
|
|
@ -721,6 +726,11 @@ func set_active_item_is_rod(active_is_rod: bool) -> void:
|
|||
_fishing_rod.visible = active_is_rod
|
||||
|
||||
|
||||
func set_active_art_kit(icon: Texture2D, should_show: bool) -> void:
|
||||
_held_art_kit_sprite.texture = icon if should_show else null
|
||||
_held_art_kit_sprite.visible = should_show and icon != null
|
||||
|
||||
|
||||
func set_held_fish(
|
||||
fish: FishDataType,
|
||||
display_scale: float,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
[gd_scene load_steps=16 format=3]
|
||||
[gd_scene load_steps=17 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://player/player.gd" id="1_script"]
|
||||
[ext_resource type="Script" path="res://inventory/fish_inventory.gd" id="2_inventory"]
|
||||
|
|
@ -13,6 +13,7 @@
|
|||
[ext_resource type="Script" path="res://player/player_ground_shadow.gd" id="11_ground_shadow"]
|
||||
[ext_resource type="Material" path="res://player/materials/player_blob_shadow.tres" id="12_blob_shadow"]
|
||||
[ext_resource type="PackedScene" path="res://art/exported/characters/base/netfishing_base_character.glb" id="13_character"]
|
||||
[ext_resource type="Script" path="res://progression/player_art_unlocks.gd" id="14_art_unlocks"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="PlayerShape"]
|
||||
radius = 0.45
|
||||
|
|
@ -99,6 +100,10 @@ script = ExtResource("9_effects")
|
|||
unique_name_in_owner = true
|
||||
script = ExtResource("10_capacity")
|
||||
|
||||
[node name="ArtUnlocks" type="Node" parent="."]
|
||||
unique_name_in_owner = true
|
||||
script = ExtResource("14_art_unlocks")
|
||||
|
||||
[node name="CatchDisplayAnchor" type="Marker3D" parent="Visuals"]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 1.45, -1.15)
|
||||
|
|
@ -130,6 +135,18 @@ shaded = false
|
|||
double_sided = true
|
||||
texture_filter = 0
|
||||
|
||||
[node name="HeldArtKitAnchor" type="Marker3D" parent="Visuals"]
|
||||
position = Vector3(0, 1.05, -0.72)
|
||||
|
||||
[node name="HeldArtKitSprite" type="Sprite3D" parent="Visuals/HeldArtKitAnchor"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
pixel_size = 0.008
|
||||
billboard = 1
|
||||
shaded = false
|
||||
double_sided = true
|
||||
texture_filter = 1
|
||||
|
||||
[node name="CameraYaw" type="Node3D" parent="."]
|
||||
unique_name_in_owner = true
|
||||
position = Vector3(0, 1.35, 0)
|
||||
|
|
|
|||
173
progression/player_art_unlocks.gd
Normal file
173
progression/player_art_unlocks.gd
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
class_name PlayerArtUnlocks
|
||||
extends Node
|
||||
|
||||
const PlayerWalletType = preload("res://economy/player_wallet.gd")
|
||||
|
||||
signal unlocks_changed(unlock_mask: int)
|
||||
|
||||
const BASE_BRUSH_SIZE: int = 1
|
||||
const BASE_GRID_SIZE: int = 16
|
||||
const BRUSH_SIZES: Array[int] = [1, 2, 3, 4]
|
||||
const GRID_SIZES: Array[int] = [16, 32, 64, 128]
|
||||
|
||||
# Stable product IDs are both the save and shop transaction boundary.
|
||||
const PRODUCT_BITS: Dictionary[StringName, int] = {
|
||||
&"marker_ocean_teal": 0,
|
||||
&"marker_coral": 1,
|
||||
&"marker_sunny": 2,
|
||||
&"marker_leaf": 3,
|
||||
&"marker_blue": 4,
|
||||
&"marker_violet": 5,
|
||||
&"marker_charcoal": 6,
|
||||
&"brush_2x": 7,
|
||||
&"brush_3x": 8,
|
||||
&"brush_4x": 9,
|
||||
&"grid_32x": 10,
|
||||
&"grid_64x": 11,
|
||||
&"grid_128x": 12,
|
||||
}
|
||||
const COLOR_PRODUCTS: Dictionary[StringName, StringName] = {
|
||||
&"marker_ocean_teal": &"ocean_teal",
|
||||
&"marker_coral": &"coral",
|
||||
&"marker_sunny": &"sunny",
|
||||
&"marker_leaf": &"leaf",
|
||||
&"marker_blue": &"blue",
|
||||
&"marker_violet": &"violet",
|
||||
&"marker_charcoal": &"charcoal",
|
||||
}
|
||||
const BRUSH_PRODUCTS: Dictionary[StringName, int] = {
|
||||
&"brush_2x": 2,
|
||||
&"brush_3x": 3,
|
||||
&"brush_4x": 4,
|
||||
}
|
||||
const GRID_PRODUCTS: Dictionary[StringName, int] = {
|
||||
&"grid_32x": 32,
|
||||
&"grid_64x": 64,
|
||||
&"grid_128x": 128,
|
||||
}
|
||||
const ALL_UNLOCK_MASK: int = (1 << 13) - 1
|
||||
|
||||
var _unlock_mask: int = 0
|
||||
|
||||
|
||||
func get_unlock_mask() -> int:
|
||||
return _unlock_mask
|
||||
|
||||
|
||||
func owns_product(product_id: StringName) -> bool:
|
||||
var bit: int = get_product_bit(product_id)
|
||||
return bit >= 0 and (_unlock_mask & (1 << bit)) != 0
|
||||
|
||||
|
||||
func unlock_product(product_id: StringName) -> bool:
|
||||
var bit: int = get_product_bit(product_id)
|
||||
if bit < 0 or owns_product(product_id):
|
||||
return false
|
||||
_unlock_mask |= 1 << bit
|
||||
unlocks_changed.emit(_unlock_mask)
|
||||
return true
|
||||
|
||||
|
||||
func purchase_product(
|
||||
product_id: StringName,
|
||||
wallet: PlayerWalletType,
|
||||
cost: int,
|
||||
) -> bool:
|
||||
if (
|
||||
wallet == null
|
||||
or cost < 0
|
||||
or owns_product(product_id)
|
||||
or get_product_bit(product_id) < 0
|
||||
or not wallet.can_afford(cost)
|
||||
):
|
||||
return false
|
||||
if not wallet.debit(cost):
|
||||
return false
|
||||
if unlock_product(product_id):
|
||||
return true
|
||||
if not wallet.credit(cost):
|
||||
push_error("Art upgrade purchase rollback failed.")
|
||||
return false
|
||||
|
||||
|
||||
func restore_mask(value: int) -> bool:
|
||||
if value < 0 or (value & ~ALL_UNLOCK_MASK) != 0:
|
||||
return false
|
||||
var changed: bool = _unlock_mask != value
|
||||
_unlock_mask = value
|
||||
if changed:
|
||||
unlocks_changed.emit(_unlock_mask)
|
||||
return true
|
||||
|
||||
|
||||
func reset_to_defaults() -> void:
|
||||
restore_mask(0)
|
||||
|
||||
|
||||
func to_save_data() -> Dictionary:
|
||||
return {"unlock_mask": _unlock_mask}
|
||||
|
||||
|
||||
func get_unlocked_color_ids() -> Array[StringName]:
|
||||
var result: Array[StringName] = [SurfaceDrawingPalette.DEFAULT_COLOR_ID]
|
||||
for product_id: StringName in COLOR_PRODUCTS:
|
||||
if owns_product(product_id):
|
||||
result.append(StringName(str(COLOR_PRODUCTS[product_id])))
|
||||
return result
|
||||
|
||||
|
||||
func get_unlocked_brush_sizes() -> Array[int]:
|
||||
var result: Array[int] = [BASE_BRUSH_SIZE]
|
||||
for product_id: StringName in BRUSH_PRODUCTS:
|
||||
if owns_product(product_id):
|
||||
result.append(int(BRUSH_PRODUCTS[product_id]))
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
|
||||
func get_unlocked_grid_sizes() -> Array[int]:
|
||||
var result: Array[int] = [BASE_GRID_SIZE]
|
||||
for product_id: StringName in GRID_PRODUCTS:
|
||||
if owns_product(product_id):
|
||||
result.append(int(GRID_PRODUCTS[product_id]))
|
||||
result.sort()
|
||||
return result
|
||||
|
||||
|
||||
func is_color_unlocked(color_id: StringName) -> bool:
|
||||
return color_id in get_unlocked_color_ids()
|
||||
|
||||
|
||||
func is_brush_size_unlocked(brush_size: int) -> bool:
|
||||
return brush_size in get_unlocked_brush_sizes()
|
||||
|
||||
|
||||
func is_grid_size_unlocked(grid_size: int) -> bool:
|
||||
return grid_size in get_unlocked_grid_sizes()
|
||||
|
||||
|
||||
static func get_product_bit(product_id: StringName) -> int:
|
||||
return int(PRODUCT_BITS.get(product_id, -1))
|
||||
|
||||
|
||||
static func is_product_id(product_id: StringName) -> bool:
|
||||
return PRODUCT_BITS.has(product_id)
|
||||
|
||||
|
||||
static func resulting_mask(current_mask: int, product_id: StringName) -> int:
|
||||
var bit: int = get_product_bit(product_id)
|
||||
if bit < 0:
|
||||
return current_mask
|
||||
return current_mask | (1 << bit)
|
||||
|
||||
|
||||
static func color_id_for_product(product_id: StringName) -> StringName:
|
||||
return StringName(str(COLOR_PRODUCTS.get(product_id, "")))
|
||||
|
||||
|
||||
static func brush_size_for_product(product_id: StringName) -> int:
|
||||
return int(BRUSH_PRODUCTS.get(product_id, -1))
|
||||
|
||||
|
||||
static func grid_size_for_product(product_id: StringName) -> int:
|
||||
return int(GRID_PRODUCTS.get(product_id, -1))
|
||||
1
progression/player_art_unlocks.gd.uid
Normal file
1
progression/player_art_unlocks.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cixpwmn33k2x2
|
||||
|
|
@ -17,6 +17,9 @@ const PlayerFishingUpgradesType = preload(
|
|||
const PlayerCoolerCapacityType = preload(
|
||||
"res://progression/player_cooler_capacity.gd"
|
||||
)
|
||||
const PlayerArtUnlocksType = preload(
|
||||
"res://progression/player_art_unlocks.gd"
|
||||
)
|
||||
|
||||
const SAVE_VERSION: int = 4
|
||||
const BASIC_ROD_ID: StringName = &"basic_fishing_rod"
|
||||
|
|
@ -36,6 +39,7 @@ class LoadSnapshot:
|
|||
var reel_speed_level: int = 0
|
||||
var barrier_power_level: int = 0
|
||||
var cooler_capacity_level: int = 0
|
||||
var art_unlock_mask: int = 0
|
||||
|
||||
|
||||
@export_range(0.05, 5.0, 0.05) var autosave_delay: float = 0.5
|
||||
|
|
@ -49,6 +53,7 @@ var _hotbar: PlayerHotbarType
|
|||
var _item_catalog: ItemCatalogType
|
||||
var _fishing_upgrades: PlayerFishingUpgradesType
|
||||
var _cooler_capacity: PlayerCoolerCapacityType
|
||||
var _art_unlocks: PlayerArtUnlocksType
|
||||
var _autosave_timer: Timer
|
||||
var _is_configured: bool = false
|
||||
var _is_restoring: bool = false
|
||||
|
|
@ -90,6 +95,7 @@ func setup(
|
|||
item_catalog: ItemCatalogType,
|
||||
fishing_upgrades: PlayerFishingUpgradesType,
|
||||
cooler_capacity: PlayerCoolerCapacityType,
|
||||
art_unlocks: PlayerArtUnlocksType,
|
||||
) -> void:
|
||||
_inventory = inventory
|
||||
_collection_log = collection_log
|
||||
|
|
@ -100,6 +106,7 @@ func setup(
|
|||
_item_catalog = item_catalog
|
||||
_fishing_upgrades = fishing_upgrades
|
||||
_cooler_capacity = cooler_capacity
|
||||
_art_unlocks = art_unlocks
|
||||
_is_configured = (
|
||||
_inventory != null
|
||||
and _collection_log != null
|
||||
|
|
@ -110,6 +117,7 @@ func setup(
|
|||
and _item_catalog != null
|
||||
and _fishing_upgrades != null
|
||||
and _cooler_capacity != null
|
||||
and _art_unlocks != null
|
||||
)
|
||||
if not _is_configured:
|
||||
push_error("PlayerSaveManager setup is missing required references.")
|
||||
|
|
@ -142,6 +150,8 @@ func setup(
|
|||
_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)
|
||||
|
||||
|
||||
func load_player_data() -> bool:
|
||||
|
|
@ -216,6 +226,9 @@ func load_player_data() -> bool:
|
|||
var cooler_restored: bool = _cooler_capacity.restore_level(
|
||||
snapshot.cooler_capacity_level
|
||||
)
|
||||
var art_restored: bool = _art_unlocks.restore_mask(
|
||||
snapshot.art_unlock_mask
|
||||
)
|
||||
_is_restoring = false
|
||||
if (
|
||||
not inventory_restored
|
||||
|
|
@ -225,6 +238,7 @@ func load_player_data() -> bool:
|
|||
or not hotbar_restored
|
||||
or not upgrades_restored
|
||||
or not cooler_restored
|
||||
or not art_restored
|
||||
):
|
||||
push_error("Validated player save could not be restored.")
|
||||
return false
|
||||
|
|
@ -431,6 +445,7 @@ func _build_save_dictionary() -> Dictionary:
|
|||
},
|
||||
"upgrades": _fishing_upgrades.to_save_data(),
|
||||
"cooler": _cooler_capacity.to_save_data(),
|
||||
"art": _art_unlocks.to_save_data(),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -450,6 +465,7 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
var hotbar_data: Dictionary = save_data["hotbar"]
|
||||
var upgrades_data: Dictionary = {}
|
||||
var cooler_data: Dictionary = {}
|
||||
var art_data: Dictionary = {}
|
||||
if typeof(save_data.get("upgrades")) == TYPE_DICTIONARY:
|
||||
upgrades_data = save_data["upgrades"]
|
||||
else:
|
||||
|
|
@ -460,6 +476,8 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
cooler_data = save_data["cooler"]
|
||||
else:
|
||||
push_warning("Saved Cooler data was missing or invalid; using defaults.")
|
||||
if typeof(save_data.get("art")) == TYPE_DICTIONARY:
|
||||
art_data = save_data["art"]
|
||||
if (
|
||||
not wallet_data.has("balance")
|
||||
or typeof(collection_data.get("discovered_fish_ids")) != TYPE_ARRAY
|
||||
|
|
@ -657,6 +675,11 @@ func _build_load_snapshot(save_data: Dictionary) -> LoadSnapshot:
|
|||
PlayerCoolerCapacityType.MAX_LEVEL,
|
||||
"cooler.capacity_level"
|
||||
)
|
||||
snapshot.art_unlock_mask = _read_integer(
|
||||
art_data.get("unlock_mask"),
|
||||
0,
|
||||
PlayerArtUnlocksType.ALL_UNLOCK_MASK,
|
||||
)
|
||||
return snapshot
|
||||
|
||||
|
||||
|
|
@ -766,6 +789,10 @@ func _on_cooler_capacity_changed(
|
|||
_mark_dirty()
|
||||
|
||||
|
||||
func _on_art_unlocks_changed(_unlock_mask: int) -> void:
|
||||
_mark_dirty()
|
||||
|
||||
|
||||
func _on_autosave_timeout() -> void:
|
||||
if _is_dirty:
|
||||
save_now()
|
||||
|
|
@ -822,6 +849,7 @@ func _restore_defaults() -> void:
|
|||
_hotbar.replace_state(default_slots, 0)
|
||||
_fishing_upgrades.reset_to_defaults()
|
||||
_cooler_capacity.reset_to_defaults()
|
||||
_art_unlocks.reset_to_defaults()
|
||||
_is_restoring = false
|
||||
_is_dirty = false
|
||||
|
||||
|
|
|
|||
97
tests/art_tools_validation.gd
Normal file
97
tests/art_tools_validation.gd
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
extends SceneTree
|
||||
|
||||
const MainScene: PackedScene = preload("res://main/main.tscn")
|
||||
const TEST_PORT: int = 18136
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
root.size = Vector2i(1280, 720)
|
||||
var main: Node = MainScene.instantiate()
|
||||
root.add_child(main)
|
||||
for _frame: int in 4:
|
||||
await process_frame
|
||||
if not bool(main.get("_application_initialized")):
|
||||
main.call("_activate_selected_data_path", "", true)
|
||||
for _frame: int in 8:
|
||||
await process_frame
|
||||
assert(bool(main.get("_application_initialized")))
|
||||
assert(bool(main.call("_prepare_private_host")))
|
||||
var save_manager := main.get("_save_manager") as PlayerSaveManager
|
||||
assert(save_manager.initialize_new_game())
|
||||
main.call("_enter_gameplay")
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
) as NetworkSurfaceDrawingService
|
||||
var game_ui := main.get_node("%GameUI") as GameUI
|
||||
var toolbar := game_ui.get_node(
|
||||
"%SurfaceDrawingToolbar"
|
||||
) as SurfaceDrawingToolbar
|
||||
assert(player != null and service != null and toolbar != null)
|
||||
assert(not service.can_activate())
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
assert(player.bag.add_item(ArtShopStock.ART_KIT_ITEM_ID, 1))
|
||||
assert(service.can_activate())
|
||||
|
||||
var paint_key := InputEventKey.new()
|
||||
paint_key.physical_keycode = KEY_P
|
||||
paint_key.pressed = true
|
||||
assert(service.handle_input(paint_key, true))
|
||||
await process_frame
|
||||
assert(service.is_active() and service.is_placement_mode())
|
||||
assert(toolbar.visible)
|
||||
assert(toolbar.position.is_equal_approx(Vector2(18.0, 18.0)))
|
||||
assert(toolbar.get_global_rect().end.x <= 1280.0)
|
||||
assert(toolbar.get_global_rect().end.y <= 720.0)
|
||||
|
||||
var brush_option := toolbar.get_node("%BrushOption") as OptionButton
|
||||
var grid_option := toolbar.get_node("%GridOption") as OptionButton
|
||||
assert(brush_option.item_count == 4)
|
||||
assert(grid_option.item_count == 4)
|
||||
assert(not brush_option.get_popup().is_item_disabled(0))
|
||||
assert(brush_option.get_popup().is_item_disabled(1))
|
||||
assert(not grid_option.get_popup().is_item_disabled(0))
|
||||
assert(grid_option.get_popup().is_item_disabled(1))
|
||||
var color_buttons: Dictionary = toolbar.get("_color_buttons")
|
||||
assert(color_buttons.size() == SurfaceDrawingPalette.COLORS.size())
|
||||
assert(not (color_buttons[&"chalk_white"] as Button).disabled)
|
||||
assert((color_buttons[&"ocean_teal"] as Button).disabled)
|
||||
|
||||
for product_id: StringName in [
|
||||
&"marker_ocean_teal", &"brush_4x", &"grid_128x",
|
||||
]:
|
||||
assert(player.art_unlocks.unlock_product(product_id))
|
||||
await process_frame
|
||||
assert(not (color_buttons[&"ocean_teal"] as Button).disabled)
|
||||
assert(not brush_option.get_popup().is_item_disabled(3))
|
||||
assert(not grid_option.get_popup().is_item_disabled(3))
|
||||
assert(service.set_color_id(&"ocean_teal"))
|
||||
assert(service.set_brush_size(4))
|
||||
assert(service.set_grid_size(128))
|
||||
assert(service.get_color_id() == &"ocean_teal")
|
||||
assert(service.get_brush_size() == 4)
|
||||
assert(service.get_grid_size() == 128)
|
||||
|
||||
assert(service.handle_input(paint_key, true))
|
||||
await process_frame
|
||||
assert(not service.is_active() and not toolbar.visible)
|
||||
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
||||
var fishing_spot := main.get_node("%FishingSpot") as FishingSpot
|
||||
fishing_spot.art_ui_toggle_requested.emit()
|
||||
await process_frame
|
||||
assert(service.is_active() and toolbar.visible)
|
||||
assert(service.get_grid_size() == 128)
|
||||
|
||||
print("Art tools validation: PASS")
|
||||
var session := main.get_node("%NetworkSession") as NetworkSession
|
||||
session.disconnect_session("")
|
||||
main.queue_free()
|
||||
await process_frame
|
||||
quit()
|
||||
1
tests/art_tools_validation.gd.uid
Normal file
1
tests/art_tools_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dr671f64t58wc
|
||||
|
|
@ -72,6 +72,7 @@ func _run() -> void:
|
|||
assert(session.set_host_open(false))
|
||||
|
||||
await _test_host_shop_purchase(main, player, shop_service)
|
||||
await _test_host_art_shop_purchase(main, player, shop_service)
|
||||
await _test_fishing_shop_sale_ui(
|
||||
main, player, catalog, sale_service, reservations
|
||||
)
|
||||
|
|
@ -205,6 +206,25 @@ func _run_multiplayer_client() -> void:
|
|||
print("Client shop result: ", _shop_result)
|
||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||
assert(not shop_service.is_local_purchase_pending())
|
||||
var required_art_balance: int = (
|
||||
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE
|
||||
)
|
||||
if player.wallet.get_balance() < required_art_balance:
|
||||
assert(player.wallet.credit(
|
||||
required_art_balance - player.wallet.get_balance()
|
||||
))
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_art_kit().is_empty())
|
||||
while _shop_result.is_empty():
|
||||
await process_frame
|
||||
assert(bool(_shop_result[1]))
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_art_upgrade(&"grid_32x").is_empty())
|
||||
while _shop_result.is_empty():
|
||||
await process_frame
|
||||
assert(bool(_shop_result[1]))
|
||||
assert(player.bag.owns_item(ArtShopStock.ART_KIT_ITEM_ID))
|
||||
assert(player.art_unlocks.is_grid_size_unlocked(32))
|
||||
print("Economy multiplayer client validation: PASS")
|
||||
session.disconnect_session("Economy client validation complete.")
|
||||
main.queue_free()
|
||||
|
|
@ -450,6 +470,42 @@ func _test_host_shop_purchase(
|
|||
assert(not shop_service.is_local_purchase_pending())
|
||||
|
||||
|
||||
func _test_host_art_shop_purchase(
|
||||
main: Node,
|
||||
player: Player,
|
||||
shop_service: NetworkShopService,
|
||||
) -> void:
|
||||
var interaction := main.get("_shop_interaction") as FishingShopInteraction
|
||||
assert(interaction != null)
|
||||
player.global_position = interaction.global_position
|
||||
for _frame: int in 4:
|
||||
await physics_frame
|
||||
var required_balance: int = (
|
||||
ArtShopStock.ART_KIT_PRICE + ArtShopStock.UPGRADE_PRICE * 3
|
||||
)
|
||||
if player.wallet.get_balance() < required_balance:
|
||||
assert(player.wallet.credit(required_balance - player.wallet.get_balance()))
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_art_kit().is_empty())
|
||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||
assert(player.bag.owns_item(ArtShopStock.ART_KIT_ITEM_ID))
|
||||
var item_catalog := main.get("item_catalog") as ItemCatalog
|
||||
assert(item_catalog != null)
|
||||
var art_item: ItemData = item_catalog.get_item_by_id(
|
||||
ArtShopStock.ART_KIT_ITEM_ID
|
||||
)
|
||||
assert(art_item != null and art_item.hotbar_allowed and art_item.equippable)
|
||||
assert(player.hotbar.assign_item(0, ArtShopStock.ART_KIT_ITEM_ID))
|
||||
for product_id: StringName in [
|
||||
&"marker_ocean_teal", &"brush_2x", &"grid_32x",
|
||||
]:
|
||||
_shop_result.clear()
|
||||
assert(not shop_service.request_art_upgrade(product_id).is_empty())
|
||||
assert(not _shop_result.is_empty() and bool(_shop_result[1]))
|
||||
assert(player.art_unlocks.owns_product(product_id))
|
||||
assert(not shop_service.is_local_purchase_pending())
|
||||
|
||||
|
||||
func _test_fishing_shop_sale_ui(
|
||||
main: Node,
|
||||
player: Player,
|
||||
|
|
@ -499,6 +555,11 @@ func _test_fishing_shop_sale_ui(
|
|||
var buy_mode := shop.get_node("%BuyModeButton") as Button
|
||||
var sell_mode := shop.get_node("%SellModeButton") as Button
|
||||
assert(buy_mode.visible and sell_mode.visible)
|
||||
var stock_sections: Array[String] = []
|
||||
for child: Node in shop.get_node("%SuppliesList").get_children():
|
||||
if child is Label:
|
||||
stock_sections.append((child as Label).text)
|
||||
assert(stock_sections == ["supplies", "art kit", "markers", "brushes", "grids"])
|
||||
await _activate_focused_button(sell_mode, ui_viewport)
|
||||
await process_frame
|
||||
assert(shop.visible and not player_menu.visible)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ func _run_host() -> void:
|
|||
main.call("_enter_gameplay")
|
||||
await physics_frame
|
||||
await physics_frame
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
|
|
@ -133,6 +136,9 @@ func _run_client() -> void:
|
|||
assert(session.supports_server_capability(
|
||||
SurfaceDrawingProtocol.CAPABILITY
|
||||
))
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ func _run() -> void:
|
|||
await physics_frame
|
||||
|
||||
var player := main.get("_player") as Player
|
||||
assert(player.bag.add_item(&"art_kit", 1))
|
||||
assert(player.art_unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK))
|
||||
var service := main.get_node(
|
||||
"%NetworkSurfaceDrawingService"
|
||||
) as NetworkSurfaceDrawingService
|
||||
|
|
@ -119,19 +121,19 @@ func _validate_marker_controls(
|
|||
var prior_mouse_mode: Input.MouseMode = Input.mouse_mode
|
||||
service.activate()
|
||||
assert(service.is_active())
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.is_placement_mode())
|
||||
if DisplayServer.get_name() != "headless":
|
||||
assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
|
||||
assert(Input.mouse_mode == Input.MOUSE_MODE_VISIBLE)
|
||||
|
||||
var placement_key := InputEventKey.new()
|
||||
placement_key.physical_keycode = KEY_R
|
||||
placement_key.pressed = true
|
||||
assert(service.handle_input(placement_key, true))
|
||||
assert(service.is_placement_mode())
|
||||
assert(not service.is_placement_mode())
|
||||
|
||||
var pointer_before: Vector2 = service.get_pointer_screen_position()
|
||||
var pointer_motion := InputEventMouseMotion.new()
|
||||
pointer_motion.screen_relative = Vector2(30.0, -12.0)
|
||||
pointer_motion.position = pointer_before + Vector2(30.0, -12.0)
|
||||
assert(service.handle_input(pointer_motion, true))
|
||||
assert(service.get_pointer_screen_position() != pointer_before)
|
||||
var zoom_event := InputEventMouseButton.new()
|
||||
|
|
@ -156,6 +158,6 @@ func _validate_marker_controls(
|
|||
assert(not service.handle_input(camera_release, true))
|
||||
|
||||
assert(service.handle_input(placement_key, true))
|
||||
assert(not service.is_placement_mode())
|
||||
assert(service.is_placement_mode())
|
||||
service.deactivate()
|
||||
assert(Input.mouse_mode == prior_mouse_mode)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ func _initialize() -> void:
|
|||
|
||||
func _run() -> void:
|
||||
_validate_palette()
|
||||
_validate_art_unlocks()
|
||||
_validate_protocol_bounds()
|
||||
_validate_grid_snapping()
|
||||
await _validate_canvas_geometry_and_collaboration()
|
||||
|
|
@ -37,13 +38,14 @@ func _validate_palette() -> void:
|
|||
|
||||
|
||||
func _validate_protocol_bounds() -> void:
|
||||
assert(SurfaceDrawingProtocol.GRID_WIDTH == 32)
|
||||
assert(SurfaceDrawingProtocol.GRID_HEIGHT == 32)
|
||||
assert(SurfaceDrawingProtocol.GRID_WIDTH == 16)
|
||||
assert(SurfaceDrawingProtocol.GRID_HEIGHT == 16)
|
||||
assert(SurfaceDrawingProtocol.GRID_SIZES == [16, 32, 64, 128])
|
||||
assert(is_equal_approx(SurfaceDrawingProtocol.CELL_SIZE, 0.075))
|
||||
assert(is_equal_approx(
|
||||
float(SurfaceDrawingProtocol.GRID_WIDTH)
|
||||
* SurfaceDrawingProtocol.CELL_SIZE,
|
||||
2.4,
|
||||
1.2,
|
||||
))
|
||||
var canvas_request: Dictionary = {
|
||||
"request_id": "canvas-1",
|
||||
|
|
@ -64,15 +66,28 @@ func _validate_protocol_bounds() -> void:
|
|||
"session_id": "session",
|
||||
"canvas_id": "canvas-1",
|
||||
"stroke_id": "stroke-1",
|
||||
"brush_size": 1,
|
||||
"edits": [{"x": 0, "y": 0, "color_id": "ocean_teal"}],
|
||||
}
|
||||
assert(SurfaceDrawingProtocol.validate_edit_request(edit_request))
|
||||
edit_request["edits"] = [{
|
||||
"x": SurfaceDrawingProtocol.GRID_WIDTH,
|
||||
"x": SurfaceDrawingProtocol.MAX_GRID_SIZE,
|
||||
"y": 0,
|
||||
"color_id": "ocean_teal",
|
||||
}]
|
||||
assert(not SurfaceDrawingProtocol.validate_edit_request(edit_request))
|
||||
edit_request["edits"] = [{"x": 0, "y": 0, "color_id": "ocean_teal"}]
|
||||
edit_request["brush_size"] = 5
|
||||
assert(not SurfaceDrawingProtocol.validate_edit_request(edit_request))
|
||||
for grid_size: int in SurfaceDrawingProtocol.GRID_SIZES:
|
||||
var sized_request: Dictionary = canvas_request.duplicate(true)
|
||||
sized_request["width"] = grid_size
|
||||
sized_request["height"] = grid_size
|
||||
assert(SurfaceDrawingProtocol.validate_canvas_request(sized_request))
|
||||
var unsupported_size: Dictionary = canvas_request.duplicate(true)
|
||||
unsupported_size["width"] = 48
|
||||
unsupported_size["height"] = 48
|
||||
assert(not SurfaceDrawingProtocol.validate_canvas_request(unsupported_size))
|
||||
var guide_request: Dictionary = {
|
||||
"request_id": "guide-1",
|
||||
"session_id": "session",
|
||||
|
|
@ -91,6 +106,20 @@ func _validate_protocol_bounds() -> void:
|
|||
assert(SurfaceDrawingProtocol.validate_undo_request(undo_request))
|
||||
|
||||
|
||||
func _validate_art_unlocks() -> void:
|
||||
var unlocks := PlayerArtUnlocks.new()
|
||||
assert(unlocks.get_unlocked_color_ids() == [&"chalk_white"])
|
||||
assert(unlocks.get_unlocked_brush_sizes() == [1])
|
||||
assert(unlocks.get_unlocked_grid_sizes() == [16])
|
||||
assert(unlocks.unlock_product(&"marker_ocean_teal"))
|
||||
assert(unlocks.unlock_product(&"brush_4x"))
|
||||
assert(unlocks.unlock_product(&"grid_128x"))
|
||||
assert(unlocks.is_color_unlocked(&"ocean_teal"))
|
||||
assert(unlocks.is_brush_size_unlocked(4))
|
||||
assert(unlocks.is_grid_size_unlocked(128))
|
||||
assert(not unlocks.restore_mask(PlayerArtUnlocks.ALL_UNLOCK_MASK + 1))
|
||||
|
||||
|
||||
func _validate_grid_snapping() -> void:
|
||||
var anchor: Dictionary = {
|
||||
"session_id": "session",
|
||||
|
|
@ -109,16 +138,16 @@ func _validate_grid_snapping() -> void:
|
|||
"cells": [],
|
||||
}
|
||||
var snapped: Dictionary = SurfaceDrawingPlacement.resolve(
|
||||
Vector3(2.28, 0.02, 0.08),
|
||||
Vector3(1.08, 0.02, 0.08),
|
||||
Vector3.UP,
|
||||
Vector3.RIGHT,
|
||||
[anchor],
|
||||
)
|
||||
assert(bool(snapped["snapped"]))
|
||||
var snapped_origin: Vector3 = snapped["origin"]
|
||||
assert(snapped_origin.is_equal_approx(Vector3(2.4, 0.0, 0.0)))
|
||||
assert(snapped_origin.is_equal_approx(Vector3(1.2, 0.0, 0.0)))
|
||||
var unsnapped: Dictionary = SurfaceDrawingPlacement.resolve(
|
||||
Vector3(1.2, 0.0, 0.0),
|
||||
Vector3(0.6, 0.0, 0.0),
|
||||
Vector3.UP,
|
||||
Vector3.RIGHT,
|
||||
[anchor],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
195
ui/surface_drawing_toolbar.gd
Normal file
195
ui/surface_drawing_toolbar.gd
Normal 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
|
||||
1
ui/surface_drawing_toolbar.gd.uid
Normal file
1
ui/surface_drawing_toolbar.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://ebnfrdwfm1gm
|
||||
57
ui/surface_drawing_toolbar.tscn
Normal file
57
ui/surface_drawing_toolbar.tscn
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue