--- /dev/null
+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.
--- /dev/null
+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
--- /dev/null
+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
+
--- /dev/null
+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
--- /dev/null
+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())
+
--- /dev/null
+
+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()
--- /dev/null
+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
+
--- /dev/null
+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
+
+
--- /dev/null
+@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
+
--- /dev/null
+@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()
+
--- /dev/null
+@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)
+
--- /dev/null
+@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
--- /dev/null
+@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)
--- /dev/null
+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()
--- /dev/null
+@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
--- /dev/null
+@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
+
--- /dev/null
+@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
+
--- /dev/null
+@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
--- /dev/null
+
+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
--- /dev/null
+
+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)
--- /dev/null
+@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()
--- /dev/null
+[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
--- /dev/null
+[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"])
--- /dev/null
+@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()
--- /dev/null
+[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"
--- /dev/null
+[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)
+}
--- /dev/null
+@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
--- /dev/null
+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)
--- /dev/null
+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
--- /dev/null
+@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()
+
--- /dev/null
+@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)
+
--- /dev/null
+[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"
--- /dev/null
+@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
--- /dev/null
+[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
--- /dev/null
+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
+
--- /dev/null
+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)
+
--- /dev/null
+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 = ""
+
--- /dev/null
+@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
+
--- /dev/null
+[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
--- /dev/null
+@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()
+
--- /dev/null
+[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:"
--- /dev/null
+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
--- /dev/null
+@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)
+
--- /dev/null
+[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")
--- /dev/null
+@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
--- /dev/null
+[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
--- /dev/null
+@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))
+
--- /dev/null
+[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
--- /dev/null
+@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)
--- /dev/null
+[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 = {}
--- /dev/null
+@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)
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+<?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>
--- /dev/null
+[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
--- /dev/null
+[plugin]
+
+name="GLoot"
+description="A universal inventory system for the Godot game engine"
+author="Peter Kish"
+version="2.4.8"
+script="gloot.gd"
--- /dev/null
+@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
--- /dev/null
+@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)
+
+
--- /dev/null
+@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
+
--- /dev/null
+@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()
+
--- /dev/null
+@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)
+ )
+
--- /dev/null
+@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()
+
--- /dev/null
+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()
+
--- /dev/null
+@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
+
--- /dev/null
+@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
+
--- /dev/null
+@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()
[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]