From 4d7c75522fdf616b27b1f014bf7120bc8f23352d Mon Sep 17 00:00:00 2001 From: Eduardo Date: Fri, 2 Aug 2024 15:36:14 +0200 Subject: [PATCH] added inventory plugin --- addons/gloot/LICENSE | 21 + .../core/constraints/constraint_manager.gd | 264 ++++++++++ .../gloot/core/constraints/grid_constraint.gd | 481 ++++++++++++++++++ .../core/constraints/inventory_constraint.gd | 67 +++ addons/gloot/core/constraints/item_map.gd | 85 ++++ addons/gloot/core/constraints/quadtree.gd | 233 +++++++++ .../core/constraints/stacks_constraint.gd | 312 ++++++++++++ .../core/constraints/weight_constraint.gd | 161 ++++++ addons/gloot/core/inventory.gd | 329 ++++++++++++ addons/gloot/core/inventory_grid.gd | 97 ++++ addons/gloot/core/inventory_grid_stacked.gd | 72 +++ addons/gloot/core/inventory_item.gd | 360 +++++++++++++ addons/gloot/core/inventory_stacked.gd | 100 ++++ addons/gloot/core/item_count.gd | 114 +++++ addons/gloot/core/item_protoset.gd | 162 ++++++ addons/gloot/core/item_ref_slot.gd | 186 +++++++ addons/gloot/core/item_slot.gd | 130 +++++ addons/gloot/core/item_slot_base.gd | 42 ++ addons/gloot/core/utils.gd | 11 + addons/gloot/core/verify.gd | 189 +++++++ addons/gloot/editor/common/choice_filter.gd | 129 +++++ addons/gloot/editor/common/choice_filter.tscn | 31 ++ .../editor/common/choice_filter_test.tscn | 21 + addons/gloot/editor/common/dict_editor.gd | 171 +++++++ addons/gloot/editor/common/dict_editor.tscn | 98 ++++ .../gloot/editor/common/dict_editor_test.tscn | 22 + addons/gloot/editor/common/editor_icons.gd | 6 + .../gloot/editor/common/multivalue_editor.gd | 53 ++ addons/gloot/editor/common/value_editor.gd | 263 ++++++++++ addons/gloot/editor/gloot_undo_redo.gd | 296 +++++++++++ .../inventory_editor/inventory_editor.gd | 127 +++++ .../inventory_editor/inventory_editor.tscn | 48 ++ .../inventory_editor/inventory_inspector.gd | 38 ++ .../inventory_editor/inventory_inspector.tscn | 54 ++ .../editor/inventory_inspector_plugin.gd | 51 ++ .../item_editor/edit_properties_button.gd | 61 +++ .../item_editor/edit_prototype_id_button.gd | 79 +++ .../editor/item_editor/properties_editor.gd | 123 +++++ .../editor/item_editor/properties_editor.tscn | 29 ++ .../editor/item_editor/prototype_id_editor.gd | 49 ++ .../item_editor/prototype_id_editor.tscn | 27 + .../item_slot_editor/item_ref_slot_button.gd | 67 +++ .../item_slot_editor/item_slot_editor.gd | 106 ++++ .../item_slot_editor/item_slot_editor.tscn | 103 ++++ .../item_slot_editor/item_slot_inspector.gd | 38 ++ .../item_slot_editor/item_slot_inspector.tscn | 66 +++ .../protoset_editor/edit_protoset_button.gd | 25 + .../protoset_editor/edit_protoset_button.tscn | 37 ++ .../editor/protoset_editor/protoset_editor.gd | 174 +++++++ .../protoset_editor/protoset_editor.tscn | 155 ++++++ addons/gloot/gloot.gd | 44 ++ addons/gloot/images/icon_ctrl_inventory.svg | 81 +++ .../images/icon_ctrl_inventory.svg.import | 37 ++ .../gloot/images/icon_ctrl_inventory_grid.svg | 66 +++ .../icon_ctrl_inventory_grid.svg.import | 37 ++ .../images/icon_ctrl_inventory_stacked.svg | 102 ++++ .../icon_ctrl_inventory_stacked.svg.import | 37 ++ addons/gloot/images/icon_ctrl_item_slot.svg | 64 +++ .../images/icon_ctrl_item_slot.svg.import | 37 ++ addons/gloot/images/icon_inventory.svg | 81 +++ addons/gloot/images/icon_inventory.svg.import | 37 ++ addons/gloot/images/icon_inventory_grid.svg | 66 +++ .../images/icon_inventory_grid.svg.import | 37 ++ .../images/icon_inventory_grid_stacked.svg | 193 +++++++ .../icon_inventory_grid_stacked.svg.import | 37 ++ .../gloot/images/icon_inventory_stacked.svg | 102 ++++ .../images/icon_inventory_stacked.svg.import | 37 ++ addons/gloot/images/icon_item.svg | 79 +++ addons/gloot/images/icon_item.svg.import | 37 ++ addons/gloot/images/icon_item_protoset.svg | 78 +++ .../images/icon_item_protoset.svg.import | 37 ++ addons/gloot/images/icon_item_ref_slot.svg | 77 +++ .../images/icon_item_ref_slot.svg.import | 37 ++ addons/gloot/images/icon_item_slot.svg | 64 +++ addons/gloot/images/icon_item_slot.svg.import | 37 ++ addons/gloot/plugin.cfg | 7 + addons/gloot/ui/ctrl_dragable.gd | 81 +++ addons/gloot/ui/ctrl_drop_zone.gd | 30 ++ addons/gloot/ui/ctrl_inventory.gd | 197 +++++++ addons/gloot/ui/ctrl_inventory_grid.gd | 294 +++++++++++ addons/gloot/ui/ctrl_inventory_grid_basic.gd | 452 ++++++++++++++++ addons/gloot/ui/ctrl_inventory_grid_ex.gd | 408 +++++++++++++++ addons/gloot/ui/ctrl_inventory_item_rect.gd | 167 ++++++ addons/gloot/ui/ctrl_inventory_stacked.gd | 74 +++ addons/gloot/ui/ctrl_item_slot.gd | 276 ++++++++++ addons/gloot/ui/ctrl_item_slot_ex.gd | 61 +++ project.godot | 2 +- 87 files changed, 9552 insertions(+), 1 deletion(-) create mode 100644 addons/gloot/LICENSE create mode 100644 addons/gloot/core/constraints/constraint_manager.gd create mode 100644 addons/gloot/core/constraints/grid_constraint.gd create mode 100644 addons/gloot/core/constraints/inventory_constraint.gd create mode 100644 addons/gloot/core/constraints/item_map.gd create mode 100644 addons/gloot/core/constraints/quadtree.gd create mode 100644 addons/gloot/core/constraints/stacks_constraint.gd create mode 100644 addons/gloot/core/constraints/weight_constraint.gd create mode 100644 addons/gloot/core/inventory.gd create mode 100644 addons/gloot/core/inventory_grid.gd create mode 100644 addons/gloot/core/inventory_grid_stacked.gd create mode 100644 addons/gloot/core/inventory_item.gd create mode 100644 addons/gloot/core/inventory_stacked.gd create mode 100644 addons/gloot/core/item_count.gd create mode 100644 addons/gloot/core/item_protoset.gd create mode 100644 addons/gloot/core/item_ref_slot.gd create mode 100644 addons/gloot/core/item_slot.gd create mode 100644 addons/gloot/core/item_slot_base.gd create mode 100644 addons/gloot/core/utils.gd create mode 100644 addons/gloot/core/verify.gd create mode 100644 addons/gloot/editor/common/choice_filter.gd create mode 100644 addons/gloot/editor/common/choice_filter.tscn create mode 100644 addons/gloot/editor/common/choice_filter_test.tscn create mode 100644 addons/gloot/editor/common/dict_editor.gd create mode 100644 addons/gloot/editor/common/dict_editor.tscn create mode 100644 addons/gloot/editor/common/dict_editor_test.tscn create mode 100644 addons/gloot/editor/common/editor_icons.gd create mode 100644 addons/gloot/editor/common/multivalue_editor.gd create mode 100644 addons/gloot/editor/common/value_editor.gd create mode 100644 addons/gloot/editor/gloot_undo_redo.gd create mode 100644 addons/gloot/editor/inventory_editor/inventory_editor.gd create mode 100644 addons/gloot/editor/inventory_editor/inventory_editor.tscn create mode 100644 addons/gloot/editor/inventory_editor/inventory_inspector.gd create mode 100644 addons/gloot/editor/inventory_editor/inventory_inspector.tscn create mode 100644 addons/gloot/editor/inventory_inspector_plugin.gd create mode 100644 addons/gloot/editor/item_editor/edit_properties_button.gd create mode 100644 addons/gloot/editor/item_editor/edit_prototype_id_button.gd create mode 100644 addons/gloot/editor/item_editor/properties_editor.gd create mode 100644 addons/gloot/editor/item_editor/properties_editor.tscn create mode 100644 addons/gloot/editor/item_editor/prototype_id_editor.gd create mode 100644 addons/gloot/editor/item_editor/prototype_id_editor.tscn create mode 100644 addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd create mode 100644 addons/gloot/editor/item_slot_editor/item_slot_editor.gd create mode 100644 addons/gloot/editor/item_slot_editor/item_slot_editor.tscn create mode 100644 addons/gloot/editor/item_slot_editor/item_slot_inspector.gd create mode 100644 addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn create mode 100644 addons/gloot/editor/protoset_editor/edit_protoset_button.gd create mode 100644 addons/gloot/editor/protoset_editor/edit_protoset_button.tscn create mode 100644 addons/gloot/editor/protoset_editor/protoset_editor.gd create mode 100644 addons/gloot/editor/protoset_editor/protoset_editor.tscn create mode 100644 addons/gloot/gloot.gd create mode 100644 addons/gloot/images/icon_ctrl_inventory.svg create mode 100644 addons/gloot/images/icon_ctrl_inventory.svg.import create mode 100644 addons/gloot/images/icon_ctrl_inventory_grid.svg create mode 100644 addons/gloot/images/icon_ctrl_inventory_grid.svg.import create mode 100644 addons/gloot/images/icon_ctrl_inventory_stacked.svg create mode 100644 addons/gloot/images/icon_ctrl_inventory_stacked.svg.import create mode 100644 addons/gloot/images/icon_ctrl_item_slot.svg create mode 100644 addons/gloot/images/icon_ctrl_item_slot.svg.import create mode 100644 addons/gloot/images/icon_inventory.svg create mode 100644 addons/gloot/images/icon_inventory.svg.import create mode 100644 addons/gloot/images/icon_inventory_grid.svg create mode 100644 addons/gloot/images/icon_inventory_grid.svg.import create mode 100644 addons/gloot/images/icon_inventory_grid_stacked.svg create mode 100644 addons/gloot/images/icon_inventory_grid_stacked.svg.import create mode 100644 addons/gloot/images/icon_inventory_stacked.svg create mode 100644 addons/gloot/images/icon_inventory_stacked.svg.import create mode 100644 addons/gloot/images/icon_item.svg create mode 100644 addons/gloot/images/icon_item.svg.import create mode 100644 addons/gloot/images/icon_item_protoset.svg create mode 100644 addons/gloot/images/icon_item_protoset.svg.import create mode 100644 addons/gloot/images/icon_item_ref_slot.svg create mode 100644 addons/gloot/images/icon_item_ref_slot.svg.import create mode 100644 addons/gloot/images/icon_item_slot.svg create mode 100644 addons/gloot/images/icon_item_slot.svg.import create mode 100644 addons/gloot/plugin.cfg create mode 100644 addons/gloot/ui/ctrl_dragable.gd create mode 100644 addons/gloot/ui/ctrl_drop_zone.gd create mode 100644 addons/gloot/ui/ctrl_inventory.gd create mode 100644 addons/gloot/ui/ctrl_inventory_grid.gd create mode 100644 addons/gloot/ui/ctrl_inventory_grid_basic.gd create mode 100644 addons/gloot/ui/ctrl_inventory_grid_ex.gd create mode 100644 addons/gloot/ui/ctrl_inventory_item_rect.gd create mode 100644 addons/gloot/ui/ctrl_inventory_stacked.gd create mode 100644 addons/gloot/ui/ctrl_item_slot.gd create mode 100644 addons/gloot/ui/ctrl_item_slot_ex.gd diff --git a/addons/gloot/LICENSE b/addons/gloot/LICENSE new file mode 100644 index 0000000..e9b4efd --- /dev/null +++ b/addons/gloot/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Peter Kish + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/addons/gloot/core/constraints/constraint_manager.gd b/addons/gloot/core/constraints/constraint_manager.gd new file mode 100644 index 0000000..82a038b --- /dev/null +++ b/addons/gloot/core/constraints/constraint_manager.gd @@ -0,0 +1,264 @@ +extends RefCounted + +const KEY_WEIGHT_CONSTRAINT = "weight_constraint" +const KEY_STACKS_CONSTRAINT = "stacks_constraint" +const KEY_GRID_CONSTRAINT = "grid_constraint" + +const Verify = preload("res://addons/gloot/core/verify.gd") +const WeightConstraint = preload("res://addons/gloot/core/constraints/weight_constraint.gd") +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") + +var _weight_constraint: WeightConstraint = null +var _stacks_constraint: StacksConstraint = null +var _grid_constraint: GridConstraint = null +var inventory: Inventory = null : + set(new_inventory): + assert(new_inventory != null, "Can't set inventory to null!") + assert(inventory == null, "Inventory already set!") + inventory = new_inventory + if _weight_constraint != null: + _weight_constraint.inventory = inventory + if _stacks_constraint != null: + _stacks_constraint.inventory = inventory + if _grid_constraint != null: + _grid_constraint.inventory = inventory + + +enum Configuration {WSG, WS, WG, SG, W, S, G, VANILLA} + + +func _init(inventory_: Inventory) -> void: + inventory = inventory_ + + +func _on_item_added(item: InventoryItem) -> void: + assert(_enforce_constraints(item), "Failed to enforce constraints!") + + # Enforcing constraints can result in the item being removed from the inventory + # (e.g. when it's merged with another item stack) + if !is_instance_valid(item.get_inventory()) || item.is_queued_for_deletion(): + item = null + + if _weight_constraint != null: + _weight_constraint._on_item_added(item) + if _stacks_constraint != null: + _stacks_constraint._on_item_added(item) + if _grid_constraint != null: + _grid_constraint._on_item_added(item) + + +func _on_item_removed(item: InventoryItem) -> void: + if _weight_constraint != null: + _weight_constraint._on_item_removed(item) + if _stacks_constraint != null: + _stacks_constraint._on_item_removed(item) + if _grid_constraint != null: + _grid_constraint._on_item_removed(item) + + +func _on_item_property_changed(item: InventoryItem, property_name: String) -> void: + if _weight_constraint != null: + _weight_constraint._on_item_property_changed(item, property_name) + if _stacks_constraint != null: + _stacks_constraint._on_item_property_changed(item, property_name) + if _grid_constraint != null: + _grid_constraint._on_item_property_changed(item, property_name) + + +func _on_pre_item_swap(item1: InventoryItem, item2: InventoryItem) -> bool: + if _weight_constraint != null && !_weight_constraint._on_pre_item_swap(item1, item2): + return false + if _stacks_constraint != null && !_stacks_constraint._on_pre_item_swap(item1, item2): + return false + if _grid_constraint != null && !_grid_constraint._on_pre_item_swap(item1, item2): + return false + return true + + +func _on_post_item_swap(item1: InventoryItem, item2: InventoryItem) -> void: + if _weight_constraint != null: + _weight_constraint._on_post_item_swap(item1, item2) + if _stacks_constraint != null: + _stacks_constraint._on_post_item_swap(item1, item2) + if _grid_constraint != null: + _grid_constraint._on_post_item_swap(item1, item2) + + +func _enforce_constraints(item: InventoryItem) -> bool: + match get_configuration(): + Configuration.G: + return _grid_constraint.move_item_to_free_spot(item) + Configuration.WG: + return _grid_constraint.move_item_to_free_spot(item) + Configuration.SG: + if _grid_constraint.move_item_to_free_spot(item): + return true + _stacks_constraint.pack_item(item) + Configuration.WSG: + if _grid_constraint.move_item_to_free_spot(item): + return true + _stacks_constraint.pack_item(item) + + return true + + +func get_configuration() -> int: + if _weight_constraint && _stacks_constraint && _grid_constraint: + return Configuration.WSG + + if _weight_constraint && _stacks_constraint: + return Configuration.WS + + if _weight_constraint && _grid_constraint: + return Configuration.WG + + if _stacks_constraint && _grid_constraint: + return Configuration.SG + + if _weight_constraint: + return Configuration.W + + if _stacks_constraint: + return Configuration.S + + if _grid_constraint: + return Configuration.G + + return Configuration.VANILLA + + +func get_space_for(item: InventoryItem) -> ItemCount: + match get_configuration(): + Configuration.W: + return _weight_constraint.get_space_for(item) + Configuration.S: + return _stacks_constraint.get_space_for(item) + Configuration.G: + return _grid_constraint.get_space_for(item) + Configuration.WS: + return _ws_get_space_for(item) + Configuration.WG: + return ItemCount.min(_grid_constraint.get_space_for(item), _weight_constraint.get_space_for(item)) + Configuration.SG: + return _sg_get_space_for(item) + Configuration.WSG: + return ItemCount.min(_sg_get_space_for(item), _ws_get_space_for(item)) + + return ItemCount.inf() + + +func _ws_get_space_for(item: InventoryItem) -> ItemCount: + var stack_size := ItemCount.new(_stacks_constraint.get_item_stack_size(item)) + var result := _weight_constraint.get_space_for(item).div(stack_size) + return result + + +func _sg_get_space_for(item: InventoryItem) -> ItemCount: + var grid_space := _grid_constraint.get_space_for(item) + var max_stack_size := ItemCount.new(_stacks_constraint.get_item_max_stack_size(item)) + var stack_size := ItemCount.new(_stacks_constraint.get_item_stack_size(item)) + var free_stacks_space := _stacks_constraint.get_free_stack_space_for(item) + return grid_space.mul(max_stack_size).add(free_stacks_space).div(stack_size) + + +func has_space_for(item: InventoryItem) -> bool: + match get_configuration(): + Configuration.W: + return _weight_constraint.has_space_for(item) + Configuration.S: + return _stacks_constraint.has_space_for(item) + Configuration.G: + return _grid_constraint.has_space_for(item) + Configuration.WS: + return _weight_constraint.has_space_for(item) + Configuration.WG: + return _weight_constraint.has_space_for(item) && _grid_constraint.has_space_for(item) + Configuration.SG: + return _sg_has_space_for(item) + Configuration.WSG: + return _sg_has_space_for(item) && _weight_constraint.has_space_for(item) + + return true + + +func _sg_has_space_for(item: InventoryItem) -> bool: + if _grid_constraint.has_space_for(item): + return true + var stack_size := ItemCount.new(_stacks_constraint.get_item_stack_size(item)) + var free_stacks_space := _stacks_constraint.get_free_stack_space_for(item) + return free_stacks_space.ge(stack_size) + + +func enable_weight_constraint(capacity: float = 0.0) -> void: + assert(_weight_constraint == null, "Weight constraint is already enabled") + _weight_constraint = WeightConstraint.new(inventory) + _weight_constraint.capacity = capacity + + +func enable_stacks_constraint() -> void: + assert(_stacks_constraint == null, "Stacks constraint is already enabled") + _stacks_constraint = StacksConstraint.new(inventory) + + +func enable_grid_constraint(size: Vector2i = GridConstraint.DEFAULT_SIZE) -> void: + assert(_grid_constraint == null, "Grid constraint is already enabled") + _grid_constraint = GridConstraint.new(inventory) + _grid_constraint.size = size + + +func get_weight_constraint() -> WeightConstraint: + return _weight_constraint + + +func get_stacks_constraint() -> StacksConstraint: + return _stacks_constraint + + +func get_grid_constraint() -> GridConstraint: + return _grid_constraint + + +func reset() -> void: + if get_weight_constraint(): + get_weight_constraint().reset() + if get_stacks_constraint(): + get_stacks_constraint().reset() + if get_grid_constraint(): + get_grid_constraint().reset() + + +func serialize() -> Dictionary: + var result := {} + + if get_weight_constraint(): + result[KEY_WEIGHT_CONSTRAINT] = get_weight_constraint().serialize() + if get_stacks_constraint(): + result[KEY_STACKS_CONSTRAINT] = get_stacks_constraint().serialize() + if get_grid_constraint(): + result[KEY_GRID_CONSTRAINT] = get_grid_constraint().serialize() + + return result + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, false, KEY_WEIGHT_CONSTRAINT, TYPE_DICTIONARY): + return false + if !Verify.dict(source, false, KEY_STACKS_CONSTRAINT, TYPE_DICTIONARY): + return false + if !Verify.dict(source, false, KEY_GRID_CONSTRAINT, TYPE_DICTIONARY): + return false + + reset() + + if source.has(KEY_WEIGHT_CONSTRAINT): + if !get_weight_constraint().deserialize(source[KEY_WEIGHT_CONSTRAINT]): + return false + if source.has(KEY_STACKS_CONSTRAINT): + if !get_stacks_constraint().deserialize(source[KEY_STACKS_CONSTRAINT]): + return false + if source.has(KEY_GRID_CONSTRAINT): + if !get_grid_constraint().deserialize(source[KEY_GRID_CONSTRAINT]): + return false + + return true diff --git a/addons/gloot/core/constraints/grid_constraint.gd b/addons/gloot/core/constraints/grid_constraint.gd new file mode 100644 index 0000000..9a57c8f --- /dev/null +++ b/addons/gloot/core/constraints/grid_constraint.gd @@ -0,0 +1,481 @@ +extends "res://addons/gloot/core/constraints/inventory_constraint.gd" + +signal size_changed + +const Verify = preload("res://addons/gloot/core/verify.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") +const QuadTree = preload("res://addons/gloot/core/constraints/quadtree.gd") +const Utils = preload("res://addons/gloot/core/utils.gd") + +# TODO: Replace KEY_WIDTH and KEY_HEIGHT with KEY_SIZE +const KEY_WIDTH: String = "width" +const KEY_HEIGHT: String = "height" +const KEY_SIZE: String = "size" +const KEY_ROTATED: String = "rotated" +const KEY_POSITIVE_ROTATION: String = "positive_rotation" +const KEY_GRID_POSITION: String = "grid_position" +const DEFAULT_SIZE: Vector2i = Vector2i(10, 10) + +var _swap_position := Vector2i.ZERO +var _quad_tree := QuadTree.new(size) + +@export var size: Vector2i = DEFAULT_SIZE : + set(new_size): + assert(inventory, "Inventory not set!") + assert(new_size.x > 0, "Inventory width must be positive!") + assert(new_size.y > 0, "Inventory height must be positive!") + var old_size = size + size = new_size + if !Engine.is_editor_hint(): + if _bounds_broken(): + size = old_size + if size != old_size: + _refresh_quad_tree() + size_changed.emit() + + +func _refresh_quad_tree() -> void: + _quad_tree = QuadTree.new(size) + for item in inventory.get_items(): + _quad_tree.add(get_item_rect(item), item) + + +func _on_inventory_set() -> void: + _refresh_quad_tree() + + +func _on_item_added(item: InventoryItem) -> void: + if item == null: + return + _quad_tree.add(get_item_rect(item), item) + + +func _on_item_removed(item: InventoryItem) -> void: + _quad_tree.remove(item) + + +func _on_item_property_changed(item: InventoryItem, property_name: String) -> void: + var relevant_properties = [ + KEY_SIZE, + KEY_ROTATED, + KEY_WIDTH, + KEY_HEIGHT, + KEY_GRID_POSITION, + ] + if property_name in relevant_properties: + _quad_tree.remove(item) + _quad_tree.add(get_item_rect(item), item) + + +func _on_pre_item_swap(item1: InventoryItem, item2: InventoryItem) -> bool: + if !_size_check(item1, item2): + return false + + if inventory.has_item(item1): + _swap_position = get_item_position(item1) + elif inventory.has_item(item2): + _swap_position = get_item_position(item2) + return true + + +func _size_check(item1: InventoryItem, item2: InventoryItem) -> bool: + var inv1 = item1.get_inventory() + var grid_constraint1: GridConstraint = null + if is_instance_valid(inv1): + grid_constraint1 = inv1._constraint_manager.get_grid_constraint() + var inv2 = item2.get_inventory() + var grid_constraint2: GridConstraint = null + if is_instance_valid(inv2): + grid_constraint2 = inv2._constraint_manager.get_grid_constraint() + + if is_instance_valid(grid_constraint1) || is_instance_valid(grid_constraint2): + return get_item_size(item1) == get_item_size(item2) + return true + + +func _on_post_item_swap(item1: InventoryItem, item2: InventoryItem) -> void: + var has1 := inventory.has_item(item1) + var has2 := inventory.has_item(item2) + if has1 && has2: + var temp_pos = get_item_position(item1) + _move_item_to_unsafe(item1, get_item_position(item2)) + _move_item_to_unsafe(item2, temp_pos) + elif has1: + move_item_to(item1, _swap_position) + elif has2: + move_item_to(item2, _swap_position) + + +func _bounds_broken() -> bool: + for item in inventory.get_items(): + if !rect_free(get_item_rect(item), item): + return true + + return false + + +func get_item_position(item: InventoryItem) -> Vector2i: + return item.get_property(KEY_GRID_POSITION, Vector2i.ZERO) + + +# TODO: Consider making a static "unsafe" version of this +func set_item_position(item: InventoryItem, new_position: Vector2i) -> bool: + var new_rect := Rect2i(new_position, get_item_size(item)) + if inventory.has_item(item) and !rect_free(new_rect, item): + return false + + item.set_property(KEY_GRID_POSITION, new_position) + return true + + +func get_item_size(item: InventoryItem) -> Vector2i: + var result: Vector2i + if is_item_rotated(item): + result.x = item.get_property(KEY_HEIGHT, 1) + result.y = item.get_property(KEY_WIDTH, 1) + else: + result.x = item.get_property(KEY_WIDTH, 1) + result.y = item.get_property(KEY_HEIGHT, 1) + return result + + +static func is_item_rotated(item: InventoryItem) -> bool: + return item.get_property(KEY_ROTATED, false) + + +static func is_item_rotation_positive(item: InventoryItem) -> bool: + return item.get_property(KEY_POSITIVE_ROTATION, false) + + +# TODO: Consider making a static "unsafe" version of this +func set_item_size(item: InventoryItem, new_size: Vector2i) -> bool: + if new_size.x < 1 || new_size.y < 1: + return false + + var new_rect := Rect2i(get_item_position(item), new_size) + if inventory.has_item(item) and !rect_free(new_rect, item): + return false + + item.set_property(KEY_WIDTH, new_size.x) + item.set_property(KEY_HEIGHT, new_size.y) + return true + + +func set_item_rotation(item: InventoryItem, rotated: bool) -> bool: + if is_item_rotated(item) == rotated: + return false + if !can_rotate_item(item): + return false + + if rotated: + item.set_property(KEY_ROTATED, true) + else: + item.clear_property(KEY_ROTATED) + + return true + + +func rotate_item(item: InventoryItem) -> bool: + return set_item_rotation(item, !is_item_rotated(item)) + + +static func set_item_rotation_direction(item: InventoryItem, positive: bool) -> void: + if positive: + item.set_property(KEY_POSITIVE_ROTATION, true) + else: + item.clear_property(KEY_POSITIVE_ROTATION) + + +func can_rotate_item(item: InventoryItem) -> bool: + var rotated_rect := get_item_rect(item) + var temp := rotated_rect.size.x + rotated_rect.size.x = rotated_rect.size.y + rotated_rect.size.y = temp + return rect_free(rotated_rect, item) + + +func get_item_rect(item: InventoryItem) -> Rect2i: + var item_pos := get_item_position(item) + var item_size := get_item_size(item) + return Rect2i(item_pos, item_size) + + +func set_item_rect(item: InventoryItem, new_rect: Rect2i) -> bool: + if !rect_free(new_rect, item): + return false + if !set_item_position(item, new_rect.position): + return false + if !set_item_size(item, new_rect.size): + return false + return true + + +func _get_prototype_size(prototype_id: String) -> Vector2i: + assert(inventory != null, "Inventory not set!") + assert(inventory.item_protoset != null, "Inventory protoset is null!") + var width: int = inventory.item_protoset.get_prototype_property(prototype_id, KEY_WIDTH, 1) + var height: int = inventory.item_protoset.get_prototype_property(prototype_id, KEY_HEIGHT, 1) + return Vector2i(width, height) + + +func _is_sorted() -> bool: + assert(inventory != null, "Inventory not set!") + for item1 in inventory.get_items(): + for item2 in inventory.get_items(): + if item1 == item2: + continue + + var rect1: Rect2i = get_item_rect(item1) + var rect2: Rect2i = get_item_rect(item2) + if rect1.intersects(rect2): + return false; + + return true + + +func add_item_at(item: InventoryItem, position: Vector2i) -> bool: + assert(inventory != null, "Inventory not set!") + + var item_size := get_item_size(item) + var rect := Rect2i(position, item_size) + if rect_free(rect): + if not inventory.add_item(item): + return false + assert(move_item_to(item, position), "Can't move the item to the given place!") + return true + + return false + + +func create_and_add_item_at(prototype_id: String, position: Vector2i) -> InventoryItem: + assert(inventory != null, "Inventory not set!") + var item_rect := Rect2i(position, _get_prototype_size(prototype_id)) + if !rect_free(item_rect): + return null + + var item = inventory.create_and_add_item(prototype_id) + if item == null: + return null + + if not move_item_to(item, position): + inventory.remove_item(item) + return null + + return item + + +func get_item_at(position: Vector2i) -> InventoryItem: + assert(inventory != null, "Inventory not set!") + var first = _quad_tree.get_first(position) + if first == null: + return null + return first.metadata + + +func get_items_under(rect: Rect2i) -> Array[InventoryItem]: + assert(inventory != null, "Inventory not set!") + var result: Array[InventoryItem] + for item in inventory.get_items(): + var item_rect := get_item_rect(item) + if item_rect.intersects(rect): + result.append(item) + return result + + +func move_item_to(item: InventoryItem, position: Vector2i) -> bool: + assert(inventory != null, "Inventory not set!") + var item_size := get_item_size(item) + var rect := Rect2i(position, item_size) + if rect_free(rect, item): + _move_item_to_unsafe(item, position) + inventory.contents_changed.emit() + return true + + return false + + +func move_item_to_free_spot(item: InventoryItem) -> bool: + if rect_free(get_item_rect(item), item): + return true + + var free_place := find_free_place(item, item) + if not free_place.success: + return false + + _move_item_to_unsafe(item, free_place.position) + return true + + +func _move_item_to_unsafe(item: InventoryItem, position: Vector2i) -> void: + item.set_property(KEY_GRID_POSITION, position) + if item.get_property(KEY_GRID_POSITION) == Vector2i.ZERO: + item.clear_property(KEY_GRID_POSITION) + + +func transfer_to(item: InventoryItem, destination: GridConstraint, position: Vector2i) -> bool: + assert(inventory != null, "Inventory not set!") + assert(destination.inventory != null, "Destination inventory not set!") + var item_size = get_item_size(item) + var rect := Rect2i(position, item_size) + if destination.rect_free(rect) && destination.add_item_at(item, position): + return true + + if _merge_to(item, destination, position): + return true + + return InventoryItem.swap(item, destination.get_item_at(position)) + + +func _merge_to(item: InventoryItem, destination: GridConstraint, position: Vector2i) -> bool: + var item_dst := destination._get_mergable_item_at(item, position) + if item_dst == null: + return false + + return inventory._constraint_manager.get_stacks_constraint().join_stacks(item_dst, item) + + +func _get_mergable_item_at(item: InventoryItem, position: Vector2i) -> InventoryItem: + if inventory._constraint_manager.get_stacks_constraint() == null: + return null + + var rect := Rect2i(position, get_item_size(item)) + var mergable_items := _get_mergable_items_under(item, rect) + for mergable_item in mergable_items: + if inventory._constraint_manager.get_stacks_constraint().stacks_joinable(item, mergable_item): + return mergable_item + return null + + +func _get_mergable_items_under(item: InventoryItem, rect: Rect2i) -> Array[InventoryItem]: + var result: Array[InventoryItem] + + for item_dst in get_items_under(rect): + if item_dst == item: + continue + if StacksConstraint.items_mergable(item_dst, item): + result.append(item_dst) + + return result + + +func rect_free(rect: Rect2i, exception: InventoryItem = null) -> bool: + assert(inventory != null, "Inventory not set!") + + if rect.position.x < 0 || rect.position.y < 0 || rect.size.x < 1 || rect.size.y < 1: + return false + if rect.position.x + rect.size.x > size.x: + return false + if rect.position.y + rect.size.y > size.y: + return false + + return _quad_tree.get_first(rect, exception) == null + + +# TODO: Check if this is needed after adding find_free_space +func find_free_place(item: InventoryItem, exception: InventoryItem = null) -> Dictionary: + var result := {success = false, position = Vector2i(-1, -1)} + var item_size = get_item_size(item) + for x in range(size.x - (item_size.x - 1)): + for y in range(size.y - (item_size.y - 1)): + var rect := Rect2i(Vector2i(x, y), item_size) + if rect_free(rect, exception): + result.success = true + result.position = Vector2i(x, y) + return result + + return result + + +func _compare_items(item1: InventoryItem, item2: InventoryItem) -> bool: + var rect1 := Rect2i(get_item_position(item1), get_item_size(item1)) + var rect2 := Rect2i(get_item_position(item2), get_item_size(item2)) + return rect1.get_area() > rect2.get_area() + + +func sort() -> bool: + assert(inventory != null, "Inventory not set!") + + var item_array: Array[InventoryItem] + for item in inventory.get_items(): + item_array.append(item) + item_array.sort_custom(_compare_items) + + for item in item_array: + _move_item_to_unsafe(item, -get_item_size(item)) + + for item in item_array: + var free_place := find_free_place(item) + if !free_place.success: + return false + move_item_to(item, free_place.position) + + return true + + +func _sort_if_needed() -> void: + if !_is_sorted() || _bounds_broken(): + sort() + + +func get_space_for(item: InventoryItem) -> ItemCount: + var occupied_rects: Array[Rect2i] + var item_size = get_item_size(item) + + var free_space := find_free_space(item_size, occupied_rects) + while free_space.success: + occupied_rects.append(Rect2i(free_space.position, item_size)) + free_space = find_free_space(item_size, occupied_rects) + return ItemCount.new(occupied_rects.size()) + + +func has_space_for(item: InventoryItem) -> bool: + var item_size = get_item_size(item) + return find_free_space(item_size).success + + +# TODO: Check if find_free_place is needed +func find_free_space(item_size: Vector2i, occupied_rects: Array[Rect2i] = []) -> Dictionary: + var result := {success = false, position = Vector2i(-1, -1)} + for x in range(size.x - (item_size.x - 1)): + for y in range(size.y - (item_size.y - 1)): + var rect := Rect2i(Vector2i(x, y), item_size) + if rect_free(rect) and not _rect_intersects_rect_array(rect, occupied_rects): + result.success = true + result.position = Vector2i(x, y) + return result + + return result + + +static func _rect_intersects_rect_array(rect: Rect2i, occupied_rects: Array[Rect2i] = []) -> bool: + for occupied_rect in occupied_rects: + if rect.intersects(occupied_rect): + return true + return false + + +func reset() -> void: + size = DEFAULT_SIZE + + +func serialize() -> Dictionary: + var result := {} + + # Store Vector2i as string to make JSON conversion easier later + result[KEY_SIZE] = var_to_str(size) + + return result + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, true, KEY_SIZE, TYPE_STRING): + return false + + reset() + + var s: Vector2i = Utils.str_to_var(source[KEY_SIZE]) + self.size = s + + return true + diff --git a/addons/gloot/core/constraints/inventory_constraint.gd b/addons/gloot/core/constraints/inventory_constraint.gd new file mode 100644 index 0000000..77f2dfc --- /dev/null +++ b/addons/gloot/core/constraints/inventory_constraint.gd @@ -0,0 +1,67 @@ +extends Object + +var inventory: Inventory = null : + set(new_inventory): + assert(new_inventory != null, "Can't set inventory to null!") + assert(inventory == null, "Inventory already set!") + inventory = new_inventory + _on_inventory_set() + + +func _init(inventory_: Inventory) -> void: + inventory = inventory_ + + +# Override this +func get_space_for(item: InventoryItem) -> ItemCount: + return ItemCount.zero() + + +# Override this +func has_space_for(item:InventoryItem) -> bool: + return false + + +# Override this +func reset() -> void: + pass + + +# Override this +func serialize() -> Dictionary: + return {} + + +# Override this +func deserialize(source: Dictionary) -> bool: + return true + + +# Override this +func _on_inventory_set() -> void: + pass + + +# Override this +func _on_item_added(item: InventoryItem) -> void: + pass + + +# Override this +func _on_item_removed(item: InventoryItem) -> void: + pass + + +# Override this +func _on_item_property_changed(item: InventoryItem, property_name: String) -> void: + pass + + +# Override this +func _on_pre_item_swap(item1: InventoryItem, item2: InventoryItem) -> bool: + return true + + +# Override this +func _on_post_item_swap(item1: InventoryItem, item2: InventoryItem) -> void: + pass diff --git a/addons/gloot/core/constraints/item_map.gd b/addons/gloot/core/constraints/item_map.gd new file mode 100644 index 0000000..eee718b --- /dev/null +++ b/addons/gloot/core/constraints/item_map.gd @@ -0,0 +1,85 @@ +var map: Array +var _free_fields: int +var free_fields : + get: + return _free_fields + set(new_free_fields): + assert(false, "free_fields is read-only!") + + +func _init(size: Vector2i) -> void: + resize(size) + + +func resize(size: Vector2i) -> void: + map = [] + map.resize(size.x) + for i in map.size(): + map[i] = [] + map[i].resize(size.y) + _free_fields = size.x * size.y + + +func fill_rect(rect: Rect2i, value) -> void: + assert(value != null, "Can't fill with null!") + _fill_rect_unsafe(rect, value) + + +func _fill_rect_unsafe(rect: Rect2i, value) -> void: + for x in range(rect.size.x): + for y in range(rect.size.y): + var map_coords := Vector2i(rect.position.x + x, rect.position.y + y) + if !contains(map_coords): + continue + if map[map_coords.x][map_coords.y] != value: + if value == null: + _free_fields += 1 + else: + _free_fields -= 1 + map[map_coords.x][map_coords.y] = value + + +func clear_rect(rect: Rect2i) -> void: + _fill_rect_unsafe(rect, null) + + +func print() -> void: + if map.is_empty(): + return + var output: String + var size = get_size() + for j in range(size.y): + for i in range(size.x): + if map[i][j]: + output = output + "x" + else: + output = output + "." + output = output + "\n" + print(output + "\n") + + +func clear() -> void: + for column in map: + column.fill(null) + var size = get_size() + _free_fields = size.x * size.y + + +func contains(position: Vector2i) -> bool: + if map.is_empty(): + return false + + var size = get_size() + return (position.x >= 0) && (position.y >= 0) && (position.x < size.x) && (position.y < size.y) + + +func get_field(position: Vector2i): + assert(contains(position), "%s out of bounds!" % position) + return map[position.x][position.y] + + +func get_size() -> Vector2i: + if map.is_empty(): + return Vector2i.ZERO + return Vector2i(map.size(), map[0].size()) + diff --git a/addons/gloot/core/constraints/quadtree.gd b/addons/gloot/core/constraints/quadtree.gd new file mode 100644 index 0000000..b610e78 --- /dev/null +++ b/addons/gloot/core/constraints/quadtree.gd @@ -0,0 +1,233 @@ + +class QtRect: + var rect: Rect2i + var metadata: Variant + + + func _init(rect_: Rect2i, metadata_: Variant) -> void: + rect = rect_ + metadata = metadata_ + + + func _to_string() -> String: + return "[R: %s, M: %s]" % [str(rect), str(metadata)] + + +class QtNode: + var quadrants: Array[QtNode] = [null, null, null, null] + var quadrant_count: int = 0 + var qt_rects: Array[QtRect] + var rect: Rect2i + + + func _init(r: Rect2i) -> void: + rect = r + + + func _to_string() -> String: + return "[R: %s]" % str(rect) + + + func is_empty() -> bool: + return (quadrant_count == 0) && qt_rects.is_empty() + + + func get_first_under_rect(test_rect: Rect2i, exception_metadata: Variant = null) -> QtRect: + for qtr in qt_rects: + if exception_metadata != null && qtr.metadata == exception_metadata: + continue + if qtr.rect.intersects(test_rect): + return qtr + + for quadrant in quadrants: + if quadrant == null: + continue + if !quadrant.rect.intersects(test_rect): + continue + var first = quadrant.get_first_under_rect(test_rect, exception_metadata) + if first != null: + return first + + return null + + + func get_first_containing_point(point: Vector2i, exception_metadata: Variant = null) -> QtRect: + for qtr in qt_rects: + if exception_metadata != null && qtr.metadata == exception_metadata: + continue + if qtr.rect.has_point(point): + return qtr + + for quadrant in quadrants: + if quadrant == null: + continue + if !quadrant.rect.has_point(point): + continue + var first = quadrant.get_first_containing_point(point, exception_metadata) + if first != null: + return first + + return null + + + func get_all_under_rect(test_rect: Rect2i, exception_metadata: Variant = null) -> Array[QtRect]: + var result: Array[QtRect] + + for qtr in qt_rects: + if exception_metadata != null && qtr.metadata == exception_metadata: + continue + if qtr.rect.intersects(test_rect): + result.append(qtr) + + for quadrant in quadrants: + if quadrant == null: + continue + if !quadrant.rect.intersects(test_rect): + continue + result.append_array(quadrant.get_all_under_rect(test_rect, exception_metadata)) + + return result + + + func get_all_containing_point(point: Vector2i, exception_metadata: Variant = null) -> Array[QtRect]: + var result: Array[QtRect] + + for qtr in qt_rects: + if exception_metadata != null && qtr.metadata == exception_metadata: + continue + if qtr.rect.has_point(point): + result.append(qtr) + + for quadrant in quadrants: + if quadrant == null: + continue + if !quadrant.rect.has_point(point): + continue + result.append_array(quadrant.get_all_containing_point(point, exception_metadata)) + + return result + + + func add(qt_rect: QtRect) -> void: + if !_can_subdivide(rect.size): + qt_rects.append(qt_rect) + return + + if is_empty(): + qt_rects.append(qt_rect) + return + + var quadrant_rects := _get_quadrant_rects(rect) + for i in quadrant_rects.size(): + var quadrant_rect := quadrant_rects[i] + if !quadrant_rect.intersects(qt_rect.rect): + continue + if quadrants[i] == null: + quadrants[i] = QtNode.new(quadrant_rect) + quadrant_count += 1 + while !qt_rects.is_empty(): + var qtr = qt_rects.pop_back() + + add(qtr) + quadrants[i].add(qt_rect) + + + func remove(metadata: Variant) -> bool: + # TODO: Optimize with a Rect2i + var result = false + for i in range(qt_rects.size() - 1, -1, -1): + if qt_rects[i].metadata == metadata: + qt_rects.remove_at(i) + result = true + + for i in range(quadrants.size()): + if quadrants[i] == null: + continue + if quadrants[i].remove(metadata): + result = true + if quadrants[i].is_empty(): + quadrants[i] = null + quadrant_count -= 1 + + _collapse() + + return result + + + func _collapse() -> void: + if quadrant_count == 0: + return + var collapsing_into: QtRect = null + for i in quadrants.size(): + if quadrants[i] == null: + continue + if quadrants[i].quadrant_count != 0: + return + for qtr in quadrants[i].qt_rects: + if collapsing_into != null && collapsing_into != qtr: + return + collapsing_into = qtr + + for i in quadrants.size(): + quadrants[i] = null + quadrant_count = 0 + qt_rects.append(collapsing_into) + + + static func _can_subdivide(size: Vector2i) -> bool: + return size.x > 1 && size.y > 1 + + + # +----+---+ + # | 0 | 1 | + # | | | + # +----+---+ (the first quadrant is rounded up when the size is odd) + # | 2 | 3 | + # +----+---+ + static func _get_quadrant_rects(rect: Rect2i) -> Array[Rect2i]: + var q0w := roundi(float(rect.size.x) / 2.0) + var q0h := roundi(float(rect.size.y) / 2.0) + var q0 := Rect2i(rect.position, Vector2i(q0w, q0h)) + var q3 := Rect2i(rect.position + q0.size, rect.size - q0.size) + var q1 := Rect2i(Vector2i(q3.position.x, q0.position.y), Vector2i(q3.size.x, q0.size.y)) + var q2 := Rect2i(Vector2i(q0.position.x, q3.position.y), Vector2i(q0.size.x, q3.size.y)) + return [q0, q1, q2, q3] + + +var _root: QtNode +var _size: Vector2i + + +func _init(size: Vector2) -> void: + _size = size + _root = QtNode.new(Rect2i(Vector2i.ZERO, _size)) + + +func get_first(at: Variant, exception_metadata: Variant = null) -> QtRect: + assert(at is Rect2i || at is Vector2i) + if at is Rect2i: + return _root.get_first_under_rect(at, exception_metadata) + if at is Vector2i: + return _root.get_first_containing_point(at, exception_metadata) + return null + + +func get_all(at: Variant, exception_metadata: Variant = null) -> Array[QtRect]: + assert(at is Rect2i || at is Vector2i) + if at is Rect2i: + return _root.get_all_under_rect(at, exception_metadata) + if at is Vector2i: + return _root.get_all_containing_point(at, exception_metadata) + return [] + + +func add(rect: Rect2i, metadata: Variant) -> void: + _root.add(QtRect.new(rect, metadata)) + + +func remove(metadata: Variant) -> bool: + return _root.remove(metadata) + + +func is_empty() -> bool: + return _root.is_empty() diff --git a/addons/gloot/core/constraints/stacks_constraint.gd b/addons/gloot/core/constraints/stacks_constraint.gd new file mode 100644 index 0000000..705162e --- /dev/null +++ b/addons/gloot/core/constraints/stacks_constraint.gd @@ -0,0 +1,312 @@ +extends "res://addons/gloot/core/constraints/inventory_constraint.gd" + +const WeightConstraint = preload("res://addons/gloot/core/constraints/weight_constraint.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") + +const KEY_STACK_SIZE: String = "stack_size" +const KEY_MAX_STACK_SIZE: String = "max_stack_size" + +const DEFAULT_STACK_SIZE: int = 1 +# TODO: Consider making the default max stack size 1 +const DEFAULT_MAX_STACK_SIZE: int = 100 + +enum MergeResult {SUCCESS = 0, FAIL, PARTIAL} + + +# TODO: Check which util functions can be made private +# TODO: Consider making these util methods work with ItemCount +static func _get_free_stack_space(item: InventoryItem) -> int: + assert(item != null, "item is null!") + return get_item_max_stack_size(item) - get_item_stack_size(item) + + +static func _has_custom_property(item: InventoryItem, property: String, value) -> bool: + assert(item != null, "item is null!") + return item.properties.has(property) && item.properties[property] == value; + + +static func get_item_stack_size(item: InventoryItem) -> int: + assert(item != null, "item is null!") + return item.get_property(KEY_STACK_SIZE, DEFAULT_STACK_SIZE) + + +static func get_item_max_stack_size(item: InventoryItem) -> int: + assert(item != null, "item is null!") + return item.get_property(KEY_MAX_STACK_SIZE, DEFAULT_MAX_STACK_SIZE) + + +static func set_item_stack_size(item: InventoryItem, stack_size: int) -> bool: + assert(item != null, "item is null!") + assert(stack_size >= 0, "stack_size can't be negative!") + if stack_size > get_item_max_stack_size(item): + return false + if stack_size == 0: + var inventory: Inventory = item.get_inventory() + if inventory != null: + inventory.remove_item(item) + item.queue_free() + return true + item.set_property(KEY_STACK_SIZE, stack_size) + return true + + +static func set_item_max_stack_size(item: InventoryItem, max_stack_size: int) -> void: + assert(item != null, "item is null!") + assert(max_stack_size > 0, "max_stack_size can't be less than 1!") + item.set_property(KEY_MAX_STACK_SIZE, max_stack_size) + + +static func get_prototype_stack_size(protoset: ItemProtoset, prototype_id: String) -> int: + assert(protoset != null, "protoset is null!") + return protoset.get_prototype_property(prototype_id, KEY_STACK_SIZE, 1.0) + + +static func get_prototype_max_stack_size(protoset: ItemProtoset, prototype_id: String) -> int: + assert(protoset != null, "protoset is null!") + return protoset.get_prototype_property(prototype_id, KEY_MAX_STACK_SIZE, 1.0) + + +func get_mergable_items(item: InventoryItem) -> Array[InventoryItem]: + assert(inventory != null, "Inventory not set!") + assert(item != null, "item is null!") + + var result: Array[InventoryItem] = [] + + for i in inventory.get_items(): + if i == item: + continue + if !items_mergable(i, item): + continue + + result.append(i) + + return result + + +static func items_mergable(item_1: InventoryItem, item_2: InventoryItem) -> bool: + # Two item stacks are mergable if they have the same prototype ID and neither of the two contain + # custom properties that the other one doesn't have (except for "stack_size", "max_stack_size", + # "grid_position", or "weight"). + assert(item_1 != null, "item_1 is null!") + assert(item_2 != null, "item_2 is null!") + + var ignore_properies: Array[String] = [ + KEY_STACK_SIZE, + KEY_MAX_STACK_SIZE, + GridConstraint.KEY_GRID_POSITION, + WeightConstraint.KEY_WEIGHT + ] + + if item_1.prototype_id != item_2.prototype_id: + return false + + for property in item_1.properties.keys(): + if property in ignore_properies: + continue + if !_has_custom_property(item_2, property, item_1.properties[property]): + return false + + for property in item_2.properties.keys(): + if property in ignore_properies: + continue + if !_has_custom_property(item_1, property, item_2.properties[property]): + return false + + return true + + +func add_item_automerge( + item: InventoryItem, + ignore_properies: Array[String] = [] +) -> bool: + assert(item != null, "Item is null!") + assert(inventory != null, "Inventory not set!") + if !inventory._constraint_manager.has_space_for(item): + return false + + var target_items = get_mergable_items(item) + for target_item in target_items: + if _merge_stacks(target_item, item) == MergeResult.SUCCESS: + return true + + assert(inventory.add_item(item)) + return true + + +static func _merge_stacks(item_dst: InventoryItem, item_src: InventoryItem) -> int: + assert(item_dst != null, "item_dst is null!") + assert(item_src != null, "item_src is null!") + assert(items_mergable(item_dst, item_src), "Items must be mergable!") + + var src_size: int = get_item_stack_size(item_src) + assert(src_size > 0, "Item stack size must be greater than 0!") + + var dst_size: int = get_item_stack_size(item_dst) + var dst_max_size: int = get_item_max_stack_size(item_dst) + var free_dst_stack_space: int = dst_max_size - dst_size + if free_dst_stack_space <= 0: + return MergeResult.FAIL + + assert(set_item_stack_size(item_src, max(src_size - free_dst_stack_space, 0))) + assert(set_item_stack_size(item_dst, min(dst_size + src_size, dst_max_size))) + + if free_dst_stack_space >= src_size: + return MergeResult.SUCCESS + + return MergeResult.PARTIAL + + +static func split_stack(item: InventoryItem, new_stack_size: int) -> InventoryItem: + assert(item != null, "item is null!") + assert(new_stack_size >= 1, "New stack size must be greater or equal to 1!") + + var stack_size = get_item_stack_size(item) + assert(stack_size > 1, "Size of the item stack must be greater than 1!") + assert( + new_stack_size < stack_size, + "New stack size must be smaller than the original stack size!" + ) + + var new_item = item.duplicate() + if new_item.get_parent(): + new_item.get_parent().remove_child(new_item) + + assert(set_item_stack_size(new_item, new_stack_size)) + assert(set_item_stack_size(item, stack_size - new_stack_size)) + return new_item + + +# TODO: Rename this +func split_stack_safe(item: InventoryItem, new_stack_size: int) -> InventoryItem: + assert(inventory != null, "inventory is null!") + assert(inventory.has_item(item), "The inventory does not contain the given item!") + + var new_item = split_stack(item, new_stack_size) + if new_item: + assert(inventory.add_item(new_item)) + return new_item + + +static func join_stacks( + item_dst: InventoryItem, + item_src: InventoryItem +) -> bool: + if item_dst == null || item_src == null: + return false + + if (!stacks_joinable(item_dst, item_src)): + return false + + # TODO: Check if this can be an assertion + _merge_stacks(item_dst, item_src) + return true + + +static func stacks_joinable( + item_dst: InventoryItem, + item_src: InventoryItem +) -> bool: + assert(item_dst != null, "item_dst is null!") + assert(item_src != null, "item_src is null!") + + if not items_mergable(item_dst, item_src): + return false + + var dst_free_space = _get_free_stack_space(item_dst) + if dst_free_space < get_item_stack_size(item_src): + return false + + return true + + +func get_space_for(item: InventoryItem) -> ItemCount: + return ItemCount.inf() + + +func has_space_for(item: InventoryItem) -> bool: + return true + + +func get_free_stack_space_for(item: InventoryItem) -> ItemCount: + assert(inventory != null, "Inventory not set!") + + var item_count = ItemCount.zero() + var mergable_items = get_mergable_items(item) + for mergable_item in mergable_items: + var free_stack_space := _get_free_stack_space(mergable_item) + item_count.add(ItemCount.new(free_stack_space)) + return item_count + + +static func pack_item(item: InventoryItem) -> void: + if !is_instance_valid(item.get_inventory()): + return + + var sc := item.get_inventory()._constraint_manager.get_stacks_constraint() + if sc == null: + return + + var mergable_items = sc.get_mergable_items(item) + for mergable_item in mergable_items: + var merge_result := _merge_stacks(mergable_item, item) + if merge_result == MergeResult.SUCCESS: + return + + +func transfer_autosplit(item: InventoryItem, destination: Inventory) -> InventoryItem: + assert(inventory._constraint_manager.get_configuration() == destination._constraint_manager.get_configuration()) + if inventory.transfer(item, destination): + return item + + var stack_size := get_item_stack_size(item) + if stack_size <= 1: + return null + + var item_count := _get_space_for_single_item(destination, item) + assert(!item_count.eq(ItemCount.inf()), "Item count shouldn't be infinite!") + + if item_count.le(ItemCount.zero()): + return null + + var new_item: InventoryItem = split_stack(item, item_count.count) + assert(new_item != null) + + assert(destination.add_item(new_item)) + return new_item + + +func _get_space_for_single_item(inventory: Inventory, item: InventoryItem) -> ItemCount: + var single_item := item.duplicate() + assert(set_item_stack_size(single_item, 1)) + var count := inventory._constraint_manager.get_space_for(single_item) + single_item.free() + return count + + +func transfer_autosplitmerge(item: InventoryItem, destination: Inventory) -> bool: + if destination._constraint_manager.has_space_for(item): + # No need for splitting + return transfer_automerge(item, destination) + + var item_count := _get_space_for_single_item(destination, item) + if item_count.eq(ItemCount.zero()): + return false + var new_item: InventoryItem = split_stack(item, item_count.count) + assert(transfer_automerge(new_item, destination)) + return true + + +func transfer_automerge(item: InventoryItem, destination: Inventory) -> bool: + assert(inventory._constraint_manager.get_configuration() == destination._constraint_manager.get_configuration()) + + if !destination._constraint_manager.has_space_for(item): + return false + for i in destination.get_items(): + if items_mergable(i, item): + _merge_stacks(i, item) + if item.is_queued_for_deletion(): + # Stack size reached 0 + return true + assert(destination.add_item(item)) + return true + diff --git a/addons/gloot/core/constraints/weight_constraint.gd b/addons/gloot/core/constraints/weight_constraint.gd new file mode 100644 index 0000000..84b3cf5 --- /dev/null +++ b/addons/gloot/core/constraints/weight_constraint.gd @@ -0,0 +1,161 @@ +extends "res://addons/gloot/core/constraints/inventory_constraint.gd" + +signal capacity_changed +signal occupied_space_changed + +const KEY_WEIGHT: String = "weight" +const KEY_CAPACITY: String = "capacity" +const KEY_OCCUPIED_SPACE: String = "occupied_space" + +const Verify = preload("res://addons/gloot/core/verify.gd") +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") + + +var capacity: float : + set(new_capacity): + if new_capacity < 0.0: + new_capacity = 0.0 + if new_capacity == capacity: + return + if new_capacity > 0.0 && occupied_space > new_capacity: + return + capacity = new_capacity + capacity_changed.emit() + +var _occupied_space: float +var occupied_space: float : + get: + return _occupied_space + set(new_occupied_space): + assert(false, "occupied_space is read-only!") + + +func _init(inventory: Inventory) -> void: + super._init(inventory) + + +func _on_inventory_set() -> void: + _calculate_occupied_space() + + +func _on_item_added(item: InventoryItem) -> void: + _calculate_occupied_space() + + +func _on_item_removed(item: InventoryItem) -> void: + _calculate_occupied_space() + + +func _on_item_property_changed(item: InventoryItem, property_name: String) -> void: + var relevant_properties = [ + KEY_WEIGHT, + StacksConstraint.KEY_STACK_SIZE, + ] + if property_name in relevant_properties: + _calculate_occupied_space() + + +func _on_pre_item_swap(item1: InventoryItem, item2: InventoryItem) -> bool: + return _can_swap(item1, item2) && _can_swap(item2, item1) + + +static func _can_swap(item_dst: InventoryItem, item_src: InventoryItem) -> bool: + var inv = item_dst.get_inventory() + if !is_instance_valid(inv): + return true + + var weight_constraint = inv._constraint_manager.get_weight_constraint() + if !is_instance_valid(weight_constraint): + return true + + if weight_constraint.has_unlimited_capacity(): + return true + + var space_needed: float = weight_constraint.occupied_space - get_item_weight(item_dst) + get_item_weight(item_src) + return space_needed <= weight_constraint.capacity + + +func has_unlimited_capacity() -> bool: + return capacity == 0.0 + + +func get_free_space() -> float: + if has_unlimited_capacity(): + return capacity + + var free_space: float = capacity - _occupied_space + if free_space < 0.0: + free_space = 0.0 + return free_space + + +func _calculate_occupied_space() -> void: + var old_occupied_space = _occupied_space + _occupied_space = 0.0 + for item in inventory.get_items(): + _occupied_space += get_item_weight(item) + + if _occupied_space != old_occupied_space: + emit_signal("occupied_space_changed") + + if !Engine.is_editor_hint(): + assert(has_unlimited_capacity() || _occupied_space <= capacity, "Inventory overflow!") + + +static func _get_item_unit_weight(item: InventoryItem) -> float: + var weight = item.get_property(KEY_WEIGHT, 1.0) + return weight + + +static func get_item_weight(item: InventoryItem) -> float: + if item == null: + return -1.0 + return StacksConstraint.get_item_stack_size(item) * _get_item_unit_weight(item) + + +static func set_item_weight(item: InventoryItem, weight: float) -> void: + assert(weight >= 0.0, "Item weight must be greater or equal to 0!") + item.set_property(KEY_WEIGHT, weight) + + +func get_space_for(item: InventoryItem) -> ItemCount: + if has_unlimited_capacity(): + return ItemCount.inf() + var unit_weight := _get_item_unit_weight(item) + return ItemCount.new(floor(get_free_space() / unit_weight)) + + +func has_space_for(item: InventoryItem) -> bool: + if has_unlimited_capacity(): + return true + var item_weight := get_item_weight(item) + return get_free_space() >= item_weight + + +func reset() -> void: + capacity = 0.0 + + +func serialize() -> Dictionary: + var result := {} + + result[KEY_CAPACITY] = capacity + # TODO: Check if this is needed + result[KEY_OCCUPIED_SPACE] = _occupied_space + + return result + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, true, KEY_CAPACITY, TYPE_FLOAT) ||\ + !Verify.dict(source, true, KEY_OCCUPIED_SPACE, TYPE_FLOAT): + return false + + reset() + capacity = source[KEY_CAPACITY] + # TODO: Check if this is needed + _occupied_space = source[KEY_OCCUPIED_SPACE] + + return true + + diff --git a/addons/gloot/core/inventory.gd b/addons/gloot/core/inventory.gd new file mode 100644 index 0000000..53627a1 --- /dev/null +++ b/addons/gloot/core/inventory.gd @@ -0,0 +1,329 @@ +@tool +@icon("res://addons/gloot/images/icon_inventory.svg") +extends Node +class_name Inventory + +signal item_added(item) +signal item_removed(item) +signal item_modified(item) +signal item_property_changed(item, property_name) +signal contents_changed +signal protoset_changed + +const ConstraintManager = preload("res://addons/gloot/core/constraints/constraint_manager.gd") + +@export var item_protoset: ItemProtoset: + set(new_item_protoset): + if new_item_protoset == item_protoset: + return + clear() + _disconnect_protoset_signals() + item_protoset = new_item_protoset + _connect_protoset_signals() + protoset_changed.emit() + update_configuration_warnings() +var _items: Array[InventoryItem] = [] +var _constraint_manager: ConstraintManager = null + +const KEY_NODE_NAME: String = "node_name" +const KEY_ITEM_PROTOSET: String = "item_protoset" +const KEY_CONSTRAINTS: String = "constraints" +const KEY_ITEMS: String = "items" +const Verify = preload("res://addons/gloot/core/verify.gd") + + +func _disconnect_protoset_signals() -> void: + if !is_instance_valid(item_protoset): + return + item_protoset.changed.disconnect(_on_protoset_changed) + + +func _connect_protoset_signals() -> void: + if !is_instance_valid(item_protoset): + return + item_protoset.changed.connect(_on_protoset_changed) + + +func _on_protoset_changed() -> void: + protoset_changed.emit() + + +func _get_configuration_warnings() -> PackedStringArray: + if item_protoset == null: + return PackedStringArray([ + "This inventory node has no protoset. Set the 'item_protoset' field to be able to " \ + + "populate the inventory with items."]) + return PackedStringArray() + + +static func _get_item_script() -> Script: + return preload("inventory_item.gd") + + +func _enter_tree(): + for child in get_children(): + if not child is InventoryItem: + continue + if has_item(child): + continue + _items.append(child) + + +func _init() -> void: + _constraint_manager = ConstraintManager.new(self) + + +func _ready() -> void: + for item in get_items(): + _connect_item_signals(item) + + +func _on_item_added(item: InventoryItem) -> void: + _items.append(item) + contents_changed.emit() + _connect_item_signals(item) + if _constraint_manager: + _constraint_manager._on_item_added(item) + item_added.emit(item) + + +func _on_item_removed(item: InventoryItem) -> void: + _items.erase(item) + contents_changed.emit() + _disconnect_item_signals(item) + if _constraint_manager: + _constraint_manager._on_item_removed(item) + item_removed.emit(item) + + +func move_item(from: int, to: int) -> void: + assert(from >= 0) + assert(from < _items.size()) + assert(to >= 0) + assert(to < _items.size()) + if from == to: + return + + var item = _items[from] + _items.remove_at(from) + _items.insert(to, item) + + contents_changed.emit() + + +func get_item_index(item: InventoryItem) -> int: + return _items.find(item) + + +func get_item_count() -> int: + return _items.size() + + +func _connect_item_signals(item: InventoryItem) -> void: + if !item.protoset_changed.is_connected(_emit_item_modified): + item.protoset_changed.connect(_emit_item_modified.bind(item)) + if !item.prototype_id_changed.is_connected(_emit_item_modified): + item.prototype_id_changed.connect(_emit_item_modified.bind(item)) + if !item.properties_changed.is_connected(_emit_item_modified): + item.properties_changed.connect(_emit_item_modified.bind(item)) + if !item.property_changed.is_connected(_on_item_property_changed): + item.property_changed.connect(_on_item_property_changed.bind(item)) + + +func _disconnect_item_signals(item:InventoryItem) -> void: + if item.protoset_changed.is_connected(_emit_item_modified): + item.protoset_changed.disconnect(_emit_item_modified) + if item.prototype_id_changed.is_connected(_emit_item_modified): + item.prototype_id_changed.disconnect(_emit_item_modified) + if item.properties_changed.is_connected(_emit_item_modified): + item.properties_changed.disconnect(_emit_item_modified) + if item.property_changed.is_connected(_on_item_property_changed): + item.property_changed.disconnect(_on_item_property_changed.bind(item)) + + +func _emit_item_modified(item: InventoryItem) -> void: + item_modified.emit(item) + + +func _on_item_property_changed(property_name: String, item: InventoryItem) -> void: + _constraint_manager._on_item_property_changed(item, property_name) + item_property_changed.emit(item, property_name) + + +func get_items() -> Array[InventoryItem]: + return _items + + +func has_item(item: InventoryItem) -> bool: + return item in _items + + +func add_item(item: InventoryItem) -> bool: + if !can_add_item(item): + return false + + if item.get_parent(): + item.get_parent().remove_child(item) + + # HACK: In case of InventoryGridStacked we can end up adding the item and + # removing it immediately, after a successful pack() call (in case the grid + # constraint has no space for the item). This causes some errors because + # Godot still tries to call the ENTER_TREE notification. To avoid that, we + # call transfer_automerge(), which should be able to pack the item without + # adding it first. + var gc := _constraint_manager.get_grid_constraint() + var sc := _constraint_manager.get_stacks_constraint() + if gc != null && sc != null && !gc.has_space_for(item): + assert(sc.transfer_automerge(item, self)) + else: + add_child(item) + + if Engine.is_editor_hint() && !item.is_queued_for_deletion(): + item.owner = get_tree().edited_scene_root + return true + + +func can_add_item(item: InventoryItem) -> bool: + if item == null || has_item(item): + return false + + if !can_hold_item(item): + return false + + if !_constraint_manager.has_space_for(item): + return false + + return true + + +func can_hold_item(item: InventoryItem) -> bool: + return true + + +func create_and_add_item(prototype_id: String) -> InventoryItem: + var item: InventoryItem = InventoryItem.new() + item.protoset = item_protoset + item.prototype_id = prototype_id + if add_item(item): + return item + else: + item.free() + return null + + +func remove_item(item: InventoryItem) -> bool: + if !_can_remove_item(item): + return false + + remove_child(item) + return true + + +func _can_remove_item(item: InventoryItem) -> bool: + return item != null && has_item(item) + + +func remove_all_items() -> void: + while get_child_count() > 0: + remove_child(get_child(0)) + _items = [] + + +func get_item_by_id(prototype_id: String) -> InventoryItem: + for item in get_items(): + if item.prototype_id == prototype_id: + return item + + return null + + +func get_items_by_id(prototype_id: String) -> Array[InventoryItem]: + var result: Array[InventoryItem] = [] + + for item in get_items(): + if item.prototype_id == prototype_id: + result.append(item) + + return result + + +func has_item_by_id(prototype_id: String) -> bool: + return get_item_by_id(prototype_id) != null + + +func transfer(item: InventoryItem, destination: Inventory) -> bool: + return destination.add_item(item) + + +func reset() -> void: + clear() + item_protoset = null + _constraint_manager.reset() + + +func clear() -> void: + for item in get_items(): + item.queue_free() + remove_all_items() + + +func serialize() -> Dictionary: + var result: Dictionary = {} + + result[KEY_NODE_NAME] = name as String + result[KEY_ITEM_PROTOSET] = _serialize_item_protoset(item_protoset) + result[KEY_CONSTRAINTS] = _constraint_manager.serialize() + if !get_items().is_empty(): + result[KEY_ITEMS] = [] + for item in get_items(): + result[KEY_ITEMS].append(item.serialize()) + + return result + + +static func _serialize_item_protoset(item_protoset: ItemProtoset) -> String: + if !is_instance_valid(item_protoset): + return "" + elif item_protoset.resource_path.is_empty(): + return item_protoset.json_data + else: + return item_protoset.resource_path + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, true, KEY_NODE_NAME, TYPE_STRING) ||\ + !Verify.dict(source, true, KEY_ITEM_PROTOSET, TYPE_STRING) ||\ + !Verify.dict(source, false, KEY_ITEMS, TYPE_ARRAY, TYPE_DICTIONARY) ||\ + !Verify.dict(source, false, KEY_CONSTRAINTS, TYPE_DICTIONARY): + return false + + clear() + item_protoset = null + + if !source[KEY_NODE_NAME].is_empty() && source[KEY_NODE_NAME] != name: + name = source[KEY_NODE_NAME] + item_protoset = _deserialize_item_protoset(source[KEY_ITEM_PROTOSET]) + # TODO: Check return value: + if source.has(KEY_CONSTRAINTS): + _constraint_manager.deserialize(source[KEY_CONSTRAINTS]) + if source.has(KEY_ITEMS): + var items = source[KEY_ITEMS] + for item_dict in items: + var item = _get_item_script().new() + # TODO: Check return value: + item.deserialize(item_dict) + assert(add_item(item), "Failed to add item '%s'. Inventory full?" % item.prototype_id) + + return true + + +static func _deserialize_item_protoset(data: String) -> ItemProtoset: + if data.is_empty(): + return null + elif data.begins_with("res://"): + return load(data) + else: + var protoset := ItemProtoset.new() + protoset.json_data = data + return protoset + diff --git a/addons/gloot/core/inventory_grid.gd b/addons/gloot/core/inventory_grid.gd new file mode 100644 index 0000000..4e519eb --- /dev/null +++ b/addons/gloot/core/inventory_grid.gd @@ -0,0 +1,97 @@ +@tool +@icon("res://addons/gloot/images/icon_inventory_grid.svg") +extends Inventory +class_name InventoryGrid + +signal size_changed + +const DEFAULT_SIZE: Vector2i = Vector2i(10, 10) + +@export var size: Vector2i = DEFAULT_SIZE : + get: + if _constraint_manager == null: + return DEFAULT_SIZE + if _constraint_manager.get_grid_constraint() == null: + return DEFAULT_SIZE + return _constraint_manager.get_grid_constraint().size + set(new_size): + _constraint_manager.get_grid_constraint().size = new_size + + +func _init() -> void: + super._init() + _constraint_manager.enable_grid_constraint() + _constraint_manager.get_grid_constraint().size_changed.connect(func(): size_changed.emit()) + + +func get_item_position(item: InventoryItem) -> Vector2i: + return _constraint_manager.get_grid_constraint().get_item_position(item) + + +func get_item_size(item: InventoryItem) -> Vector2i: + return _constraint_manager.get_grid_constraint().get_item_size(item) + + +func get_item_rect(item: InventoryItem) -> Rect2i: + return _constraint_manager.get_grid_constraint().get_item_rect(item) + + +func set_item_rotation(item: InventoryItem, rotated: bool) -> bool: + return _constraint_manager.get_grid_constraint().set_item_rotation(item, rotated) + + +func rotate_item(item: InventoryItem) -> bool: + return _constraint_manager.get_grid_constraint().rotate_item(item) + + +func is_item_rotated(item: InventoryItem) -> bool: + return _constraint_manager.get_grid_constraint().is_item_rotated(item) + + +func can_rotate_item(item: InventoryItem) -> bool: + return _constraint_manager.get_grid_constraint().can_rotate_item(item) + + +func set_item_rotation_direction(item: InventoryItem, positive: bool) -> void: + _constraint_manager.set_item_rotation_direction(item, positive) + + +func is_item_rotation_positive(item: InventoryItem) -> bool: + return _constraint_manager.get_grid_constraint().is_item_rotation_positive(item) + + +func add_item_at(item: InventoryItem, position: Vector2i) -> bool: + return _constraint_manager.get_grid_constraint().add_item_at(item, position) + + +func create_and_add_item_at(prototype_id: String, position: Vector2i) -> InventoryItem: + return _constraint_manager.get_grid_constraint().create_and_add_item_at(prototype_id, position) + + +func get_item_at(position: Vector2i) -> InventoryItem: + return _constraint_manager.get_grid_constraint().get_item_at(position) + + +func get_items_under(rect: Rect2i) -> Array[InventoryItem]: + return _constraint_manager.get_grid_constraint().get_items_under(rect) + + +func move_item_to(item: InventoryItem, position: Vector2i) -> bool: + return _constraint_manager.get_grid_constraint().move_item_to(item, position) + + +func transfer_to(item: InventoryItem, destination: Inventory, position: Vector2i) -> bool: + return _constraint_manager.get_grid_constraint().transfer_to(item, destination._constraint_manager.get_grid_constraint(), position) + + +func rect_free(rect: Rect2i, exception: InventoryItem = null) -> bool: + return _constraint_manager.get_grid_constraint().rect_free(rect, exception) + + +func find_free_place(item: InventoryItem) -> Dictionary: + return _constraint_manager.get_grid_constraint().find_free_place(item) + + +func sort() -> bool: + return _constraint_manager.get_grid_constraint().sort() + diff --git a/addons/gloot/core/inventory_grid_stacked.gd b/addons/gloot/core/inventory_grid_stacked.gd new file mode 100644 index 0000000..934775a --- /dev/null +++ b/addons/gloot/core/inventory_grid_stacked.gd @@ -0,0 +1,72 @@ +@tool +@icon("res://addons/gloot/images/icon_inventory_grid_stacked.svg") +extends InventoryGrid +class_name InventoryGridStacked + +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") + + +func _init() -> void: + super._init() + _constraint_manager.enable_stacks_constraint() + + +func has_place_for(item: InventoryItem) -> bool: + return _constraint_manager.has_space_for(item) + + +func add_item_automerge(item: InventoryItem) -> bool: + return _constraint_manager.get_stacks_constraint().add_item_automerge(item) + + +func split(item: InventoryItem, new_stack_size: int) -> InventoryItem: + return _constraint_manager.get_stacks_constraint().split_stack_safe(item, new_stack_size) + + +func join(item_dst: InventoryItem, item_src: InventoryItem) -> bool: + return _constraint_manager.get_stacks_constraint().join_stacks(item_dst, item_src) + + +static func get_item_stack_size(item: InventoryItem) -> int: + return StacksConstraint.get_item_stack_size(item) + + +static func set_item_stack_size(item: InventoryItem, new_stack_size: int) -> bool: + return StacksConstraint.set_item_stack_size(item, new_stack_size) + + +static func get_item_max_stack_size(item: InventoryItem) -> int: + return StacksConstraint.get_item_max_stack_size(item) + + +static func set_item_max_stack_size(item: InventoryItem, new_stack_size: int) -> void: + StacksConstraint.set_item_max_stack_size(item, new_stack_size) + + +func get_prototype_stack_size(prototype_id: String) -> int: + return _constraint_manager.get_stacks_constraint().get_prototype_stack_size(item_protoset, prototype_id) + + +func get_prototype_max_stack_size(prototype_id: String) -> int: + return _constraint_manager.get_stacks_constraint().get_prototype_max_stack_size(item_protoset, prototype_id) + + +func transfer_automerge(item: InventoryItem, destination: Inventory) -> bool: + return _constraint_manager.get_stacks_constraint().transfer_automerge(item, destination) + + +func transfer_autosplitmerge(item: InventoryItem, destination: Inventory) -> bool: + return _constraint_manager.get_stacks_constraint().transfer_autosplitmerge(item, destination) + + +static func pack(item: InventoryItem) -> void: + return StacksConstraint.pack_item(item) + + +func transfer_to(item: InventoryItem, destination: Inventory, position: Vector2i) -> bool: + return _constraint_manager.get_grid_constraint().transfer_to(item, destination._constraint_manager.get_grid_constraint(), position) + + +func _get_mergable_item_at(item: InventoryItem, position: Vector2i) -> InventoryItem: + return _constraint_manager.get_grid_constraint()._get_mergable_item_at(item, position) + diff --git a/addons/gloot/core/inventory_item.gd b/addons/gloot/core/inventory_item.gd new file mode 100644 index 0000000..8e8919a --- /dev/null +++ b/addons/gloot/core/inventory_item.gd @@ -0,0 +1,360 @@ +@tool +@icon("res://addons/gloot/images/icon_item.svg") +extends Node +class_name InventoryItem + +signal protoset_changed +signal prototype_id_changed +signal properties_changed +signal property_changed(property_name) +signal added_to_inventory(inventory) +signal removed_from_inventory(inventory) +signal equipped_in_slot(item_slot) +signal removed_from_slot(item_slot) + +const Utils = preload("res://addons/gloot/core/utils.gd") + +@export var protoset: ItemProtoset : + set(new_protoset): + if new_protoset == protoset: + return + + if (_inventory != null) && (new_protoset != _inventory.item_protoset): + return + + _disconnect_protoset_signals() + protoset = new_protoset + _connect_protoset_signals() + + # Reset the prototype ID (pick the first prototype from the protoset) + if protoset && protoset._prototypes && protoset._prototypes.keys().size() > 0: + prototype_id = protoset._prototypes.keys()[0] + else: + prototype_id = "" + + protoset_changed.emit() + update_configuration_warnings() + +@export var prototype_id: String : + set(new_prototype_id): + if new_prototype_id == prototype_id: + return + if protoset == null && !new_prototype_id.is_empty(): + return + if (protoset != null) && (!protoset.has_prototype(new_prototype_id)): + return + prototype_id = new_prototype_id + _reset_properties() + update_configuration_warnings() + prototype_id_changed.emit() + +@export var properties: Dictionary : + set(new_properties): + properties = new_properties + properties_changed.emit() + update_configuration_warnings() + +var _inventory: Inventory : + set(new_inventory): + if new_inventory == _inventory: + return + _inventory = new_inventory + if _inventory: + protoset = _inventory.item_protoset +var _item_slot: ItemSlot + +const KEY_PROTOSET: String = "protoset" +const KEY_PROTOTYE_ID: String = "prototype_id" +const KEY_PROPERTIES: String = "properties" +const KEY_NODE_NAME: String = "node_name" +const KEY_TYPE: String = "type" +const KEY_VALUE: String = "value" + +const KEY_IMAGE: String = "image" +const KEY_NAME: String = "name" + +const Verify = preload("res://addons/gloot/core/verify.gd") + +func _connect_protoset_signals() -> void: + if protoset == null: + return + protoset.changed.connect(_on_protoset_changed) + + +func _disconnect_protoset_signals() -> void: + if protoset == null: + return + protoset.changed.disconnect(_on_protoset_changed) + + +func _on_protoset_changed() -> void: + update_configuration_warnings() + + +func _get_configuration_warnings() -> PackedStringArray: + if !protoset: + return PackedStringArray() + + if !protoset.has_prototype(prototype_id): + return PackedStringArray(["Undefined prototype '%s'. Check the item protoset!" % prototype_id]) + + return PackedStringArray() + + +func _reset_properties() -> void: + if !protoset || prototype_id.is_empty(): + properties = {} + return + + # Reset (erase) all properties from the current prototype but preserve the rest + var prototype: Dictionary = protoset.get_prototype(prototype_id) + var keys: Array = properties.keys().duplicate() + for property in keys: + if prototype.has(property): + properties.erase(property) + + +func _notification(what): + if what == NOTIFICATION_PARENTED: + _on_parented(get_parent()) + elif what == NOTIFICATION_UNPARENTED: + _on_unparented() + + +func _on_parented(parent: Node) -> void: + if parent is Inventory: + _on_added_to_inventory(parent as Inventory) + else: + _inventory = null + + if parent is ItemSlot: + _link_to_slot(parent as ItemSlot) + else: + _unlink_from_slot() + + +func _on_added_to_inventory(inventory: Inventory) -> void: + assert(inventory != null) + _inventory = inventory + + added_to_inventory.emit(_inventory) + _inventory._on_item_added(self) + + +func _on_unparented() -> void: + if _inventory: + _on_removed_from_inventory(_inventory) + _inventory = null + + _unlink_from_slot() + + +func _on_removed_from_inventory(inventory: Inventory) -> void: + if inventory: + removed_from_inventory.emit(inventory) + inventory._on_item_removed(self) + + +func _link_to_slot(item_slot: ItemSlot) -> void: + _item_slot = item_slot + _item_slot._on_item_added(self) + equipped_in_slot.emit(item_slot) + + +func _unlink_from_slot() -> void: + if _item_slot == null: + return + var temp_slot := _item_slot + _item_slot = null + temp_slot._on_item_removed() + removed_from_slot.emit(temp_slot) + + +func get_inventory() -> Inventory: + return _inventory + + +func get_item_slot() -> ItemSlot: + return _item_slot + + +static func swap(item1: InventoryItem, item2: InventoryItem) -> bool: + if item1 == null || item2 == null || item1 == item2: + return false + + var owner1 = item1.get_inventory() + if owner1 == null: + owner1 = item1.get_item_slot() + var owner2 = item2.get_inventory() + if owner2 == null: + owner2 = item2.get_item_slot() + if owner1 == null || owner2 == null: + return false + + if owner1 is Inventory: + if !owner1._constraint_manager._on_pre_item_swap(item1, item2): + return false + if owner2 is Inventory && owner1 != owner2: + if !owner2._constraint_manager._on_pre_item_swap(item1, item2): + return false + + var idx1 = _remove_item_from_owner(item1, owner1) + var idx2 = _remove_item_from_owner(item2, owner2) + if !_add_item_to_owner(item1, owner2, idx2): + _add_item_to_owner(item1, owner1, idx1) + _add_item_to_owner(item2, owner2, idx2) + return false + if !_add_item_to_owner(item2, owner1, idx1): + _add_item_to_owner(item1, owner1, idx1) + _add_item_to_owner(item2, owner2, idx2) + return false + + if owner1 is Inventory: + owner1._constraint_manager._on_post_item_swap(item1, item2) + if owner2 is Inventory && owner1 != owner2: + owner2._constraint_manager._on_post_item_swap(item1, item2) + + return true; + + +static func _remove_item_from_owner(item: InventoryItem, item_owner) -> int: + if item_owner is Inventory: + var inventory := (item_owner as Inventory) + var item_idx = inventory.get_item_index(item) + inventory.remove_item(item) + return item_idx + + # TODO: Consider removing/deprecating ItemSlot.remember_source_inventory + var item_slot := (item_owner as ItemSlot) + var temp_remember_source_inventory = item_slot.remember_source_inventory + item_slot.remember_source_inventory = false + item_slot.clear() + item_slot.remember_source_inventory = temp_remember_source_inventory + return 0 + + +static func _add_item_to_owner(item: InventoryItem, item_owner, index: int) -> bool: + if item_owner is Inventory: + var inventory := (item_owner as Inventory) + if inventory.add_item(item): + inventory.move_item(inventory.get_item_index(item), index) + return true + return false + return (item_owner as ItemSlot).equip(item) + + +func get_property(property_name: String, default_value = null) -> Variant: + # Note: The protoset editor still doesn't support arrays and dictionaries, + # but those can still be added via JSON definitions or via code. + if properties.has(property_name): + var value = properties[property_name] + if typeof(value) == TYPE_DICTIONARY || typeof(value) == TYPE_ARRAY: + return value.duplicate() + return value + + if protoset && protoset.prototype_has_property(prototype_id, property_name): + var value = protoset.get_prototype_property(prototype_id, property_name, default_value) + if typeof(value) == TYPE_DICTIONARY || typeof(value) == TYPE_ARRAY: + return value.duplicate() + return value + + return default_value + + +func set_property(property_name: String, value) -> void: + if properties.has(property_name) && properties[property_name] == value: + return + properties[property_name] = value + property_changed.emit(property_name) + properties_changed.emit() + + +func clear_property(property_name: String) -> void: + if properties.has(property_name): + properties.erase(property_name) + property_changed.emit(property_name) + properties_changed.emit() + + +func reset() -> void: + protoset = null + prototype_id = "" + properties = {} + + +func serialize() -> Dictionary: + var result: Dictionary = {} + + result[KEY_NODE_NAME] = name as String + result[KEY_PROTOSET] = Inventory._serialize_item_protoset(protoset) + result[KEY_PROTOTYE_ID] = prototype_id + if !properties.is_empty(): + result[KEY_PROPERTIES] = {} + for property_name in properties.keys(): + result[KEY_PROPERTIES][property_name] = _serialize_property(property_name) + + return result + + +func _serialize_property(property_name: String) -> Dictionary: + # Store all properties as strings for JSON support. + var result: Dictionary = {} + var property_value = properties[property_name] + var property_type = typeof(property_value) + result = { + KEY_TYPE: property_type, + KEY_VALUE: var_to_str(property_value) + } + return result; + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, true, KEY_NODE_NAME, TYPE_STRING) ||\ + !Verify.dict(source, true, KEY_PROTOSET, TYPE_STRING) ||\ + !Verify.dict(source, true, KEY_PROTOTYE_ID, TYPE_STRING) ||\ + !Verify.dict(source, false, KEY_PROPERTIES, TYPE_DICTIONARY): + return false + + reset() + + if !source[KEY_NODE_NAME].is_empty() && source[KEY_NODE_NAME] != name: + name = source[KEY_NODE_NAME] + protoset = Inventory._deserialize_item_protoset(source[KEY_PROTOSET]) + prototype_id = source[KEY_PROTOTYE_ID] + if source.has(KEY_PROPERTIES): + for key in source[KEY_PROPERTIES].keys(): + properties[key] = _deserialize_property(source[KEY_PROPERTIES][key]) + if properties[key] == null: + properties = {} + return false + + return true + + +func _deserialize_property(data: Dictionary): + # Properties are stored as strings for JSON support. + var result = Utils.str_to_var(data[KEY_VALUE]) + var expected_type: int = data[KEY_TYPE] + var property_type: int = typeof(result) + if property_type != expected_type: + print("Property has unexpected type: %s. Expected: %s" % + [Verify.type_names[property_type], Verify.type_names[expected_type]]) + return null + return result + + +func get_texture() -> Texture2D: + var texture_path = get_property(KEY_IMAGE) + if texture_path && texture_path != "" && ResourceLoader.exists(texture_path): + var texture = load(texture_path) + if texture is Texture2D: + return texture + return null + + +func get_title() -> String: + var title = get_property(KEY_NAME, prototype_id) + if !(title is String): + title = prototype_id + + return title diff --git a/addons/gloot/core/inventory_stacked.gd b/addons/gloot/core/inventory_stacked.gd new file mode 100644 index 0000000..cca608f --- /dev/null +++ b/addons/gloot/core/inventory_stacked.gd @@ -0,0 +1,100 @@ +@tool +@icon("res://addons/gloot/images/icon_inventory_stacked.svg") +extends Inventory +class_name InventoryStacked + +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") + +signal capacity_changed +signal occupied_space_changed + +@export var capacity: float : + get: + if _constraint_manager == null: + return 0.0 + if _constraint_manager.get_weight_constraint() == null: + return 0.0 + return _constraint_manager.get_weight_constraint().capacity + set(new_capacity): + _constraint_manager.get_weight_constraint().capacity = new_capacity +var occupied_space: float : + get: + if _constraint_manager == null: + return 0.0 + if _constraint_manager.get_weight_constraint() == null: + return 0.0 + return _constraint_manager.get_weight_constraint().occupied_space + set(new_occupied_space): + assert(false, "occupied_space is read-only!") + + +func _init() -> void: + super._init() + _constraint_manager.enable_weight_constraint() + _constraint_manager.enable_stacks_constraint() + _constraint_manager.get_weight_constraint().capacity_changed.connect(func(): capacity_changed.emit()) + _constraint_manager.get_weight_constraint().occupied_space_changed.connect(func(): occupied_space_changed.emit()) + + +func has_unlimited_capacity() -> bool: + return _constraint_manager.get_weight_constraint().has_unlimited_capacity() + + +func get_free_space() -> float: + return _constraint_manager.get_weight_constraint().get_free_space() + + +func has_place_for(item: InventoryItem) -> bool: + return _constraint_manager.has_space_for(item) + + +func add_item_automerge(item: InventoryItem) -> bool: + return _constraint_manager.get_stacks_constraint().add_item_automerge(item) + + +func split(item: InventoryItem, new_stack_size: int) -> InventoryItem: + return _constraint_manager.get_stacks_constraint().split_stack_safe(item, new_stack_size) + + +static func join(item_dst: InventoryItem, item_src: InventoryItem) -> bool: + return StacksConstraint.join_stacks(item_dst, item_src) + + +static func get_item_stack_size(item: InventoryItem) -> int: + return StacksConstraint.get_item_stack_size(item) + + +static func set_item_stack_size(item: InventoryItem, new_stack_size: int) -> bool: + return StacksConstraint.set_item_stack_size(item, new_stack_size) + + +static func get_item_max_stack_size(item: InventoryItem) -> int: + return StacksConstraint.get_item_max_stack_size(item) + + +static func set_item_max_stack_size(item: InventoryItem, new_stack_size: int) -> void: + StacksConstraint.set_item_max_stack_size(item, new_stack_size) + + +func get_prototype_stack_size(prototype_id: String) -> int: + return StacksConstraint.get_prototype_stack_size(item_protoset, prototype_id) + + +func get_prototype_max_stack_size(prototype_id: String) -> int: + return StacksConstraint.get_prototype_max_stack_size(item_protoset, prototype_id) + + +func transfer_autosplit(item: InventoryItem, destination: InventoryStacked) -> bool: + return _constraint_manager.get_stacks_constraint().transfer_autosplit(item, destination) != null + + +func transfer_automerge(item: InventoryItem, destination: InventoryStacked) -> bool: + return _constraint_manager.get_stacks_constraint().transfer_automerge(item, destination) + + +func transfer_autosplitmerge(item: InventoryItem, destination: InventoryStacked) -> bool: + return _constraint_manager.get_stacks_constraint().transfer_autosplitmerge(item, destination) + + +static func pack(item: InventoryItem) -> void: + return StacksConstraint.pack_item(item) diff --git a/addons/gloot/core/item_count.gd b/addons/gloot/core/item_count.gd new file mode 100644 index 0000000..4e3493c --- /dev/null +++ b/addons/gloot/core/item_count.gd @@ -0,0 +1,114 @@ +class_name ItemCount + +const Inf: int = -1 + +@export var count: int = 0 : + set(new_count): + if new_count < 0: + new_count = -1 + count = new_count + + +func _init(count_: int = 0) -> void: + if count_ < 0: + count_ = -1 + count = count_ + + +func is_inf() -> bool: + return count < 0 + + +func add(item_count_: ItemCount) -> ItemCount: + if item_count_.is_inf(): + count = Inf + elif !self.is_inf(): + count += item_count_.count + + return self + + +func mul(item_count_: ItemCount) -> ItemCount: + if (count == 0): + return self + if item_count_.is_inf(): + count = Inf + return self + if item_count_.count == 0: + count = 0 + return self + if self.is_inf(): + return self + + count *= item_count_.count + return self + + +func div(item_count_: ItemCount) -> ItemCount: + assert(item_count_.count > 0 || item_count_.is_inf(), "Can't devide by zero!") + if (count == 0): + return self + if item_count_.is_inf() && self.is_inf(): + count = 1 + return self + if self.is_inf(): + return self + if item_count_.is_inf(): + count = 0 + return self + + count /= item_count_.count + return self + + +func eq(item_count_: ItemCount) -> bool: + return item_count_.count == count + + +func less(item_count_: ItemCount) -> bool: + if item_count_.is_inf(): + if self.is_inf(): + return false + return true + + if self.is_inf(): + return false + + return count < item_count_.count + + +func le(item_count_: ItemCount) -> bool: + return self.less(item_count_) || self.eq(item_count_) + + +func gt(item_count_: ItemCount) -> bool: + if item_count_.is_inf(): + if self.is_inf(): + return false + return false + + if self.is_inf(): + return true + + return count > item_count_.count + + +func ge(item_count_: ItemCount) -> bool: + return self.gt(item_count_) || self.eq(item_count_) + + +static func min(item_count_l: ItemCount, item_count_r: ItemCount) -> ItemCount: + if item_count_l.less(item_count_r): + return item_count_l + return item_count_r + + +static func inf() -> ItemCount: + return ItemCount.new(Inf) + + +static func zero() -> ItemCount: + return ItemCount.new(0) + + +# TODO: Implement max() diff --git a/addons/gloot/core/item_protoset.gd b/addons/gloot/core/item_protoset.gd new file mode 100644 index 0000000..86e9f85 --- /dev/null +++ b/addons/gloot/core/item_protoset.gd @@ -0,0 +1,162 @@ +@tool +@icon("res://addons/gloot/images/icon_item_protoset.svg") +class_name ItemProtoset +extends Resource + +const Utils = preload("res://addons/gloot/core/utils.gd") + +const KEY_ID: String = "id" + +@export_multiline var json_data: String : + set(new_json_data): + json_data = new_json_data + if !json_data.is_empty(): + parse(json_data) + _save() + +var _prototypes: Dictionary = {} : + set(new_prototypes): + _prototypes = new_prototypes + _update_json_data() + _save() + + +func parse(json: String) -> void: + _prototypes.clear() + + var test_json_conv: JSON = JSON.new() + assert(test_json_conv.parse(json) == OK, "Failed to parse JSON!") + var parse_result = test_json_conv.data + assert(parse_result is Array, "JSON file must contain an array!") + + for prototype in parse_result: + assert(prototype is Dictionary, "Item prototype must be a dictionary!") + assert(prototype.has(KEY_ID), "Item prototype must have an '%s' property!" % KEY_ID) + assert(prototype[KEY_ID] is String, "'%s' property must be a string!" % KEY_ID) + + var id = prototype[KEY_ID] + assert(!_prototypes.has(id), "Item prototype ID '%s' already in use!" % id) + _prototypes[id] = prototype + _unstringify_prototype(_prototypes[id]) + + +func _to_json() -> String: + var result: Array[Dictionary] + for prototype_id in _prototypes.keys(): + result.append(get_prototype(prototype_id)) + + for prototype in result: + _stringify_prototype(prototype) + + var indent = "\t" + if ProjectSettings.get_setting("gloot/JSON_serialization/indent_using_spaces", true): + indent = "" + for i in ProjectSettings.get_setting("gloot/JSON_serialization/indent_size", 4): + indent += " " + + return JSON.stringify( + result, + indent, + ProjectSettings.get_setting("gloot/JSON_serialization/sort_keys", true), + ProjectSettings.get_setting("gloot/JSON_serialization/full_precision", false), + ) + + +func _stringify_prototype(prototype: Dictionary) -> void: + for key in prototype.keys(): + var type = typeof(prototype[key]) + if (type != TYPE_STRING) and (type != TYPE_FLOAT): + prototype[key] = var_to_str(prototype[key]) + + +func _unstringify_prototype(prototype: Dictionary) -> void: + for key in prototype.keys(): + var type = typeof(prototype[key]) + if type == TYPE_STRING: + var variant = Utils.str_to_var(prototype[key]) + if variant != null: + prototype[key] = variant + + +func _update_json_data() -> void: + json_data = _to_json() + + +func _save() -> void: + if !Engine.is_editor_hint(): + return + emit_changed() + if !resource_path.is_empty(): + ResourceSaver.save(self) + + +func get_prototype(id: StringName) -> Variant: + assert(has_prototype(id), "No prototype with ID: %s" % id) + return _prototypes[id] + + +func add_prototype(id: String) -> void: + assert(!has_prototype(id), "Prototype with ID already exists") + _prototypes[id] = {KEY_ID: id} + _update_json_data() + _save() + + +func remove_prototype(id: String) -> void: + assert(has_prototype(id), "No prototype with ID: %s" % id) + _prototypes.erase(id) + _update_json_data() + _save() + + +func duplicate_prototype(id: String) -> void: + assert(has_prototype(id), "No prototype with ID: %s" % id) + var new_id = "%s_duplicate" % id + var new_dict = _prototypes[id].duplicate() + new_dict[KEY_ID] = new_id + _prototypes[new_id] = new_dict + _update_json_data() + _save() + + +func rename_prototype(id: String, new_id: String) -> void: + assert(has_prototype(id), "No prototype with ID: %s" % id) + assert(!has_prototype(new_id), "Prototype with ID already exists") + add_prototype(new_id) + _prototypes[new_id] = _prototypes[id].duplicate() + _prototypes[new_id][KEY_ID] = new_id + remove_prototype(id) + _update_json_data() + _save() + + +func set_prototype_properties(id: String, new_properties: Dictionary) -> void: + _prototypes[id] = new_properties + _update_json_data() + _save() + + +func has_prototype(id: String) -> bool: + return _prototypes.has(id) + + +func set_prototype_property(id: String, property_name: String, value) -> void: + assert(has_prototype(id), "No prototype with ID: %s" % id) + var prototype = get_prototype(id) + prototype[property_name] = value + + +func get_prototype_property(id: String, property_name: String, default_value = null) -> Variant: + if has_prototype(id): + var prototype = get_prototype(id) + if !prototype.is_empty() && prototype.has(property_name): + return prototype[property_name] + + return default_value + + +func prototype_has_property(id: String, property_name: String) -> bool: + if has_prototype(id): + return get_prototype(id).has(property_name) + + return false diff --git a/addons/gloot/core/item_ref_slot.gd b/addons/gloot/core/item_ref_slot.gd new file mode 100644 index 0000000..e83f6d8 --- /dev/null +++ b/addons/gloot/core/item_ref_slot.gd @@ -0,0 +1,186 @@ +@tool +@icon("res://addons/gloot/images/icon_item_ref_slot.svg") +class_name ItemRefSlot +extends "res://addons/gloot/core/item_slot_base.gd" + +signal inventory_changed + +const Verify = preload("res://addons/gloot/core/verify.gd") +const KEY_ITEM_INDEX: String = "item_index" +const EMPTY_SLOT = -1 + +@export var inventory_path: NodePath : + set(new_inv_path): + if inventory_path == new_inv_path: + return + inventory_path = new_inv_path + update_configuration_warnings() + _set_inventory_from_path(inventory_path) + +var _wr_item: WeakRef = weakref(null) +var _wr_inventory: WeakRef = weakref(null) +@export var _equipped_item: int = EMPTY_SLOT : set = _set_equipped_item_index +var inventory: Inventory = null : + get = _get_inventory, set = _set_inventory + + +func _get_configuration_warnings() -> PackedStringArray: + if inventory_path.is_empty(): + return PackedStringArray([ + "Inventory path not set! Inventory path needs to point to an inventory node, so " +\ + "items from that inventory can be equipped in the slot."]) + return PackedStringArray() + + +func _set_equipped_item_index(new_value: int) -> void: + _equipped_item = new_value + equip_by_index(new_value) + + +func _ready() -> void: + _set_inventory_from_path(inventory_path) + equip_by_index(_equipped_item) + + +func _set_inventory_from_path(path: NodePath) -> bool: + if path.is_empty(): + return false + + var node: Node = null + + if is_inside_tree(): + node = get_node_or_null(inventory_path) + + if node == null || !(node is Inventory): + return false + + clear() + _set_inventory(node) + return true + + +func _set_inventory(inventory: Inventory) -> void: + if inventory == _wr_inventory.get_ref(): + return + + if _get_inventory() != null: + _disconnect_inventory_signals() + + clear() + _wr_inventory = weakref(inventory) + inventory_changed.emit() + + if _get_inventory() != null: + _connect_inventory_signals() + + +func _connect_inventory_signals() -> void: + if _get_inventory() == null: + return + + if !_get_inventory().item_removed.is_connected(_on_item_removed): + _get_inventory().item_removed.connect(_on_item_removed) + + +func _disconnect_inventory_signals() -> void: + if _get_inventory() == null: + return + + if _get_inventory().item_removed.is_connected(_on_item_removed): + _get_inventory().item_removed.disconnect(_on_item_removed) + + +func _on_item_removed(item: InventoryItem) -> void: + clear() + + +func _get_inventory() -> Inventory: + return _wr_inventory.get_ref() + + +func equip(item: InventoryItem) -> bool: + if !can_hold_item(item): + return false + + if _wr_item.get_ref() == item: + return false + + if get_item() != null && !clear(): + return false + + _wr_item = weakref(item) + _equipped_item = _get_inventory().get_item_index(item) + item_equipped.emit() + return true + + +func equip_by_index(index: int) -> bool: + if _get_inventory() == null: + return false + if index < 0: + return false + if index >= _get_inventory().get_item_count(): + return false + return equip(_get_inventory().get_items()[index]) + + +func clear() -> bool: + if get_item() == null: + return false + + _wr_item = weakref(null) + _equipped_item = EMPTY_SLOT + cleared.emit() + return true + + +func get_item() -> InventoryItem: + return _wr_item.get_ref() + + +func can_hold_item(item: InventoryItem) -> bool: + if item == null: + return false + + if _get_inventory() == null || !_get_inventory().has_item(item): + return false + + return true + + +func reset() -> void: + clear() + + +func serialize() -> Dictionary: + var result: Dictionary = {} + var item : InventoryItem = _wr_item.get_ref() + + if item != null && item.get_inventory() != null: + result[KEY_ITEM_INDEX] = item.get_inventory().get_item_index(item) + + return result + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, false, KEY_ITEM_INDEX, [TYPE_INT, TYPE_FLOAT]): + return false + + reset() + + if source.has(KEY_ITEM_INDEX): + var item_index: int = source[KEY_ITEM_INDEX] + if !_equip_item_with_index(item_index): + return false + + return true + + +func _equip_item_with_index(item_index: int) -> bool: + if _get_inventory() == null: + return false + if item_index >= _get_inventory().get_item_count(): + return false + equip(_get_inventory().get_items()[item_index]) + return true + diff --git a/addons/gloot/core/item_slot.gd b/addons/gloot/core/item_slot.gd new file mode 100644 index 0000000..e780217 --- /dev/null +++ b/addons/gloot/core/item_slot.gd @@ -0,0 +1,130 @@ +@tool +@icon("res://addons/gloot/images/icon_item_slot.svg") +class_name ItemSlot +extends "res://addons/gloot/core/item_slot_base.gd" + +signal protoset_changed + +const Verify = preload("res://addons/gloot/core/verify.gd") +const KEY_ITEM: String = "item" + +@export var item_protoset: ItemProtoset: + set(new_item_protoset): + if new_item_protoset == item_protoset: + return + if _item: + _item = null + item_protoset = new_item_protoset + protoset_changed.emit() + update_configuration_warnings() +@export var remember_source_inventory: bool = true + +var _wr_source_inventory: WeakRef = weakref(null) +var _item: InventoryItem + + +func _get_configuration_warnings() -> PackedStringArray: + if item_protoset == null: + return PackedStringArray([ + "This item slot has no protoset. Set the 'item_protoset' field to be able to equip items."]) + return PackedStringArray() + + +func equip(item: InventoryItem) -> bool: + if !can_hold_item(item): + return false + + if item.get_parent() == self: + return false + + if get_item() != null && !clear(): + return false + + _wr_source_inventory = weakref(item.get_inventory()) + + if item.get_parent(): + item.get_parent().remove_child(item) + + add_child(item) + if Engine.is_editor_hint(): + item.owner = get_tree().edited_scene_root + return true + + +func _on_item_added(item: InventoryItem) -> void: + _item = item + item_equipped.emit() + + +func clear() -> bool: + return _clear_impl(remember_source_inventory) + + +func _clear_impl(return_item: bool) -> bool: + if get_item() == null: + return false + + if return_item && _return_item_to_source_inventory(): + return true + + remove_child(get_item()) + return true + + +func _return_item_to_source_inventory() -> bool: + var inventory: Inventory = (_wr_source_inventory.get_ref() as Inventory) + if inventory != null: + if inventory.add_item(get_item()): + return true + return false + + +func _on_item_removed() -> void: + _item = null + _wr_source_inventory = weakref(null) + cleared.emit() + + +func get_item() -> InventoryItem: + return _item + + +func can_hold_item(item: InventoryItem) -> bool: + assert(item_protoset != null, "Item protoset not set!") + if item == null: + return false + if item_protoset != item.protoset: + return false + + return true + + +func reset() -> void: + if _item: + _item.queue_free() + _clear_impl(false) + + +func serialize() -> Dictionary: + var result: Dictionary = {} + + if _item != null: + result[KEY_ITEM] = _item.serialize() + + return result + + +func deserialize(source: Dictionary) -> bool: + if !Verify.dict(source, false, KEY_ITEM, [TYPE_DICTIONARY]): + return false + + reset() + + if source.has(KEY_ITEM): + var item := InventoryItem.new() + if !item.deserialize(source[KEY_ITEM]): + return false + equip(item) + + return true + diff --git a/addons/gloot/core/item_slot_base.gd b/addons/gloot/core/item_slot_base.gd new file mode 100644 index 0000000..0c9679e --- /dev/null +++ b/addons/gloot/core/item_slot_base.gd @@ -0,0 +1,42 @@ +@tool +@icon("res://addons/gloot/images/icon_item_slot.svg") +class_name ItemSlotBase +extends Node + +signal item_equipped +signal cleared + + +# Override this +func equip(item: InventoryItem) -> bool: + return false + + +# Override this +func clear() -> bool: + return false + + +# Override this +func get_item() -> InventoryItem: + return null + + +# Override this +func can_hold_item(item: InventoryItem) -> bool: + return false + + +# Override this +func reset() -> void: + pass + + +# Override this +func serialize() -> Dictionary: + return {} + + +# Override this +func deserialize(source: Dictionary) -> bool: + return false \ No newline at end of file diff --git a/addons/gloot/core/utils.gd b/addons/gloot/core/utils.gd new file mode 100644 index 0000000..a4294ea --- /dev/null +++ b/addons/gloot/core/utils.gd @@ -0,0 +1,11 @@ + +static func str_to_var(s: String) -> Variant: + var variant = str_to_var(s) + # str_to_var considers all strings that start with a digit convertable to + # int/float (which is not consistent with String.is_valid_int and + # String.is_valid_float). + if typeof(variant) == TYPE_INT && !s.is_valid_int(): + variant = null + if typeof(variant) == TYPE_FLOAT && !s.is_valid_float(): + variant = null + return variant diff --git a/addons/gloot/core/verify.gd b/addons/gloot/core/verify.gd new file mode 100644 index 0000000..db6679d --- /dev/null +++ b/addons/gloot/core/verify.gd @@ -0,0 +1,189 @@ + +const type_names: Array = [ + "null", + "bool", + "int", + "float", + "String", + "Vector2", + "Vector2i", + "Rect2", + "Rect2i", + "Vector3", + "Vector3i", + "Transform2D", + "Vector4", + "Vector4i", + "Plane", + "Quaternion", + "AABB", + "Basis", + "Transform3D", + "Projection", + "Color", + "StringName", + "NodePath", + "RID", + "Object", + "Callable", + "Signal", + "Dictionary", + "Array", + "PackedByteArray", + "PackedInt32Array", + "PackedInt64Array", + "PackedFloat32Array", + "PackedFloat64Array", + "PackedStringArray", + "PackedVector2Array", + "PackedVector3Array", + "PackedColorArray" +] + + +static func create_var(type: int): + match type: + TYPE_BOOL: + return false + TYPE_INT: + return 0 + TYPE_FLOAT: + return 0.0 + TYPE_STRING: + return "" + TYPE_VECTOR2: + return Vector2() + TYPE_VECTOR2I: + return Vector2i() + TYPE_RECT2: + return Rect2() + TYPE_RECT2I: + return Rect2i() + TYPE_VECTOR3: + return Vector3() + TYPE_VECTOR3I: + return Vector3i() + TYPE_VECTOR4: + return Vector4() + TYPE_VECTOR4I: + return Vector4i() + TYPE_TRANSFORM2D: + return Transform2D() + TYPE_PLANE: + return Plane() + TYPE_QUATERNION: + return Quaternion() + TYPE_AABB: + return AABB() + TYPE_BASIS: + return Basis() + TYPE_TRANSFORM3D: + return Transform3D() + TYPE_PROJECTION : + return Projection() + TYPE_COLOR: + return Color() + TYPE_STRING_NAME: + return "" + TYPE_NODE_PATH: + return NodePath() + TYPE_RID: + return RID() + TYPE_OBJECT: + return Object.new() + TYPE_DICTIONARY: + return {} + TYPE_ARRAY: + return [] + TYPE_PACKED_BYTE_ARRAY: + return PackedByteArray() + TYPE_PACKED_INT32_ARRAY: + return PackedInt32Array() + TYPE_PACKED_INT64_ARRAY: + return PackedInt64Array() + TYPE_PACKED_FLOAT32_ARRAY: + return PackedFloat32Array() + TYPE_PACKED_FLOAT64_ARRAY: + return PackedFloat64Array() + TYPE_PACKED_STRING_ARRAY: + return PackedStringArray() + TYPE_PACKED_VECTOR2_ARRAY: + return PackedVector2Array() + TYPE_PACKED_VECTOR3_ARRAY: + return PackedVector3Array() + TYPE_PACKED_COLOR_ARRAY: + return PackedColorArray() + return null + + +static func dict(dict: Dictionary, + mandatory: bool, + key: String, + expected_value_type, + expected_array_type: int = -1) -> bool: + + if !dict.has(key): + if !mandatory: + return true + print("Missing key: '%s'!" % key) + return false + + if expected_value_type is int: + return _check_dict_key_type(dict, key, expected_value_type, expected_array_type) + elif expected_value_type is Array: + return _check_dict_key_type_multi(dict, key, expected_value_type) + + print("Warning: 'value_type' must be either int or Array!") + return false + + +static func _check_dict_key_type(dict: Dictionary, + key: String, + expected_value_type: int, + expected_array_type: int = -1) -> bool: + + var t: int = typeof(dict[key]) + if t != expected_value_type: + print("Key '%s' has wrong type! Expected '%s', got '%s'!" % + [key, type_names[expected_value_type], type_names[t]]) + return false + + if expected_value_type == TYPE_ARRAY && expected_array_type >= 0: + return _check_dict_key_array_type(dict, key, expected_array_type) + + return true + + +static func _check_dict_key_array_type(dict: Dictionary, key: String, expected_array_type: int): + var array: Array = dict[key] + for i in range(array.size()): + if typeof(array[i]) != expected_array_type: + print("Array element %d has wrong type! Expected '%s', got '%s'!" % + [i, type_names[expected_array_type], type_names[array[i]]]) + return false + + return true + + +static func _check_dict_key_type_multi(dict: Dictionary, + key: String, + expected_value_types: Array) -> bool: + + var t: int = typeof(dict[key]) + if !(t in expected_value_types): + print("Key '%s' has wrong type! Got '%s', but expected one of the following:" % + [key, type_names[t]]) + for expected_type in expected_value_types: + print(" %s" % type_names[expected_type]) + return false + + return true + + +static func vector_positive(v) -> bool: + assert(v is Vector2 || v is Vector2i, "v must be a Vector2 or a Vector2i!") + return v.x >= 0 && v.y >= 0 + + +static func rect_positive(rect: Rect2) -> bool: + return vector_positive(rect.position) && vector_positive(rect.size) diff --git a/addons/gloot/editor/common/choice_filter.gd b/addons/gloot/editor/common/choice_filter.gd new file mode 100644 index 0000000..77d51a6 --- /dev/null +++ b/addons/gloot/editor/common/choice_filter.gd @@ -0,0 +1,129 @@ +@tool +extends Control + +signal choice_picked(value_index) +signal choice_selected(value_index) + + +@onready var lbl_filter: Label = $HBoxContainer/Label +@onready var line_edit: LineEdit = $HBoxContainer/LineEdit +@onready var item_list: ItemList = $ItemList +@onready var btn_pick: Button = $Button +@export var pick_button_visible: bool = true : + set(new_pick_button_visible): + pick_button_visible = new_pick_button_visible + if btn_pick: + btn_pick.visible = pick_button_visible +@export var pick_text: String : + set(new_pick_text): + pick_text = new_pick_text + if btn_pick: + btn_pick.text = pick_text +@export var pick_icon: Texture2D : + set(new_pick_icon): + pick_icon = new_pick_icon + if btn_pick: + btn_pick.icon = pick_icon +@export var filter_text: String = "Filter:" : + set(new_filter_text): + filter_text = new_filter_text + if lbl_filter: + lbl_filter.text = filter_text +@export var filter_icon: Texture2D : + set(new_filter_icon): + filter_icon = new_filter_icon + if line_edit: + line_edit.right_icon = filter_icon +@export var values: Array[String] + + +func refresh() -> void: + _clear() + _populate() + + +func _clear() -> void: + if item_list: + item_list.clear() + + +func _populate() -> void: + if line_edit == null || item_list == null: + return + + if values == null || values.size() == 0: + return + + for value_index in range(values.size()): + var value = values[value_index] + assert(value is String, "values must be an array of strings!") + + if !line_edit.text.is_empty() && !(line_edit.text.to_lower() in value.to_lower()): + continue + + item_list.add_item(value) + item_list.set_item_metadata(item_list.get_item_count() - 1, value_index) + + +func _ready() -> void: + btn_pick.pressed.connect(_on_btn_pick) + line_edit.text_changed.connect(_on_filter_text_changed) + item_list.item_activated.connect(_on_item_activated) + item_list.item_selected.connect(_on_item_selected) + refresh() + if btn_pick: + btn_pick.text = pick_text + btn_pick.icon = pick_icon + btn_pick.visible = pick_button_visible + if lbl_filter: + lbl_filter.text = filter_text + if line_edit: + line_edit.right_icon = filter_icon + + +func _on_btn_pick() -> void: + var selected_items: PackedInt32Array = item_list.get_selected_items() + if selected_items.size() == 0: + return + + var selected_item = selected_items[0] + var selected_value_index = item_list.get_item_metadata(selected_item) + choice_picked.emit(selected_value_index) + + +func _on_filter_text_changed(_new_text: String) -> void: + refresh() + + +func _on_item_activated(index: int) -> void: + var selected_value_index = item_list.get_item_metadata(index) + choice_picked.emit(selected_value_index) + + +func _on_item_selected(index: int) -> void: + var selected_value_index = item_list.get_item_metadata(index) + choice_selected.emit(selected_value_index) + + +func get_selected_item() -> int: + var selected := item_list.get_selected_items() + if selected.size() > 0: + return item_list.get_item_metadata(selected[0]) + return -1 + + +func get_selected_text() -> String: + var selected := get_selected_item() + if selected >= 0: + return values[selected] + + return "" + + +func set_values(new_values: Array) -> void: + values.clear() + for new_value in new_values: + if typeof(new_value) == TYPE_STRING: + values.push_back(new_value) + + refresh() diff --git a/addons/gloot/editor/common/choice_filter.tscn b/addons/gloot/editor/common/choice_filter.tscn new file mode 100644 index 0000000..561f8bb --- /dev/null +++ b/addons/gloot/editor/common/choice_filter.tscn @@ -0,0 +1,31 @@ +[gd_scene load_steps=2 format=3 uid="uid://dj577duf8yjeb"] + +[ext_resource type="Script" path="res://addons/gloot/editor/common/choice_filter.gd" id="1"] + +[node name="ChoiceFilter" type="VBoxContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="HBoxContainer"] +layout_mode = 2 +text = "Filter:" + +[node name="LineEdit" type="LineEdit" parent="HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +clear_button_enabled = true + +[node name="ItemList" type="ItemList" parent="."] +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Button" type="Button" parent="."] +layout_mode = 2 diff --git a/addons/gloot/editor/common/choice_filter_test.tscn b/addons/gloot/editor/common/choice_filter_test.tscn new file mode 100644 index 0000000..062709d --- /dev/null +++ b/addons/gloot/editor/common/choice_filter_test.tscn @@ -0,0 +1,21 @@ +[gd_scene load_steps=2 format=3 uid="uid://rfjw5a8ppj1b"] + +[ext_resource type="PackedScene" uid="uid://dj577duf8yjeb" path="res://addons/gloot/editor/common/choice_filter.tscn" id="1"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ChoiceFilter" parent="." instance=ExtResource("1")] +layout_mode = 0 +anchors_preset = 0 +anchor_right = 0.0 +anchor_bottom = 0.0 +offset_right = 217.0 +offset_bottom = 267.0 +pick_text = "Pick" +values = Array[String](["foo", "bar", "baz"]) diff --git a/addons/gloot/editor/common/dict_editor.gd b/addons/gloot/editor/common/dict_editor.gd new file mode 100644 index 0000000..3801161 --- /dev/null +++ b/addons/gloot/editor/common/dict_editor.gd @@ -0,0 +1,171 @@ +@tool +extends Control + +signal value_changed(key, value) +signal value_removed(key) + +const Verify = preload("res://addons/gloot/core/verify.gd") +const ValueEditor = preload("res://addons/gloot/editor/common/value_editor.gd") +const supported_types: Array[int] = [ + TYPE_BOOL, + TYPE_INT, + TYPE_FLOAT, + TYPE_STRING, + TYPE_VECTOR2, + TYPE_VECTOR2I, + TYPE_RECT2, + TYPE_RECT2I, + TYPE_VECTOR3, + TYPE_VECTOR3I, + TYPE_PLANE, + TYPE_QUATERNION, + TYPE_AABB, + TYPE_COLOR, +] + +@onready var grid_container = $VBoxContainer/ScrollContainer/GridContainer +@onready var lbl_name = $VBoxContainer/ScrollContainer/GridContainer/LblTitleName +@onready var lbl_type = $VBoxContainer/ScrollContainer/GridContainer/LblTitleType +@onready var lbl_value = $VBoxContainer/ScrollContainer/GridContainer/LblTitleValue +@onready var ctrl_dummy = $VBoxContainer/ScrollContainer/GridContainer/CtrlDummy +@onready var edt_property_name = $VBoxContainer/HBoxContainer/EdtPropertyName +@onready var opt_type = $VBoxContainer/HBoxContainer/OptType +@onready var btn_add = $VBoxContainer/HBoxContainer/BtnAdd + +@export var dictionary: Dictionary : + set(new_dictionary): + dictionary = new_dictionary + refresh() +@export var color_map: Dictionary : + set(new_color_map): + color_map = new_color_map + refresh() +@export var remove_button_map: Dictionary : + set(new_remove_button_map): + remove_button_map = new_remove_button_map + refresh() +@export var immutable_keys: Array[String] : + set(new_immutable_keys): + immutable_keys = new_immutable_keys + refresh() +@export var default_color: Color = Color.WHITE : + set(new_default_color): + default_color = new_default_color + refresh() + + +func _ready() -> void: + btn_add.pressed.connect(_on_btn_add) + edt_property_name.text_submitted.connect(_on_text_entered) + refresh() + + +func _on_btn_add() -> void: + var name: String = edt_property_name.text + var type: int = opt_type.get_selected_id() + if _add_dict_field(name, type): + value_changed.emit(name, dictionary[name]) + refresh() + + +func _on_text_entered(_new_text: String) -> void: + _on_btn_add() + + +func _add_dict_field(name: String, type: int) -> bool: + if (name.is_empty() || type < 0 || dictionary.has(name)): + return false + dictionary[name] = Verify.create_var(type) + return true + + +func refresh() -> void: + if !is_inside_tree(): + return + _clear() + lbl_name.add_theme_color_override("font_color", default_color) + lbl_type.add_theme_color_override("font_color", default_color) + lbl_value.add_theme_color_override("font_color", default_color) + + _refresh_add_property() + _populate() + + +func _refresh_add_property() -> void: + for type in supported_types: + opt_type.add_item(Verify.type_names[type], type) + opt_type.select(supported_types.find(TYPE_STRING)) + + +func _clear() -> void: + edt_property_name.text = "" + opt_type.clear() + + for child in grid_container.get_children(): + if (child == lbl_name) || (child == lbl_type) || (child == lbl_value) || (child == ctrl_dummy): + continue + child.queue_free() + + +func _populate() -> void: + for key in dictionary.keys(): + var color: Color = default_color + if color_map.has(key) && typeof(color_map[key]) == TYPE_COLOR: + color = color_map[key] + + _add_key(key, color) + + +func _add_key(key: String, color: Color) -> void: + if !(key is String): + return + + var value = dictionary[key] + _add_label(key, color) + _add_label(Verify.type_names[typeof(dictionary[key])], color) + _add_value_editor(key) + _add_remove_button(key) + + +func _add_label(key: String, color: Color) -> void: + var label: Label = Label.new() + label.text = key + label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + label.add_theme_color_override("font_color", color) + grid_container.add_child(label) + + +func _add_value_editor(key: String) -> void: + var value_editor: Control = ValueEditor.new() + value_editor.value = dictionary[key] + value_editor.size_flags_horizontal = SIZE_EXPAND_FILL + value_editor.enabled = (not key in immutable_keys) + value_editor.value_changed.connect(_on_value_changed.bind(key, value_editor)) + grid_container.add_child(value_editor) + + +func _on_value_changed(key: String, value_editor: Control) -> void: + dictionary[key] = value_editor.value + value_changed.emit(key, value_editor.value) + + +func _add_remove_button(key: String) -> void: + var button: Button = Button.new() + button.text = "Remove" + if remove_button_map.has(key): + button.text = remove_button_map[key].text + button.disabled = remove_button_map[key].disabled + button.icon = remove_button_map[key].icon + button.pressed.connect(_on_remove_button.bind(key)) + grid_container.add_child(button) + + +func _on_remove_button(key: String) -> void: + dictionary.erase(key) + value_removed.emit(key) + refresh() + + +func set_remove_button_config(key: String, config: Dictionary) -> void: + remove_button_map[key] = config + refresh() diff --git a/addons/gloot/editor/common/dict_editor.tscn b/addons/gloot/editor/common/dict_editor.tscn new file mode 100644 index 0000000..f43fc71 --- /dev/null +++ b/addons/gloot/editor/common/dict_editor.tscn @@ -0,0 +1,98 @@ +[gd_scene load_steps=2 format=3 uid="uid://digtudobrw3xb"] + +[ext_resource type="Script" path="res://addons/gloot/editor/common/dict_editor.gd" id="1"] + +[node name="DictEditor" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +script = ExtResource("1") +dictionary = { +"name": "John Doe" +} + +[node name="VBoxContainer" type="VBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ScrollContainer" type="ScrollContainer" parent="VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="GridContainer" type="GridContainer" parent="VBoxContainer/ScrollContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +columns = 4 + +[node name="LblTitleName" type="Label" parent="VBoxContainer/ScrollContainer/GridContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "Name" + +[node name="LblTitleType" type="Label" parent="VBoxContainer/ScrollContainer/GridContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "Type" + +[node name="LblTitleValue" type="Label" parent="VBoxContainer/ScrollContainer/GridContainer"] +layout_mode = 2 +theme_override_colors/font_color = Color(1, 1, 1, 1) +text = "Value" + +[node name="CtrlDummy" type="Control" parent="VBoxContainer/ScrollContainer/GridContainer"] +layout_mode = 2 + +[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"] +layout_mode = 2 + +[node name="EdtPropertyName" type="LineEdit" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="OptType" type="OptionButton" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +item_count = 14 +selected = 3 +popup/item_0/text = "bool" +popup/item_0/id = 1 +popup/item_1/text = "int" +popup/item_1/id = 2 +popup/item_2/text = "float" +popup/item_2/id = 3 +popup/item_3/text = "String" +popup/item_3/id = 4 +popup/item_4/text = "Vector2" +popup/item_4/id = 5 +popup/item_5/text = "Vector2i" +popup/item_5/id = 6 +popup/item_6/text = "Rect2" +popup/item_6/id = 7 +popup/item_7/text = "Rect2i" +popup/item_7/id = 8 +popup/item_8/text = "Vector3" +popup/item_8/id = 9 +popup/item_9/text = "Vector3i" +popup/item_9/id = 10 +popup/item_10/text = "Plane" +popup/item_10/id = 14 +popup/item_11/text = "Quaternion" +popup/item_11/id = 15 +popup/item_12/text = "AABB" +popup/item_12/id = 16 +popup/item_13/text = "Color" +popup/item_13/id = 20 + +[node name="BtnAdd" type="Button" parent="VBoxContainer/HBoxContainer"] +layout_mode = 2 +text = "Add Property" diff --git a/addons/gloot/editor/common/dict_editor_test.tscn b/addons/gloot/editor/common/dict_editor_test.tscn new file mode 100644 index 0000000..4907376 --- /dev/null +++ b/addons/gloot/editor/common/dict_editor_test.tscn @@ -0,0 +1,22 @@ +[gd_scene load_steps=2 format=3 uid="uid://cewbvw0n01ea2"] + +[ext_resource type="PackedScene" uid="uid://digtudobrw3xb" path="res://addons/gloot/editor/common/dict_editor.tscn" id="1"] + +[node name="Control" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="DictEditor" parent="." instance=ExtResource("1")] +layout_mode = 1 +dictionary = { +"id": 0, +"name": "Bob", +"position": Vector2(0, 0) +} +color_map = { +"name": Color(0, 0.654902, 0.0392157, 1) +} diff --git a/addons/gloot/editor/common/editor_icons.gd b/addons/gloot/editor/common/editor_icons.gd new file mode 100644 index 0000000..faed61e --- /dev/null +++ b/addons/gloot/editor/common/editor_icons.gd @@ -0,0 +1,6 @@ +@tool + +static func get_icon(icon_name: String) -> Texture2D: + var gui = EditorInterface.get_base_control() + var icon = gui.get_theme_icon(icon_name, "EditorIcons") + return icon diff --git a/addons/gloot/editor/common/multivalue_editor.gd b/addons/gloot/editor/common/multivalue_editor.gd new file mode 100644 index 0000000..cd789ab --- /dev/null +++ b/addons/gloot/editor/common/multivalue_editor.gd @@ -0,0 +1,53 @@ +extends GridContainer + +signal value_changed(value_index) + +const Utils = preload("res://addons/gloot/core/utils.gd") + +var values: Array = [] : + set(new_values): + assert(!is_inside_tree(), "Can't set values once the node is inside a tree") + values = new_values +var titles: Array = [] : + set(new_titles): + assert(!is_inside_tree(), "Can't set titles once the node is inside a tree") + titles = new_titles +var enabled: bool = true +var type: int = TYPE_FLOAT + + +func _ready() -> void: + for i in values.size(): + var hbox: HBoxContainer = HBoxContainer.new() + hbox.size_flags_horizontal = SIZE_EXPAND_FILL + + if i < titles.size(): + var label: Label = Label.new() + label.text = "%s:" % titles[i] + hbox.add_child(label) + else: + var dummy: Control = Control.new() + hbox.add_child(dummy) + + var line_edit: LineEdit = LineEdit.new() + line_edit.text = var_to_str(values[i]) + line_edit.size_flags_horizontal = SIZE_EXPAND_FILL + line_edit.text_submitted.connect(_on_line_edit_value_entered.bind(line_edit, i)) + line_edit.focus_exited.connect(_on_line_edit_focus_exited.bind(line_edit, i)) + line_edit.editable = enabled + hbox.add_child(line_edit) + + add_child(hbox) + + +func _on_line_edit_value_entered(_text: String, line_edit: LineEdit, idx: int) -> void: + _on_line_edit_focus_exited(line_edit, idx) + + +func _on_line_edit_focus_exited(line_edit: LineEdit, idx: int) -> void: + var value = Utils.str_to_var(line_edit.text) + if typeof(value) != type: + line_edit.text = var_to_str(values[idx]) + return + values[idx] = value + value_changed.emit(idx) diff --git a/addons/gloot/editor/common/value_editor.gd b/addons/gloot/editor/common/value_editor.gd new file mode 100644 index 0000000..1fa69c9 --- /dev/null +++ b/addons/gloot/editor/common/value_editor.gd @@ -0,0 +1,263 @@ +extends MarginContainer + +signal value_changed + +const MultivalueEditor = preload("res://addons/gloot/editor/common/multivalue_editor.gd") +const Utils = preload("res://addons/gloot/core/utils.gd") + +var value : + set(new_value): + value = new_value + call_deferred("_refresh") +var enabled: bool = true + + +func _ready(): + _refresh() + + +func _refresh(): + _clear() + _add_control() + + +func _clear() -> void: + for c in get_children(): + c.queue_free() + + +func _add_control() -> void: + var type = typeof(value) + var control: Control = null + + match type: + TYPE_COLOR: + control = _create_color_picker() + TYPE_BOOL: + control = _create_checkbox() + TYPE_VECTOR2: + control = _create_v2_editor() + TYPE_VECTOR2I: + control = _create_v2i_editor() + TYPE_VECTOR3: + control = _create_v3_editor() + TYPE_VECTOR3I: + control = _create_v3i_editor() + TYPE_RECT2: + control = _create_r2_editor() + TYPE_RECT2I: + control = _create_r2i_editor() + TYPE_PLANE: + control = _create_plane_editor() + TYPE_QUATERNION: + control = _create_quat_editor() + TYPE_AABB: + control = _create_aabb_editor() + _: + control = _create_line_edit() + + add_child(control) + + +func _create_line_edit() -> LineEdit: + var line_edit: LineEdit = LineEdit.new() + line_edit.text = var_to_str(value) + line_edit.editable = enabled + _expand_control(line_edit) + line_edit.text_submitted.connect(_on_line_edit_value_entered.bind(line_edit)) + line_edit.focus_exited.connect(_on_line_edit_focus_exited.bind(line_edit)) + return line_edit + + +func _on_line_edit_value_entered(_text: String, line_edit: LineEdit) -> void: + _on_line_edit_focus_exited(line_edit) + + +func _on_line_edit_focus_exited(line_edit: LineEdit) -> void: + var new_value = Utils.str_to_var(line_edit.text) + if typeof(new_value) != typeof(value): + line_edit.text = var_to_str(value) + return + value = new_value + value_changed.emit() + + +func _create_color_picker() -> ColorPickerButton: + var picker: ColorPickerButton = ColorPickerButton.new() + picker.color = value + picker.disabled = !enabled + _expand_control(picker) + picker.popup_closed.connect(_on_color_picked.bind(picker)) + return picker + + +func _on_color_picked(picker: ColorPickerButton) -> void: + value = picker.color + value_changed.emit() + + +func _create_checkbox() -> CheckButton: + var checkbox: CheckButton = CheckButton.new() + checkbox.button_pressed = value + checkbox.disabled = !enabled + _expand_control(checkbox) + checkbox.pressed.connect(_on_checkbox.bind(checkbox)) + return checkbox + + +func _on_checkbox(checkbox: CheckButton) -> void: + value = checkbox.button_pressed + value_changed.emit() + + +func _create_v2_editor() -> Control: + var values = [value.x, value.y] + var titles = ["X", "Y"] + var v2_editor = _create_multifloat_editor(2, enabled, values, titles, _on_v2_value_changed) + return v2_editor + + +func _create_v2i_editor() -> Control: + var values = [value.x, value.y] + var titles = ["X", "Y"] + var v2_editor = _create_multiint_editor(2, enabled, values, titles, _on_v2_value_changed) + return v2_editor + + +func _on_v2_value_changed(_idx: int, v2_editor: Control) -> void: + value.x = v2_editor.values[0] + value.y = v2_editor.values[1] + value_changed.emit() + + +func _create_v3_editor() -> Control: + var values = [value.x, value.y, value.z] + var titles = ["X", "Y", "Z"] + var v3_editor = _create_multifloat_editor(3, enabled, values, titles, _on_v3_value_changed) + return v3_editor + + +func _create_v3i_editor() -> Control: + var values = [value.x, value.y, value.z] + var titles = ["X", "Y", "Z"] + var v3_editor = _create_multiint_editor(3, enabled, values, titles, _on_v3_value_changed) + return v3_editor + + +func _on_v3_value_changed(_idx: int, v3_editor: Control) -> void: + value.x = v3_editor.values[0] + value.y = v3_editor.values[1] + value.z = v3_editor.values[2] + value_changed.emit() + + +func _create_r2_editor() -> Control: + var values = [value.position.x, value.position.y, value.size.x, value.size.y] + var titles = ["Position X", "Position Y", "Size X", "Size Y"] + var r2_editor = _create_multifloat_editor(2, enabled, values, titles, _on_r2_value_changed) + return r2_editor + + +func _create_r2i_editor() -> Control: + var values = [value.position.x, value.position.y, value.size.x, value.size.y] + var titles = ["Position X", "Position Y", "Size X", "Size Y"] + var r2_editor = _create_multiint_editor(2, enabled, values, titles, _on_r2_value_changed) + return r2_editor + + +func _on_r2_value_changed(_idx: int, r2_editor: Control) -> void: + value.position.x = r2_editor.values[0] + value.position.y = r2_editor.values[1] + value.size.x = r2_editor.values[2] + value.size.y = r2_editor.values[3] + value_changed.emit() + + +func _create_plane_editor() -> Control: + var values = [value.x, value.y, value.z, value.d] + var titles = ["X", "Y", "Z", "D"] + var editor = _create_multifloat_editor(2, enabled, values, titles, _on_plane_value_changed) + return editor + + +func _on_plane_value_changed(_idx: int, plane_editor: Control) -> void: + value.x = plane_editor.values[0] + value.y = plane_editor.values[1] + value.z = plane_editor.values[2] + value.d = plane_editor.values[3] + value_changed.emit() + + +func _create_quat_editor() -> Control: + var values = [value.x, value.y, value.z, value.w] + var titles = ["X", "Y", "Z", "W"] + var editor = _create_multifloat_editor(2, enabled, values, titles, _on_quat_value_changed) + return editor + + +func _on_quat_value_changed(_idx: int, quat_editor: Control) -> void: + value.x = quat_editor.values[0] + value.y = quat_editor.values[1] + value.z = quat_editor.values[2] + value.d = quat_editor.values[3] + value_changed.emit() + + +func _create_aabb_editor() -> Control: + var values = [value.position.x, value.position.y, value.position.z, \ + value.size.x, value.size.y, value.size.z] + var titles = ["Position X", "Position Y", "Position Z", "Size X", "Size Y", "Size Z"] + var editor = _create_multifloat_editor(3, enabled, values, titles, _on_aabb_value_changed) + return editor + + +func _on_aabb_value_changed(_idx: int, aabb_editor: Control) -> void: + value.position.x = aabb_editor.values[0] + value.position.y = aabb_editor.values[1] + value.position.z = aabb_editor.values[2] + value.size.x = aabb_editor.values[3] + value.size.y = aabb_editor.values[4] + value.size.z = aabb_editor.values[5] + value_changed.emit() + + +func _create_multifloat_editor( + columns: int, + enabled: bool, + values: Array, + titles: Array, + value_changed_handler: Callable) -> Control: + return _create_multivalue_editor(columns, enabled, TYPE_FLOAT, values, titles, value_changed_handler) + + +func _create_multiint_editor( + columns: int, + enabled: bool, + values: Array, + titles: Array, + value_changed_handler: Callable) -> Control: + return _create_multivalue_editor(columns, enabled, TYPE_INT, values, titles, value_changed_handler) + + +func _create_multivalue_editor( + columns: int, + enabled: bool, + type: int, + values: Array, + titles: Array, + value_changed_handler: Callable) -> Control: + var multivalue_editor = MultivalueEditor.new() + multivalue_editor.columns = columns + multivalue_editor.enabled = enabled + multivalue_editor.type = type + multivalue_editor.values = values + multivalue_editor.titles = titles + _expand_control(multivalue_editor) + multivalue_editor.value_changed.connect(value_changed_handler.bind(multivalue_editor)) + return multivalue_editor + + +func _expand_control(c: Control) -> void: + c.size_flags_horizontal = SIZE_EXPAND_FILL + c.anchor_right = 1.0 + c.anchor_bottom = 1.0 diff --git a/addons/gloot/editor/gloot_undo_redo.gd b/addons/gloot/editor/gloot_undo_redo.gd new file mode 100644 index 0000000..efe6090 --- /dev/null +++ b/addons/gloot/editor/gloot_undo_redo.gd @@ -0,0 +1,296 @@ +@tool +extends Object + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") + + +static func _get_undo_redo_manager(): + var gloot = load("res://addons/gloot/gloot.gd") + assert(gloot.instance()) + var undo_redo_manager = gloot.instance().get_undo_redo() + assert(undo_redo_manager) + return undo_redo_manager + + +static func add_inventory_item(inventory: Inventory, prototype_id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_inv_state := inventory.serialize() + if inventory.create_and_add_item(prototype_id) == null: + return + var new_inv_state := inventory.serialize() + + undo_redo_manager.create_action("Add Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_inventory", inventory, new_inv_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_inventory", inventory, old_inv_state) + undo_redo_manager.commit_action() + + +static func remove_inventory_item(inventory: Inventory, item: InventoryItem) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_inv_state := inventory.serialize() + if !inventory.remove_item(item): + return + var new_inv_state := inventory.serialize() + + undo_redo_manager.create_action("Remove Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_inventory", inventory, new_inv_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_inventory", inventory, old_inv_state) + undo_redo_manager.commit_action() + + +static func remove_inventory_items(inventory: Inventory, items: Array[InventoryItem]) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_inv_state := inventory.serialize() + for item in items: + assert(inventory.remove_item(item)) + var new_inv_state := inventory.serialize() + + undo_redo_manager.create_action("Remove Inventory Items") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_inventory", inventory, new_inv_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_inventory", inventory, old_inv_state) + undo_redo_manager.commit_action() + + +static func set_item_properties(item: InventoryItem, new_properties: Dictionary) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var inventory: Inventory = item.get_inventory() + if inventory: + undo_redo_manager.create_action("Set item properties") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_item_properties", inventory, inventory.get_item_index(item), new_properties) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_item_properties", inventory, inventory.get_item_index(item), item.properties) + undo_redo_manager.commit_action() + else: + undo_redo_manager.create_action("Set item properties") + undo_redo_manager.add_undo_property(item, "properties", item.properties) + undo_redo_manager.add_do_property(item, "properties", new_properties) + undo_redo_manager.commit_action() + + +static func set_item_prototype_id(item: InventoryItem, new_prototype_id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var inventory: Inventory = item.get_inventory() + if inventory: + undo_redo_manager.create_action("Set prototype_id") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_item_prototype_id", inventory, inventory.get_item_index(item), new_prototype_id) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_item_prototype_id", inventory, inventory.get_item_index(item), item.prototype_id) + undo_redo_manager.commit_action() + else: + undo_redo_manager.create_action("Set prototype_id") + undo_redo_manager.add_undo_property(item, "prototype_id", item.prototype_id) + undo_redo_manager.add_do_property(item, "prototype_id", new_prototype_id) + undo_redo_manager.commit_action() + + +static func _set_inventory(inventory: Inventory, inventory_data: Dictionary) -> void: + inventory.deserialize(inventory_data) + + +static func _set_item_prototype_id(inventory: Inventory, item_index: int, new_prototype_id: String): + assert(item_index < inventory.get_item_count()) + inventory.get_items()[item_index].prototype_id = new_prototype_id + + +static func _set_item_properties(inventory: Inventory, item_index: int, new_properties: Dictionary): + assert(item_index < inventory.get_item_count()) + inventory.get_items()[item_index].properties = new_properties.duplicate() + + +static func equip_item_in_item_slot(item_slot: ItemSlotBase, item: InventoryItem) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_slot_state := item_slot.serialize() + if !item_slot.equip(item): + return + var new_slot_state := item_slot.serialize() + + undo_redo_manager.create_action("Equip Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_item_slot", item_slot, new_slot_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_item_slot", item_slot, old_slot_state) + undo_redo_manager.commit_action() + + +static func clear_item_slot(item_slot: ItemSlotBase) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_slot_state := item_slot.serialize() + if !item_slot.clear(): + return + var new_slot_state := item_slot.serialize() + + undo_redo_manager.create_action("Clear Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_item_slot", item_slot, new_slot_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_item_slot", item_slot, old_slot_state) + undo_redo_manager.commit_action() + + +static func _set_item_slot(item_slot: ItemSlotBase, item_slot_data: Dictionary) -> void: + item_slot.deserialize(item_slot_data) + + +static func move_inventory_item(inventory: InventoryGrid, item: InventoryItem, to: Vector2i) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_position := inventory.get_item_position(item) + if old_position == to: + return + var item_index := inventory.get_item_index(item) + + undo_redo_manager.create_action("Move Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_move_item", inventory, item_index, to) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_move_item", inventory, item_index, old_position) + undo_redo_manager.commit_action() + + +static func swap_inventory_items(item1: InventoryItem, item2: InventoryItem) -> void: + + var undo_redo_manager = _get_undo_redo_manager() + + var inventories: Array[Inventory] = [item1.get_inventory(), item2.get_inventory()] + var old_inv_states: Array[Dictionary] = [{}, {}] + var new_inv_states: Array[Dictionary] = [{}, {}] + old_inv_states[0] = inventories[0].serialize() + old_inv_states[1] = inventories[1].serialize() + if !InventoryItem.swap(item1, item2): + return + new_inv_states[0] = inventories[0].serialize() + new_inv_states[1] = inventories[1].serialize() + + undo_redo_manager.create_action("Swap Inventory Items") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_inventories", inventories, new_inv_states) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_inventories", inventories, old_inv_states) + undo_redo_manager.commit_action() + + +static func _set_inventories(inventories: Array[Inventory], inventory_data: Array[Dictionary]) -> void: + assert(inventories.size() == inventory_data.size()) + for i in range(inventories.size()): + inventories[i].deserialize(inventory_data[i]) + + +static func rotate_inventory_item(inventory: InventoryGrid, item: InventoryItem) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + if !inventory.can_rotate_item(item): + return + + var old_rotation := inventory.is_item_rotated(item) + var item_index := inventory.get_item_index(item) + + undo_redo_manager.create_action("Rotate Inventory Item") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_item_rotation", inventory, item_index, !old_rotation) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_item_rotation", inventory, item_index, old_rotation) + undo_redo_manager.commit_action() + + +static func _move_item(inventory: InventoryGrid, item_index: int, to: Vector2i) -> void: + assert(item_index >= 0 && item_index < inventory.get_item_count()) + var item = inventory.get_items()[item_index] + inventory.move_item_to(item, to) + + +static func _set_item_rotation(inventory: InventoryGrid, item_index: int, rotation: bool) -> void: + assert(item_index >= 0 && item_index < inventory.get_item_count()) + var item = inventory.get_items()[item_index] + inventory.set_item_rotation(item, rotation) + + +static func join_inventory_items( + inventory: InventoryGridStacked, + item_dst: InventoryItem, + item_src: InventoryItem +) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_inv_state := inventory.serialize() + if !inventory.join(item_dst, item_src): + return + var new_inv_state := inventory.serialize() + + undo_redo_manager.create_action("Join Inventory Items") + undo_redo_manager.add_do_method(GlootUndoRedo, "_set_inventory", inventory, new_inv_state) + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_inventory", inventory, old_inv_state) + undo_redo_manager.commit_action() + + +static func rename_prototype(protoset: ItemProtoset, id: String, new_id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_prototypes = _prototypes_deep_copy(protoset) + + undo_redo_manager.create_action("Rename Prototype") + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_prototypes", protoset, old_prototypes) + undo_redo_manager.add_do_method(protoset, "rename_prototype", id, new_id) + undo_redo_manager.commit_action() + + +static func add_prototype(protoset: ItemProtoset, id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_prototypes = _prototypes_deep_copy(protoset) + + undo_redo_manager.create_action("Add Prototype") + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_prototypes", protoset, old_prototypes) + undo_redo_manager.add_do_method(protoset, "add_prototype", id) + undo_redo_manager.commit_action() + + +static func remove_prototype(protoset: ItemProtoset, id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_prototypes = _prototypes_deep_copy(protoset) + + undo_redo_manager.create_action("Remove Prototype") + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_prototypes", protoset, old_prototypes) + undo_redo_manager.add_do_method(protoset, "remove_prototype", id) + undo_redo_manager.commit_action() + + +static func duplicate_prototype(protoset: ItemProtoset, id: String) -> void: + var undo_redo_manager = _get_undo_redo_manager() + + var old_prototypes = _prototypes_deep_copy(protoset) + + undo_redo_manager.create_action("Duplicate Prototype") + undo_redo_manager.add_undo_method(GlootUndoRedo, "_set_prototypes", protoset, old_prototypes) + undo_redo_manager.add_do_method(protoset, "duplicate_prototype", id) + undo_redo_manager.commit_action() + + +static func _prototypes_deep_copy(protoset: ItemProtoset) -> Dictionary: + var result = protoset._prototypes.duplicate() + for prototype_id in result.keys(): + result[prototype_id] = protoset._prototypes[prototype_id].duplicate() + return result + + +static func _set_prototypes(protoset: ItemProtoset, prototypes: Dictionary) -> void: + protoset._prototypes = prototypes + + +static func set_prototype_properties(protoset: ItemProtoset, + prototype_id: String, + new_properties: Dictionary) -> void: + var undo_redo_manager = _get_undo_redo_manager() + assert(protoset.has_prototype(prototype_id)) + var old_properties = protoset.get_prototype(prototype_id).duplicate() + + undo_redo_manager.create_action("Set prototype properties") + undo_redo_manager.add_undo_method( + protoset, + "set_prototype_properties", + prototype_id, + old_properties + ) + undo_redo_manager.add_do_method( + protoset, + "set_prototype_properties", + prototype_id, + new_properties + ) + undo_redo_manager.commit_action() + diff --git a/addons/gloot/editor/inventory_editor/inventory_editor.gd b/addons/gloot/editor/inventory_editor/inventory_editor.gd new file mode 100644 index 0000000..0fb7d51 --- /dev/null +++ b/addons/gloot/editor/inventory_editor/inventory_editor.gd @@ -0,0 +1,127 @@ +@tool +extends Control + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var hsplit_container = $HSplitContainer +@onready var prototype_id_filter = $HSplitContainer/ChoiceFilter +@onready var inventory_control_container = $HSplitContainer/VBoxContainer +@onready var btn_edit = $HSplitContainer/VBoxContainer/HBoxContainer/BtnEdit +@onready var btn_remove = $HSplitContainer/VBoxContainer/HBoxContainer/BtnRemove +@onready var scroll_container = $HSplitContainer/VBoxContainer/ScrollContainer +var inventory: Inventory : + set(new_inventory): + disconnect_inventory_signals() + inventory = new_inventory + connect_inventory_signals() + + _refresh() +var _inventory_control: Control + + +func connect_inventory_signals(): + if !inventory: + return + + if inventory is InventoryStacked: + inventory.capacity_changed.connect(_refresh) + if inventory is InventoryGrid: + inventory.size_changed.connect(_refresh) + inventory.protoset_changed.connect(_refresh) + + if !inventory.item_protoset: + return + inventory.item_protoset.changed.connect(_refresh) + + +func disconnect_inventory_signals(): + if !inventory: + return + + if inventory is InventoryStacked: + inventory.capacity_changed.disconnect(_refresh) + if inventory is InventoryGrid: + inventory.size_changed.disconnect(_refresh) + inventory.protoset_changed.disconnect(_refresh) + + if !inventory.item_protoset: + return + inventory.item_protoset.changed.disconnect(_refresh) + + +func _refresh() -> void: + if !is_inside_tree() || inventory == null || inventory.item_protoset == null: + return + + # Remove the inventory control, if present + if _inventory_control: + scroll_container.remove_child(_inventory_control) + _inventory_control.queue_free() + _inventory_control = null + + # Create the appropriate inventory control and populate it + if inventory is InventoryGrid: + _inventory_control = CtrlInventoryGrid.new() + _inventory_control.grid_color = Color.GRAY + _inventory_control.draw_selections = true + elif inventory is InventoryStacked: + _inventory_control = CtrlInventoryStacked.new() + elif inventory is Inventory: + _inventory_control = CtrlInventory.new() + _inventory_control.size_flags_horizontal = SIZE_EXPAND_FILL + _inventory_control.size_flags_vertical = SIZE_EXPAND_FILL + _inventory_control.inventory = inventory + _inventory_control.inventory_item_activated.connect(_on_inventory_item_activated) + _inventory_control.inventory_item_context_activated.connect(_on_inventory_item_context_activated) + + scroll_container.add_child(_inventory_control) + + # Set prototype_id_filter values + prototype_id_filter.set_values(inventory.item_protoset._prototypes.keys()) + + +func _on_inventory_item_activated(item: InventoryItem) -> void: + GlootUndoRedo.remove_inventory_item(inventory, item) + + +func _on_inventory_item_context_activated(item: InventoryItem) -> void: + GlootUndoRedo.rotate_inventory_item(inventory, item) + + +func _ready() -> void: + prototype_id_filter.pick_icon = EditorIcons.get_icon("Add") + prototype_id_filter.filter_icon = EditorIcons.get_icon("Search") + btn_edit.icon = EditorIcons.get_icon("Edit") + btn_remove.icon = EditorIcons.get_icon("Remove") + + prototype_id_filter.choice_picked.connect(_on_prototype_id_picked) + btn_edit.pressed.connect(_on_btn_edit) + btn_remove.pressed.connect(_on_btn_remove) + _refresh() + + +func _on_prototype_id_picked(index: int) -> void: + var prototype_id = prototype_id_filter.values[index] + GlootUndoRedo.add_inventory_item(inventory, prototype_id) + + +func _on_btn_edit() -> void: + var selected_item: InventoryItem = _inventory_control.get_selected_inventory_item() + if selected_item != null: + # Call it deferred, so that the control can clean up + call_deferred("_select_node", selected_item) + + +func _on_btn_remove() -> void: + var selected_items: Array[InventoryItem] = _inventory_control.get_selected_inventory_items() + for selected_item in selected_items: + if selected_item != null: + GlootUndoRedo.remove_inventory_item(inventory, selected_item) + + +static func _select_node(node: Node) -> void: + EditorInterface.get_selection().clear() + EditorInterface.get_selection().add_node(node) + EditorInterface.edit_node(node) + diff --git a/addons/gloot/editor/inventory_editor/inventory_editor.tscn b/addons/gloot/editor/inventory_editor/inventory_editor.tscn new file mode 100644 index 0000000..f67fa23 --- /dev/null +++ b/addons/gloot/editor/inventory_editor/inventory_editor.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=3 format=3 uid="uid://c6e4cxvjdxhdo"] + +[ext_resource type="PackedScene" uid="uid://dj577duf8yjeb" path="res://addons/gloot/editor/common/choice_filter.tscn" id="1"] +[ext_resource type="Script" path="res://addons/gloot/editor/inventory_editor/inventory_editor.gd" id="2"] + +[node name="InventoryEditor" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2") + +[node name="HSplitContainer" type="HSplitContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ChoiceFilter" parent="HSplitContainer" instance=ExtResource("1")] +layout_mode = 2 +pick_text = "Add" +filter_text = "Filter Prototypes:" + +[node name="VBoxContainer" type="VBoxContainer" parent="HSplitContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="ScrollContainer" type="ScrollContainer" parent="HSplitContainer/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="HBoxContainer" type="HBoxContainer" parent="HSplitContainer/VBoxContainer"] +layout_mode = 2 + +[node name="BtnEdit" type="Button" parent="HSplitContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Edit" + +[node name="BtnRemove" type="Button" parent="HSplitContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Remove" diff --git a/addons/gloot/editor/inventory_editor/inventory_inspector.gd b/addons/gloot/editor/inventory_editor/inventory_inspector.gd new file mode 100644 index 0000000..8ffcea7 --- /dev/null +++ b/addons/gloot/editor/inventory_editor/inventory_inspector.gd @@ -0,0 +1,38 @@ +@tool +extends Control + +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var inventory_editor: Control = $HBoxContainer/InventoryEditor +@onready var btn_expand: Button = $HBoxContainer/BtnExpand +@onready var _window_dialog: Window = $Window +@onready var _inventory_editor: Control = $Window/MarginContainer/InventoryEditor + +var inventory: Inventory : + set(new_inventory): + inventory = new_inventory + if inventory_editor: + inventory_editor.inventory = inventory + + +func init(inventory_: Inventory) -> void: + inventory = inventory_ + + +func _ready() -> void: + if inventory_editor: + inventory_editor.inventory = inventory + _apply_editor_settings() + btn_expand.icon = EditorIcons.get_icon("DistractionFree") + btn_expand.pressed.connect(on_btn_expand) + _window_dialog.close_requested.connect(func(): _window_dialog.hide()) + + +func on_btn_expand() -> void: + _inventory_editor.inventory = inventory + _window_dialog.popup_centered() + + +func _apply_editor_settings() -> void: + var control_height: int = ProjectSettings.get_setting("gloot/inspector_control_height") + custom_minimum_size.y = control_height diff --git a/addons/gloot/editor/inventory_editor/inventory_inspector.tscn b/addons/gloot/editor/inventory_editor/inventory_inspector.tscn new file mode 100644 index 0000000..db8edf2 --- /dev/null +++ b/addons/gloot/editor/inventory_editor/inventory_inspector.tscn @@ -0,0 +1,54 @@ +[gd_scene load_steps=3 format=3 uid="uid://bef418tvtf7h6"] + +[ext_resource type="PackedScene" uid="uid://c6e4cxvjdxhdo" path="res://addons/gloot/editor/inventory_editor/inventory_editor.tscn" id="1"] +[ext_resource type="Script" path="res://addons/gloot/editor/inventory_editor/inventory_inspector.gd" id="2"] + +[node name="InventoryInspector" type="Control"] +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("2") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="InventoryEditor" parent="HBoxContainer" instance=ExtResource("1")] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="BtnExpand" type="Button" parent="HBoxContainer"] +layout_mode = 2 + +[node name="Window" type="Window" parent="."] +title = "Edit Inventory" +size = Vector2i(800, 600) +visible = false +exclusive = true +min_size = Vector2i(400, 300) + +[node name="MarginContainer" type="MarginContainer" parent="Window"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="InventoryEditor" parent="Window/MarginContainer" instance=ExtResource("1")] +layout_mode = 2 diff --git a/addons/gloot/editor/inventory_inspector_plugin.gd b/addons/gloot/editor/inventory_inspector_plugin.gd new file mode 100644 index 0000000..fa09e9f --- /dev/null +++ b/addons/gloot/editor/inventory_inspector_plugin.gd @@ -0,0 +1,51 @@ +extends EditorInspectorPlugin + +const EditProtosetButton = preload("res://addons/gloot/editor/protoset_editor/edit_protoset_button.tscn") +const InventoryInspector = preload("res://addons/gloot/editor/inventory_editor/inventory_inspector.tscn") +const ItemSlotInspector = preload("res://addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn") +const ItemRefSlotButton = preload("res://addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd") +const EditPropertiesButton = preload("res://addons/gloot/editor/item_editor/edit_properties_button.gd") +const EditPrototypeIdButton = preload("res://addons/gloot/editor/item_editor/edit_prototype_id_button.gd") + + +func _can_handle(object: Object) -> bool: + return (object is Inventory) || \ + (object is InventoryItem) || \ + (object is ItemSlot) || \ + (object is ItemRefSlot) || \ + (object is ItemProtoset) + + +func _parse_begin(object: Object) -> void: + if object is Inventory: + var inventory_inspector := InventoryInspector.instantiate() + inventory_inspector.init(object as Inventory) + add_custom_control(inventory_inspector) + if object is ItemSlot: + var item_slot_inspector := ItemSlotInspector.instantiate() + item_slot_inspector.init(object as ItemSlot) + add_custom_control(item_slot_inspector) + if object is ItemProtoset: + var edit_protoset_button := EditProtosetButton.instantiate() + edit_protoset_button.init(object as ItemProtoset) + add_custom_control(edit_protoset_button) + + +func _parse_property(object: Object, + type: Variant.Type, + name: String, + hint: PropertyHint, + hint_string: String, + usage: int, + wide: bool) -> bool: + if (object is InventoryItem) && name == "properties": + add_property_editor(name, EditPropertiesButton.new()) + return true + if (object is InventoryItem) && name == "prototype_id": + add_property_editor(name, EditPrototypeIdButton.new()) + return true + if (object is ItemRefSlot) && name == "_equipped_item": + add_property_editor(name, ItemRefSlotButton.new()) + return true + return false + diff --git a/addons/gloot/editor/item_editor/edit_properties_button.gd b/addons/gloot/editor/item_editor/edit_properties_button.gd new file mode 100644 index 0000000..e0d5342 --- /dev/null +++ b/addons/gloot/editor/item_editor/edit_properties_button.gd @@ -0,0 +1,61 @@ +extends EditorProperty + +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") +const PropertiesEditor = preload("res://addons/gloot/editor/item_editor/properties_editor.tscn") +const POPUP_SIZE = Vector2i(800, 300) + +var current_value: Dictionary +var updating: bool = false +var _btn_prototype_id: Button +var _properties_editor: Window + + +func _init(): + _properties_editor = PropertiesEditor.instantiate() + add_child(_properties_editor) + + _btn_prototype_id = Button.new() + _btn_prototype_id.text = "Edit Properties" + _btn_prototype_id.pressed.connect(_on_btn_edit) + _btn_prototype_id.icon = EditorIcons.get_icon("Edit") + add_child(_btn_prototype_id) + + +func _ready() -> void: + var item: InventoryItem = get_edited_object() + if !item: + return + _properties_editor.item = item + item.properties_changed.connect(update_property) + + if !item.protoset: + return + item.protoset.changed.connect(_on_protoset_changed) + + _refresh_button() + + +func _on_btn_edit() -> void: + _properties_editor.popup_centered(POPUP_SIZE) + + +func update_property() -> void: + var new_value = get_edited_object()[get_edited_property()] + if new_value == current_value: + return + + updating = true + current_value = new_value + updating = false + + +func _on_protoset_changed() -> void: + _refresh_button() + + +func _refresh_button() -> void: + var item: InventoryItem = get_edited_object() + if !item || !item.protoset: + return + _btn_prototype_id.disabled = !item.protoset.has_prototype(item.prototype_id) + diff --git a/addons/gloot/editor/item_editor/edit_prototype_id_button.gd b/addons/gloot/editor/item_editor/edit_prototype_id_button.gd new file mode 100644 index 0000000..1e3b6a3 --- /dev/null +++ b/addons/gloot/editor/item_editor/edit_prototype_id_button.gd @@ -0,0 +1,79 @@ +extends EditorProperty + + +const PrototypeIdEditor = preload("res://addons/gloot/editor/item_editor/prototype_id_editor.tscn") +const POPUP_SIZE = Vector2i(300, 300) +const COLOR_INVALID = Color.RED +var current_value: String +var updating: bool = false +var _prototype_id_editor: Window +var _btn_prototype_id: Button + + +func _init(): + _prototype_id_editor = PrototypeIdEditor.instantiate() + add_child(_prototype_id_editor) + + _btn_prototype_id = Button.new() + _btn_prototype_id.text = "Prototype ID" + _btn_prototype_id.pressed.connect(_on_btn_prototype_id) + add_child(_btn_prototype_id) + + +func _ready() -> void: + var item: InventoryItem = get_edited_object() + _prototype_id_editor.item = item + item.prototype_id_changed.connect(_refresh_button) + if item.protoset: + item.protoset.changed.connect(_refresh_button) + _refresh_button() + + +func _on_btn_prototype_id() -> void: + # TODO: Figure out how to show a popup at mouse position + # _window_dialog.popup(Rect2i(_get_popup_at_mouse_position(POPUP_SIZE), POPUP_SIZE)) + _prototype_id_editor.popup_centered(POPUP_SIZE) + + +func _get_popup_at_mouse_position(size: Vector2i) -> Vector2i: + var global_mouse_pos: Vector2i = Vector2i(get_global_mouse_position()) + var local_mouse_pos: Vector2i = global_mouse_pos + \ + DisplayServer.window_get_position(DisplayServer.MAIN_WINDOW_ID) + + # Prevent the popup from positioning partially out of screen + var screen_size: Vector2i = DisplayServer.screen_get_size(DisplayServer.SCREEN_OF_MAIN_WINDOW) + var popup_pos: Vector2i + popup_pos.x = clamp(local_mouse_pos.x, 0, screen_size.x - size.x) + popup_pos.y = clamp(local_mouse_pos.y, 0, screen_size.y - size.y) + + return popup_pos + + +func update_property() -> void: + var new_value = get_edited_object()[get_edited_property()] + if new_value == current_value: + return + + updating = true + current_value = new_value + _refresh_button() + updating = false + + +func _refresh_button() -> void: + var item: InventoryItem = get_edited_object() + _btn_prototype_id.text = item.prototype_id + _btn_prototype_id.disabled = false + if item.protoset == null: + _btn_prototype_id.disabled = true + return + + if !item.protoset.has_prototype(item.prototype_id): + _btn_prototype_id.add_theme_color_override("font_color", COLOR_INVALID) + _btn_prototype_id.add_theme_color_override("font_color_hover", COLOR_INVALID) + _btn_prototype_id.tooltip_text = "Invalid prototype ID!" + else: + _btn_prototype_id.remove_theme_color_override("font_color") + _btn_prototype_id.remove_theme_color_override("font_color_hover") + _btn_prototype_id.tooltip_text = "" + diff --git a/addons/gloot/editor/item_editor/properties_editor.gd b/addons/gloot/editor/item_editor/properties_editor.gd new file mode 100644 index 0000000..0ccee74 --- /dev/null +++ b/addons/gloot/editor/item_editor/properties_editor.gd @@ -0,0 +1,123 @@ +@tool +extends Window + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") +const DictEditor = preload("res://addons/gloot/editor/common/dict_editor.tscn") +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") +const COLOR_OVERRIDDEN = Color.GREEN +const COLOR_INVALID = Color.RED +var IMMUTABLE_KEYS: Array[String] = [ItemProtoset.KEY_ID, GridConstraint.KEY_GRID_POSITION] + +@onready var _margin_container: MarginContainer = $"MarginContainer" +@onready var _dict_editor: Control = $"MarginContainer/DictEditor" +var item: InventoryItem = null : + set(new_item): + if new_item == null: + return + assert(item == null, "Item already set!") + item = new_item + if item.protoset: + item.protoset.changed.connect(_refresh) + _refresh() + + +func _ready() -> void: + about_to_popup.connect(func(): _refresh()) + close_requested.connect(func(): hide()) + _dict_editor.value_changed.connect(func(key: String, new_value): _on_value_changed(key, new_value)) + _dict_editor.value_removed.connect(func(key: String): _on_value_removed(key)) + hide() + + +func _on_value_changed(key: String, new_value) -> void: + var new_properties = item.properties.duplicate() + new_properties[key] = new_value + + var item_prototype: Dictionary = item.protoset.get_prototype(item.prototype_id) + if item_prototype.has(key) && (item_prototype[key] == new_value): + new_properties.erase(key) + + if new_properties.hash() == item.properties.hash(): + return + + GlootUndoRedo.set_item_properties(item, new_properties) + _refresh() + + +func _on_value_removed(key: String) -> void: + var new_properties = item.properties.duplicate() + new_properties.erase(key) + + if new_properties.hash() == item.properties.hash(): + return + + GlootUndoRedo.set_item_properties(item, new_properties) + _refresh() + + +func _refresh() -> void: + if _dict_editor.btn_add: + _dict_editor.btn_add.icon = EditorIcons.get_icon("Add") + _dict_editor.dictionary = _get_dictionary() + _dict_editor.color_map = _get_color_map() + _dict_editor.remove_button_map = _get_remove_button_map() + _dict_editor.immutable_keys = IMMUTABLE_KEYS + _dict_editor.refresh() + + +func _get_dictionary() -> Dictionary: + if item == null: + return {} + + if !item.protoset: + return {} + + if !item.protoset.has_prototype(item.prototype_id): + return {} + + var result: Dictionary = item.protoset.get_prototype(item.prototype_id).duplicate() + for key in item.properties.keys(): + result[key] = item.properties[key] + return result + + +func _get_color_map() -> Dictionary: + if item == null: + return {} + + if !item.protoset: + return {} + + var result: Dictionary = {} + var dictionary: Dictionary = _get_dictionary() + for key in dictionary.keys(): + if item.properties.has(key): + result[key] = COLOR_OVERRIDDEN + if key == ItemProtoset.KEY_ID && !item.protoset.has_prototype(dictionary[key]): + result[key] = COLOR_INVALID + + return result + + +func _get_remove_button_map() -> Dictionary: + if item == null: + return {} + + if !item.protoset: + return {} + + var result: Dictionary = {} + var dictionary: Dictionary = _get_dictionary() + for key in dictionary.keys(): + result[key] = {} + if item.protoset.get_prototype(item.prototype_id).has(key): + result[key]["text"] = "" + result[key]["icon"] = EditorIcons.get_icon("Reload") + else: + result[key]["text"] = "" + result[key]["icon"] = EditorIcons.get_icon("Remove") + + result[key]["disabled"] = (not key in item.properties) or (key in IMMUTABLE_KEYS) + return result + diff --git a/addons/gloot/editor/item_editor/properties_editor.tscn b/addons/gloot/editor/item_editor/properties_editor.tscn new file mode 100644 index 0000000..95842d5 --- /dev/null +++ b/addons/gloot/editor/item_editor/properties_editor.tscn @@ -0,0 +1,29 @@ +[gd_scene load_steps=3 format=3 uid="uid://de2c4q3rk76nu"] + +[ext_resource type="Script" path="res://addons/gloot/editor/item_editor/properties_editor.gd" id="1_4ikx6"] +[ext_resource type="PackedScene" uid="uid://digtudobrw3xb" path="res://addons/gloot/editor/common/dict_editor.tscn" id="1_f5dhm"] + +[node name="PropertiesEditor" type="Window"] +title = "Edit Item Properties" +position = Vector2i(0, 36) +size = Vector2i(800, 300) +visible = false +exclusive = true +min_size = Vector2i(400, 200) +script = ExtResource("1_4ikx6") + +[node name="MarginContainer" type="MarginContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="DictEditor" parent="MarginContainer" instance=ExtResource("1_f5dhm")] +layout_mode = 2 diff --git a/addons/gloot/editor/item_editor/prototype_id_editor.gd b/addons/gloot/editor/item_editor/prototype_id_editor.gd new file mode 100644 index 0000000..ed7f6a8 --- /dev/null +++ b/addons/gloot/editor/item_editor/prototype_id_editor.gd @@ -0,0 +1,49 @@ +@tool +extends Window + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const ChoiceFilter = preload("res://addons/gloot/editor/common/choice_filter.tscn") +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") +const POPUP_MARGIN = 10 + +@onready var _margin_container: MarginContainer = $"MarginContainer" +@onready var _choice_filter: Control = $"MarginContainer/ChoiceFilter" +var item: InventoryItem = null : + set(new_item): + if new_item == null: + return + assert(item == null, "Item already set!") + item = new_item + if item.protoset: + item.protoset.changed.connect(_refresh) + _refresh() + + +func _ready() -> void: + _choice_filter.filter_icon = EditorIcons.get_icon("Search") + about_to_popup.connect(func(): _refresh()) + close_requested.connect(func(): hide()) + _choice_filter.choice_picked.connect(func(value_index: int): _on_choice_picked(value_index)) + hide() + + +func _on_choice_picked(value_index: int) -> void: + assert(item, "Item not set!") + var new_prototype_id = _choice_filter.values[value_index] + if new_prototype_id != item.prototype_id: + GlootUndoRedo.set_item_prototype_id(item, new_prototype_id) + hide() + + +func _refresh() -> void: + _choice_filter.values.clear() + _choice_filter.values.append_array(_get_prototype_ids()) + _choice_filter.refresh() + + +func _get_prototype_ids() -> Array: + if item == null || !item.protoset: + return [] + + return item.protoset._prototypes.keys() + diff --git a/addons/gloot/editor/item_editor/prototype_id_editor.tscn b/addons/gloot/editor/item_editor/prototype_id_editor.tscn new file mode 100644 index 0000000..c73b651 --- /dev/null +++ b/addons/gloot/editor/item_editor/prototype_id_editor.tscn @@ -0,0 +1,27 @@ +[gd_scene load_steps=3 format=3 uid="uid://bb341bh2pdb6u"] + +[ext_resource type="Script" path="res://addons/gloot/editor/item_editor/prototype_id_editor.gd" id="1_a8scy"] +[ext_resource type="PackedScene" uid="uid://dj577duf8yjeb" path="res://addons/gloot/editor/common/choice_filter.tscn" id="1_prwl8"] + +[node name="PrototypeIdEditor" type="Window"] +title = "Select Prototype ID" +size = Vector2i(300, 300) +visible = false +exclusive = true +script = ExtResource("1_a8scy") + +[node name="MarginContainer" type="MarginContainer" parent="."] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="ChoiceFilter" parent="MarginContainer" instance=ExtResource("1_prwl8")] +layout_mode = 2 +pick_text = "Select" +filter_text = "Filter Prototypes:" diff --git a/addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd b/addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd new file mode 100644 index 0000000..0e1abfe --- /dev/null +++ b/addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd @@ -0,0 +1,67 @@ +extends EditorProperty + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") + +var updating: bool = false +var _option_button: OptionButton + + +func _init(): + _option_button = OptionButton.new() + add_child(_option_button) + add_focusable(_option_button) + _option_button.item_selected.connect(_on_item_selected) + + +func _ready() -> void: + var item_ref_slot: ItemRefSlot = get_edited_object() + item_ref_slot.inventory_changed.connect(_refresh_option_button) + item_ref_slot.item_equipped.connect(_refresh_option_button) + item_ref_slot.cleared.connect(_refresh_option_button) + _refresh_option_button() + + +func _refresh_option_button() -> void: + _clear_option_button() + _populate_option_button() + + +func _clear_option_button() -> void: + _option_button.clear() + _option_button.add_item("None") + _option_button.set_item_metadata(0, null) + _option_button.select(0) + + +func _populate_option_button() -> void: + if !get_edited_object(): + return + + var item_ref_slot: ItemRefSlot = get_edited_object() + if !item_ref_slot.inventory: + return + + var equipped_item_index := 0 + for item in item_ref_slot.inventory.get_items(): + _option_button.add_icon_item(item.get_texture(), item.get_title()) + var option_item_index = _option_button.get_item_count() - 1 + _option_button.set_item_metadata(option_item_index, item) + if item == item_ref_slot.get_item(): + equipped_item_index = option_item_index + + _option_button.select(equipped_item_index) + + +func _on_item_selected(item_index: int) -> void: + if !get_edited_object() || updating: + return + + updating = true + var item_ref_slot: ItemRefSlot = get_edited_object() + var selected_item: InventoryItem = _option_button.get_item_metadata(item_index) + if item_ref_slot.get_item() != selected_item: + if selected_item == null: + GlootUndoRedo.clear_item_slot(item_ref_slot) + else: + GlootUndoRedo.equip_item_in_item_slot(item_ref_slot, selected_item) + updating = false diff --git a/addons/gloot/editor/item_slot_editor/item_slot_editor.gd b/addons/gloot/editor/item_slot_editor/item_slot_editor.gd new file mode 100644 index 0000000..a162103 --- /dev/null +++ b/addons/gloot/editor/item_slot_editor/item_slot_editor.gd @@ -0,0 +1,106 @@ +@tool +extends Control + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var hsplit_container = $HSplitContainer +@onready var prototype_id_filter = $HSplitContainer/ChoiceFilter +@onready var btn_edit = $HSplitContainer/VBoxContainer/HBoxContainer/BtnEdit +@onready var btn_clear = $HSplitContainer/VBoxContainer/HBoxContainer/BtnClear +@onready var ctrl_item_slot = $HSplitContainer/VBoxContainer/CtrlItemSlot + +var item_slot: ItemSlot : + set(new_item_slot): + disconnect_item_slot_signals() + item_slot = new_item_slot + ctrl_item_slot.item_slot = item_slot + connect_item_slot_signals() + + _refresh() + + +func connect_item_slot_signals(): + if !item_slot: + return + + item_slot.item_equipped.connect(_refresh) + item_slot.cleared.connect(_refresh) + + if !item_slot.item_protoset: + return + item_slot.item_protoset.changed.connect(_refresh) + item_slot.protoset_changed.connect(_refresh) + + +func disconnect_item_slot_signals(): + if !item_slot: + return + + item_slot.item_equipped.disconnect(_refresh) + item_slot.cleared.disconnect(_refresh) + + if !item_slot.item_protoset: + return + item_slot.item_protoset.changed.disconnect(_refresh) + item_slot.protoset_changed.disconnect(_refresh) + + +func init(item_slot_: ItemSlot) -> void: + item_slot = item_slot_ + + +func _refresh() -> void: + if !is_inside_tree() || item_slot == null || item_slot.item_protoset == null: + return + prototype_id_filter.set_values(item_slot.item_protoset._prototypes.keys()) + + +func _ready() -> void: + _apply_editor_settings() + + prototype_id_filter.pick_icon = EditorIcons.get_icon("Add") + prototype_id_filter.filter_icon = EditorIcons.get_icon("Search") + btn_edit.icon = EditorIcons.get_icon("Edit") + btn_clear.icon = EditorIcons.get_icon("Remove") + + prototype_id_filter.choice_picked.connect(_on_prototype_id_picked) + btn_edit.pressed.connect(_on_btn_edit) + btn_clear.pressed.connect(_on_btn_clear) + + ctrl_item_slot.item_slot = item_slot + _refresh() + + +func _apply_editor_settings() -> void: + var control_height: int = ProjectSettings.get_setting("gloot/inspector_control_height") + custom_minimum_size.y = control_height + + +func _on_prototype_id_picked(index: int) -> void: + var prototype_id = prototype_id_filter.values[index] + var item := InventoryItem.new() + if item_slot.get_item() != null: + item_slot.get_item().queue_free() + item.protoset = item_slot.item_protoset + item.prototype_id = prototype_id + GlootUndoRedo.equip_item_in_item_slot(item_slot, item) + + +func _on_btn_edit() -> void: + if item_slot.get_item() != null: + # Call it deferred, so that the control can clean up + call_deferred("_select_node", item_slot.get_item()) + + +func _on_btn_clear() -> void: + if item_slot.get_item() != null: + item_slot.get_item().queue_free() + GlootUndoRedo.clear_item_slot(item_slot) + + +static func _select_node(node: Node) -> void: + EditorInterface.get_selection().clear() + EditorInterface.get_selection().add_node(node) + EditorInterface.edit_node(node) + diff --git a/addons/gloot/editor/item_slot_editor/item_slot_editor.tscn b/addons/gloot/editor/item_slot_editor/item_slot_editor.tscn new file mode 100644 index 0000000..67a00ff --- /dev/null +++ b/addons/gloot/editor/item_slot_editor/item_slot_editor.tscn @@ -0,0 +1,103 @@ +[gd_scene load_steps=12 format=3 uid="uid://bgs0xwufm4k6k"] + +[ext_resource type="Script" path="res://addons/gloot/editor/item_slot_editor/item_slot_editor.gd" id="1_d7a2m"] +[ext_resource type="PackedScene" uid="uid://dj577duf8yjeb" path="res://addons/gloot/editor/common/choice_filter.tscn" id="2_lcnj8"] +[ext_resource type="Script" path="res://addons/gloot/ui/ctrl_item_slot.gd" id="3_421wi"] + +[sub_resource type="Image" id="Image_ktvjb"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_t45ni"] +image = SubResource("Image_ktvjb") + +[sub_resource type="Image" id="Image_rhg3a"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 184, 224, 224, 224, 240, 224, 224, 224, 232, 224, 224, 224, 186, 227, 227, 227, 62, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 254, 224, 224, 224, 254, 224, 224, 224, 123, 224, 224, 224, 32, 224, 224, 224, 33, 225, 225, 225, 125, 224, 224, 224, 254, 224, 224, 224, 254, 226, 226, 226, 69, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 125, 224, 224, 224, 255, 225, 225, 225, 174, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 240, 224, 224, 224, 255, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 35, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 232, 224, 224, 224, 255, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 228, 228, 228, 37, 224, 224, 224, 255, 224, 224, 224, 228, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 186, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 130, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 62, 224, 224, 224, 255, 224, 224, 224, 254, 225, 225, 225, 126, 225, 225, 225, 34, 227, 227, 227, 36, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 122, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 69, 225, 225, 225, 174, 224, 224, 224, 233, 224, 224, 224, 228, 224, 224, 224, 173, 226, 226, 226, 77, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 227, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_3hnnf"] +image = SubResource("Image_rhg3a") + +[sub_resource type="Image" id="Image_8ww1c"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 182, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 170, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 234, 224, 224, 224, 234, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 85, 225, 225, 225, 85, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_yqex5"] +image = SubResource("Image_8ww1c") + +[sub_resource type="Image" id="Image_240jw"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ip6wh"] +image = SubResource("Image_240jw") + +[node name="ItemSlotEditor" type="Control"] +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_d7a2m") + +[node name="HSplitContainer" type="HSplitContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="ChoiceFilter" parent="HSplitContainer" instance=ExtResource("2_lcnj8")] +layout_mode = 2 +pick_text = "Equip" +pick_icon = SubResource("ImageTexture_t45ni") +filter_text = "Filter Prototypes:" +filter_icon = SubResource("ImageTexture_3hnnf") + +[node name="VBoxContainer" type="VBoxContainer" parent="HSplitContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="CtrlItemSlot" type="Control" parent="HSplitContainer/VBoxContainer"] +custom_minimum_size = Vector2(32, 32) +layout_mode = 2 +size_flags_vertical = 3 +script = ExtResource("3_421wi") + +[node name="HBoxContainer" type="HBoxContainer" parent="HSplitContainer/VBoxContainer"] +layout_mode = 2 + +[node name="BtnEdit" type="Button" parent="HSplitContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Edit" +icon = SubResource("ImageTexture_yqex5") + +[node name="BtnClear" type="Button" parent="HSplitContainer/VBoxContainer/HBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +text = "Clear" +icon = SubResource("ImageTexture_ip6wh") diff --git a/addons/gloot/editor/item_slot_editor/item_slot_inspector.gd b/addons/gloot/editor/item_slot_editor/item_slot_inspector.gd new file mode 100644 index 0000000..ed0d07b --- /dev/null +++ b/addons/gloot/editor/item_slot_editor/item_slot_inspector.gd @@ -0,0 +1,38 @@ +@tool +extends Control + +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var item_slot_editor: Control = $HBoxContainer/ItemSlotEditor +@onready var btn_expand: Button = $HBoxContainer/BtnExpand +@onready var _window_dialog: Window = $Window +@onready var _item_slot_editor: Control = $Window/MarginContainer/ItemSlotEditor + +var item_slot: ItemSlot : + set(new_item_slot): + item_slot = new_item_slot + if item_slot_editor: + item_slot_editor.item_slot = item_slot + + +func init(item_slot_: ItemSlot) -> void: + item_slot = item_slot_ + + +func _ready() -> void: + if item_slot_editor: + item_slot_editor.item_slot = item_slot + _apply_editor_settings() + btn_expand.icon = EditorIcons.get_icon("DistractionFree") + btn_expand.pressed.connect(on_btn_expand) + _window_dialog.close_requested.connect(func(): _window_dialog.hide()) + + +func on_btn_expand() -> void: + _item_slot_editor.item_slot = item_slot + _window_dialog.popup_centered() + + +func _apply_editor_settings() -> void: + var control_height: int = ProjectSettings.get_setting("gloot/inspector_control_height") + custom_minimum_size.y = control_height diff --git a/addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn b/addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn new file mode 100644 index 0000000..d5fcc8f --- /dev/null +++ b/addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn @@ -0,0 +1,66 @@ +[gd_scene load_steps=5 format=3 uid="uid://b8bv63d2djwv3"] + +[ext_resource type="Script" path="res://addons/gloot/editor/item_slot_editor/item_slot_inspector.gd" id="1_4gsgr"] +[ext_resource type="PackedScene" uid="uid://bgs0xwufm4k6k" path="res://addons/gloot/editor/item_slot_editor/item_slot_editor.tscn" id="2_ysqy6"] + +[sub_resource type="Image" id="Image_ump51"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 4, 255, 255, 255, 4, 255, 255, 255, 4, 255, 255, 255, 4, 255, 255, 255, 3, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 127, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 135, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 140, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 213, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 136, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 213, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 210, 224, 224, 224, 138, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 126, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 213, 224, 224, 224, 255, 226, 226, 226, 103, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 107, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 226, 226, 226, 103, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 105, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 107, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 109, 232, 232, 232, 22, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 127, 255, 255, 255, 0, 224, 224, 224, 24, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 105, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 109, 224, 224, 224, 255, 224, 224, 224, 213, 232, 232, 232, 22, 255, 255, 255, 0, 224, 224, 224, 129, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 140, 224, 224, 224, 216, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 213, 225, 225, 225, 142, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 230, 230, 230, 20, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 232, 232, 232, 22, 224, 224, 224, 213, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 138, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 142, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_eojh7"] +image = SubResource("Image_ump51") + +[node name="ItemSlotInspector" type="Control"] +custom_minimum_size = Vector2(0, 200) +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("1_4gsgr") + +[node name="HBoxContainer" type="HBoxContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="ItemSlotEditor" parent="HBoxContainer" instance=ExtResource("2_ysqy6")] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="BtnExpand" type="Button" parent="HBoxContainer"] +layout_mode = 2 +icon = SubResource("ImageTexture_eojh7") + +[node name="Window" type="Window" parent="."] +title = "Edit Item Slot" +size = Vector2i(800, 600) +visible = false +exclusive = true +min_size = Vector2i(400, 300) + +[node name="MarginContainer" type="MarginContainer" parent="Window"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_constants/margin_left = 10 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_right = 10 +theme_override_constants/margin_bottom = 10 + +[node name="ItemSlotEditor" parent="Window/MarginContainer" instance=ExtResource("2_ysqy6")] +layout_mode = 2 diff --git a/addons/gloot/editor/protoset_editor/edit_protoset_button.gd b/addons/gloot/editor/protoset_editor/edit_protoset_button.gd new file mode 100644 index 0000000..408ff63 --- /dev/null +++ b/addons/gloot/editor/protoset_editor/edit_protoset_button.gd @@ -0,0 +1,25 @@ +@tool +extends Button + +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var window_dialog: Window = $"%Window" +@onready var protoset_editor: Control = $"%ProtosetEditor" + +var protoset: ItemProtoset : + set(new_protoset): + protoset = new_protoset + if protoset_editor: + protoset_editor.protoset = protoset + + +func init(protoset_: ItemProtoset) -> void: + protoset = protoset_ + + +func _ready() -> void: + icon = EditorIcons.get_icon("Edit") + window_dialog.close_requested.connect(func(): protoset.notify_property_list_changed()) + protoset_editor.protoset = protoset + pressed.connect(func(): window_dialog.popup_centered(window_dialog.size)) + diff --git a/addons/gloot/editor/protoset_editor/edit_protoset_button.tscn b/addons/gloot/editor/protoset_editor/edit_protoset_button.tscn new file mode 100644 index 0000000..57d03d3 --- /dev/null +++ b/addons/gloot/editor/protoset_editor/edit_protoset_button.tscn @@ -0,0 +1,37 @@ +[gd_scene load_steps=5 format=3 uid="uid://bjme7iuv3j6yb"] + +[ext_resource type="PackedScene" uid="uid://cyj0avrwjowl" path="res://addons/gloot/editor/protoset_editor/protoset_editor.tscn" id="1"] +[ext_resource type="Script" path="res://addons/gloot/editor/protoset_editor/edit_protoset_button.gd" id="2"] + +[sub_resource type="Image" id="Image_tnk37"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 182, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 170, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 234, 224, 224, 224, 234, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 85, 225, 225, 225, 85, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_18gso"] +image = SubResource("Image_tnk37") + +[node name="EditProtosetButton" type="Button"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "Edit Protoset" +icon = SubResource("ImageTexture_18gso") +script = ExtResource("2") + +[node name="Window" type="Window" parent="."] +unique_name_in_owner = true +title = "Edit Protoset" +size = Vector2i(1000, 600) +visible = false +exclusive = true +min_size = Vector2i(800, 200) + +[node name="ProtosetEditor" parent="Window" instance=ExtResource("1")] +unique_name_in_owner = true diff --git a/addons/gloot/editor/protoset_editor/protoset_editor.gd b/addons/gloot/editor/protoset_editor/protoset_editor.gd new file mode 100644 index 0000000..f880efc --- /dev/null +++ b/addons/gloot/editor/protoset_editor/protoset_editor.gd @@ -0,0 +1,174 @@ +@tool +extends Control + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const EditorIcons = preload("res://addons/gloot/editor/common/editor_icons.gd") + +@onready var prototype_filter = $"%PrototypeFilter" +@onready var property_editor = $"%PropertyEditor" +@onready var txt_prototype_id = $"%TxtPrototypeName" +@onready var btn_add_prototype = $"%BtnAddPrototype" +@onready var btn_duplicate_prototype = $"%BtnDuplicatePrototype" +@onready var btn_remove_prototype = $"%BtnRemovePrototype" +@onready var btn_rename_prototype = $"%BtnRenamePrototype" + +var protoset: ItemProtoset : + set(new_protoset): + protoset = new_protoset + if protoset: + protoset.changed.connect(_on_protoset_changed) + _refresh() +var selected_prototype_id: String = "" + + +func _ready() -> void: + prototype_filter.choice_selected.connect(_on_prototype_selected) + property_editor.value_changed.connect(_on_property_changed) + property_editor.value_removed.connect(_on_property_removed) + txt_prototype_id.text_changed.connect(_on_prototype_id_changed) + txt_prototype_id.text_submitted.connect(_on_prototype_id_entered) + btn_add_prototype.pressed.connect(_on_btn_add_prototype) + btn_duplicate_prototype.pressed.connect(_on_btn_duplicate_prototype) + btn_rename_prototype.pressed.connect(_on_btn_rename_prototype) + btn_remove_prototype.pressed.connect(_on_btn_remove_prototype) + + btn_add_prototype.icon = EditorIcons.get_icon("Add") + btn_duplicate_prototype.icon = EditorIcons.get_icon("Duplicate") + btn_rename_prototype.icon = EditorIcons.get_icon("Edit") + btn_remove_prototype.icon = EditorIcons.get_icon("Remove") + prototype_filter.filter_icon = EditorIcons.get_icon("Search") + _refresh() + + +func _refresh() -> void: + if !visible: + return + + _clear() + _populate() + _refresh_btn_add_prototype() + _refresh_btn_rename_prototype() + _refresh_btn_remove_prototype() + _refresh_btn_duplicate_prototype() + _inspect_prototype_id(selected_prototype_id) + + +func _clear() -> void: + prototype_filter.values.clear() + property_editor.dictionary.clear() + property_editor.refresh() + + +func _populate() -> void: + if protoset: + # TODO: Avoid accessing "private" members (_prototypes) + prototype_filter.set_values(protoset._prototypes.keys().duplicate()) + + +func _refresh_btn_add_prototype() -> void: + btn_add_prototype.disabled = txt_prototype_id.text.is_empty() ||\ + protoset.has_prototype(txt_prototype_id.text) + + +func _refresh_btn_rename_prototype() -> void: + btn_rename_prototype.disabled = txt_prototype_id.text.is_empty() ||\ + protoset.has_prototype(txt_prototype_id.text) + + +func _refresh_btn_remove_prototype() -> void: + btn_remove_prototype.disabled = prototype_filter.get_selected_text().is_empty() + + +func _refresh_btn_duplicate_prototype() -> void: + btn_duplicate_prototype.disabled = prototype_filter.get_selected_text().is_empty() + + +func _on_protoset_changed() -> void: + _refresh() + + +func _on_prototype_selected(index: int) -> void: + selected_prototype_id = prototype_filter.values[index] + _inspect_prototype_id(selected_prototype_id) + _refresh_btn_remove_prototype() + _refresh_btn_duplicate_prototype() + + +func _inspect_prototype_id(prototype_id: String) -> void: + if !protoset || !protoset.has_prototype(prototype_id): + return + + var prototype: Dictionary = protoset.get_prototype(prototype_id).duplicate() + + property_editor.dictionary = prototype + property_editor.immutable_keys = [ItemProtoset.KEY_ID] as Array[String] + property_editor.remove_button_map = {} + + for property_name in prototype.keys(): + property_editor.set_remove_button_config(property_name, { + "text": "", + "disabled": property_name == ItemProtoset.KEY_ID, + "icon": EditorIcons.get_icon("Remove"), + }) + + +func _on_property_changed(property_name: String, new_value) -> void: + if selected_prototype_id.is_empty(): + return + var new_properties = protoset.get_prototype(selected_prototype_id).duplicate() + new_properties[property_name] = new_value + + if new_properties.hash() == protoset.get_prototype(selected_prototype_id).hash(): + return + + GlootUndoRedo.set_prototype_properties(protoset, selected_prototype_id, new_properties) + + +func _on_property_removed(property_name: String) -> void: + if selected_prototype_id.is_empty(): + return + var new_properties = protoset.get_prototype(selected_prototype_id).duplicate() + new_properties.erase(property_name) + + GlootUndoRedo.set_prototype_properties(protoset, selected_prototype_id, new_properties) + + +func _on_prototype_id_changed(_prototype_id: String) -> void: + _refresh_btn_add_prototype() + _refresh_btn_rename_prototype() + + +func _on_prototype_id_entered(prototype_id: String) -> void: + _add_prototype_id(prototype_id) + + +func _on_btn_add_prototype() -> void: + _add_prototype_id(txt_prototype_id.text) + + +func _on_btn_duplicate_prototype() -> void: + GlootUndoRedo.duplicate_prototype(protoset, selected_prototype_id) + + +func _on_btn_rename_prototype() -> void: + if selected_prototype_id.is_empty(): + return + + GlootUndoRedo.rename_prototype(protoset, + selected_prototype_id, + txt_prototype_id.text) + txt_prototype_id.text = "" + + +func _add_prototype_id(prototype_id: String) -> void: + GlootUndoRedo.add_prototype(protoset, prototype_id) + txt_prototype_id.text = "" + + +func _on_btn_remove_prototype() -> void: + if selected_prototype_id.is_empty(): + return + + var prototype_id = selected_prototype_id + if !prototype_id.is_empty(): + GlootUndoRedo.remove_prototype(protoset, prototype_id) diff --git a/addons/gloot/editor/protoset_editor/protoset_editor.tscn b/addons/gloot/editor/protoset_editor/protoset_editor.tscn new file mode 100644 index 0000000..77aca4e --- /dev/null +++ b/addons/gloot/editor/protoset_editor/protoset_editor.tscn @@ -0,0 +1,155 @@ +[gd_scene load_steps=14 format=3 uid="uid://cyj0avrwjowl"] + +[ext_resource type="PackedScene" uid="uid://dj577duf8yjeb" path="res://addons/gloot/editor/common/choice_filter.tscn" id="1"] +[ext_resource type="PackedScene" uid="uid://digtudobrw3xb" path="res://addons/gloot/editor/common/dict_editor.tscn" id="2"] +[ext_resource type="Script" path="res://addons/gloot/editor/protoset_editor/protoset_editor.gd" id="3"] + +[sub_resource type="Image" id="Image_3d3lf"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 184, 224, 224, 224, 240, 224, 224, 224, 232, 224, 224, 224, 186, 227, 227, 227, 62, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 129, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 122, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 68, 224, 224, 224, 254, 224, 224, 224, 254, 224, 224, 224, 123, 224, 224, 224, 32, 224, 224, 224, 33, 225, 225, 225, 125, 224, 224, 224, 254, 224, 224, 224, 254, 226, 226, 226, 69, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 184, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 125, 224, 224, 224, 255, 225, 225, 225, 174, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 240, 224, 224, 224, 255, 231, 231, 231, 31, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 35, 224, 224, 224, 255, 224, 224, 224, 233, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 232, 224, 224, 224, 255, 224, 224, 224, 32, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 228, 228, 228, 37, 224, 224, 224, 255, 224, 224, 224, 228, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 186, 224, 224, 224, 255, 224, 224, 224, 123, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 130, 224, 224, 224, 255, 224, 224, 224, 173, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 227, 227, 227, 62, 224, 224, 224, 255, 224, 224, 224, 254, 225, 225, 225, 126, 225, 225, 225, 34, 227, 227, 227, 36, 224, 224, 224, 131, 224, 224, 224, 255, 224, 224, 224, 255, 226, 226, 226, 77, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 122, 224, 224, 224, 254, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 226, 226, 226, 69, 225, 225, 225, 174, 224, 224, 224, 233, 224, 224, 224, 228, 224, 224, 224, 173, 226, 226, 226, 77, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 255, 224, 224, 224, 210, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 210, 224, 224, 224, 227, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 225, 225, 225, 34, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_ooqbn"] +image = SubResource("Image_3d3lf") + +[sub_resource type="Image" id="Image_7dqyh"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_dehry"] +image = SubResource("Image_7dqyh") + +[sub_resource type="Image" id="Image_nsl4i"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 225, 225, 225, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_cfl22"] +image = SubResource("Image_nsl4i") + +[sub_resource type="Image" id="Image_o82dw"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 182, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 180, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 171, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 170, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 231, 231, 231, 21, 224, 224, 224, 234, 224, 224, 224, 234, 231, 231, 231, 21, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 225, 225, 225, 85, 225, 225, 225, 85, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_qoier"] +image = SubResource("Image_o82dw") + +[sub_resource type="Image" id="Image_ey4cw"] +data = { +"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 227, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 224, 224, 224, 73, 224, 224, 224, 226, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 255, 224, 224, 224, 225, 226, 226, 226, 70, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0), +"format": "RGBA8", +"height": 16, +"mipmaps": false, +"width": 16 +} + +[sub_resource type="ImageTexture" id="ImageTexture_i8glk"] +image = SubResource("Image_ey4cw") + +[node name="ProtosetEditor" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +script = ExtResource("3") + +[node name="Gui" type="HSplitContainer" parent="."] +layout_mode = 0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_left = 5.0 +offset_top = 5.0 +offset_right = -5.0 +offset_bottom = -5.0 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="Gui"] +layout_mode = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 + +[node name="Label" type="Label" parent="Gui/VBoxContainer"] +layout_mode = 2 +text = "Prototypes" + +[node name="PrototypeFilter" parent="Gui/VBoxContainer" instance=ExtResource("1")] +unique_name_in_owner = true +layout_mode = 2 +pick_button_visible = false +filter_text = "Prototype Filter:" +filter_icon = SubResource("ImageTexture_ooqbn") + +[node name="HBoxContainer2" type="HBoxContainer" parent="Gui/VBoxContainer"] +layout_mode = 2 + +[node name="TxtPrototypeName" type="LineEdit" parent="Gui/VBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Prototype ID" + +[node name="BtnAddPrototype" type="Button" parent="Gui/VBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Add a new prototype with the entered ID" +disabled = true +text = "Add" +icon = SubResource("ImageTexture_dehry") + +[node name="BtnDuplicatePrototype" type="Button" parent="Gui/VBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Duplicate the selected prototype" +disabled = true +text = "Duplicate" +icon = SubResource("ImageTexture_cfl22") + +[node name="BtnRenamePrototype" type="Button" parent="Gui/VBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Rename the selected prototype" +disabled = true +text = "Rename" +icon = SubResource("ImageTexture_qoier") + +[node name="BtnRemovePrototype" type="Button" parent="Gui/VBoxContainer/HBoxContainer2"] +unique_name_in_owner = true +layout_mode = 2 +tooltip_text = "Remove the selected prototype" +disabled = true +text = "Remove" +icon = SubResource("ImageTexture_i8glk") + +[node name="VBoxContainer2" type="VBoxContainer" parent="Gui"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Label" type="Label" parent="Gui/VBoxContainer2"] +layout_mode = 2 +text = "Properties" + +[node name="PropertyEditor" parent="Gui/VBoxContainer2" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +dictionary = {} diff --git a/addons/gloot/gloot.gd b/addons/gloot/gloot.gd new file mode 100644 index 0000000..dde288b --- /dev/null +++ b/addons/gloot/gloot.gd @@ -0,0 +1,44 @@ +@tool +extends EditorPlugin + +var inspector_plugin: EditorInspectorPlugin +static var _instance: EditorPlugin + + +func _init() -> void: + _instance = self + + +static func instance() -> EditorPlugin: + return _instance + + +func _enter_tree() -> void: + inspector_plugin = preload("res://addons/gloot/editor/inventory_inspector_plugin.gd").new() + add_inspector_plugin(inspector_plugin) + + _add_settings() + + +func _exit_tree() -> void: + remove_inspector_plugin(inspector_plugin) + +func _add_settings() -> void: + _add_setting("gloot/inspector_control_height", TYPE_INT, 200) + _add_setting("gloot/item_dragging_deadzone_radius", TYPE_FLOAT, 8.0) + _add_setting("gloot/JSON_serialization/indent_using_spaces", TYPE_BOOL, true) + _add_setting("gloot/JSON_serialization/indent_size", TYPE_INT, 4) + _add_setting("gloot/JSON_serialization/sort_keys", TYPE_BOOL, true) + _add_setting("gloot/JSON_serialization/full_precision", TYPE_BOOL, false) + + +func _add_setting(name: String, type: int, value) -> void: + if !ProjectSettings.has_setting(name): + ProjectSettings.set(name, value) + + var property_info = { + "name": name, + "type": type + } + ProjectSettings.add_property_info(property_info) + ProjectSettings.set_initial_value(name, value) diff --git a/addons/gloot/images/icon_ctrl_inventory.svg b/addons/gloot/images/icon_ctrl_inventory.svg new file mode 100644 index 0000000..bc3c078 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory.svg @@ -0,0 +1,81 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_ctrl_inventory.svg.import b/addons/gloot/images/icon_ctrl_inventory.svg.import new file mode 100644 index 0000000..e0564b6 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3qajhvbxfn0u" +path="res://.godot/imported/icon_ctrl_inventory.svg-15b4e53d474d4ffa0c2404498dced829.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_ctrl_inventory.svg" +dest_files=["res://.godot/imported/icon_ctrl_inventory.svg-15b4e53d474d4ffa0c2404498dced829.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_ctrl_inventory_grid.svg b/addons/gloot/images/icon_ctrl_inventory_grid.svg new file mode 100644 index 0000000..c21532b --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory_grid.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gloot/images/icon_ctrl_inventory_grid.svg.import b/addons/gloot/images/icon_ctrl_inventory_grid.svg.import new file mode 100644 index 0000000..707a9e0 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory_grid.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ceycgo83466hi" +path="res://.godot/imported/icon_ctrl_inventory_grid.svg-291cad207b210b53ba149c754ed5c788.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_ctrl_inventory_grid.svg" +dest_files=["res://.godot/imported/icon_ctrl_inventory_grid.svg-291cad207b210b53ba149c754ed5c788.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_ctrl_inventory_stacked.svg b/addons/gloot/images/icon_ctrl_inventory_stacked.svg new file mode 100644 index 0000000..4f28b24 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory_stacked.svg @@ -0,0 +1,102 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_ctrl_inventory_stacked.svg.import b/addons/gloot/images/icon_ctrl_inventory_stacked.svg.import new file mode 100644 index 0000000..e222e86 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_inventory_stacked.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://t7dislu7ps55" +path="res://.godot/imported/icon_ctrl_inventory_stacked.svg-dae2677f1073d9d3b7050efd77843e49.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_ctrl_inventory_stacked.svg" +dest_files=["res://.godot/imported/icon_ctrl_inventory_stacked.svg-dae2677f1073d9d3b7050efd77843e49.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_ctrl_item_slot.svg b/addons/gloot/images/icon_ctrl_item_slot.svg new file mode 100644 index 0000000..423d9ec --- /dev/null +++ b/addons/gloot/images/icon_ctrl_item_slot.svg @@ -0,0 +1,64 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gloot/images/icon_ctrl_item_slot.svg.import b/addons/gloot/images/icon_ctrl_item_slot.svg.import new file mode 100644 index 0000000..f6befd8 --- /dev/null +++ b/addons/gloot/images/icon_ctrl_item_slot.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://duv44v5w55i01" +path="res://.godot/imported/icon_ctrl_item_slot.svg-c3cfacb616f4870b137dbf35ff5497fe.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_ctrl_item_slot.svg" +dest_files=["res://.godot/imported/icon_ctrl_item_slot.svg-c3cfacb616f4870b137dbf35ff5497fe.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_inventory.svg b/addons/gloot/images/icon_inventory.svg new file mode 100644 index 0000000..1f41da8 --- /dev/null +++ b/addons/gloot/images/icon_inventory.svg @@ -0,0 +1,81 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_inventory.svg.import b/addons/gloot/images/icon_inventory.svg.import new file mode 100644 index 0000000..dadd703 --- /dev/null +++ b/addons/gloot/images/icon_inventory.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://df2n5aculnj82" +path="res://.godot/imported/icon_inventory.svg-50bec89eff0070d5b4c81ef749399472.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_inventory.svg" +dest_files=["res://.godot/imported/icon_inventory.svg-50bec89eff0070d5b4c81ef749399472.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_inventory_grid.svg b/addons/gloot/images/icon_inventory_grid.svg new file mode 100644 index 0000000..38e05f7 --- /dev/null +++ b/addons/gloot/images/icon_inventory_grid.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gloot/images/icon_inventory_grid.svg.import b/addons/gloot/images/icon_inventory_grid.svg.import new file mode 100644 index 0000000..0508e6f --- /dev/null +++ b/addons/gloot/images/icon_inventory_grid.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://kd75uhabf1gk" +path="res://.godot/imported/icon_inventory_grid.svg-aa0d41ee8a4783e96656b0c95fc25600.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_inventory_grid.svg" +dest_files=["res://.godot/imported/icon_inventory_grid.svg-aa0d41ee8a4783e96656b0c95fc25600.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_inventory_grid_stacked.svg b/addons/gloot/images/icon_inventory_grid_stacked.svg new file mode 100644 index 0000000..75e6b60 --- /dev/null +++ b/addons/gloot/images/icon_inventory_grid_stacked.svg @@ -0,0 +1,193 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_inventory_grid_stacked.svg.import b/addons/gloot/images/icon_inventory_grid_stacked.svg.import new file mode 100644 index 0000000..3c371fe --- /dev/null +++ b/addons/gloot/images/icon_inventory_grid_stacked.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bo38cy4myqvpu" +path="res://.godot/imported/icon_inventory_grid_stacked.svg-33302053cec4b39ebb5b7a7b1bea5293.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_inventory_grid_stacked.svg" +dest_files=["res://.godot/imported/icon_inventory_grid_stacked.svg-33302053cec4b39ebb5b7a7b1bea5293.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_inventory_stacked.svg b/addons/gloot/images/icon_inventory_stacked.svg new file mode 100644 index 0000000..c5f3471 --- /dev/null +++ b/addons/gloot/images/icon_inventory_stacked.svg @@ -0,0 +1,102 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_inventory_stacked.svg.import b/addons/gloot/images/icon_inventory_stacked.svg.import new file mode 100644 index 0000000..67a4717 --- /dev/null +++ b/addons/gloot/images/icon_inventory_stacked.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ch1f50xu37dvo" +path="res://.godot/imported/icon_inventory_stacked.svg-e621060af73c305939da691316b0c966.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_inventory_stacked.svg" +dest_files=["res://.godot/imported/icon_inventory_stacked.svg-e621060af73c305939da691316b0c966.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_item.svg b/addons/gloot/images/icon_item.svg new file mode 100644 index 0000000..dc265c1 --- /dev/null +++ b/addons/gloot/images/icon_item.svg @@ -0,0 +1,79 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_item.svg.import b/addons/gloot/images/icon_item.svg.import new file mode 100644 index 0000000..87f2f07 --- /dev/null +++ b/addons/gloot/images/icon_item.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://qlt0i7muj2tj" +path="res://.godot/imported/icon_item.svg-19f0c54092b35b57bc3510f8accf9092.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_item.svg" +dest_files=["res://.godot/imported/icon_item.svg-19f0c54092b35b57bc3510f8accf9092.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_item_protoset.svg b/addons/gloot/images/icon_item_protoset.svg new file mode 100644 index 0000000..159b4ac --- /dev/null +++ b/addons/gloot/images/icon_item_protoset.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/addons/gloot/images/icon_item_protoset.svg.import b/addons/gloot/images/icon_item_protoset.svg.import new file mode 100644 index 0000000..816fddd --- /dev/null +++ b/addons/gloot/images/icon_item_protoset.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dbepw1v0gp80y" +path="res://.godot/imported/icon_item_protoset.svg-3584e3e1df3de7b231a248b80bd83194.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_item_protoset.svg" +dest_files=["res://.godot/imported/icon_item_protoset.svg-3584e3e1df3de7b231a248b80bd83194.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_item_ref_slot.svg b/addons/gloot/images/icon_item_ref_slot.svg new file mode 100644 index 0000000..c5da49e --- /dev/null +++ b/addons/gloot/images/icon_item_ref_slot.svg @@ -0,0 +1,77 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gloot/images/icon_item_ref_slot.svg.import b/addons/gloot/images/icon_item_ref_slot.svg.import new file mode 100644 index 0000000..ceb9a0d --- /dev/null +++ b/addons/gloot/images/icon_item_ref_slot.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://dagqmcoxvm3m6" +path="res://.godot/imported/icon_item_ref_slot.svg-c8a8d9826d793965417f19dda840f923.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_item_ref_slot.svg" +dest_files=["res://.godot/imported/icon_item_ref_slot.svg-c8a8d9826d793965417f19dda840f923.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/images/icon_item_slot.svg b/addons/gloot/images/icon_item_slot.svg new file mode 100644 index 0000000..d516908 --- /dev/null +++ b/addons/gloot/images/icon_item_slot.svg @@ -0,0 +1,64 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/addons/gloot/images/icon_item_slot.svg.import b/addons/gloot/images/icon_item_slot.svg.import new file mode 100644 index 0000000..4027698 --- /dev/null +++ b/addons/gloot/images/icon_item_slot.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://davfwu4aexp7f" +path="res://.godot/imported/icon_item_slot.svg-7205eab4ab2da79bb58a80cb68b321bf.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://addons/gloot/images/icon_item_slot.svg" +dest_files=["res://.godot/imported/icon_item_slot.svg-7205eab4ab2da79bb58a80cb68b321bf.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +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/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 diff --git a/addons/gloot/plugin.cfg b/addons/gloot/plugin.cfg new file mode 100644 index 0000000..0ba1162 --- /dev/null +++ b/addons/gloot/plugin.cfg @@ -0,0 +1,7 @@ +[plugin] + +name="GLoot" +description="A universal inventory system for the Godot game engine" +author="Peter Kish" +version="2.4.8" +script="gloot.gd" diff --git a/addons/gloot/ui/ctrl_dragable.gd b/addons/gloot/ui/ctrl_dragable.gd new file mode 100644 index 0000000..a6acee1 --- /dev/null +++ b/addons/gloot/ui/ctrl_dragable.gd @@ -0,0 +1,81 @@ +@tool +extends Control + +const CtrlDragable = preload("res://addons/gloot/ui/ctrl_dragable.gd") +const CtrlDropZone = preload("res://addons/gloot/ui/ctrl_drop_zone.gd") + +# Somewhat hacky way to do static signals: +# https://stackoverflow.com/questions/77026156/how-to-write-a-static-event-emitter-in-gdscript/77026952#77026952 + +static var dragable_grabbed: Signal = (func(): + if (CtrlDragable as Object).has_user_signal("dragable_grabbed"): + return (CtrlDragable as Object).dragable_grabbed + (CtrlDragable as Object).add_user_signal("dragable_grabbed") + return Signal(CtrlDragable, "dragable_grabbed") +).call() + +static var dragable_dropped: Signal = (func(): + if (CtrlDragable as Object).has_user_signal("dragable_dropped"): + return (CtrlDragable as Object).dragable_dropped + (CtrlDragable as Object).add_user_signal("dragable_dropped") + return Signal(CtrlDragable, "dragable_dropped") +).call() + +signal grabbed(position) +signal dropped(zone, position) + +static var _grabbed_dragable: CtrlDragable = null +static var _grab_offset: Vector2 + +var _enabled: bool = true + + +static func get_grabbed_dragable() -> CtrlDragable: + if !is_instance_valid(_grabbed_dragable): + return null + return _grabbed_dragable + + +static func get_grab_offset() -> Vector2: + return _grab_offset + + +static func get_grab_offset_local_to(control: Control) -> Vector2: + return CtrlDragable.get_grab_offset() / control.get_global_transform().get_scale() + + +func _get_drag_data(at_position: Vector2): + if !_enabled: + return null + + _grabbed_dragable = self + _grab_offset = at_position * get_global_transform().get_scale() + dragable_grabbed.emit(_grabbed_dragable, _grab_offset) + grabbed.emit(_grab_offset) + + var preview = Control.new() + var sub_preview = create_preview() + sub_preview.position = -get_grab_offset() + preview.add_child(sub_preview) + set_drag_preview(preview) + return self + + +func create_preview() -> Control: + return null + + +func activate() -> void: + _enabled = true + + +func deactivate() -> void: + _enabled = false + + +func is_active() -> bool: + return _enabled + + +func is_dragged() -> bool: + return _grabbed_dragable == self diff --git a/addons/gloot/ui/ctrl_drop_zone.gd b/addons/gloot/ui/ctrl_drop_zone.gd new file mode 100644 index 0000000..ec8579a --- /dev/null +++ b/addons/gloot/ui/ctrl_drop_zone.gd @@ -0,0 +1,30 @@ +@tool +extends Control + +signal dragable_dropped(dragable, position) + +const CtrlDragable = preload("res://addons/gloot/ui/ctrl_dragable.gd") + + +func activate() -> void: + mouse_filter = Control.MOUSE_FILTER_PASS + + +func deactivate() -> void: + mouse_filter = Control.MOUSE_FILTER_IGNORE + + +func is_active() -> bool: + return (mouse_filter != Control.MOUSE_FILTER_IGNORE) + + +func _can_drop_data(at_position: Vector2, data) -> bool: + return data is CtrlDragable + + +func _drop_data(at_position: Vector2, data) -> void: + var local_offset := CtrlDragable.get_grab_offset_local_to(self) + dragable_dropped.emit(data, at_position - local_offset) + CtrlDragable.dragable_dropped.emit(data, self, at_position - local_offset) + + diff --git a/addons/gloot/ui/ctrl_inventory.gd b/addons/gloot/ui/ctrl_inventory.gd new file mode 100644 index 0000000..facc1ec --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory.gd @@ -0,0 +1,197 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_inventory.svg") +class_name CtrlInventory +extends Control + +signal inventory_item_activated(item) +signal inventory_item_context_activated(item) + +enum SelectMode {SELECT_SINGLE = ItemList.SELECT_SINGLE, SELECT_MULTI = ItemList.SELECT_MULTI} + +@export var inventory_path: NodePath : + set(new_inv_path): + inventory_path = new_inv_path + var node: Node = get_node_or_null(inventory_path) + + if node == null: + return + + if is_inside_tree(): + assert(node is Inventory) + + inventory = node + update_configuration_warnings() + + +@export var default_item_icon: Texture2D +@export_enum("Single", "Multi") var select_mode: int = SelectMode.SELECT_SINGLE : + set(new_select_mode): + if select_mode == new_select_mode: + return + select_mode = new_select_mode + if is_instance_valid(_item_list): + _item_list.deselect_all(); + _item_list.select_mode = select_mode +var inventory: Inventory = null : + set(new_inventory): + if new_inventory == inventory: + return + + _disconnect_inventory_signals() + inventory = new_inventory + _connect_inventory_signals() + + _queue_refresh() +var _vbox_container: VBoxContainer +var _item_list: ItemList +var _refresh_queued: bool = false + + +func _get_configuration_warnings() -> PackedStringArray: + if inventory_path.is_empty(): + return PackedStringArray([ + "This node is not linked to an inventory, so it can't display any content.\n" + \ + "Set the inventory_path property to point to an Inventory node."]) + return PackedStringArray() + + +func _ready(): + if Engine.is_editor_hint(): + # Clean up, in case it is duplicated in the editor + if is_instance_valid(_vbox_container): + _vbox_container.queue_free() + + _vbox_container = VBoxContainer.new() + _vbox_container.size_flags_horizontal = SIZE_EXPAND_FILL + _vbox_container.size_flags_vertical = SIZE_EXPAND_FILL + _vbox_container.anchor_right = 1.0 + _vbox_container.anchor_bottom = 1.0 + add_child(_vbox_container) + + _item_list = ItemList.new() + _item_list.size_flags_horizontal = SIZE_EXPAND_FILL + _item_list.size_flags_vertical = SIZE_EXPAND_FILL + _item_list.item_activated.connect(_on_list_item_activated) + _item_list.item_clicked.connect(_on_list_item_clicked) + _item_list.select_mode = select_mode + _vbox_container.add_child(_item_list) + + if has_node(inventory_path): + inventory = get_node(inventory_path) + + _queue_refresh() + + +func _connect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + + if !inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.connect(_queue_refresh) + if !inventory.item_property_changed.is_connected(_on_item_property_changed): + inventory.item_property_changed.connect(_on_item_property_changed) + + +func _disconnect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + + if inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.disconnect(_queue_refresh) + if inventory.item_property_changed.is_connected(_on_item_property_changed): + inventory.item_property_changed.disconnect(_on_item_property_changed) + + +func _on_list_item_activated(index: int) -> void: + inventory_item_activated.emit(_get_inventory_item(index)) + + +func _on_list_item_clicked(index: int, at_position: Vector2, mouse_button_index: int) -> void: + if mouse_button_index == MOUSE_BUTTON_RIGHT: + inventory_item_context_activated.emit(_get_inventory_item(index)) + + +func _on_item_property_changed(_item: InventoryItem, property_name: String) -> void: + if property_name in [InventoryItem.KEY_NAME, InventoryItem.KEY_IMAGE]: + _queue_refresh() + + +func _process(_delta) -> void: + if _refresh_queued: + _refresh() + _refresh_queued = false + + +func _queue_refresh() -> void: + _refresh_queued = true + + +func _refresh() -> void: + if is_inside_tree(): + _clear_list() + _populate_list() + + +func _clear_list() -> void: + if is_instance_valid(_item_list): + _item_list.clear() + + +func _populate_list() -> void: + if !is_instance_valid(inventory): + return + + for item in inventory.get_items(): + var texture := item.get_texture() + if !texture: + texture = default_item_icon + _item_list.add_item(_get_item_title(item), texture) + _item_list.set_item_metadata(_item_list.get_item_count() - 1, item) + + +func _get_item_title(item: InventoryItem) -> String: + if item == null: + return "" + + var title = item.get_title() + var stack_size: int = InventoryStacked.get_item_stack_size(item) + if stack_size > 1: + title = "%s (x%d)" % [title, stack_size] + + return title + + +func get_selected_inventory_item() -> InventoryItem: + if _item_list.get_selected_items().is_empty(): + return null + + return _get_inventory_item(_item_list.get_selected_items()[0]) + + +func get_selected_inventory_items() -> Array[InventoryItem]: + var result: Array[InventoryItem] + var indexes = _item_list.get_selected_items() + for i in indexes: + result.append(_get_inventory_item(i)) + return result + + +func _get_inventory_item(index: int) -> InventoryItem: + assert(index >= 0) + assert(index < _item_list.get_item_count()) + + return _item_list.get_item_metadata(index) + + +func deselect_inventory_item() -> void: + _item_list.deselect_all() + + +func select_inventory_item(item: InventoryItem) -> void: + _item_list.deselect_all() + for index in _item_list.item_count: + if _item_list.get_item_metadata(index) != item: + continue + _item_list.select(index) + return + diff --git a/addons/gloot/ui/ctrl_inventory_grid.gd b/addons/gloot/ui/ctrl_inventory_grid.gd new file mode 100644 index 0000000..d7fbb76 --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory_grid.gd @@ -0,0 +1,294 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_inventory_grid.svg") +class_name CtrlInventoryGrid +extends Control + +signal item_dropped(item, offset) +signal selection_changed +signal inventory_item_activated(item) +signal inventory_item_context_activated(item) +signal item_mouse_entered(item) +signal item_mouse_exited(item) + +const CtrlInventoryGridBasic = preload("res://addons/gloot/ui/ctrl_inventory_grid_basic.gd") + +class GridControl extends Control: + var color: Color = Color.BLACK : + set(new_color): + if new_color == color: + return + color = new_color + queue_redraw() + var dimensions: Vector2i = Vector2i.ZERO : + set(new_dimensions): + if new_dimensions == dimensions: + return + dimensions = new_dimensions + queue_redraw() + + func _init(color_: Color, dimensions_: Vector2i) -> void: + color = color_ + dimensions = dimensions_ + + func _draw() -> void: + var rect = Rect2(Vector2.ZERO, size) + draw_rect(rect, color, false) + + if dimensions.x < 1 || dimensions.y < 1: + return + + for i in range(1, dimensions.x): + var from: Vector2 = Vector2(i * size.x / dimensions.x, 0) + var to: Vector2 = Vector2(i * size.x / dimensions.x, size.y) + draw_line(from, to, color) + for j in range(1, dimensions.y): + var from: Vector2 = Vector2(0, j * size.y / dimensions.y) + var to: Vector2 = Vector2(size.x, j * size.y / dimensions.y) + draw_line(from, to, color) + + +@export var inventory_path: NodePath : + set(new_inv_path): + if new_inv_path == inventory_path: + return + inventory_path = new_inv_path + var node: Node = get_node_or_null(inventory_path) + + if node == null: + return + + if is_inside_tree(): + assert(node is InventoryGrid) + + inventory = node + update_configuration_warnings() +@export var default_item_texture: Texture2D : + set(new_default_item_texture): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.default_item_texture = new_default_item_texture + default_item_texture = new_default_item_texture +@export var stretch_item_sprites: bool = true : + set(new_stretch_item_sprites): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.stretch_item_sprites = new_stretch_item_sprites + stretch_item_sprites = new_stretch_item_sprites +@export var field_dimensions: Vector2 = Vector2(32, 32) : + set(new_field_dimensions): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.field_dimensions = new_field_dimensions + field_dimensions = new_field_dimensions +@export var item_spacing: int = 0 : + set(new_item_spacing): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.item_spacing = new_item_spacing + item_spacing = new_item_spacing +@export var draw_grid: bool = true : + set(new_draw_grid): + if new_draw_grid == draw_grid: + return + draw_grid = new_draw_grid + _queue_refresh() +@export var grid_color: Color = Color.BLACK : + set(new_grid_color): + if(new_grid_color == grid_color): + return + grid_color = new_grid_color + _queue_refresh() +@export var draw_selections: bool = false : + set(new_draw_selections): + if new_draw_selections == draw_selections: + return + draw_selections = new_draw_selections + _queue_refresh() +@export var selection_color: Color = Color.GRAY : + set(new_selection_color): + if(new_selection_color == selection_color): + return + selection_color = new_selection_color + _queue_refresh() +@export_enum("Single", "Multi") var select_mode: int = CtrlInventoryGridBasic.SelectMode.SELECT_SINGLE : + set(new_select_mode): + if select_mode == new_select_mode: + return + select_mode = new_select_mode + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.select_mode = select_mode + +var inventory: InventoryGrid = null : + set(new_inventory): + if inventory == new_inventory: + return + + _disconnect_inventory_signals() + inventory = new_inventory + _connect_inventory_signals() + + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.inventory = inventory + _queue_refresh() + +var _ctrl_grid: GridControl = null +var _ctrl_selection: Control = null +var _ctrl_inventory_grid_basic: CtrlInventoryGridBasic = null +var _refresh_queued: bool = false + + +func _connect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + if !inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.connect(_queue_refresh) + if !inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.connect(_on_inventory_resized) + + +func _disconnect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + if inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.disconnect(_queue_refresh) + if inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.disconnect(_on_inventory_resized) + + +func _on_inventory_resized() -> void: + _queue_refresh() + + +func _get_configuration_warnings() -> PackedStringArray: + if inventory_path.is_empty(): + return PackedStringArray([ + "This node is not linked to an inventory and can't display any content.\n" + \ + "Set the inventory_path property to point to an InventoryGrid node."]) + return PackedStringArray() + + +func _ready() -> void: + if Engine.is_editor_hint(): + # Clean up, in case it is duplicated in the editor + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.queue_free() + _ctrl_grid.queue_free() + _ctrl_selection.queue_free() + + if has_node(inventory_path): + inventory = get_node_or_null(inventory_path) + + _ctrl_inventory_grid_basic = CtrlInventoryGridBasic.new() + _ctrl_inventory_grid_basic.inventory = inventory + _ctrl_inventory_grid_basic.field_dimensions = field_dimensions + _ctrl_inventory_grid_basic.item_spacing = item_spacing + _ctrl_inventory_grid_basic.default_item_texture = default_item_texture + _ctrl_inventory_grid_basic.stretch_item_sprites = stretch_item_sprites + _ctrl_inventory_grid_basic.name = "CtrlInventoryGridBasic" + _ctrl_inventory_grid_basic.resized.connect(_update_size) + _ctrl_inventory_grid_basic.select_mode = select_mode + + _ctrl_inventory_grid_basic.item_dropped.connect(func(item: InventoryItem, drop_position: Vector2): + item_dropped.emit(item, drop_position) + ) + _ctrl_inventory_grid_basic.selection_changed.connect(func(): + _queue_refresh() + selection_changed.emit() + ) + _ctrl_inventory_grid_basic.inventory_item_activated.connect(func(item: InventoryItem): + inventory_item_activated.emit(item) + ) + _ctrl_inventory_grid_basic.inventory_item_context_activated.connect(func(item: InventoryItem): + inventory_item_context_activated.emit(item) + ) + _ctrl_inventory_grid_basic.item_mouse_entered.connect(func(item: InventoryItem): item_mouse_entered.emit(item)) + _ctrl_inventory_grid_basic.item_mouse_exited.connect(func(item: InventoryItem): item_mouse_exited.emit(item)) + + _ctrl_grid = GridControl.new(grid_color, _get_inventory_dimensions()) + _ctrl_grid.color = grid_color + _ctrl_grid.dimensions = _get_inventory_dimensions() + _ctrl_grid.name = "CtrlGrid" + + _ctrl_selection = Control.new() + _ctrl_selection.visible = draw_selections + + add_child(_ctrl_grid) + add_child(_ctrl_selection) + add_child(_ctrl_inventory_grid_basic) + + _update_size() + _queue_refresh() + + +func _process(_delta) -> void: + if _refresh_queued: + _refresh() + _refresh_queued = false + + +func _refresh() -> void: + if is_instance_valid(_ctrl_grid): + _ctrl_grid.dimensions = _get_inventory_dimensions() + _ctrl_grid.color = grid_color + _ctrl_grid.visible = draw_grid + else: + _ctrl_grid.hide() + + if is_instance_valid(_ctrl_selection) && is_instance_valid(_ctrl_inventory_grid_basic): + for child in _ctrl_selection.get_children(): + child.queue_free() + for selected_inventory_item in _ctrl_inventory_grid_basic.get_selected_inventory_items(): + var rect := _ctrl_inventory_grid_basic.get_item_rect(selected_inventory_item) + var selection_rect := ColorRect.new() + selection_rect.color = selection_color + selection_rect.position = rect.position + selection_rect.size = rect.size + _ctrl_selection.add_child(selection_rect) + _ctrl_selection.visible = draw_selections + + +func _queue_refresh() -> void: + _refresh_queued = true + + +func _get_inventory_dimensions() -> Vector2i: + var inventory_grid = _get_inventory() + if !is_instance_valid(inventory_grid): + return Vector2i.ZERO + return _ctrl_inventory_grid_basic.inventory.size + + +func _update_size() -> void: + custom_minimum_size = _ctrl_inventory_grid_basic.size + size = _ctrl_inventory_grid_basic.size + _ctrl_grid.custom_minimum_size = _ctrl_inventory_grid_basic.size + _ctrl_grid.size = _ctrl_inventory_grid_basic.size + + +func _get_inventory() -> InventoryGrid: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return null + if !is_instance_valid(_ctrl_inventory_grid_basic.inventory): + return null + return _ctrl_inventory_grid_basic.inventory + + +func deselect_inventory_item() -> void: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return + _ctrl_inventory_grid_basic.deselect_inventory_item() + + +func select_inventory_item(item: InventoryItem) -> void: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return + _ctrl_inventory_grid_basic.select_inventory_item(item) + + +func get_selected_inventory_item() -> InventoryItem: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return null + return _ctrl_inventory_grid_basic.get_selected_inventory_item() + + +func get_selected_inventory_items() -> Array[InventoryItem]: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return [] + return _ctrl_inventory_grid_basic.get_selected_inventory_items() + diff --git a/addons/gloot/ui/ctrl_inventory_grid_basic.gd b/addons/gloot/ui/ctrl_inventory_grid_basic.gd new file mode 100644 index 0000000..3e6af5c --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory_grid_basic.gd @@ -0,0 +1,452 @@ +@tool +extends Control + +signal item_dropped(item, offset) +signal selection_changed +signal inventory_item_activated(item) +signal inventory_item_context_activated(item) +signal item_mouse_entered(item) +signal item_mouse_exited(item) + +const GlootUndoRedo = preload("res://addons/gloot/editor/gloot_undo_redo.gd") +const CtrlInventoryItemRect = preload("res://addons/gloot/ui/ctrl_inventory_item_rect.gd") +const CtrlDropZone = preload("res://addons/gloot/ui/ctrl_drop_zone.gd") +const CtrlDragable = preload("res://addons/gloot/ui/ctrl_dragable.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") + +enum SelectMode {SELECT_SINGLE = 0, SELECT_MULTI = 1} + +@export var field_dimensions: Vector2 = Vector2(32, 32) : + set(new_field_dimensions): + if new_field_dimensions == field_dimensions: + return + field_dimensions = new_field_dimensions + _queue_refresh() +@export var item_spacing: int = 0 : + set(new_item_spacing): + if new_item_spacing == item_spacing: + return + item_spacing = new_item_spacing + _queue_refresh() +@export var inventory_path: NodePath : + set(new_inv_path): + if new_inv_path == inventory_path: + return + inventory_path = new_inv_path + var node: Node = get_node_or_null(inventory_path) + + if node == null: + return + + if is_inside_tree(): + assert(node is InventoryGrid) + + inventory = node + update_configuration_warnings() +@export var default_item_texture: Texture2D : + set(new_default_item_texture): + if new_default_item_texture == default_item_texture: + return + default_item_texture = new_default_item_texture + _queue_refresh() +@export var stretch_item_sprites: bool = true : + set(new_stretch_item_sprites): + stretch_item_sprites = new_stretch_item_sprites + _queue_refresh() +@export_enum("Single", "Multi") var select_mode: int = SelectMode.SELECT_SINGLE : + set(new_select_mode): + if select_mode == new_select_mode: + return + select_mode = new_select_mode + _clear_selection() +var inventory: InventoryGrid = null : + set(new_inventory): + if inventory == new_inventory: + return + + _clear_selection() + + _disconnect_inventory_signals() + inventory = new_inventory + _connect_inventory_signals() + + _queue_refresh() +var _ctrl_item_container: Control = null +var _ctrl_drop_zone: CtrlDropZone = null +var _selected_items: Array[InventoryItem] = [] +var _refresh_queued: bool = false + + +func _get_configuration_warnings() -> PackedStringArray: + if inventory_path.is_empty(): + return PackedStringArray([ + "This node is not linked to an inventory and it can't display any content.\n" + \ + "Set the inventory_path property to point to an InventoryGrid node."]) + return PackedStringArray() + + +func _ready() -> void: + if Engine.is_editor_hint(): + # Clean up, in case it is duplicated in the editor + if is_instance_valid(_ctrl_item_container): + _ctrl_item_container.queue_free() + + mouse_filter = Control.MOUSE_FILTER_IGNORE + + _ctrl_item_container = Control.new() + _ctrl_item_container.size = size + _ctrl_item_container.mouse_filter = Control.MOUSE_FILTER_IGNORE + resized.connect(func(): _ctrl_item_container.size = size) + add_child(_ctrl_item_container) + + _ctrl_drop_zone = CtrlDropZone.new() + _ctrl_drop_zone.dragable_dropped.connect(_on_dragable_dropped) + _ctrl_drop_zone.size = size + resized.connect(func(): _ctrl_drop_zone.size = size) + CtrlDragable.dragable_grabbed.connect(func(dragable: CtrlDragable, grab_position: Vector2): + _ctrl_drop_zone.activate() + ) + CtrlDragable.dragable_dropped.connect(func(dragable: CtrlDragable, zone: CtrlDropZone, drop_position: Vector2): + _ctrl_drop_zone.deactivate() + ) + add_child(_ctrl_drop_zone) + + if has_node(inventory_path): + inventory = get_node_or_null(inventory_path) + + _queue_refresh() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_DRAG_END: + _ctrl_drop_zone.deactivate() + + +func _connect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + + if !inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.connect(_queue_refresh) + if !inventory.item_property_changed.is_connected(_on_item_property_changed): + inventory.item_property_changed.connect(_on_item_property_changed) + if !inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.connect(_on_inventory_resized) + if !inventory.item_removed.is_connected(_on_item_removed): + inventory.item_removed.connect(_on_item_removed) + + +func _disconnect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + + if inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.disconnect(_queue_refresh) + if inventory.item_property_changed.is_connected(_on_item_property_changed): + inventory.item_property_changed.disconnect(_on_item_property_changed) + if inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.disconnect(_on_inventory_resized) + if inventory.item_removed.is_connected(_on_item_removed): + inventory.item_removed.disconnect(_on_item_removed) + + +func _on_item_property_changed(_item: InventoryItem, property_name: String) -> void: + var relevant_properties = [ + GridConstraint.KEY_WIDTH, + GridConstraint.KEY_HEIGHT, + GridConstraint.KEY_SIZE, + GridConstraint.KEY_ROTATED, + GridConstraint.KEY_GRID_POSITION, + InventoryItem.KEY_IMAGE, + ] + if property_name in relevant_properties: + _queue_refresh() + + +func _on_inventory_resized() -> void: + _queue_refresh() + + +func _on_item_removed(item: InventoryItem) -> void: + _deselect(item) + + +func _process(_delta) -> void: + if _refresh_queued: + _refresh() + _refresh_queued = false + + +func _queue_refresh() -> void: + _refresh_queued = true + + +func _refresh() -> void: + _ctrl_drop_zone.deactivate() + custom_minimum_size = _get_inventory_size_px() + size = custom_minimum_size + + _clear_list() + _populate_list() + + +func _get_inventory_size_px() -> Vector2: + if !is_instance_valid(inventory): + return Vector2.ZERO + + var result := Vector2(inventory.size.x * field_dimensions.x, \ + inventory.size.y * field_dimensions.y) + + # Also take item spacing into consideration + result += Vector2(inventory.size - Vector2i.ONE) * item_spacing + + return result + + +func _clear_list() -> void: + if !is_instance_valid(_ctrl_item_container): + return + + for ctrl_inventory_item in _ctrl_item_container.get_children(): + _ctrl_item_container.remove_child(ctrl_inventory_item) + ctrl_inventory_item.queue_free() + + +func _populate_list() -> void: + if !is_instance_valid(inventory) || !is_instance_valid(_ctrl_item_container): + return + + for item in inventory.get_items(): + var ctrl_inventory_item = CtrlInventoryItemRect.new() + ctrl_inventory_item.texture = default_item_texture + ctrl_inventory_item.item = item + ctrl_inventory_item.grabbed.connect(_on_item_grab.bind(ctrl_inventory_item)) + ctrl_inventory_item.dropped.connect(_on_item_drop.bind(ctrl_inventory_item)) + ctrl_inventory_item.activated.connect(_on_item_activated.bind(ctrl_inventory_item)) + ctrl_inventory_item.context_activated.connect(_on_item_context_activated.bind(ctrl_inventory_item)) + ctrl_inventory_item.mouse_entered.connect(_on_item_mouse_entered.bind(ctrl_inventory_item)) + ctrl_inventory_item.mouse_exited.connect(_on_item_mouse_exited.bind(ctrl_inventory_item)) + ctrl_inventory_item.clicked.connect(_on_item_clicked.bind(ctrl_inventory_item)) + ctrl_inventory_item.size = _get_item_sprite_size(item) + + ctrl_inventory_item.position = _get_field_position(inventory.get_item_position(item)) + ctrl_inventory_item.stretch_mode = TextureRect.STRETCH_KEEP_CENTERED + if stretch_item_sprites: + ctrl_inventory_item.stretch_mode = TextureRect.STRETCH_SCALE + + _ctrl_item_container.add_child(ctrl_inventory_item) + + +func _on_item_grab(offset: Vector2, ctrl_inventory_item: CtrlInventoryItemRect) -> void: + _clear_selection() + + +func _on_item_drop(zone: CtrlDropZone, drop_position: Vector2, ctrl_inventory_item: CtrlInventoryItemRect) -> void: + var item: InventoryItem = ctrl_inventory_item.item + # The item might have been freed in case the item stack has been moved and merged with another + # stack. + if is_instance_valid(item) and inventory.has_item(item): + if zone == null: + item_dropped.emit(item, drop_position + ctrl_inventory_item.position) + + +func _get_item_sprite_size(item: InventoryItem) -> Vector2: + var item_size := inventory.get_item_size(item) + var sprite_size := Vector2(item_size) * field_dimensions + + # Also take item spacing into consideration + sprite_size += (Vector2(item_size) - Vector2.ONE) * item_spacing + + return sprite_size + + +func _on_item_activated(ctrl_inventory_item: CtrlInventoryItemRect) -> void: + var item = ctrl_inventory_item.item + if !item: + return + + inventory_item_activated.emit(item) + + +func _on_item_context_activated(ctrl_inventory_item: CtrlInventoryItemRect) -> void: + var item = ctrl_inventory_item.item + if !item: + return + + inventory_item_context_activated.emit(item) + + +func _on_item_mouse_entered(ctrl_inventory_item) -> void: + item_mouse_entered.emit(ctrl_inventory_item.item) + + +func _on_item_mouse_exited(ctrl_inventory_item) -> void: + item_mouse_exited.emit(ctrl_inventory_item.item) + + +func _on_item_clicked(ctrl_inventory_item) -> void: + var item = ctrl_inventory_item.item + if !is_instance_valid(item): + return + + if select_mode == SelectMode.SELECT_MULTI && Input.is_key_pressed(KEY_CTRL): + if !_is_item_selected(item): + _select(item) + else: + _deselect(item) + else: + _clear_selection() + _select(item) + + +func _select(item: InventoryItem) -> void: + if item in _selected_items: + return + + if (item != null) && !inventory.has_item(item): + return + + _selected_items.append(item) + selection_changed.emit() + + +func _is_item_selected(item: InventoryItem) -> bool: + return item in _selected_items + + +func _deselect(item: InventoryItem) -> void: + if !(item in _selected_items): + return + var idx := _selected_items.find(item) + if idx < 0: + return + _selected_items.remove_at(idx) + selection_changed.emit() + + +func _clear_selection() -> void: + if _selected_items.is_empty(): + return + _selected_items.clear() + selection_changed.emit() + + +func _on_dragable_dropped(dragable: CtrlDragable, drop_position: Vector2) -> void: + var item: InventoryItem = dragable.item + if item == null: + return + + if !is_instance_valid(inventory): + return + + if inventory.has_item(item): + _handle_item_move(item, drop_position) + else: + _handle_item_transfer(item, drop_position) + + +func _handle_item_move(item: InventoryItem, drop_position: Vector2) -> void: + var field_coords = get_field_coords(drop_position + (field_dimensions / 2)) + if _move_item(item, field_coords): + return + if _merge_item(item, field_coords): + return + _swap_items(item, field_coords) + + +func _handle_item_transfer(item: InventoryItem, drop_position: Vector2) -> void: + var source_inventory: InventoryGrid = item.get_inventory() + + var field_coords = get_field_coords(drop_position + (field_dimensions / 2)) + if source_inventory != null: + if source_inventory.item_protoset != inventory.item_protoset: + return + source_inventory.transfer_to(item, inventory, field_coords) + elif !inventory.add_item_at(item, field_coords): + _swap_items(item, field_coords) + + +func get_field_coords(local_pos: Vector2) -> Vector2i: + # We have to consider the item spacing when calculating field coordinates, thus we expand the + # size of each field by Vector2(item_spacing, item_spacing). + var field_dimensions_ex = field_dimensions + Vector2(item_spacing, item_spacing) + + # We also don't want the item spacing to disturb snapping to the closest field, so we add half + # the spacing to the local coordinates. + var local_pos_ex = local_pos + (Vector2(item_spacing, item_spacing) / 2) + + var x: int = local_pos_ex.x / (field_dimensions_ex.x) + var y: int = local_pos_ex.y / (field_dimensions_ex.y) + return Vector2i(x, y) + + +func get_selected_inventory_item() -> InventoryItem: + if _selected_items.is_empty(): + return null + return _selected_items[0] + + +func get_selected_inventory_items() -> Array[InventoryItem]: + return _selected_items.duplicate() + + +func _move_item(item: InventoryItem, move_position: Vector2i) -> bool: + if !inventory.rect_free(Rect2i(move_position, inventory.get_item_size(item)), item): + return false + if Engine.is_editor_hint(): + GlootUndoRedo.move_inventory_item(inventory, item, move_position) + return true + inventory.move_item_to(item, move_position) + return true + + +func _merge_item(item_src: InventoryItem, position: Vector2i) -> bool: + if !(inventory is InventoryGridStacked): + return false + + var item_dst = (inventory as InventoryGridStacked)._get_mergable_item_at(item_src, position) + if item_dst == null: + return false + + if Engine.is_editor_hint(): + GlootUndoRedo.join_inventory_items(inventory, item_dst, item_src) + else: + (inventory as InventoryGridStacked).join(item_dst, item_src) + return true + + +func _swap_items(item: InventoryItem, position: Vector2i) -> bool: + var item2 = inventory.get_item_at(position) + if item2 == null: + return false + + if Engine.is_editor_hint(): + GlootUndoRedo.swap_inventory_items(item, item2) + else: + InventoryItem.swap(item, item2) + return true + + +func _get_field_position(field_coords: Vector2i) -> Vector2: + var field_position = Vector2(field_coords.x * field_dimensions.x, \ + field_coords.y * field_dimensions.y) + field_position += Vector2(item_spacing * field_coords) + return field_position + + +func deselect_inventory_item() -> void: + _clear_selection() + + +func select_inventory_item(item: InventoryItem) -> void: + _select(item) + + +func get_item_rect(item: InventoryItem) -> Rect2: + if !is_instance_valid(item): + return Rect2() + return Rect2( + _get_field_position(inventory.get_item_position(item)), + _get_item_sprite_size(item) + ) + diff --git a/addons/gloot/ui/ctrl_inventory_grid_ex.gd b/addons/gloot/ui/ctrl_inventory_grid_ex.gd new file mode 100644 index 0000000..69c544f --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory_grid_ex.gd @@ -0,0 +1,408 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_inventory_grid.svg") +class_name CtrlInventoryGridEx +extends Control + +signal item_dropped(item, offset) +signal selection_changed +signal inventory_item_activated(item) +signal inventory_item_context_activated(item) +signal item_mouse_entered(item) +signal item_mouse_exited(item) + +const Verify = preload("res://addons/gloot/core/verify.gd") +const CtrlInventoryGridBasic = preload("res://addons/gloot/ui/ctrl_inventory_grid_basic.gd") +const CtrlInventoryItemRect = preload("res://addons/gloot/ui/ctrl_inventory_item_rect.gd") +const CtrlDragable = preload("res://addons/gloot/ui/ctrl_dragable.gd") + + +class PriorityPanel extends Panel: + enum StylePriority {HIGH = 0, MEDIUM = 1, LOW = 2} + + var regular_style: StyleBox + var hover_style: StyleBox + var _styles: Array[StyleBox] = [null, null, null] + + + func _init(regular_style_: StyleBox, hover_style_: StyleBox) -> void: + regular_style = regular_style_ + hover_style = hover_style_ + + + func _ready() -> void: + set_style(regular_style) + mouse_entered.connect(func(): + set_style(hover_style) + ) + mouse_exited.connect(func(): + set_style(regular_style) + ) + + + func set_style(style: StyleBox, priority: int = StylePriority.LOW) -> void: + if priority > 2 || priority < 0: + return + if _styles[priority] == style: + return + + _styles[priority] = style + + for i in range(0, 3): + if _styles[i] != null: + _set_panel_style(_styles[i]) + return + + + func _set_panel_style(style: StyleBox) -> void: + remove_theme_stylebox_override("panel") + if style != null: + add_theme_stylebox_override("panel", style) + + +class SelectionPanel extends Panel: + func set_style(style: StyleBox) -> void: + remove_theme_stylebox_override("panel") + if style != null: + add_theme_stylebox_override("panel", style) + + +@export var inventory_path: NodePath : + set(new_inv_path): + if new_inv_path == inventory_path: + return + inventory_path = new_inv_path + var node: Node = get_node_or_null(inventory_path) + + if node == null: + return + + if is_inside_tree(): + assert(node is InventoryGrid) + + inventory = node + update_configuration_warnings() +@export var default_item_texture: Texture2D : + set(new_default_item_texture): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.default_item_texture = new_default_item_texture + default_item_texture = new_default_item_texture +@export var stretch_item_sprites: bool = true : + set(new_stretch_item_sprites): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.stretch_item_sprites = new_stretch_item_sprites + stretch_item_sprites = new_stretch_item_sprites +@export var field_dimensions: Vector2 = Vector2(32, 32) : + set(new_field_dimensions): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.field_dimensions = new_field_dimensions + field_dimensions = new_field_dimensions +@export var item_spacing: int = 0 : + set(new_item_spacing): + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.item_spacing = new_item_spacing + item_spacing = new_item_spacing +@export_enum("Single", "Multi") var select_mode: int = CtrlInventoryGridBasic.SelectMode.SELECT_SINGLE : + set(new_select_mode): + if select_mode == new_select_mode: + return + select_mode = new_select_mode + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.select_mode = select_mode + +@export_group("Custom Styles") +@export var field_style: StyleBox : + set(new_field_style): + field_style = new_field_style + _queue_refresh() +@export var field_highlighted_style: StyleBox : + set(new_field_highlighted_style): + field_highlighted_style = new_field_highlighted_style + _queue_refresh() +@export var field_selected_style: StyleBox : + set(new_field_selected_style): + field_selected_style = new_field_selected_style + _queue_refresh() +@export var selection_style: StyleBox : + set(new_selection_style): + selection_style = new_selection_style + _queue_refresh() + +var inventory: InventoryGrid = null : + set(new_inventory): + if inventory == new_inventory: + return + + _disconnect_inventory_signals() + inventory = new_inventory + _connect_inventory_signals() + + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.inventory = inventory + _queue_refresh() +var _ctrl_inventory_grid_basic: CtrlInventoryGridBasic = null +var _field_background_grid: Control = null +var _field_backgrounds: Array = [] +var _selection_panels: Control = null +var _refresh_queued: bool = false + + +func _connect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + if !inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.connect(_queue_refresh) + if !inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.connect(_on_inventory_resized) + + +func _disconnect_inventory_signals() -> void: + if !is_instance_valid(inventory): + return + if inventory.contents_changed.is_connected(_queue_refresh): + inventory.contents_changed.disconnect(_queue_refresh) + if inventory.size_changed.is_connected(_on_inventory_resized): + inventory.size_changed.disconnect(_on_inventory_resized) + + +func _process(_delta) -> void: + if _refresh_queued: + _refresh() + _refresh_queued = false + + +func _refresh() -> void: + _refresh_field_background_grid() + _refresh_selection_panel() + + +func _queue_refresh() -> void: + _refresh_queued = true + + +func _refresh_selection_panel() -> void: + if !is_instance_valid(_selection_panels): + return + + for child in _selection_panels.get_children(): + child.queue_free() + + var selected_items := _ctrl_inventory_grid_basic.get_selected_inventory_items() + _selection_panels.visible = (!selected_items.is_empty()) && (selection_style != null) + if selected_items.is_empty(): + return + + for selected_item in selected_items: + var selection_panel := SelectionPanel.new() + var rect := _ctrl_inventory_grid_basic.get_item_rect(selected_item) + selection_panel.position = rect.position + selection_panel.size = rect.size + selection_panel.set_style(selection_style) + selection_panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + _selection_panels.add_child(selection_panel) + + +func _refresh_field_background_grid() -> void: + if is_instance_valid(_field_background_grid): + while _field_background_grid.get_child_count() > 0: + _field_background_grid.get_children()[0].queue_free() + _field_background_grid.remove_child(_field_background_grid.get_children()[0]) + _field_backgrounds = [] + + if !is_instance_valid(inventory): + return + + for i in range(inventory.size.x): + _field_backgrounds.append([]) + for j in range(inventory.size.y): + var field_panel: PriorityPanel = PriorityPanel.new(field_style, field_highlighted_style) + field_panel.visible = (field_style != null) + field_panel.size = field_dimensions + field_panel.position = _ctrl_inventory_grid_basic._get_field_position(Vector2i(i, j)) + _field_background_grid.add_child(field_panel) + _field_backgrounds[i].append(field_panel) + + +func _ready() -> void: + if Engine.is_editor_hint(): + # Clean up, in case it is duplicated in the editor + if is_instance_valid(_ctrl_inventory_grid_basic): + _ctrl_inventory_grid_basic.queue_free() + _field_background_grid.queue_free() + + if has_node(inventory_path): + inventory = get_node_or_null(inventory_path) + + _field_background_grid = Control.new() + _field_background_grid.name = "FieldBackgrounds" + add_child(_field_background_grid) + + _ctrl_inventory_grid_basic = CtrlInventoryGridBasic.new() + _ctrl_inventory_grid_basic.inventory = inventory + _ctrl_inventory_grid_basic.field_dimensions = field_dimensions + _ctrl_inventory_grid_basic.item_spacing = item_spacing + _ctrl_inventory_grid_basic.default_item_texture = default_item_texture + _ctrl_inventory_grid_basic.stretch_item_sprites = stretch_item_sprites + _ctrl_inventory_grid_basic.name = "CtrlInventoryGridBasic" + _ctrl_inventory_grid_basic.resized.connect(_update_size) + _ctrl_inventory_grid_basic.item_dropped.connect(func(item: InventoryItem, drop_position: Vector2): + item_dropped.emit(item, drop_position) + ) + _ctrl_inventory_grid_basic.inventory_item_activated.connect(func(item: InventoryItem): + inventory_item_activated.emit(item) + ) + _ctrl_inventory_grid_basic.inventory_item_context_activated.connect(func(item: InventoryItem): + inventory_item_context_activated.emit(item) + ) + _ctrl_inventory_grid_basic.item_mouse_entered.connect(_on_item_mouse_entered) + _ctrl_inventory_grid_basic.item_mouse_exited.connect(_on_item_mouse_exited) + _ctrl_inventory_grid_basic.selection_changed.connect(_on_selection_changed) + _ctrl_inventory_grid_basic.select_mode = select_mode + add_child(_ctrl_inventory_grid_basic) + + _selection_panels = Control.new() + _selection_panels.mouse_filter = Control.MOUSE_FILTER_IGNORE + _selection_panels.name = "SelectionPanels" + add_child(_selection_panels) + + CtrlDragable.dragable_dropped.connect(func(_grabbed_dragable, _zone, _local_drop_position): + _fill_background(field_style, PriorityPanel.StylePriority.LOW) + ) + + _update_size() + _queue_refresh() + + +func _notification(what: int) -> void: + if what == NOTIFICATION_DRAG_END: + _fill_background(field_style, PriorityPanel.StylePriority.LOW) + + +func _update_size() -> void: + custom_minimum_size = _ctrl_inventory_grid_basic.size + size = _ctrl_inventory_grid_basic.size + + +func _on_item_mouse_entered(item: InventoryItem) -> void: + _set_item_background(item, field_highlighted_style, PriorityPanel.StylePriority.MEDIUM) + item_mouse_entered.emit(item) + + +func _on_item_mouse_exited(item: InventoryItem) -> void: + _set_item_background(item, null, PriorityPanel.StylePriority.MEDIUM) + item_mouse_exited.emit(item) + + +func _on_selection_changed() -> void: + _handle_selection_change() + selection_changed.emit() + + +func _handle_selection_change() -> void: + if !is_instance_valid(inventory): + return + _refresh_selection_panel() + + if !field_selected_style: + return + for item in inventory.get_items(): + if item in _ctrl_inventory_grid_basic.get_selected_inventory_items(): + _set_item_background(item, field_selected_style, PriorityPanel.StylePriority.HIGH) + else: + _set_item_background(item, null, PriorityPanel.StylePriority.HIGH) + + +func _on_inventory_resized() -> void: + _refresh_field_background_grid() + + +func _input(event) -> void: + if !(event is InputEventMouseMotion): + return + if !is_instance_valid(inventory): + return + + if !field_highlighted_style: + return + _highlight_grabbed_item(field_highlighted_style) + + +func _highlight_grabbed_item(style: StyleBox): + var grabbed_item: InventoryItem = _get_global_grabbed_item() + if !grabbed_item: + return + + var global_grabbed_item_pos: Vector2 = _get_global_grabbed_item_local_pos() + if !_is_hovering(global_grabbed_item_pos): + _fill_background(field_style, PriorityPanel.StylePriority.LOW) + return + + _fill_background(field_style, PriorityPanel.StylePriority.LOW) + + var grabbed_item_coords := _ctrl_inventory_grid_basic.get_field_coords(global_grabbed_item_pos + (field_dimensions / 2)) + var item_size := inventory.get_item_size(grabbed_item) + var rect := Rect2i(grabbed_item_coords, item_size) + if !Rect2i(Vector2i.ZERO, inventory.size).encloses(rect): + return + _set_rect_background(rect, style, PriorityPanel.StylePriority.LOW) + + +func _is_hovering(local_pos: Vector2) -> bool: + return get_rect().has_point(local_pos) + + +func _set_item_background(item: InventoryItem, style: StyleBox, priority: int) -> bool: + if !item: + return false + + _set_rect_background(inventory.get_item_rect(item), style, priority) + return true + + +func _set_rect_background(rect: Rect2i, style: StyleBox, priority: int) -> void: + var h_range = min(rect.size.x + rect.position.x, inventory.size.x) + for i in range(rect.position.x, h_range): + var v_range = min(rect.size.y + rect.position.y, inventory.size.y) + for j in range(rect.position.y, v_range): + _field_backgrounds[i][j].set_style(style, priority) + + +func _fill_background(style: StyleBox, priority: int) -> void: + for panel in _field_background_grid.get_children(): + panel.set_style(style, priority) + + +func _get_global_grabbed_item() -> InventoryItem: + if CtrlDragable.get_grabbed_dragable() == null: + return null + return (CtrlDragable.get_grabbed_dragable() as CtrlInventoryItemRect).item + + +func _get_global_grabbed_item_local_pos() -> Vector2: + if CtrlDragable.get_grabbed_dragable(): + return get_local_mouse_position() - CtrlDragable.get_grab_offset_local_to(self) + return Vector2(-1, -1) + + +func deselect_inventory_item() -> void: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return + _ctrl_inventory_grid_basic.deselect_inventory_item() + + +func select_inventory_item(item: InventoryItem) -> void: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return + _ctrl_inventory_grid_basic.select_inventory_item(item) + + +func get_selected_inventory_item() -> InventoryItem: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return null + return _ctrl_inventory_grid_basic.get_selected_inventory_item() + + +func get_selected_inventory_items() -> Array[InventoryItem]: + if !is_instance_valid(_ctrl_inventory_grid_basic): + return [] + return _ctrl_inventory_grid_basic.get_selected_inventory_items() + diff --git a/addons/gloot/ui/ctrl_inventory_item_rect.gd b/addons/gloot/ui/ctrl_inventory_item_rect.gd new file mode 100644 index 0000000..0a77b89 --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory_item_rect.gd @@ -0,0 +1,167 @@ +extends "res://addons/gloot/ui/ctrl_dragable.gd" + +const CtrlInventoryItemRect = preload("res://addons/gloot/ui/ctrl_inventory_item_rect.gd") +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") +const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd") + +signal activated +signal clicked +signal context_activated + +var item: InventoryItem : + set(new_item): + if item == new_item: + return + + _disconnect_item_signals() + _connect_item_signals(new_item) + + item = new_item + if item: + texture = item.get_texture() + activate() + else: + texture = null + deactivate() + _update_stack_size() +var texture: Texture2D : + set(new_texture): + if new_texture == texture: + return + texture = new_texture + _update_texture() +var stretch_mode: TextureRect.StretchMode = TextureRect.StretchMode.STRETCH_SCALE : + set(new_stretch_mode): + if stretch_mode == new_stretch_mode: + return + stretch_mode = new_stretch_mode + if is_instance_valid(_texture_rect): + _texture_rect.stretch_mode = stretch_mode +var item_slot: ItemSlot +var _texture_rect: TextureRect +var _stack_size_label: Label +static var _stored_preview_size: Vector2 +static var _stored_preview_offset: Vector2 + + +func _connect_item_signals(new_item: InventoryItem) -> void: + if new_item == null: + return + + if !new_item.protoset_changed.is_connected(_refresh): + new_item.protoset_changed.connect(_refresh) + if !new_item.prototype_id_changed.is_connected(_refresh): + new_item.prototype_id_changed.connect(_refresh) + if !new_item.property_changed.is_connected(_on_item_property_changed): + new_item.property_changed.connect(_on_item_property_changed) + + +func _disconnect_item_signals() -> void: + if !is_instance_valid(item): + return + + if item.protoset_changed.is_connected(_refresh): + item.protoset_changed.disconnect(_refresh) + if item.prototype_id_changed.is_connected(_refresh): + item.prototype_id_changed.disconnect(_refresh) + if item.property_changed.is_connected(_on_item_property_changed): + item.property_changed.disconnect(_on_item_property_changed) + + +func _on_item_property_changed(property_name: String) -> void: + var relevant_properties = [ + StacksConstraint.KEY_STACK_SIZE, + GridConstraint.KEY_WIDTH, + GridConstraint.KEY_HEIGHT, + GridConstraint.KEY_SIZE, + GridConstraint.KEY_ROTATED, + GridConstraint.KEY_GRID_POSITION, + InventoryItem.KEY_IMAGE, + ] + if property_name in relevant_properties: + _refresh() + + +func _ready() -> void: + _texture_rect = TextureRect.new() + _texture_rect.mouse_filter = Control.MOUSE_FILTER_IGNORE + _texture_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE + _texture_rect.stretch_mode = stretch_mode + _stack_size_label = Label.new() + _stack_size_label.mouse_filter = Control.MOUSE_FILTER_IGNORE + _stack_size_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT + _stack_size_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM + add_child(_texture_rect) + add_child(_stack_size_label) + + resized.connect(func(): + _texture_rect.size = size + _stack_size_label.size = size + ) + + if item == null: + deactivate() + + _refresh() + + +func _update_texture() -> void: + if !is_instance_valid(_texture_rect): + return + _texture_rect.texture = texture + if is_instance_valid(item) && GridConstraint.is_item_rotated(item): + _texture_rect.size = Vector2(size.y, size.x) + if GridConstraint.is_item_rotation_positive(item): + _texture_rect.position = Vector2(_texture_rect.size.y, 0) + _texture_rect.rotation = PI/2 + else: + _texture_rect.position = Vector2(0, _texture_rect.size.x) + _texture_rect.rotation = -PI/2 + + else: + _texture_rect.size = size + _texture_rect.position = Vector2.ZERO + _texture_rect.rotation = 0 + + +func _update_stack_size() -> void: + if !is_instance_valid(_stack_size_label): + return + if !is_instance_valid(item): + _stack_size_label.text = "" + return + var stack_size: int = StacksConstraint.get_item_stack_size(item) + if stack_size <= 1: + _stack_size_label.text = "" + else: + _stack_size_label.text = "%d" % stack_size + _stack_size_label.size = size + + +func _refresh() -> void: + _update_texture() + _update_stack_size() + + +func create_preview() -> Control: + var preview = CtrlInventoryItemRect.new() + preview.item = item + preview.texture = texture + preview.size = size + preview.stretch_mode = stretch_mode + return preview + + +func _gui_input(event: InputEvent) -> void: + if !(event is InputEventMouseButton): + return + + var mb_event: InputEventMouseButton = event + if mb_event.button_index == MOUSE_BUTTON_LEFT: + if mb_event.double_click: + activated.emit() + else: + clicked.emit() + elif mb_event.button_index == MOUSE_BUTTON_MASK_RIGHT: + context_activated.emit() + diff --git a/addons/gloot/ui/ctrl_inventory_stacked.gd b/addons/gloot/ui/ctrl_inventory_stacked.gd new file mode 100644 index 0000000..56f5b58 --- /dev/null +++ b/addons/gloot/ui/ctrl_inventory_stacked.gd @@ -0,0 +1,74 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_inventory_stacked.svg") +class_name CtrlInventoryStacked +extends CtrlInventory + +@export var progress_bar_visible: bool = true : + set(new_progress_bar_visible): + progress_bar_visible = new_progress_bar_visible + if _progress_bar: + _progress_bar.visible = progress_bar_visible +@export var label_visible: bool = true : + set(new_label_visible): + label_visible = new_label_visible + if _label: + _label.visible = label_visible +var _progress_bar: ProgressBar +var _label: Label + + +func _ready(): + super._ready() + + _progress_bar = ProgressBar.new() + _progress_bar.size_flags_horizontal = SIZE_EXPAND_FILL + _progress_bar.show_percentage = false + _progress_bar.visible = progress_bar_visible + _progress_bar.custom_minimum_size.y = 20 + _vbox_container.add_child(_progress_bar) + + _label = Label.new() + _label.anchor_right = 1.0 + _label.anchor_bottom = 1.0 + _label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + _label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER + _progress_bar.add_child(_label) + + _queue_refresh() + + +func _connect_inventory_signals() -> void: + if !inventory: + return + + super._connect_inventory_signals() + + if !inventory.capacity_changed.is_connected(_queue_refresh): + inventory.capacity_changed.connect(_queue_refresh) + if !inventory.occupied_space_changed.is_connected(_queue_refresh): + inventory.occupied_space_changed.connect(_queue_refresh) + + +func _disconnect_inventory_signals() -> void: + if !inventory: + return + + super._disconnect_inventory_signals() + + if !inventory.capacity_changed.is_connected(_queue_refresh): + inventory.capacity_changed.disconnect(_queue_refresh) + if !inventory.occupied_space_changed.is_connected(_queue_refresh): + inventory.occupied_space_changed.disconnect(_queue_refresh) + + +func _refresh(): + super._refresh() + if is_instance_valid(_label): + _label.visible = label_visible + _label.text = "%d/%d" % [inventory.occupied_space, inventory.capacity] + if is_instance_valid(_progress_bar): + _progress_bar.visible = progress_bar_visible + _progress_bar.min_value = 0 + _progress_bar.max_value = inventory.capacity + _progress_bar.value = inventory.occupied_space + diff --git a/addons/gloot/ui/ctrl_item_slot.gd b/addons/gloot/ui/ctrl_item_slot.gd new file mode 100644 index 0000000..95a9888 --- /dev/null +++ b/addons/gloot/ui/ctrl_item_slot.gd @@ -0,0 +1,276 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_item_slot.svg") +class_name CtrlItemSlot +extends Control + +const CtrlInventoryItemRect = preload("res://addons/gloot/ui/ctrl_inventory_item_rect.gd") +const CtrlDropZone = preload("res://addons/gloot/ui/ctrl_drop_zone.gd") +const CtrlDragable = preload("res://addons/gloot/ui/ctrl_dragable.gd") +const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd") + +signal item_mouse_entered +signal item_mouse_exited + +@export var item_slot_path: NodePath : + set(new_item_slot_path): + if item_slot_path == new_item_slot_path: + return + item_slot_path = new_item_slot_path + var node: Node = get_node_or_null(item_slot_path) + + if node == null: + _clear() + return + + if is_inside_tree(): + assert(node is ItemSlotBase) + + item_slot = node + _refresh() + update_configuration_warnings() +@export var default_item_icon: Texture2D : + set(new_default_item_icon): + if default_item_icon == new_default_item_icon: + return + default_item_icon = new_default_item_icon + _refresh() +@export var item_texture_visible: bool = true : + set(new_item_texture_visible): + if item_texture_visible == new_item_texture_visible: + return + item_texture_visible = new_item_texture_visible + if is_instance_valid(_ctrl_inventory_item_rect): + _ctrl_inventory_item_rect.visible = item_texture_visible +@export var label_visible: bool = true : + set(new_label_visible): + if label_visible == new_label_visible: + return + label_visible = new_label_visible + if is_instance_valid(_label): + _label.visible = label_visible +@export_group("Icon Behavior", "icon_") +@export var icon_stretch_mode: TextureRect.StretchMode = TextureRect.StretchMode.STRETCH_KEEP_CENTERED : + set(new_icon_stretch_mode): + if icon_stretch_mode == new_icon_stretch_mode: + return + icon_stretch_mode = new_icon_stretch_mode + if is_instance_valid(_ctrl_inventory_item_rect): + _ctrl_inventory_item_rect.stretch_mode = icon_stretch_mode +@export_group("Text Behavior", "label_") +@export var label_horizontal_alignment: HorizontalAlignment = HORIZONTAL_ALIGNMENT_CENTER : + set(new_label_horizontal_alignment): + if label_horizontal_alignment == new_label_horizontal_alignment: + return + label_horizontal_alignment = new_label_horizontal_alignment + if is_instance_valid(_label): + _label.horizontal_alignment = label_horizontal_alignment +@export var label_vertical_alignment: VerticalAlignment = VERTICAL_ALIGNMENT_CENTER : + set(new_label_vertical_alignment): + if label_vertical_alignment == new_label_vertical_alignment: + return + label_vertical_alignment = new_label_vertical_alignment + if is_instance_valid(_label): + _label.vertical_alignment = label_vertical_alignment +@export var label_text_overrun_behavior: TextServer.OverrunBehavior : + set(new_label_text_overrun_behavior): + if label_text_overrun_behavior == new_label_text_overrun_behavior: + return + label_text_overrun_behavior = new_label_text_overrun_behavior + if is_instance_valid(_label): + _label.text_overrun_behavior = label_text_overrun_behavior +@export var label_clip_text: bool : + set(new_label_clip_text): + if label_clip_text == new_label_clip_text: + return + label_clip_text = new_label_clip_text + if is_instance_valid(_label): + _label.clip_text = label_clip_text +var item_slot: ItemSlotBase : + set(new_item_slot): + if new_item_slot == item_slot: + return + + _disconnect_item_slot_signals() + item_slot = new_item_slot + _connect_item_slot_signals() + + _refresh() +var _hbox_container: HBoxContainer +var _ctrl_inventory_item_rect: CtrlInventoryItemRect +var _label: Label +var _ctrl_drop_zone: CtrlDropZone + + +func _get_configuration_warnings() -> PackedStringArray: + if item_slot_path.is_empty(): + return PackedStringArray([ + "This node is not linked to an item slot, so it can't display any content.\n" + \ + "Set the item_slot_path property to point to an ItemSlotBase node."]) + return PackedStringArray() + + +func _connect_item_slot_signals() -> void: + if !is_instance_valid(item_slot): + return + + if !item_slot.item_equipped.is_connected(_refresh): + item_slot.item_equipped.connect(_refresh) + if !item_slot.cleared.is_connected(_refresh): + item_slot.cleared.connect(_refresh) + + +func _disconnect_item_slot_signals() -> void: + if !is_instance_valid(item_slot): + return + + if item_slot.item_equipped.is_connected(_refresh): + item_slot.item_equipped.disconnect(_refresh) + if item_slot.cleared.is_connected(_refresh): + item_slot.cleared.disconnect(_refresh) + + +func _ready(): + if Engine.is_editor_hint(): + # Clean up, in case it is duplicated in the editor + if is_instance_valid(_hbox_container): + _hbox_container.queue_free() + + var node: Node = get_node_or_null(item_slot_path) + if is_inside_tree() && node: + assert(node is ItemSlotBase) + item_slot = node + + _hbox_container = HBoxContainer.new() + _hbox_container.size_flags_horizontal = SIZE_EXPAND_FILL + _hbox_container.size_flags_vertical = SIZE_EXPAND_FILL + add_child(_hbox_container) + _hbox_container.resized.connect(func(): size = _hbox_container.size) + + _ctrl_inventory_item_rect = CtrlInventoryItemRect.new() + _ctrl_inventory_item_rect.visible = item_texture_visible + _ctrl_inventory_item_rect.size_flags_horizontal = SIZE_EXPAND_FILL + _ctrl_inventory_item_rect.size_flags_vertical = SIZE_EXPAND_FILL + _ctrl_inventory_item_rect.item_slot = item_slot + _ctrl_inventory_item_rect.stretch_mode = icon_stretch_mode + _ctrl_inventory_item_rect.mouse_entered.connect(_on_mouse_entered) + _ctrl_inventory_item_rect.mouse_exited.connect(_on_mouse_exited) + _hbox_container.add_child(_ctrl_inventory_item_rect) + + _ctrl_drop_zone = CtrlDropZone.new() + _ctrl_drop_zone.dragable_dropped.connect(_on_dragable_dropped) + _ctrl_drop_zone.size = size + resized.connect(func(): _ctrl_drop_zone.size = size) + CtrlDragable.dragable_grabbed.connect(_on_any_dragable_grabbed) + CtrlDragable.dragable_dropped.connect(_on_any_dragable_dropped) + add_child(_ctrl_drop_zone) + _ctrl_drop_zone.deactivate() + + _label = Label.new() + _label.visible = label_visible + _label.size_flags_horizontal = SIZE_EXPAND_FILL + _label.size_flags_vertical = SIZE_EXPAND_FILL + _label.horizontal_alignment = label_horizontal_alignment + _label.vertical_alignment = label_vertical_alignment + _label.text_overrun_behavior = label_text_overrun_behavior + _label.clip_text = label_clip_text + _hbox_container.add_child(_label) + + _hbox_container.size = size + resized.connect(func(): + _hbox_container.size = size + ) + + _refresh() + + +func _on_dragable_dropped(dragable: CtrlDragable, drop_position: Vector2) -> void: + var item = (dragable as CtrlInventoryItemRect).item + + if !item: + return + if !is_instance_valid(item_slot): + return + + if !item_slot.can_hold_item(item): + return + + if item == item_slot.get_item(): + return + + if _join_stacks(item_slot.get_item(), item): + return + + if _swap_items(item_slot.get_item(), item): + return + + item_slot.equip(item) + + +func _join_stacks(item_dst: InventoryItem, item_src: InventoryItem) -> bool: + if item_dst == null: + return false + if !is_instance_valid(item_dst.get_inventory()): + return false + if item_dst.get_inventory()._constraint_manager.get_stacks_constraint() == null: + return false + return StacksConstraint.join_stacks(item_dst, item_src) + + +func _swap_items(item1: InventoryItem, item2: InventoryItem) -> bool: + if item_slot.get_item() == null: + return false + if item_slot is ItemRefSlot: + # No support for swapping (planning to deprecate ItemRefSlot) + return false + + return InventoryItem.swap(item1, item2) + + +func _on_any_dragable_grabbed(dragable: CtrlDragable, grab_position: Vector2): + _ctrl_drop_zone.activate() + + +func _on_any_dragable_dropped(dragable: CtrlDragable, zone: CtrlDropZone, drop_position: Vector2): + _ctrl_drop_zone.deactivate() + + +func _on_mouse_entered(): + var item = item_slot.get_item() + emit_signal("item_mouse_entered", item) + + +func _on_mouse_exited(): + var item = item_slot.get_item() + emit_signal("item_mouse_exited", item) + + +func _notification(what: int) -> void: + if what == NOTIFICATION_DRAG_END: + _ctrl_drop_zone.deactivate() + + +func _refresh() -> void: + _clear() + + if !is_instance_valid(item_slot): + return + + if item_slot.get_item() == null: + return + + var item = item_slot.get_item() + if is_instance_valid(_label): + _label.text = item.get_property(InventoryItem.KEY_NAME, item.prototype_id) + if is_instance_valid(_ctrl_inventory_item_rect): + _ctrl_inventory_item_rect.item = item + if item.get_texture(): + _ctrl_inventory_item_rect.texture = item.get_texture() + + +func _clear() -> void: + if is_instance_valid(_label): + _label.text = "" + if is_instance_valid(_ctrl_inventory_item_rect): + _ctrl_inventory_item_rect.item = null + _ctrl_inventory_item_rect.texture = default_item_icon + diff --git a/addons/gloot/ui/ctrl_item_slot_ex.gd b/addons/gloot/ui/ctrl_item_slot_ex.gd new file mode 100644 index 0000000..69b508d --- /dev/null +++ b/addons/gloot/ui/ctrl_item_slot_ex.gd @@ -0,0 +1,61 @@ +@tool +@icon("res://addons/gloot/images/icon_ctrl_item_slot.svg") +class_name CtrlItemSlotEx +extends CtrlItemSlot + +@export var slot_style: StyleBox : + set(new_slot_style): + slot_style = new_slot_style + _refresh() +@export var slot_highlighted_style: StyleBox : + set(new_slot_highlighted_style): + slot_highlighted_style = new_slot_highlighted_style + _refresh() +var _background_panel: Panel + + +func _ready(): + super._ready() + resized.connect(func(): + if is_instance_valid(_background_panel): + _background_panel.size = size + ) + + +func _refresh() -> void: + super._refresh() + _update_background() + + +func _update_background() -> void: + if !is_instance_valid(_background_panel): + _background_panel = Panel.new() + add_child(_background_panel) + move_child(_background_panel, 0) + + _background_panel.size = size + _background_panel.show() + if slot_style: + _set_panel_style(_background_panel, slot_style) + else: + _background_panel.hide() + + +func _set_panel_style(panel: Panel, style: StyleBox) -> void: + panel.remove_theme_stylebox_override("panel") + if style != null: + panel.add_theme_stylebox_override("panel", style) + + +func _on_mouse_entered(): + if slot_highlighted_style: + _set_panel_style(_background_panel, slot_highlighted_style) + super._on_mouse_entered() + + +func _on_mouse_exited(): + if slot_style: + _set_panel_style(_background_panel, slot_style) + else: + _background_panel.hide() + super._on_mouse_exited() diff --git a/project.godot b/project.godot index 75bc2f0..9f1b82d 100644 --- a/project.godot +++ b/project.godot @@ -25,7 +25,7 @@ Globals="*res://scripts/globals.gd" [editor_plugins] -enabled=PackedStringArray("res://addons/label_font_auto_sizer/plugin.cfg", "res://addons/phantom_camera/plugin.cfg", "res://addons/save_system/plugin.cfg", "res://addons/scene_manager/plugin.cfg", "res://addons/script-ide/plugin.cfg") +enabled=PackedStringArray("res://addons/gloot/plugin.cfg", "res://addons/label_font_auto_sizer/plugin.cfg", "res://addons/phantom_camera/plugin.cfg", "res://addons/save_system/plugin.cfg", "res://addons/scene_manager/plugin.cfg", "res://addons/script-ide/plugin.cfg") [input] -- 2.30.2