added inventory plugin
authorEduardo <[email protected]>
Fri, 2 Aug 2024 13:36:14 +0000 (15:36 +0200)
committerEduardo <[email protected]>
Fri, 2 Aug 2024 13:36:14 +0000 (15:36 +0200)
87 files changed:
addons/gloot/LICENSE [new file with mode: 0644]
addons/gloot/core/constraints/constraint_manager.gd [new file with mode: 0644]
addons/gloot/core/constraints/grid_constraint.gd [new file with mode: 0644]
addons/gloot/core/constraints/inventory_constraint.gd [new file with mode: 0644]
addons/gloot/core/constraints/item_map.gd [new file with mode: 0644]
addons/gloot/core/constraints/quadtree.gd [new file with mode: 0644]
addons/gloot/core/constraints/stacks_constraint.gd [new file with mode: 0644]
addons/gloot/core/constraints/weight_constraint.gd [new file with mode: 0644]
addons/gloot/core/inventory.gd [new file with mode: 0644]
addons/gloot/core/inventory_grid.gd [new file with mode: 0644]
addons/gloot/core/inventory_grid_stacked.gd [new file with mode: 0644]
addons/gloot/core/inventory_item.gd [new file with mode: 0644]
addons/gloot/core/inventory_stacked.gd [new file with mode: 0644]
addons/gloot/core/item_count.gd [new file with mode: 0644]
addons/gloot/core/item_protoset.gd [new file with mode: 0644]
addons/gloot/core/item_ref_slot.gd [new file with mode: 0644]
addons/gloot/core/item_slot.gd [new file with mode: 0644]
addons/gloot/core/item_slot_base.gd [new file with mode: 0644]
addons/gloot/core/utils.gd [new file with mode: 0644]
addons/gloot/core/verify.gd [new file with mode: 0644]
addons/gloot/editor/common/choice_filter.gd [new file with mode: 0644]
addons/gloot/editor/common/choice_filter.tscn [new file with mode: 0644]
addons/gloot/editor/common/choice_filter_test.tscn [new file with mode: 0644]
addons/gloot/editor/common/dict_editor.gd [new file with mode: 0644]
addons/gloot/editor/common/dict_editor.tscn [new file with mode: 0644]
addons/gloot/editor/common/dict_editor_test.tscn [new file with mode: 0644]
addons/gloot/editor/common/editor_icons.gd [new file with mode: 0644]
addons/gloot/editor/common/multivalue_editor.gd [new file with mode: 0644]
addons/gloot/editor/common/value_editor.gd [new file with mode: 0644]
addons/gloot/editor/gloot_undo_redo.gd [new file with mode: 0644]
addons/gloot/editor/inventory_editor/inventory_editor.gd [new file with mode: 0644]
addons/gloot/editor/inventory_editor/inventory_editor.tscn [new file with mode: 0644]
addons/gloot/editor/inventory_editor/inventory_inspector.gd [new file with mode: 0644]
addons/gloot/editor/inventory_editor/inventory_inspector.tscn [new file with mode: 0644]
addons/gloot/editor/inventory_inspector_plugin.gd [new file with mode: 0644]
addons/gloot/editor/item_editor/edit_properties_button.gd [new file with mode: 0644]
addons/gloot/editor/item_editor/edit_prototype_id_button.gd [new file with mode: 0644]
addons/gloot/editor/item_editor/properties_editor.gd [new file with mode: 0644]
addons/gloot/editor/item_editor/properties_editor.tscn [new file with mode: 0644]
addons/gloot/editor/item_editor/prototype_id_editor.gd [new file with mode: 0644]
addons/gloot/editor/item_editor/prototype_id_editor.tscn [new file with mode: 0644]
addons/gloot/editor/item_slot_editor/item_ref_slot_button.gd [new file with mode: 0644]
addons/gloot/editor/item_slot_editor/item_slot_editor.gd [new file with mode: 0644]
addons/gloot/editor/item_slot_editor/item_slot_editor.tscn [new file with mode: 0644]
addons/gloot/editor/item_slot_editor/item_slot_inspector.gd [new file with mode: 0644]
addons/gloot/editor/item_slot_editor/item_slot_inspector.tscn [new file with mode: 0644]
addons/gloot/editor/protoset_editor/edit_protoset_button.gd [new file with mode: 0644]
addons/gloot/editor/protoset_editor/edit_protoset_button.tscn [new file with mode: 0644]
addons/gloot/editor/protoset_editor/protoset_editor.gd [new file with mode: 0644]
addons/gloot/editor/protoset_editor/protoset_editor.tscn [new file with mode: 0644]
addons/gloot/gloot.gd [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory.svg [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory.svg.import [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory_grid.svg [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory_grid.svg.import [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory_stacked.svg [new file with mode: 0644]
addons/gloot/images/icon_ctrl_inventory_stacked.svg.import [new file with mode: 0644]
addons/gloot/images/icon_ctrl_item_slot.svg [new file with mode: 0644]
addons/gloot/images/icon_ctrl_item_slot.svg.import [new file with mode: 0644]
addons/gloot/images/icon_inventory.svg [new file with mode: 0644]
addons/gloot/images/icon_inventory.svg.import [new file with mode: 0644]
addons/gloot/images/icon_inventory_grid.svg [new file with mode: 0644]
addons/gloot/images/icon_inventory_grid.svg.import [new file with mode: 0644]
addons/gloot/images/icon_inventory_grid_stacked.svg [new file with mode: 0644]
addons/gloot/images/icon_inventory_grid_stacked.svg.import [new file with mode: 0644]
addons/gloot/images/icon_inventory_stacked.svg [new file with mode: 0644]
addons/gloot/images/icon_inventory_stacked.svg.import [new file with mode: 0644]
addons/gloot/images/icon_item.svg [new file with mode: 0644]
addons/gloot/images/icon_item.svg.import [new file with mode: 0644]
addons/gloot/images/icon_item_protoset.svg [new file with mode: 0644]
addons/gloot/images/icon_item_protoset.svg.import [new file with mode: 0644]
addons/gloot/images/icon_item_ref_slot.svg [new file with mode: 0644]
addons/gloot/images/icon_item_ref_slot.svg.import [new file with mode: 0644]
addons/gloot/images/icon_item_slot.svg [new file with mode: 0644]
addons/gloot/images/icon_item_slot.svg.import [new file with mode: 0644]
addons/gloot/plugin.cfg [new file with mode: 0644]
addons/gloot/ui/ctrl_dragable.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_drop_zone.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory_grid.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory_grid_basic.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory_grid_ex.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory_item_rect.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_inventory_stacked.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_item_slot.gd [new file with mode: 0644]
addons/gloot/ui/ctrl_item_slot_ex.gd [new file with mode: 0644]
project.godot

diff --git a/addons/gloot/LICENSE b/addons/gloot/LICENSE
new file mode 100644 (file)
index 0000000..e9b4efd
--- /dev/null
@@ -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 (file)
index 0000000..82a038b
--- /dev/null
@@ -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 (file)
index 0000000..9a57c8f
--- /dev/null
@@ -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 (file)
index 0000000..77f2dfc
--- /dev/null
@@ -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 (file)
index 0000000..eee718b
--- /dev/null
@@ -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 (file)
index 0000000..b610e78
--- /dev/null
@@ -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 (file)
index 0000000..705162e
--- /dev/null
@@ -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 (file)
index 0000000..84b3cf5
--- /dev/null
@@ -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 (file)
index 0000000..53627a1
--- /dev/null
@@ -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 (file)
index 0000000..4e519eb
--- /dev/null
@@ -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 (file)
index 0000000..934775a
--- /dev/null
@@ -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 (file)
index 0000000..8e8919a
--- /dev/null
@@ -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 (file)
index 0000000..cca608f
--- /dev/null
@@ -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 (file)
index 0000000..4e3493c
--- /dev/null
@@ -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 (file)
index 0000000..86e9f85
--- /dev/null
@@ -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 (file)
index 0000000..e83f6d8
--- /dev/null
@@ -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 (file)
index 0000000..e780217
--- /dev/null
@@ -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 (file)
index 0000000..0c9679e
--- /dev/null
@@ -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 (file)
index 0000000..a4294ea
--- /dev/null
@@ -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 (file)
index 0000000..db6679d
--- /dev/null
@@ -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 (file)
index 0000000..77d51a6
--- /dev/null
@@ -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 (file)
index 0000000..561f8bb
--- /dev/null
@@ -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 (file)
index 0000000..062709d
--- /dev/null
@@ -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 (file)
index 0000000..3801161
--- /dev/null
@@ -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 (file)
index 0000000..f43fc71
--- /dev/null
@@ -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 (file)
index 0000000..4907376
--- /dev/null
@@ -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 (file)
index 0000000..faed61e
--- /dev/null
@@ -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 (file)
index 0000000..cd789ab
--- /dev/null
@@ -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 (file)
index 0000000..1fa69c9
--- /dev/null
@@ -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 (file)
index 0000000..efe6090
--- /dev/null
@@ -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 (file)
index 0000000..0fb7d51
--- /dev/null
@@ -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 (file)
index 0000000..f67fa23
--- /dev/null
@@ -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 (file)
index 0000000..8ffcea7
--- /dev/null
@@ -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 (file)
index 0000000..db8edf2
--- /dev/null
@@ -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 (file)
index 0000000..fa09e9f
--- /dev/null
@@ -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 (file)
index 0000000..e0d5342
--- /dev/null
@@ -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 (file)
index 0000000..1e3b6a3
--- /dev/null
@@ -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 (file)
index 0000000..0ccee74
--- /dev/null
@@ -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 (file)
index 0000000..95842d5
--- /dev/null
@@ -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 (file)
index 0000000..ed7f6a8
--- /dev/null
@@ -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 (file)
index 0000000..c73b651
--- /dev/null
@@ -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 (file)
index 0000000..0e1abfe
--- /dev/null
@@ -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 (file)
index 0000000..a162103
--- /dev/null
@@ -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 (file)
index 0000000..67a00ff
--- /dev/null
@@ -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 (file)
index 0000000..ed0d07b
--- /dev/null
@@ -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 (file)
index 0000000..d5fcc8f
--- /dev/null
@@ -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 (file)
index 0000000..408ff63
--- /dev/null
@@ -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 (file)
index 0000000..57d03d3
--- /dev/null
@@ -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 (file)
index 0000000..f880efc
--- /dev/null
@@ -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 (file)
index 0000000..77aca4e
--- /dev/null
@@ -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 (file)
index 0000000..dde288b
--- /dev/null
@@ -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 (file)
index 0000000..bc3c078
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_ctrl_inventory.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="7.375"
+     inkscape:cx="-7.9357232"
+     inkscape:cy="19.258617"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <path
+     style="fill:#00e436;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M -1e-7,3.9999998 V 12 l 7.9999998,4 V 7.9999997 Z"
+     id="path818"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#008751;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,7.9999997 16,3.9999998 V 12 l -8.0000003,4 z"
+     id="path820"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#008751;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,-1e-7 -1e-7,3.9999998 7.9999997,7.9999997 Z"
+     id="path1432"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="cccc" />
+  <path
+     style="fill:#00e436;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,-1e-7 16,3.9999998 7.9999997,7.9999997 Z"
+     id="path1434"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="cccc" />
+</svg>
diff --git a/addons/gloot/images/icon_ctrl_inventory.svg.import b/addons/gloot/images/icon_ctrl_inventory.svg.import
new file mode 100644 (file)
index 0000000..e0564b6
--- /dev/null
@@ -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 (file)
index 0000000..c21532b
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_ctrl_inventory_grid.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="20.85965"
+     inkscape:cx="15.770515"
+     inkscape:cy="6.878746"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520"
+       spacingx="1"
+       spacingy="1" />
+  </sodipodi:namedview>
+  <path
+     style="opacity:1;fill:#00e436;fill-opacity:1;stroke:none;stroke-width:0.06666668;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+     d="M 8.0000003,3.9999999 0,8 8.0000003,12 16,8 Z m 0,1.3333334 L 10,6.3333334 l -1.9999997,1 -2,-1 z m 3.3333337,1.6666668 2,0.9999999 -2,0.9999999 L 9.333334,8 Z m -6.6666671,0 L 6.666667,8 4.6666669,8.9999999 2.6666669,8 Z M 8.0000003,8.6666667 10,9.6666669 8.0000003,10.666666 l -2,-0.9999991 z"
+     id="rect2092"
+     inkscape:connector-curvature="0" />
+</svg>
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 (file)
index 0000000..707a9e0
--- /dev/null
@@ -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 (file)
index 0000000..4f28b24
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_ctrl_inventory_stacked.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="29.5"
+     inkscape:cx="7.4306916"
+     inkscape:cy="0.86544775"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <g
+     id="g6871">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path1436"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#00e436;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path818"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#008751;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path820"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#1d2b53;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g6879"
+     transform="translate(0,-5)">
+    <path
+       style="fill:#00e436;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       id="path6873"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#008751;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       id="path6875"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#1d2b53;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       id="path6877"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
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 (file)
index 0000000..e222e86
--- /dev/null
@@ -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 (file)
index 0000000..423d9ec
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_ctrl_item_slot.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="29.5"
+     inkscape:cx="5.2005339"
+     inkscape:cy="13.100097"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <path
+     style="fill:none;stroke:#00e436;stroke-width:1.16666663px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 1,4.5 v 7 l 7,3.5 7,-3.5 v -7 L 8,1 Z"
+     id="path835"
+     inkscape:connector-curvature="0" />
+</svg>
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 (file)
index 0000000..f6befd8
--- /dev/null
@@ -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 (file)
index 0000000..1f41da8
--- /dev/null
@@ -0,0 +1,81 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_inventory.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="7.375"
+     inkscape:cx="-7.9357232"
+     inkscape:cy="19.258617"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <path
+     style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M -1e-7,3.9999998 V 12 l 7.9999998,4 V 7.9999997 Z"
+     id="path818"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,7.9999997 16,3.9999998 V 12 l -8.0000003,4 z"
+     id="path820"
+     inkscape:connector-curvature="0" />
+  <path
+     style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,-1e-7 -1e-7,3.9999998 7.9999997,7.9999997 Z"
+     id="path1432"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="cccc" />
+  <path
+     style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1.33333325px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="M 7.9999997,-1e-7 16,3.9999998 7.9999997,7.9999997 Z"
+     id="path1434"
+     inkscape:connector-curvature="0"
+     sodipodi:nodetypes="cccc" />
+</svg>
diff --git a/addons/gloot/images/icon_inventory.svg.import b/addons/gloot/images/icon_inventory.svg.import
new file mode 100644 (file)
index 0000000..dadd703
--- /dev/null
@@ -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 (file)
index 0000000..38e05f7
--- /dev/null
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_inventory_grid.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="41.7193"
+     inkscape:cx="10.1668"
+     inkscape:cy="7.4508754"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520"
+       spacingx="1"
+       spacingy="1" />
+  </sodipodi:namedview>
+  <path
+     style="opacity:1;fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:0.06666668;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
+     d="M 8.0000003,3.9999999 0,8 8.0000003,12 16,8 Z m 0,1.3333334 L 10,6.3333334 l -1.9999997,1 -2,-1 z m 3.3333337,1.6666668 2,0.9999999 -2,0.9999999 L 9.333334,8 Z m -6.6666671,0 L 6.666667,8 4.6666669,8.9999999 2.6666669,8 Z M 8.0000003,8.6666667 10,9.6666669 8.0000003,10.666666 l -2,-0.9999991 z"
+     id="rect2092"
+     inkscape:connector-curvature="0" />
+</svg>
diff --git a/addons/gloot/images/icon_inventory_grid.svg.import b/addons/gloot/images/icon_inventory_grid.svg.import
new file mode 100644 (file)
index 0000000..0508e6f
--- /dev/null
@@ -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 (file)
index 0000000..75e6b60
--- /dev/null
@@ -0,0 +1,193 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_inventory_grid_stacked.svg"
+   inkscape:version="1.2.2 (732a01da63, 2022-12-09)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1377"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="41.7193"
+     inkscape:cx="5.5250208"
+     inkscape:cy="7.4785531"
+     inkscape:window-x="-8"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true"
+     inkscape:showpageshadow="2"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <g
+     id="g5732"
+     transform="matrix(0.4375,0,0,0.4375,0,4.75)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path1436"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path818"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path820"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g2341"
+     transform="matrix(0.4375,0,0,0.4375,9,4.75)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2335"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2337"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2339"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g2349"
+     transform="matrix(0.4375,0,0,0.4375,4.5,7)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2343"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2345"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2347"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g2357"
+     transform="matrix(0.4375,0,0,0.4375,9,2.75)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2351"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2353"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2355"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g2365"
+     transform="matrix(0.4375,0,0,0.4375,0,2.75)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2359"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2361"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2363"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g2373"
+     transform="matrix(0.4375,0,0,0.4375,0,0.75)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2367"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2369"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path2371"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
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 (file)
index 0000000..3c371fe
--- /dev/null
@@ -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 (file)
index 0000000..c5f3471
--- /dev/null
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_inventory_stacked.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="5.2149125"
+     inkscape:cx="-41.453203"
+     inkscape:cy="37.560901"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <g
+     id="g5732">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path1436"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path818"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path820"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+  <g
+     id="g845"
+     transform="translate(0,-5)">
+    <path
+       style="fill:#ffec27;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="M 0,9 8,5 16,9 8,15 Z"
+       id="path839"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#ffa300;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 0,9 v 3 l 8,4 v -3 z"
+       id="path841"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+    <path
+       style="fill:#ab5236;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 8,13 8,-4 v 3 l -8,4 z"
+       id="path843"
+       inkscape:connector-curvature="0"
+       sodipodi:nodetypes="ccccc" />
+  </g>
+</svg>
diff --git a/addons/gloot/images/icon_inventory_stacked.svg.import b/addons/gloot/images/icon_inventory_stacked.svg.import
new file mode 100644 (file)
index 0000000..67a4717
--- /dev/null
@@ -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 (file)
index 0000000..dc265c1
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_item.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="29.5"
+     inkscape:cx="13.828609"
+     inkscape:cy="10.287679"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <g
+     id="g8594"
+     transform="matrix(1.3333333,0,0,1.3333333,-2.6666667,-2.6666667)">
+    <path
+       sodipodi:nodetypes="ccccc"
+       inkscape:connector-curvature="0"
+       id="path822"
+       d="M 2,5 8,2 14,5 8,12.372881 Z"
+       style="fill:#ffffff;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path818"
+       d="m 2,5 v 6 l 6,3 V 8 Z"
+       style="fill:#29adff;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path820"
+       d="m 8,8 6,-3 v 6 l -6,3 z"
+       style="fill:#1d2b53;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/addons/gloot/images/icon_item.svg.import b/addons/gloot/images/icon_item.svg.import
new file mode 100644 (file)
index 0000000..87f2f07
--- /dev/null
@@ -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 (file)
index 0000000..159b4ac
--- /dev/null
@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_item_protoset.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="59"
+     inkscape:cx="7.6342845"
+     inkscape:cy="9.1659924"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <g
+     id="g9161"
+     transform="matrix(1.1666667,0,0,1.1666667,-1.3333333,-1.3333333)">
+    <path
+       inkscape:connector-curvature="0"
+       id="path835"
+       d="m 2,5 v 6 l 6,3 6,-3 V 5 L 8,2 Z"
+       style="fill:none;stroke:#c2c3c7;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path837"
+       d="m 2,5 6,3 v 6"
+       style="fill:none;stroke:#c2c3c7;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+    <path
+       inkscape:connector-curvature="0"
+       id="path839"
+       d="M 8,8 14,5"
+       style="fill:none;stroke:#c2c3c7;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+  </g>
+</svg>
diff --git a/addons/gloot/images/icon_item_protoset.svg.import b/addons/gloot/images/icon_item_protoset.svg.import
new file mode 100644 (file)
index 0000000..816fddd
--- /dev/null
@@ -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 (file)
index 0000000..c5da49e
--- /dev/null
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_item_ref_slot.svg"
+   inkscape:version="1.3.2 (091e20e, 2023-11-25)"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:dc="http://purl.org/dc/elements/1.1/">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2560"
+     inkscape:window-height="1368"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="29.5"
+     inkscape:cx="3.0847458"
+     inkscape:cy="9.0169492"
+     inkscape:window-x="0"
+     inkscape:window-y="0"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true"
+     inkscape:showpageshadow="0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#505050">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520"
+       originx="0"
+       originy="0"
+       spacingy="1"
+       spacingx="1"
+       units="px"
+       visible="true" />
+  </sodipodi:namedview>
+  <path
+     style="fill:none;stroke:#29adff;stroke-width:1.16666663px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 1,4.5 v 7 l 7,3.5 7,-3.5 v -7 L 8,1 Z"
+     id="path835"
+     inkscape:connector-curvature="0" />
+  <path
+     style="font-weight:bold;font-size:16px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans Bold';fill:#29adff"
+     d="M 11.863281,6.6523436 9.2851565,8.0039061 11.863281,9.3632811 11.269531,10.464844 8.667969,9.0273436 V 11.714844 H 7.339844 V 9.0273436 L 4.730469,10.464844 4.136719,9.3632811 6.746094,8.0039061 4.136719,6.6523436 4.730469,5.5507811 7.339844,6.9726561 v -2.6875 h 1.328125 v 2.6875 l 2.601562,-1.421875 z"
+     id="text1"
+     aria-label="*" />
+</svg>
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 (file)
index 0000000..ceb9a0d
--- /dev/null
@@ -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 (file)
index 0000000..d516908
--- /dev/null
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   height="16"
+   viewBox="0 0 16 16"
+   width="16"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="icon_item_slot.svg"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="2498"
+     inkscape:window-height="1417"
+     id="namedview6"
+     showgrid="true"
+     inkscape:zoom="41.7193"
+     inkscape:cx="9.8330418"
+     inkscape:cy="10.97595"
+     inkscape:window-x="54"
+     inkscape:window-y="-8"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg4"
+     inkscape:snap-grids="true"
+     inkscape:snap-bbox="true"
+     inkscape:snap-center="true"
+     inkscape:snap-others="true">
+    <inkscape:grid
+       type="xygrid"
+       id="grid4520" />
+  </sodipodi:namedview>
+  <path
+     style="fill:none;stroke:#29adff;stroke-width:1.16666663px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+     d="m 1,4.5 v 7 l 7,3.5 7,-3.5 v -7 L 8,1 Z"
+     id="path835"
+     inkscape:connector-curvature="0" />
+</svg>
diff --git a/addons/gloot/images/icon_item_slot.svg.import b/addons/gloot/images/icon_item_slot.svg.import
new file mode 100644 (file)
index 0000000..4027698
--- /dev/null
@@ -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 (file)
index 0000000..0ba1162
--- /dev/null
@@ -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 (file)
index 0000000..a6acee1
--- /dev/null
@@ -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 (file)
index 0000000..ec8579a
--- /dev/null
@@ -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 (file)
index 0000000..facc1ec
--- /dev/null
@@ -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 (file)
index 0000000..d7fbb76
--- /dev/null
@@ -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 (file)
index 0000000..3e6af5c
--- /dev/null
@@ -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 (file)
index 0000000..69c544f
--- /dev/null
@@ -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 (file)
index 0000000..0a77b89
--- /dev/null
@@ -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 (file)
index 0000000..56f5b58
--- /dev/null
@@ -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 (file)
index 0000000..95a9888
--- /dev/null
@@ -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 (file)
index 0000000..69b508d
--- /dev/null
@@ -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()
index 75bc2f00cb94ae72e2bea8a86ac852b75e324c35..9f1b82d5a9e7e4ee97030dec702b76124919c20c 100644 (file)
@@ -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]