diff --git a/inventory/inventory_backpack/InventoryBackpack.gd b/inventory/inventory_backpack/InventoryBackpack.gd new file mode 100644 index 0000000..37e1124 --- /dev/null +++ b/inventory/inventory_backpack/InventoryBackpack.gd @@ -0,0 +1,233 @@ +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) + +@onready var backpack_icon: ColorRect = $BackpackIcon +@onready var floating_item: ColorRect = $FloatingItem +@onready var animation_player: AnimationPlayer = $AnimationPlayer + +func _ready() -> void: + 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", backpack_icon.position, 0.35).set_trans(Tween.TRANS_SINE_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", Vector2(backpack_icon.position.x + 20, backpack_icon.position.y + 20), 0.35).set_trans(Tween.TRANS_SINE_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: + if event is InputEventMouseButton and event.pressed and event.button_index == 1: + if _state == State.IDLE: + transition_to(State.OPEN) + elif _state == State.OPEN: + transition_to(State.IDLE) + +func _notification(what: int) -> void: + if what == NOTIFICATION_PREDELETE: + _kill_tween() diff --git a/inventory/inventory_backpack/InventoryBackpack.gd.uid b/inventory/inventory_backpack/InventoryBackpack.gd.uid new file mode 100644 index 0000000..64ac216 --- /dev/null +++ b/inventory/inventory_backpack/InventoryBackpack.gd.uid @@ -0,0 +1 @@ +uid://2x3g0ethsdcgo \ No newline at end of file diff --git a/inventory/inventory_backpack/InventoryBackpack.tscn b/inventory/inventory_backpack/InventoryBackpack.tscn new file mode 100644 index 0000000..ec05ce0 --- /dev/null +++ b/inventory/inventory_backpack/InventoryBackpack.tscn @@ -0,0 +1,31 @@ +[gd_scene format=3 uid="uid://1406xmcnkygw0"] + +[ext_resource type="Script" uid="uid://2x3g0ethsdcgo" path="res://inventory/inventory_backpack/InventoryBackpack.gd" id="1"] + +[node name="InventoryBackpack" type="Control" unique_id=1000000001] +layout_mode = 3 +anchors_preset = 2 +anchor_right = 1.0 +offset_top = 10.0 +offset_right = -10.0 +offset_bottom = 70.0 +script = ExtResource("1") + +[node name="BackpackIcon" type="ColorRect" parent="." unique_id=1000000002] +layout_mode = 0 +offset_left = 0.0 +offset_top = 0.0 +offset_right = 60.0 +offset_bottom = 60.0 +color = Color(0.4, 0.6, 0.9, 1) + +[node name="FloatingItem" type="ColorRect" parent="." unique_id=1000000003] +layout_mode = 0 +offset_left = 20.0 +offset_top = -30.0 +offset_right = 50.0 +offset_bottom = 0.0 +color = Color(1, 0.6, 0.2, 1) +visible = false + +[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1000000004] diff --git a/inventory/inventory_backpack/InventoryBackpack.tscn.uid b/inventory/inventory_backpack/InventoryBackpack.tscn.uid new file mode 100644 index 0000000..02d7565 --- /dev/null +++ b/inventory/inventory_backpack/InventoryBackpack.tscn.uid @@ -0,0 +1 @@ +uid://1406xmcnkygw0 \ No newline at end of file diff --git a/inventory/inventory_overlay/InventoryOverlay.gd b/inventory/inventory_overlay/InventoryOverlay.gd new file mode 100644 index 0000000..9da4584 --- /dev/null +++ b/inventory/inventory_overlay/InventoryOverlay.gd @@ -0,0 +1,215 @@ +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: + 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: + 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() + 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.add_child(slot) + + grid.columns = SLOTS_PER_ROW + +func _on_slot_clicked(item_id: String) -> void: + 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: + _selected_slot = child + _drag_start_time = Time.get_ticks_msec() / 1000.0 + _is_dragging = true + _create_drag_preview(child) + elif _selected_slot.item_id == item_id: + _handle_release_same_item() + 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 _gui_input(event: InputEvent) -> void: + if not _is_visible: + return + + if event is InputEventMouseButton: + if event.button_index == 1 and event.pressed: + if not _is_dragging: + pass + elif event.button_index == 1 and not event.pressed: + if _is_dragging: + if _hovered_slot == null: + _clear_selection() + hide_overlay() + close_requested.emit() + elif _hovered_slot == _selected_slot: + _handle_release_same_item() + else: + combine_requested.emit(_selected_slot.item_id, _hovered_slot.item_id) + _clear_selection() + return + + if event is InputEventMouseMotion and _is_dragging and _dragged_item: + _dragged_item.position = get_local_mouse_position() - Vector2(24, 24) + var panel_rect = panel.get_global_rect() + if not panel_rect.has_point(get_global_mouse_position()): + _clear_selection() + 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 diff --git a/inventory/inventory_overlay/InventoryOverlay.gd.uid b/inventory/inventory_overlay/InventoryOverlay.gd.uid new file mode 100644 index 0000000..e6bcfaf --- /dev/null +++ b/inventory/inventory_overlay/InventoryOverlay.gd.uid @@ -0,0 +1 @@ +uid://3mkdj9s1oe1jz \ No newline at end of file diff --git a/inventory/inventory_overlay/InventoryOverlay.tscn b/inventory/inventory_overlay/InventoryOverlay.tscn new file mode 100644 index 0000000..a2cc18b --- /dev/null +++ b/inventory/inventory_overlay/InventoryOverlay.tscn @@ -0,0 +1,63 @@ +[gd_scene format=3 uid="uid://1p46uzngsih9o"] + +[ext_resource type="Script" uid="uid://3mkdj9s1oe1jz" 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=3000000001] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +script = ExtResource("1") + +[node name="Background" type="ColorRect" parent="." unique_id=3000000002] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0, 0, 0, 0.7) + +[node name="InventoryPanel" type="Control" parent="." unique_id=3000000003] +layout_mode = 1 +anchors_preset = 7 +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=3000000004] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(0.15, 0.15, 0.2, 1) + +[node name="ItemGrid" type="GridContainer" parent="InventoryPanel" unique_id=3000000005] +layout_mode = 1 +anchors_preset = 0 +offset_left = 10.0 +offset_top = 10.0 +offset_right = -10.0 +offset_bottom = -50.0 + +[node name="HoverLabel" type="Label" parent="InventoryPanel" unique_id=3000000006] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.0 +anchor_top = 1.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_bottom = -10.0 +grow_horizontal = 2 +horizontal_alignment = 1 +label_settings = SubResource("LabelSettings_inv") diff --git a/inventory/inventory_overlay/InventoryOverlay.tscn.uid b/inventory/inventory_overlay/InventoryOverlay.tscn.uid new file mode 100644 index 0000000..82c7b5e --- /dev/null +++ b/inventory/inventory_overlay/InventoryOverlay.tscn.uid @@ -0,0 +1 @@ +uid://1p46uzngsih9o \ No newline at end of file diff --git a/inventory/inventory_overlay/InventorySlot.gd b/inventory/inventory_overlay/InventorySlot.gd new file mode 100644 index 0000000..a6c744f --- /dev/null +++ b/inventory/inventory_overlay/InventorySlot.gd @@ -0,0 +1,55 @@ +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: + if event is InputEventMouseButton: + if event.button_index == 1 and event.pressed: + 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) diff --git a/inventory/inventory_overlay/InventorySlot.gd.uid b/inventory/inventory_overlay/InventorySlot.gd.uid new file mode 100644 index 0000000..f0e989a --- /dev/null +++ b/inventory/inventory_overlay/InventorySlot.gd.uid @@ -0,0 +1 @@ +uid://oegm753jbl9m \ No newline at end of file diff --git a/inventory/inventory_overlay/InventorySlot.tscn b/inventory/inventory_overlay/InventorySlot.tscn new file mode 100644 index 0000000..30d4b6e --- /dev/null +++ b/inventory/inventory_overlay/InventorySlot.tscn @@ -0,0 +1,26 @@ +[gd_scene format=3 uid="uid://1esl88fgtd2p6"] + +[ext_resource type="Script" uid="uid://oegm753jbl9m" path="res://inventory/inventory_overlay/InventorySlot.gd" id="1"] + +[node name="InventorySlot" type="Control" unique_id=2000000001] +custom_minimum_size = Vector2i(64, 64) +script = ExtResource("1") + +[node name="ItemBox" type="ColorRect" parent="." unique_id=2000000002] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.062 +anchor_top = 0.062 +anchor_right = 0.938 +anchor_bottom = 0.938 +color = Color(1, 0.6, 0.2, 1) + +[node name="HoverHighlight" type="ColorRect" parent="." unique_id=2000000003] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.0 +anchor_top = 0.0 +anchor_right = 1.0 +anchor_bottom = 1.0 +color = Color(1, 1, 1, 0.3) +visible = false diff --git a/inventory/inventory_overlay/InventorySlot.tscn.uid b/inventory/inventory_overlay/InventorySlot.tscn.uid new file mode 100644 index 0000000..021fcfa --- /dev/null +++ b/inventory/inventory_overlay/InventorySlot.tscn.uid @@ -0,0 +1 @@ +uid://1esl88fgtd2p6 \ No newline at end of file