diff --git a/classes/Item.gd b/classes/Item.gd index 6e616b6f..88b8093d 100644 --- a/classes/Item.gd +++ b/classes/Item.gd @@ -2,6 +2,8 @@ extends StaticBody2D class_name Item +signal amount_changed(new_amount: int) + enum MODE { LOOT, ITEMSLOT } enum ITEM_TYPE { EQUIPMENT, CONSUMABLE, CURRENCY } @@ -24,7 +26,12 @@ var expire_timer: Timer var drop_rate: float = 0.0 -var amount: int = 1 +var amount_max: int = 1 +var amount: int = 1: + set(val): + amount = val + amount_changed.emit(amount) + var price: int = 0 var value: int = 0 @@ -66,6 +73,10 @@ func _ready(): add_child(expire_timer) +func get_icon() -> Texture: + return $Icon.texture + + func start_expire_timer(): expire_timer.start(expire_time) @@ -89,10 +100,18 @@ func server_use(player: Player) -> bool: ITEM_TYPE.CONSUMABLE: if boost.hp > 0: player.stats.heal(self.name, boost.hp) + + if amount <= 1: + player.inventory.server_remove_item(uuid) + else: + player.inventory.server_set_item_amount(uuid, amount - 1) return true + ITEM_TYPE.EQUIPMENT: if player.equipment and player.equipment.server_equip_item(self): + player.inventory.server_remove_item(uuid) return true + else: GodotLogger.info("%s could not equip item %s" % [player.name, item_class]) return false @@ -115,5 +134,11 @@ func from_json(data: Dictionary) -> bool: return true +static func instance_from_json(data: Dictionary) -> Item: + var new_item := Item.new() + new_item.from_json(data) + return new_item + + func _on_expire_timer_timeout(): queue_free() diff --git a/components/connection/PlayerSpawnSynchronizer/PlayerSpawnSynchronizer.gd b/components/connection/PlayerSpawnSynchronizer/PlayerSpawnSynchronizer.gd index 91638bbc..4fef5be7 100644 --- a/components/connection/PlayerSpawnSynchronizer/PlayerSpawnSynchronizer.gd +++ b/components/connection/PlayerSpawnSynchronizer/PlayerSpawnSynchronizer.gd @@ -35,7 +35,9 @@ func _ready(): user_authenticator.server_player_logged_in.connect(_on_server_player_logged_in) # Connect to the disconnect signal, this is the trigger to emit the signal to remove the player from the server instance - _multiplayer_connection.multiplayer_api.peer_disconnected.connect(_on_server_peer_disconnected) + _multiplayer_connection.multiplayer_api.peer_disconnected.connect( + _on_server_peer_disconnected + ) else: pass diff --git a/components/player/equipmentsynchronizercomponent/EquipmentSynchronizerComponent.gd b/components/player/equipmentsynchronizercomponent/EquipmentSynchronizerComponent.gd index 9cbc428b..3f3fa08f 100644 --- a/components/player/equipmentsynchronizercomponent/EquipmentSynchronizerComponent.gd +++ b/components/player/equipmentsynchronizercomponent/EquipmentSynchronizerComponent.gd @@ -14,7 +14,7 @@ var _target_node: Node var _equipment_synchronizer_rpc: EquipmentSynchronizerRPC = null -var items = { +var items: Dictionary = { "Head": null, "Body": null, "Legs": null, @@ -144,7 +144,7 @@ func _unequip_item(item_uuid: String) -> Item: func get_item(item_uuid: String) -> Item: - for equipment_slot in items: + for equipment_slot: String in items: var item: Item = items[equipment_slot] if item != null and item.uuid == item_uuid: return item @@ -156,7 +156,7 @@ func get_boost() -> Boost: var boost: Boost = Boost.new() boost.identifier = "equipment" - for equipment_slot in items: + for equipment_slot: String in items: var item: Item = items[equipment_slot] if item != null: boost.combine_boost(item.boost) @@ -167,7 +167,7 @@ func get_boost() -> Boost: func to_json() -> Dictionary: var output: Dictionary = {} - for slot in items: + for slot: String in items: if items[slot] != null: var item: Item = items[slot] output[slot] = item.to_json() @@ -176,7 +176,7 @@ func to_json() -> Dictionary: func from_json(data: Dictionary) -> bool: - for slot in data: + for slot: String in data: if not slot in items: GodotLogger.warn("Slot=[%s] does not exist in equipment items" % slot) return false diff --git a/components/player/inventorysynchronizercomponent/InventorySynchronizerComponent.gd b/components/player/inventorysynchronizercomponent/InventorySynchronizerComponent.gd index e4b02d0a..3b11f7c8 100644 --- a/components/player/inventorysynchronizercomponent/InventorySynchronizerComponent.gd +++ b/components/player/inventorysynchronizercomponent/InventorySynchronizerComponent.gd @@ -5,6 +5,7 @@ class_name InventorySynchronizerComponent signal loaded signal item_added(item_uuid: String, item_class: String) signal item_removed(item_uuid: String) +signal item_amount_changed(item_uuid: String) signal gold_added(total: int, amount: int) signal gold_removed(total: int, amount: int) @@ -65,26 +66,62 @@ func server_sync_inventory(peer_id: int): _inventory_synchronizer_rpc.sync_response(peer_id, to_json()) +## Attempts to stack items or add them if there's nowhere else to stack them. Stacking is purely server sided. func server_add_item(item: Item) -> bool: if not _target_node.multiplayer_connection.is_server(): return false + #Currency is directly added to gold if item.item_type == Item.ITEM_TYPE.CURRENCY: server_add_gold(item.amount) return true item.collision_layer = 0 + #Inventory full if items.size() >= size: return false - items.append(item) + #Try to stack + var existing_item: Item = null + + #Look for an item that has available space. + for i_item in get_items_by_class(item.item_class): + if i_item.amount < i_item.amount_max: + existing_item = i_item + break + + #If found, start the stacking. + if existing_item is Item: + var remaining_space: int = existing_item.amount_max - existing_item.amount + var amount_to_add: int = min(item.amount, remaining_space) + var surplus: int = max(0, item.amount - remaining_space) + + #If there's space remaining, add some of this item's amount to the existing one. + #And remove it from the former. + #This is delegated to the server_set_item_amount() function which synchronizes amounts by itself. + if remaining_space > 0: + server_set_item_amount(item.uuid, item.amount - amount_to_add) + server_set_item_amount(existing_item.uuid, existing_item.amount + amount_to_add) + + #Any remaining amount is added as a separate item + if surplus > 0: + server_add_item(item) + _inventory_synchronizer_rpc.add_item( + _target_node.peer_id, item.name, item.item_class, item.amount + ) - _inventory_synchronizer_rpc.add_item( - _target_node.peer_id, item.name, item.item_class, item.amount - ) + return true - return true + #Adding the item from scratch + else: + items.append(item) + + _inventory_synchronizer_rpc.add_item( + _target_node.peer_id, item.name, item.item_class, item.amount + ) + + return true func client_add_item(item_uuid: String, item_class: String, amount: int): @@ -118,6 +155,24 @@ func client_remove_item(item_uuid: String): item_removed.emit(item_uuid) +func server_set_item_amount(item_uuid: String, amount: int): + if not _target_node.multiplayer_connection.is_server(): + return false + + var item: Item = get_item(item_uuid) + if item != null: + item.amount = amount + _inventory_synchronizer_rpc.change_item_amount(_target_node.peer_id, item_uuid, amount) + + +func client_change_item_amount(item_uuid: String, amount: int): + var item: Item = get_item(item_uuid) + if item != null: + item.amount = amount + + item_amount_changed.emit(item_uuid) + + func get_item(item_uuid: String) -> Item: for item in items: if item.uuid == item_uuid: @@ -126,13 +181,21 @@ func get_item(item_uuid: String) -> Item: return null +## Returns the first instance of an item of this class +func get_items_by_class(item_class: String) -> Array[Item]: + var output: Array[Item] = [] + for item: Item in items: + if item.item_class == item_class: + output.append(item) + return output + + func server_use_item(item_uuid: String): if not _target_node.multiplayer_connection.is_server(): return false var item: Item = get_item(item_uuid) if item and item.server_use(_target_node): - server_remove_item(item_uuid) return true return false diff --git a/components/player/inventorysynchronizercomponent/InventorySynchronizerRPC.gd b/components/player/inventorysynchronizercomponent/InventorySynchronizerRPC.gd index 027e033e..1788a4d6 100644 --- a/components/player/inventorysynchronizercomponent/InventorySynchronizerRPC.gd +++ b/components/player/inventorysynchronizercomponent/InventorySynchronizerRPC.gd @@ -38,6 +38,10 @@ func remove_item(peer_id: int, item_uuid: String): _remove_item.rpc_id(peer_id, item_uuid) +func change_item_amount(peer_id: int, item_uuid: String, amount: int): + _change_item_amount.rpc_id(peer_id, item_uuid, amount) + + func add_gold(peer_id: int, total: int, amount: int): _add_gold.rpc_id(peer_id, total, amount) @@ -128,6 +132,22 @@ func _remove_item(u: String): ) +@rpc("call_remote", "authority", "reliable") +func _change_item_amount(u: String, a: int): + if _multiplayer_connection.client_player == null: + return + + if _multiplayer_connection.client_player.component_list.has( + InventorySynchronizerComponent.COMPONENT_NAME + ): + ( + _multiplayer_connection + . client_player + . component_list[InventorySynchronizerComponent.COMPONENT_NAME] + . client_change_item_amount(u, a) + ) + + @rpc("call_remote", "authority", "reliable") func _add_gold(t: int, a: int): if _multiplayer_connection.client_player == null: diff --git a/scenes/items/consumables/healthpotion/HealthPotion.gd b/scenes/items/consumables/healthpotion/HealthPotion.gd index f9a950f1..5f788800 100644 --- a/scenes/items/consumables/healthpotion/HealthPotion.gd +++ b/scenes/items/consumables/healthpotion/HealthPotion.gd @@ -7,3 +7,4 @@ func _init(): item_class = "HealthPotion" item_type = ITEM_TYPE.CONSUMABLE boost.hp = 25 + amount_max = 6 diff --git a/scenes/player/inventory/Inventory.gd b/scenes/player/inventory/Inventory.gd index 3962e662..8769d32d 100644 --- a/scenes/player/inventory/Inventory.gd +++ b/scenes/player/inventory/Inventory.gd @@ -12,7 +12,7 @@ var gold := 0: gold = amount $VBoxContainer/GoldValue.text = str(amount) -var panels = [] +var panels: Array[Array] = [] var mouse_above_this_panel: InventoryPanel var location_cache = {} @@ -50,6 +50,7 @@ func _input(event): if visible: hide() else: + update_inventory() show() @@ -67,6 +68,23 @@ func get_panel_at_pos(pos: Vector2) -> InventoryPanel: return $GridContainer.get_node(panel_path) +func get_panels(occupied_only: bool) -> Array[InventoryPanel]: + var output: Array[InventoryPanel] = [] + var temp_arr: Array = [] + + if occupied_only: + for row: Array in panels: + for panel: InventoryPanel in row: + if panel.item is Item: + temp_arr.append(panel) + else: + for row: Array in panels: + temp_arr += row + + output.assign(temp_arr) + return output + + func swap_items(from: Panel, to: Panel): var temp_item: Item = to.item @@ -74,6 +92,44 @@ func swap_items(from: Panel, to: Panel): from.item = temp_item +func update_inventory(): + var occupied_panels: Array[InventoryPanel] = get_panels(true) + var panel_item_uuid_dictionary: Dictionary = {} + var panels_with_invalid_items: Dictionary = {} + + #Ensure all panels have an item + assert( + occupied_panels.all( + func(panel_occupied: InventoryPanel): return panel_occupied.item is Item + ) + ) + + #Store the uuid of the item that each panel has to accelerate the rest of the update. + for panel: InventoryPanel in occupied_panels: + panel_item_uuid_dictionary[panel.item.uuid] = panel + + #All panels are assumed invalid until proven otherwise. + panels_with_invalid_items = panel_item_uuid_dictionary.duplicate() + + #Check every inventory item + for inv_item: Item in inventory_synchronizer.items: + #Unmark as invalid those who have been found in the inventory + panels_with_invalid_items.erase(inv_item.uuid) + + #No item with this uuid has been found in the displayed inventory, add it. + if not inv_item.uuid in panel_item_uuid_dictionary.keys(): + place_item_at_free_slot(inv_item) + + else: + #Item found, update it. + panel_item_uuid_dictionary[inv_item.uuid].item = inv_item + panel_item_uuid_dictionary[inv_item.uuid].queue_redraw() + + #Clear any panels with items that are NOT in the inventory + for item_uuid: String in panels_with_invalid_items: + panels_with_invalid_items[item_uuid].item = null + + func place_item_at_free_slot(item: Item) -> bool: for y in range(SIZE.y): for x in range(SIZE.x): @@ -81,6 +137,7 @@ func place_item_at_free_slot(item: Item) -> bool: var panel: InventoryPanel = get_panel_at_pos(pos) if panel.item == null: panel.item = item + panel.queue_redraw() return true return false @@ -133,3 +190,9 @@ func _on_item_removed(item_uuid: String): if panel.item and panel.item.uuid == item_uuid: panel.item = null return + + +func _on_redraw_required(): + for row: Array in panels: + for panel: InventoryPanel in row: + panel.queue_redraw() diff --git a/scenes/player/inventory/InventoryPanel.gd b/scenes/player/inventory/InventoryPanel.gd index 363486ae..189bfdd5 100644 --- a/scenes/player/inventory/InventoryPanel.gd +++ b/scenes/player/inventory/InventoryPanel.gd @@ -2,14 +2,24 @@ extends Panel class_name InventoryPanel +const DEFAULT_FONT: Font = preload("res://addons/gut/fonts/LobsterTwo-Regular.ttf") + @export var item: Item: set(new_item): + if item is Item and item.amount_changed.is_connected(on_item_amount_changed): + item.amount_changed.disconnect(on_item_amount_changed) + item = new_item - if item: + + if item is Item: + if not item.amount_changed.is_connected(on_item_amount_changed): + item.amount_changed.connect(on_item_amount_changed) $TextureRect.texture = item.get_node("Icon").texture else: $TextureRect.texture = null + queue_redraw() + @onready var inventory: Inventory = $"../.." @onready var drag_panel = $"../../DragPanel" @@ -41,6 +51,19 @@ func _ready(): mouse_exited.connect(_on_mouse_exited) +func _draw(): + if item is Item and item.amount != 1: + var font_height: int = 14 + draw_string( + DEFAULT_FONT, + Vector2(0, size.y - font_height), + str(item.amount), + HORIZONTAL_ALIGNMENT_CENTER, + -1, + font_height + ) + + func _gui_input(event: InputEvent): if event.is_action_pressed("j_left_click"): if item: @@ -67,7 +90,7 @@ func _gui_input(event: InputEvent): drag_panel.hide() -func _physics_process(_delta): +func _physics_process(_delta: float): if selected: drag_panel.position = get_local_mouse_position() + drag_panel_offset @@ -78,3 +101,7 @@ func _on_mouse_entered(): func _on_mouse_exited(): inventory.mouse_above_this_panel = null + + +func on_item_amount_changed(_amount: int): + queue_redraw()