sugary-panda (#1)

Reviewed-on: #1
Co-authored-by: Bryce <bryce@brycecovertoperations.com>
Co-committed-by: Bryce <bryce@brycecovertoperations.com>
This commit was merged in pull request #1.
This commit is contained in:
2026-04-28 22:05:11 -07:00
committed by notid
parent dee6216873
commit 639060fa7f
30 changed files with 2402 additions and 6 deletions

View File

@@ -0,0 +1,101 @@
extends Node
# Signals
signal item_acquired(item_id: String)
signal item_removed(item_id: String)
signal inventory_changed
signal combination_attempted(item_a_id: String, item_b_id: String)
# State
var inventory: Array[String] = []
var obtained_items: Dictionary = {}
var stripped_items: Array[String] = []
var selected_item: String = ""
# Item definitions registry
var _item_definitions: Dictionary = {}
# -- Queries --
func has_item(item_id: String) -> bool:
return inventory.has(item_id)
func has_obtained(item_id: String) -> bool:
return obtained_items.has(item_id)
func has_any_of(item_ids: Array[String]) -> bool:
if item_ids.is_empty():
return false
for item_id in item_ids:
if has_item(item_id):
return true
return false
func has_obtained_any_of(item_ids: Array[String]) -> bool:
if item_ids.is_empty():
return false
for item_id in item_ids:
if has_obtained(item_id):
return true
return false
func has_obtained_all_of(item_ids: Array[String]) -> bool:
if item_ids.is_empty():
return true
for item_id in item_ids:
if not has_obtained(item_id):
return false
return true
# -- Mutations --
func acquire_item(item_id: String) -> void:
inventory.append(item_id)
obtained_items[item_id] = true
item_acquired.emit(item_id)
inventory_changed.emit()
func remove_item(item_id: String, quiet: bool = false) -> void:
var idx = inventory.find(item_id)
if idx == -1:
return
inventory.remove_at(idx)
if not quiet:
item_removed.emit(item_id)
inventory_changed.emit()
func bulk_strip_items(event_items: Array[String], exempt_items: Array[String] = []) -> void:
var remaining: Array[String] = []
for item_id in inventory:
if event_items.has(item_id) and not exempt_items.has(item_id):
stripped_items.append(item_id)
else:
remaining.append(item_id)
inventory = remaining
inventory_changed.emit()
# -- Items --
func register_item(item_def: ItemDefinition) -> void:
_item_definitions[item_def.id] = item_def
func get_item_definition(item_id: String) -> ItemDefinition:
return _item_definitions.get(item_id, null)
# -- Combinations --
func attempt_combine(item_a_id: String, item_b_id: String) -> void:
combination_attempted.emit(item_a_id, item_b_id)
# -- Selection --
func select_item(item_id: String) -> void:
selected_item = item_id
func clear_selection() -> void:
selected_item = ""

View File

@@ -0,0 +1 @@
uid://2am6u2ux8s96o

View File

@@ -0,0 +1,7 @@
extends Resource
class_name ItemDefinition
@export var id: String = ""
@export var name: String = ""
@export var icon: Texture2D
@export var combination_category: String = ""

View File

@@ -0,0 +1 @@
uid://34ph99jcuowua

View File

@@ -0,0 +1,238 @@
extends Control
class_name InventoryBackpack
signal overlay_show_requested
signal overlay_hide_requested
signal item_selected(item_id: String)
signal returning_to_idle
signal skip_action_requested
enum State { IDLE, OPEN, SELECTED, ACQUIRE, REMOVE }
var _state: State = State.IDLE
var _animating: bool = false
var _active_tween: Tween = null
var _floating_item_color: Color = Color(1, 1, 1, 0)
var _home_position: Vector2 = Vector2(0, 0)
@onready var backpack_icon: ColorRect = $BackpackIcon
@onready var floating_item: ColorRect = $FloatingItem
@onready var animation_player: AnimationPlayer = $AnimationPlayer
func _ready() -> void:
_home_position = backpack_icon.position
floating_item.modulate = Color(1, 1, 1, 0)
floating_item.visible = false
InventoryManager.item_acquired.connect(_on_item_acquired)
InventoryManager.item_removed.connect(_on_item_removed)
InventoryManager.inventory_changed.connect(_on_inventory_changed)
_update_floating_item()
func get_state() -> State:
return _state
func is_busy() -> bool:
return _animating
func _update_floating_item() -> void:
if InventoryManager.selected_item:
var def = InventoryManager.get_item_definition(InventoryManager.selected_item)
if def:
floating_item.visible = true
floating_item.modulate = Color(1, 0.6, 0.2, 1)
else:
floating_item.visible = true
floating_item.modulate = Color(0.8, 0.8, 0.8, 1)
else:
floating_item.visible = false
func transition_to(new_state: State) -> void:
if _animating and new_state != _state:
_kill_tween()
match new_state:
State.IDLE:
_transition_to_idle()
State.OPEN:
_transition_to_open()
State.SELECTED:
_transition_to_selected()
State.ACQUIRE:
_transition_to_acquire()
State.REMOVE:
_transition_to_remove()
func _transition_to_idle() -> void:
if _state == State.IDLE:
return
_set_state(State.IDLE)
overlay_hide_requested.emit()
backpack_icon.modulate = Color(1, 1, 1, 1)
floating_item.visible = false
var tween = create_tween().bind_node(self)
_active_tween = tween
_animating = true
tween.tween_property(backpack_icon, "rotation", 0.0, 0.35).set_trans(Tween.TRANS_LINEAR)
tween.tween_property(backpack_icon, "position", _home_position, 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
tween.tween_callback(_on_transition_complete.bind(State.IDLE))
func _transition_to_open() -> void:
var can_open = _check_guards()
if not can_open:
if _state == State.IDLE:
skip_action_requested.emit()
return
_set_state(State.OPEN)
var tween = create_tween().bind_node(self)
_active_tween = tween
_animating = true
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
tween.tween_property(backpack_icon, "position", _home_position + Vector2(20, 20), 0.35).set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
tween.tween_callback(_on_transition_complete.bind(State.OPEN))
tween.tween_callback(overlay_show_requested.emit)
func _transition_to_selected() -> void:
if _state == State.SELECTED:
return
_set_state(State.SELECTED)
_update_floating_item()
var tween = create_tween().bind_node(self)
_active_tween = tween
_animating = true
floating_item.visible = true
floating_item.modulate = Color(1, 1, 1, 0)
tween.tween_property(floating_item, "modulate", Color(1, 1, 1, 1), 0.5).set_trans(Tween.TRANS_LINEAR)
tween.tween_callback(_on_transition_complete.bind(State.SELECTED))
func _transition_to_acquire() -> void:
if _state == State.ACQUIRE or _state == State.REMOVE:
_kill_tween()
_set_state(State.ACQUIRE)
var tween = create_tween().bind_node(self)
_active_tween = tween
_animating = true
backpack_icon.modulate = Color(1, 1, 1, 1)
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
tween.tween_callback(_start_acquire_item_anim)
func _start_acquire_item_anim() -> void:
var tween = create_tween().bind_node(self)
_active_tween = tween
floating_item.visible = true
floating_item.modulate = Color(1, 1, 1, 0)
floating_item.position = Vector2(backpack_icon.position.x - 20, backpack_icon.position.y - 40)
tween.tween_property(floating_item, "modulate", Color(1, 1, 1, 1), 0.5).set_trans(Tween.TRANS_LINEAR)
tween.tween_property(floating_item, "position", backpack_icon.position, 0.5).set_trans(Tween.TRANS_LINEAR)
tween.tween_callback(_on_acquire_complete)
func _on_acquire_complete() -> void:
floating_item.visible = false
transition_to(State.IDLE)
func _transition_to_remove() -> void:
if _state == State.ACQUIRE or _state == State.REMOVE:
_kill_tween()
_set_state(State.REMOVE)
var tween = create_tween().bind_node(self)
_active_tween = tween
_animating = true
backpack_icon.modulate = Color(1, 1, 1, 1)
tween.tween_property(backpack_icon, "rotation", PI / 4, 0.35).set_trans(Tween.TRANS_LINEAR)
tween.tween_callback(_start_remove_item_anim)
func _start_remove_item_anim() -> void:
var tween = create_tween().bind_node(self)
_active_tween = tween
floating_item.visible = true
floating_item.modulate = Color(1, 1, 1, 1)
floating_item.position = backpack_icon.position
tween.tween_property(floating_item, "position", Vector2(backpack_icon.position.x - 20, backpack_icon.position.y - 80), 0.5).set_trans(Tween.TRANS_LINEAR)
tween.parallel().tween_property(floating_item, "modulate", Color(1, 1, 1, 0), 0.5).set_trans(Tween.TRANS_LINEAR)
tween.tween_callback(_on_remove_complete)
func _on_remove_complete() -> void:
floating_item.visible = false
var was_selected = InventoryManager.selected_item != ""
transition_to(State.IDLE)
if was_selected:
InventoryManager.clear_selection()
func _set_state(new_state: State) -> void:
_state = new_state
func _check_guards() -> bool:
var main_game = get_node_or_null("/root/Node2D")
if not main_game:
return true
if main_game.is_script_running:
return false
var fade = get_node_or_null("/root/Node2D/SceneDisplay/Fade")
if fade and fade.modulate.a > 0.5:
return false
return true
func _kill_tween() -> void:
if _active_tween:
_active_tween.kill()
_active_tween = null
_animating = false
func _on_transition_complete(completed_state: State) -> void:
_animating = false
_active_tween = null
if completed_state == State.OPEN:
pass
elif completed_state == State.IDLE:
returning_to_idle.emit()
func _on_item_acquired(item_id: String) -> void:
if _state == State.IDLE:
transition_to(State.ACQUIRE)
func _on_item_removed(item_id: String) -> void:
if _state == State.IDLE:
if InventoryManager.selected_item == item_id:
InventoryManager.clear_selection()
_update_floating_item()
transition_to(State.REMOVE)
else:
transition_to(State.REMOVE)
elif _state == State.SELECTED and InventoryManager.selected_item == item_id:
InventoryManager.clear_selection()
_update_floating_item()
transition_to(State.REMOVE)
func _on_inventory_changed() -> void:
_update_floating_item()
func _gui_input(event: InputEvent) -> void:
print("[BACKPACK] _gui_input: %s, state=%s" % [event, State.keys()[_state]])
if event is InputEventMouseButton and event.pressed and event.button_index == 1:
if _state == State.IDLE:
print("[BACKPACK] transitioning to OPEN")
transition_to(State.OPEN)
elif _state == State.OPEN:
print("[BACKPACK] transitioning to IDLE")
transition_to(State.IDLE)
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
_kill_tween()

View File

@@ -0,0 +1 @@
uid://2x3g0ethsdcgo

View File

@@ -0,0 +1,30 @@
[gd_scene format=3 uid="uid://dxkyfas46q7ef"]
[ext_resource type="Script" uid="uid://v8du0eptw65c" path="res://inventory/inventory_backpack/InventoryBackpack.gd" id="1"]
[node name="InventoryBackpack" type="Control" unique_id=1000000001]
layout_mode = 3
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
offset_top = -70.0
offset_right = 70.0
grow_vertical = 0
script = ExtResource("1")
[node name="BackpackIcon" type="ColorRect" parent="." unique_id=1000000002]
layout_mode = 0
offset_right = 60.0
offset_bottom = 60.0
mouse_filter = 1
color = Color(0.4, 0.6, 0.9, 1)
[node name="FloatingItem" type="ColorRect" parent="." unique_id=1000000003]
visible = false
layout_mode = 0
offset_left = 20.0
offset_top = -30.0
offset_right = 50.0
color = Color(1, 0.6, 0.2, 1)
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1000000004]

View File

@@ -0,0 +1 @@
uid://1406xmcnkygw0

View File

@@ -0,0 +1,204 @@
extends Control
class_name InventoryOverlay
signal close_requested
signal item_confirmed(item_id: String)
signal combine_requested(item_a_id: String, item_b_id: String)
signal inspect_requested(item_id: String)
var input_active: bool = false
var _is_visible: bool = false
var _selected_slot: InventorySlot = null
var _drag_start_time: float = 0.0
var _is_dragging: bool = false
var _dragged_item: Control = null
var _hovered_slot: InventorySlot = null
var _fade_tween: Tween = null
const FADE_DURATION: float = 0.2
const LONG_PRESS_THRESHOLD: float = 0.5
const SLOTS_PER_ROW: int = 8
const SLOT_SIZE: Vector2i = Vector2i(64, 64)
@onready var background: ColorRect = $Background
@onready var panel: Control = $InventoryPanel
@onready var frame: ColorRect = $InventoryPanel/Frame
@onready var grid: GridContainer = $InventoryPanel/ItemGrid
@onready var hover_label: Label = $InventoryPanel/HoverLabel
func _ready() -> void:
hide()
background.mouse_filter = Control.MOUSE_FILTER_STOP
InventoryManager.inventory_changed.connect(_refresh_grid)
InventoryManager.combination_attempted.connect(_on_combination_attempted)
func show_overlay() -> void:
print("[OVERLAY] show_overlay called, inventory has %d items" % InventoryManager.inventory.size())
_refresh_grid()
if _fade_tween:
_fade_tween.kill()
_fade_tween = null
_is_visible = true
show()
modulate = Color(1, 1, 1, 0)
input_active = false
var tween = create_tween().bind_node(self)
_fade_tween = tween
tween.tween_property(self, "modulate", Color(1, 1, 1, 1), FADE_DURATION)
tween.tween_callback(_on_fade_in_complete)
func hide_overlay() -> void:
print("[OVERLAY] hide_overlay called")
if _fade_tween:
_fade_tween.kill()
_fade_tween = null
input_active = false
_clear_selection()
var tween = create_tween().bind_node(self)
_fade_tween = tween
tween.tween_property(self, "modulate", Color(1, 1, 1, 0), FADE_DURATION)
tween.tween_callback(_on_fade_out_complete)
func _on_fade_in_complete() -> void:
input_active = true
func _on_fade_out_complete() -> void:
_is_visible = false
hide()
input_active = false
func is_active() -> bool:
return _is_visible and input_active
func _refresh_grid() -> void:
for child in grid.get_children():
child.queue_free()
for item_id in InventoryManager.inventory:
var def = InventoryManager.get_item_definition(item_id)
if not def:
def = ItemDefinition.new()
def.id = item_id
def.name = item_id
var slot_scene = load("res://inventory/inventory_overlay/InventorySlot.tscn")
var slot: InventorySlot = slot_scene.instantiate()
grid.add_child(slot)
slot.set_item(def)
slot.clicked.connect(_on_slot_clicked)
slot.right_clicked.connect(_on_slot_right_clicked)
slot.hovered.connect(_on_slot_hovered)
slot.unhovered.connect(_on_slot_unhovered)
grid.columns = SLOTS_PER_ROW
func _on_slot_clicked(item_id: String) -> void:
print("[OVERLAY] _on_slot_clicked: '%s', input_active=%s" % [item_id, input_active])
if not input_active:
return
for child in grid.get_children():
if child is InventorySlot and child.item_id == item_id:
if _selected_slot == null:
InventoryManager.select_item(item_id)
item_confirmed.emit(item_id)
hide_overlay()
elif _selected_slot.item_id == item_id:
InventoryManager.select_item(item_id)
item_confirmed.emit(item_id)
hide_overlay()
else:
combine_requested.emit(_selected_slot.item_id, item_id)
_clear_selection()
break
func _on_slot_right_clicked(item_id: String) -> void:
if not input_active:
return
inspect_requested.emit(item_id)
hide_overlay()
func _on_slot_hovered(item_id: String) -> void:
if not input_active:
return
_hovered_slot = null
for child in grid.get_children():
if child is InventorySlot and child.item_id == item_id:
_hovered_slot = child
break
_update_hover_label()
func _on_slot_unhovered(item_id: String) -> void:
if _hovered_slot and _hovered_slot.item_id == item_id:
_hovered_slot = null
_update_hover_label()
func _create_drag_preview(slot: InventorySlot) -> void:
var preview = ColorRect.new()
preview.color = Color(1, 0.6, 0.2, 0.7)
preview.size = Vector2(48, 48)
preview.position = slot.global_position - global_position
panel.add_child(preview)
_dragged_item = preview
func _handle_release_same_item() -> void:
var elapsed = Time.get_ticks_msec() / 1000.0 - _drag_start_time
if elapsed <= LONG_PRESS_THRESHOLD:
item_confirmed.emit(_selected_slot.item_id)
hide_overlay()
else:
_clear_selection()
func _clear_selection() -> void:
_selected_slot = null
_is_dragging = false
if _dragged_item:
_dragged_item.queue_free()
_dragged_item = null
_update_hover_label()
func _update_hover_label() -> void:
if _selected_slot and _hovered_slot and _selected_slot.item_id != _hovered_slot.item_id:
var def_a = InventoryManager.get_item_definition(_selected_slot.item_id)
var def_b = InventoryManager.get_item_definition(_hovered_slot.item_id)
var name_a = _selected_slot.item_id
var name_b = _hovered_slot.item_id
if def_a:
name_a = def_a.name
if def_b:
name_b = def_b.name
hover_label.text = "Use %s with %s" % [name_a, name_b]
elif _hovered_slot:
var def = InventoryManager.get_item_definition(_hovered_slot.item_id)
if def:
hover_label.text = def.name
else:
hover_label.text = _hovered_slot.item_id
else:
hover_label.text = ""
func _on_background_gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == 1 and event.pressed:
hide_overlay()
close_requested.emit()
func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == 1 and event.pressed:
var panel_rect = panel.get_global_rect()
if not panel_rect.has_point(get_global_mouse_position()):
hide_overlay()
close_requested.emit()
func _on_combination_attempted(item_a_id: String, item_b_id: String) -> void:
pass
func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE:
if _fade_tween:
_fade_tween.kill()
_fade_tween = null

View File

@@ -0,0 +1 @@
uid://3mkdj9s1oe1jz

View File

@@ -0,0 +1,77 @@
[gd_scene format=3 uid="uid://djoycn4xfa8p3"]
[ext_resource type="Script" uid="uid://bkpafveapyv8n" path="res://inventory/inventory_overlay/InventoryOverlay.gd" id="1"]
[sub_resource type="LabelSettings" id="LabelSettings_inv"]
font_size = 20
outline_size = 3
outline_color = Color(0, 0, 0, 1)
[node name="InventoryOverlay" type="Control" unique_id=-1294967295]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1")
[node name="Background" type="ColorRect" parent="." unique_id=-1294967294]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 1
color = Color(0, 0, 0, 0.7)
[node name="InventoryPanel" type="Control" parent="." unique_id=-1294967293]
layout_mode = 1
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -350.0
offset_top = -150.0
offset_right = 350.0
offset_bottom = 150.0
grow_horizontal = 2
grow_vertical = 2
[node name="Frame" type="ColorRect" parent="InventoryPanel" unique_id=-1294967292]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 1
grow_horizontal = 2
grow_vertical = 2
color = Color(0.15, 0.15, 0.2, 1)
[node name="ItemGrid" type="GridContainer" parent="InventoryPanel" unique_id=-1294967291]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 10.0
offset_top = 10.0
offset_right = -10.0
offset_bottom = -50.0
grow_horizontal = 2
grow_vertical = 2
[node name="HoverLabel" type="Label" parent="InventoryPanel" unique_id=-1294967290]
layout_mode = 1
anchors_preset = 12
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_bottom = -10.0
grow_horizontal = 2
grow_vertical = 2
label_settings = SubResource("LabelSettings_inv")
horizontal_alignment = 1
[connection signal="gui_input" from="Background" to="." method="_on_background_gui_input"]

View File

@@ -0,0 +1 @@
uid://1p46uzngsih9o

View File

@@ -0,0 +1,57 @@
extends Control
class_name InventorySlot
signal clicked(item_id: String)
signal right_clicked(item_id: String)
signal hovered(item_id: String)
signal unhovered(item_id: String)
var item_id: String = ""
var is_hovered: bool = false
@onready var item_box: ColorRect = $ItemBox
@onready var hover_highlight: ColorRect = $HoverHighlight
func _ready() -> void:
hover_highlight.visible = false
func set_item(item_def: ItemDefinition) -> void:
item_id = item_def.id
item_box.color = Color(1, 0.6, 0.2, 1)
func set_item_color(color: Color) -> void:
item_box.color = color
func set_hover(hovered: bool) -> void:
is_hovered = hovered
hover_highlight.visible = hovered
if hovered:
item_box.color = item_box.color.lightened(0.2)
else:
var def = InventoryManager.get_item_definition(item_id)
if def:
item_box.color = Color(1, 0.6, 0.2, 1)
else:
item_box.color = Color(0.8, 0.8, 0.8, 1)
func _gui_input(event: InputEvent) -> void:
print("[SLOT:%s] _gui_input: %s" % [item_id, event])
if event is InputEventMouseButton:
if event.button_index == 1 and event.pressed:
print("[SLOT:%s] emitting clicked" % item_id)
clicked.emit(item_id)
elif event.button_index == 2:
right_clicked.emit(item_id)
func _input(event: InputEvent) -> void:
if event is InputEventMouseMotion:
var rect = get_global_rect()
var mouse_pos = get_global_mouse_position()
var was_hovered = is_hovered
is_hovered = rect.has_point(mouse_pos)
if is_hovered != was_hovered:
set_hover(is_hovered)
if is_hovered:
hovered.emit(item_id)
else:
unhovered.emit(item_id)

View File

@@ -0,0 +1 @@
uid://oegm753jbl9m

View File

@@ -0,0 +1,32 @@
[gd_scene format=3 uid="uid://c7depvvxf5s6l"]
[ext_resource type="Script" uid="uid://oegm753jbmam" path="res://inventory/inventory_overlay/InventorySlot.gd" id="1"]
[node name="InventorySlot" type="Control" unique_id=2000000001]
custom_minimum_size = Vector2(64, 64)
layout_mode = 3
anchors_preset = 0
script = ExtResource("1")
[node name="ItemBox" type="ColorRect" parent="." unique_id=2000000002]
layout_mode = 1
anchors_preset = -1
anchor_left = 0.062
anchor_top = 0.062
anchor_right = 0.938
anchor_bottom = 0.938
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 1
color = Color(1, 0.6, 0.2, 1)
[node name="HoverHighlight" type="ColorRect" parent="." unique_id=2000000003]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 1
color = Color(1, 1, 1, 0.3)

View File

@@ -0,0 +1 @@
uid://1esl88fgtd2p6

View File

@@ -0,0 +1,6 @@
{
"item_id": "splash",
"name": "Splash",
"icon": "res://splash.png",
"combination_category": "potion"
}