重新初始化项目
This commit is contained in:
@@ -1,179 +0,0 @@
|
||||
extends Node
|
||||
|
||||
const FEDORA_ICON: Texture2D = preload("res://assets/items/hats/icons/fedora.png")
|
||||
const HEADPHONES_ICON: Texture2D = preload("res://assets/items/hats/icons/headphones.png")
|
||||
const PIRATE_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/pirate_hat.png")
|
||||
const SHERIFF_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/sheriff_hat.png")
|
||||
const SOMBRERO_ICON: Texture2D = preload("res://assets/items/hats/icons/sombrero.png")
|
||||
const WIZARD_HAT_ICON: Texture2D = preload("res://assets/items/hats/icons/wizard_hat.png")
|
||||
const BACKPACK_ICON: Texture2D = preload("res://assets/items/backpacks/icons/backpack.png")
|
||||
const SWORD_ICON: Texture2D = preload("res://assets/items/weapons/icons/sword.png")
|
||||
const SWORD_BIG_ICON: Texture2D = preload("res://assets/items/weapons/icons/sword_big.png")
|
||||
const AXE_ICON: Texture2D = preload("res://assets/items/weapons/icons/axe.png")
|
||||
const CHICKEN_LEG_ICON: Texture2D = preload("res://assets/items/misc/icons/chicken_leg.png")
|
||||
const BONE_ICON: Texture2D = preload("res://assets/items/misc/icons/bone.png")
|
||||
const CHALICE_ICON: Texture2D = preload("res://assets/items/misc/icons/chalice.png")
|
||||
|
||||
var items: Dictionary = {}
|
||||
|
||||
|
||||
func _ready():
|
||||
_create_sample_items()
|
||||
|
||||
|
||||
func get_item(item_id: String) -> Item:
|
||||
return items.get(item_id)
|
||||
|
||||
|
||||
func get_all_items() -> Dictionary:
|
||||
return items
|
||||
|
||||
|
||||
func _create_sample_items():
|
||||
_create_hat_item("fedora", "Fedora", "A timeless felt fedora.", "res://scenes/items/hats/fedora.tscn", FEDORA_ICON)
|
||||
_create_hat_item(
|
||||
"headphones",
|
||||
"Headphones",
|
||||
"Comfortable over-ear headphones.",
|
||||
"res://scenes/items/hats/headphones.tscn",
|
||||
HEADPHONES_ICON
|
||||
)
|
||||
_create_hat_item(
|
||||
"pirate_hat",
|
||||
"Pirate Hat",
|
||||
"A weathered hat for a daring pirate.",
|
||||
"res://scenes/items/hats/pirate_hat.tscn",
|
||||
PIRATE_HAT_ICON
|
||||
)
|
||||
_create_hat_item(
|
||||
"sheriff_hat",
|
||||
"Sheriff Hat",
|
||||
"A frontier hat with a sheriff badge.",
|
||||
"res://scenes/items/hats/sheriff_hat.tscn",
|
||||
SHERIFF_HAT_ICON
|
||||
)
|
||||
_create_hat_item(
|
||||
"sombrero", "Sombrero", "A broad and colorful sombrero.", "res://scenes/items/hats/sombrero.tscn", SOMBRERO_ICON
|
||||
)
|
||||
_create_hat_item(
|
||||
"wizard_hat",
|
||||
"Wizard Hat",
|
||||
"A pointed hat filled with arcane style.",
|
||||
"res://scenes/items/hats/wizard_hat.tscn",
|
||||
WIZARD_HAT_ICON
|
||||
)
|
||||
_create_item(
|
||||
"backpack",
|
||||
"Backpack",
|
||||
"A sturdy backpack worn on the back.",
|
||||
Item.ItemType.BACKPACK,
|
||||
"res://scenes/items/backpacks/backpack.tscn",
|
||||
BACKPACK_ICON,
|
||||
true,
|
||||
60
|
||||
)
|
||||
_create_item(
|
||||
"sword",
|
||||
"Sword",
|
||||
"A balanced hand sword.",
|
||||
Item.ItemType.WEAPON,
|
||||
"res://scenes/items/weapons/sword.tscn",
|
||||
SWORD_ICON,
|
||||
true,
|
||||
80
|
||||
)
|
||||
_create_item(
|
||||
"sword_big",
|
||||
"Big Sword",
|
||||
"A large two-handed sword.",
|
||||
Item.ItemType.WEAPON,
|
||||
"res://scenes/items/weapons/sword_big.tscn",
|
||||
SWORD_BIG_ICON,
|
||||
true,
|
||||
120
|
||||
)
|
||||
_create_item(
|
||||
"axe",
|
||||
"Small Axe",
|
||||
"A compact axe with a sharp steel head.",
|
||||
Item.ItemType.WEAPON,
|
||||
"res://scenes/items/weapons/axe.tscn",
|
||||
AXE_ICON,
|
||||
true,
|
||||
95
|
||||
)
|
||||
_create_item(
|
||||
"chicken_leg",
|
||||
"Chicken Leg",
|
||||
"A cooked chicken leg.",
|
||||
Item.ItemType.MISC,
|
||||
"res://scenes/items/misc/chicken_leg.tscn",
|
||||
CHICKEN_LEG_ICON,
|
||||
false,
|
||||
5
|
||||
)
|
||||
_create_item(
|
||||
"bone",
|
||||
"Bone",
|
||||
"A weathered bone.",
|
||||
Item.ItemType.MISC,
|
||||
"res://scenes/items/misc/bone.tscn",
|
||||
BONE_ICON,
|
||||
false,
|
||||
2
|
||||
)
|
||||
_create_item(
|
||||
"chalice",
|
||||
"Chalice",
|
||||
"A decorative golden chalice.",
|
||||
Item.ItemType.MISC,
|
||||
"res://scenes/items/misc/chalice.tscn",
|
||||
CHALICE_ICON,
|
||||
false,
|
||||
35
|
||||
)
|
||||
|
||||
|
||||
func _create_item(
|
||||
item_id: String,
|
||||
item_name: String,
|
||||
item_description: String,
|
||||
item_type: Item.ItemType,
|
||||
scene_path: String,
|
||||
item_icon: Texture2D,
|
||||
equipable: bool,
|
||||
item_value: int
|
||||
) -> void:
|
||||
var item := Item.new()
|
||||
item.id = item_id
|
||||
item.name = item_name
|
||||
item.description = item_description
|
||||
item.item_type = item_type
|
||||
item.rarity = Item.ItemRarity.COMMON
|
||||
item.stackable = not equipable
|
||||
item.max_stack = 99 if item.stackable else 1
|
||||
item.value = item_value
|
||||
item.icon = item_icon
|
||||
item.scene_path = scene_path
|
||||
if equipable:
|
||||
item.context_options.append(Item.ContextOptions.EQUIP)
|
||||
item.context_options.append(Item.ContextOptions.DROP)
|
||||
items[item.id] = item
|
||||
|
||||
|
||||
func _create_hat_item(
|
||||
item_id: String, item_name: String, item_description: String, scene_path: String, item_icon: Texture2D
|
||||
) -> void:
|
||||
var hat := Item.new()
|
||||
hat.id = item_id
|
||||
hat.name = item_name
|
||||
hat.description = item_description
|
||||
hat.item_type = Item.ItemType.HAT
|
||||
hat.rarity = Item.ItemRarity.COMMON
|
||||
hat.stackable = false
|
||||
hat.value = 25
|
||||
hat.icon = item_icon
|
||||
hat.scene_path = scene_path
|
||||
hat.context_options.append(Item.ContextOptions.EQUIP)
|
||||
hat.context_options.append(Item.ContextOptions.DROP)
|
||||
items[hat.id] = hat
|
||||
@@ -1 +0,0 @@
|
||||
uid://bsraoqq3rst1j
|
||||
@@ -1,202 +0,0 @@
|
||||
extends Node
|
||||
|
||||
signal player_connected(peer_id, player_info)
|
||||
signal server_disconnected
|
||||
|
||||
const SERVER_ADDRESS: String = "127.0.0.1"
|
||||
const SERVER_PORT: int = 8080
|
||||
const MAX_PLAYERS: int = 10
|
||||
const MAX_NICK_LENGTH := 24
|
||||
const MAX_ADDRESS_LENGTH := 253
|
||||
|
||||
var players = {}
|
||||
var player_info = {"nick": "host", "skin": Character.SkinColor.BLUE}
|
||||
var _session_active := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.peer_disconnected.connect(_on_player_disconnected)
|
||||
multiplayer.peer_connected.connect(_on_player_connected)
|
||||
multiplayer.connected_to_server.connect(_on_connected_ok)
|
||||
|
||||
|
||||
func start_host(nickname: String, skin_color_str: String):
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
var error = peer.create_server(SERVER_PORT, MAX_PLAYERS)
|
||||
if error:
|
||||
return error
|
||||
|
||||
peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
_session_active = true
|
||||
|
||||
player_info["nick"] = sanitize_nickname(nickname, "Host_" + str(multiplayer.get_unique_id()))
|
||||
player_info["skin"] = skin_str_to_e(skin_color_str)
|
||||
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
|
||||
players[1] = player_info
|
||||
player_connected.emit(1, player_info)
|
||||
|
||||
|
||||
func join_game(nickname: String, skin_color_str: String, address: String = SERVER_ADDRESS):
|
||||
address = sanitize_address(address)
|
||||
if address.is_empty():
|
||||
return ERR_INVALID_PARAMETER
|
||||
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
var error = peer.create_client(address, SERVER_PORT)
|
||||
if error:
|
||||
return error
|
||||
|
||||
peer.host.compress(ENetConnection.COMPRESS_RANGE_CODER)
|
||||
multiplayer.multiplayer_peer = peer
|
||||
_session_active = true
|
||||
|
||||
player_info["nick"] = sanitize_nickname(nickname, "Player_" + str(multiplayer.get_unique_id()))
|
||||
player_info["skin"] = skin_str_to_e(skin_color_str)
|
||||
|
||||
|
||||
func _on_connected_ok():
|
||||
var peer_id = multiplayer.get_unique_id()
|
||||
players[peer_id] = player_info
|
||||
player_connected.emit(peer_id, player_info)
|
||||
_register_player.rpc_id(1, player_info)
|
||||
|
||||
|
||||
func _on_player_connected(id):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
for peer_id in players:
|
||||
_sync_registered_player.rpc_id(id, peer_id, players[peer_id])
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func _register_player(new_player_info):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
if not (new_player_info is Dictionary):
|
||||
return
|
||||
var new_player_id = multiplayer.get_remote_sender_id()
|
||||
if new_player_id == 0:
|
||||
return
|
||||
if players.has(new_player_id):
|
||||
return
|
||||
var sanitized_info = sanitize_player_info(new_player_info, "Player_" + str(new_player_id))
|
||||
players[new_player_id] = sanitized_info
|
||||
player_connected.emit(new_player_id, sanitized_info)
|
||||
_sync_registered_player.rpc(new_player_id, sanitized_info)
|
||||
|
||||
|
||||
func _on_player_disconnected(id):
|
||||
players.erase(id)
|
||||
|
||||
|
||||
func _on_connection_failed():
|
||||
_finish_session()
|
||||
|
||||
|
||||
func _on_server_disconnected():
|
||||
_finish_session()
|
||||
|
||||
|
||||
func leave_game() -> void:
|
||||
var peer := multiplayer.multiplayer_peer
|
||||
if peer:
|
||||
peer.close()
|
||||
_finish_session()
|
||||
|
||||
|
||||
func _finish_session() -> void:
|
||||
var had_session := _session_active or multiplayer.multiplayer_peer != null or not players.is_empty()
|
||||
_session_active = false
|
||||
multiplayer.multiplayer_peer = null
|
||||
players.clear()
|
||||
if had_session:
|
||||
server_disconnected.emit()
|
||||
|
||||
|
||||
func skin_str_to_e(s):
|
||||
match str(s).strip_edges().to_lower():
|
||||
"blue":
|
||||
return Character.SkinColor.BLUE
|
||||
"yellow":
|
||||
return Character.SkinColor.YELLOW
|
||||
"green":
|
||||
return Character.SkinColor.GREEN
|
||||
"red":
|
||||
return Character.SkinColor.RED
|
||||
_:
|
||||
return Character.SkinColor.BLUE
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func _sync_registered_player(peer_id: int, registered_player_info: Dictionary):
|
||||
if multiplayer.is_server():
|
||||
return
|
||||
if players.has(peer_id):
|
||||
return
|
||||
var sanitized_info = sanitize_player_info(registered_player_info, "Player_" + str(peer_id))
|
||||
players[peer_id] = sanitized_info
|
||||
player_connected.emit(peer_id, sanitized_info)
|
||||
|
||||
|
||||
func sanitize_player_info(info: Dictionary, fallback_nick: String) -> Dictionary:
|
||||
return {
|
||||
"nick": sanitize_nickname(str(info.get("nick", "")), fallback_nick),
|
||||
"skin": sanitize_skin_value(info.get("skin", Character.SkinColor.BLUE))
|
||||
}
|
||||
|
||||
|
||||
func sanitize_nickname(nickname: String, fallback: String) -> String:
|
||||
var clean := ""
|
||||
var last_was_space := false
|
||||
for i in range(nickname.length()):
|
||||
var codepoint := nickname.unicode_at(i)
|
||||
if codepoint <= 31 or codepoint == 127:
|
||||
if not last_was_space:
|
||||
clean += " "
|
||||
last_was_space = true
|
||||
continue
|
||||
|
||||
var character := nickname.substr(i, 1)
|
||||
if character == " ":
|
||||
if last_was_space:
|
||||
continue
|
||||
last_was_space = true
|
||||
else:
|
||||
last_was_space = false
|
||||
clean += character
|
||||
|
||||
clean = clean.strip_edges()
|
||||
if clean.is_empty():
|
||||
clean = fallback.strip_edges()
|
||||
if clean.length() > MAX_NICK_LENGTH:
|
||||
clean = clean.substr(0, MAX_NICK_LENGTH).strip_edges()
|
||||
if clean.is_empty():
|
||||
clean = "Player"
|
||||
return clean
|
||||
|
||||
|
||||
func sanitize_address(address: String) -> String:
|
||||
var clean = address.strip_edges()
|
||||
if clean.is_empty():
|
||||
return SERVER_ADDRESS
|
||||
if clean.length() > MAX_ADDRESS_LENGTH:
|
||||
return ""
|
||||
if clean.contains("://") or clean.contains("/") or clean.contains("\\") or clean.contains(":"):
|
||||
return ""
|
||||
return clean
|
||||
|
||||
|
||||
func sanitize_skin_value(value) -> Character.SkinColor:
|
||||
if value is int:
|
||||
match value:
|
||||
Character.SkinColor.BLUE, Character.SkinColor.YELLOW, Character.SkinColor.GREEN, Character.SkinColor.RED:
|
||||
return value
|
||||
_:
|
||||
return Character.SkinColor.BLUE
|
||||
return skin_str_to_e(str(value))
|
||||
@@ -1 +0,0 @@
|
||||
uid://cedubc87s78fo
|
||||
@@ -1,57 +0,0 @@
|
||||
class_name InventorySlot
|
||||
extends RefCounted
|
||||
|
||||
var item_id: String = ""
|
||||
var quantity: int = 0
|
||||
|
||||
|
||||
func is_empty() -> bool:
|
||||
return item_id.is_empty() or quantity <= 0
|
||||
|
||||
|
||||
func add_item(item: Item, amount: int = 1) -> int:
|
||||
var max_quantity = item.max_stack if item.stackable else 1
|
||||
if is_empty():
|
||||
item_id = item.id
|
||||
quantity = min(amount, max_quantity)
|
||||
return amount - quantity
|
||||
if item_id == item.id and item.stackable:
|
||||
var space_available = item.max_stack - quantity
|
||||
var amount_to_add = min(amount, space_available)
|
||||
quantity += amount_to_add
|
||||
return amount - amount_to_add
|
||||
return amount
|
||||
|
||||
|
||||
func remove_item(amount: int = 1) -> int:
|
||||
var removed = min(amount, quantity)
|
||||
quantity -= removed
|
||||
if quantity <= 0:
|
||||
clear()
|
||||
return removed
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
item_id = ""
|
||||
quantity = 0
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
return {"item_id": item_id, "quantity": quantity}
|
||||
|
||||
|
||||
func from_dict(data: Dictionary) -> void:
|
||||
item_id = data.get("item_id", "")
|
||||
quantity = data.get("quantity", 0)
|
||||
|
||||
if is_empty():
|
||||
clear()
|
||||
return
|
||||
|
||||
var item = ItemDatabase.get_item(item_id)
|
||||
if not item:
|
||||
clear()
|
||||
return
|
||||
|
||||
var max_quantity = item.max_stack if item.stackable else 1
|
||||
quantity = clampi(quantity, 1, max_quantity)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c7dfwkeylor2j
|
||||
@@ -1,27 +0,0 @@
|
||||
class_name Item
|
||||
extends Resource
|
||||
|
||||
enum ItemType { WEAPON, CONSUMABLE, TOOL, MISC, HAT, BACKPACK }
|
||||
|
||||
enum ItemRarity { COMMON, UNCOMMON, RARE, EPIC, LEGENDARY }
|
||||
|
||||
enum ContextOptions {
|
||||
DROP,
|
||||
EQUIP,
|
||||
UNEQUIP,
|
||||
}
|
||||
|
||||
@export var id: String = ""
|
||||
@export var name: String = ""
|
||||
@export var description: String = ""
|
||||
@export var icon: Texture2D
|
||||
|
||||
@export var stackable: bool = true
|
||||
@export var max_stack: int = 99
|
||||
|
||||
@export var item_type: ItemType = ItemType.MISC
|
||||
@export var rarity: ItemRarity = ItemRarity.COMMON
|
||||
@export var value: int = 0
|
||||
@export var context_options: Array[Item.ContextOptions] = []
|
||||
|
||||
@export var scene_path: String = ""
|
||||
@@ -1 +0,0 @@
|
||||
uid://bbq3lkpgmwr1t
|
||||
@@ -1,267 +0,0 @@
|
||||
class_name PlayerInventory
|
||||
extends RefCounted
|
||||
|
||||
const BASE_INVENTORY_SIZE := 16
|
||||
const BACKPACK_BONUS_SLOTS := 4
|
||||
const MAX_INVENTORY_SIZE := BASE_INVENTORY_SIZE + BACKPACK_BONUS_SLOTS
|
||||
var slots: Array[InventorySlot] = []
|
||||
var equipped_weapon: InventorySlot = InventorySlot.new()
|
||||
var equipped_hat: InventorySlot = InventorySlot.new()
|
||||
var equipped_backpack: InventorySlot = InventorySlot.new()
|
||||
|
||||
|
||||
func _init():
|
||||
_initialize_slots()
|
||||
|
||||
|
||||
func _initialize_slots():
|
||||
slots.clear()
|
||||
for i in range(MAX_INVENTORY_SIZE):
|
||||
slots.append(InventorySlot.new())
|
||||
|
||||
|
||||
func get_active_slot_count() -> int:
|
||||
return MAX_INVENTORY_SIZE if not equipped_backpack.is_empty() else BASE_INVENTORY_SIZE
|
||||
|
||||
|
||||
func is_slot_active(index: int) -> bool:
|
||||
return index >= 0 and index < get_active_slot_count()
|
||||
|
||||
|
||||
func get_slot(index: int) -> InventorySlot:
|
||||
if index >= 0 and index < slots.size():
|
||||
return slots[index]
|
||||
return null
|
||||
|
||||
|
||||
func get_equipped_slot(item_type: Item.ItemType) -> InventorySlot:
|
||||
match item_type:
|
||||
Item.ItemType.WEAPON:
|
||||
return equipped_weapon
|
||||
Item.ItemType.HAT:
|
||||
return equipped_hat
|
||||
Item.ItemType.BACKPACK:
|
||||
return equipped_backpack
|
||||
_:
|
||||
return null
|
||||
|
||||
|
||||
func equip_from_slot(index: int, item_type: Item.ItemType) -> bool:
|
||||
if not is_slot_active(index):
|
||||
return false
|
||||
|
||||
var backpack_slot: InventorySlot = get_slot(index)
|
||||
var equipped_slot: InventorySlot = get_equipped_slot(item_type)
|
||||
if not backpack_slot or backpack_slot.is_empty() or not equipped_slot:
|
||||
return false
|
||||
|
||||
var item: Item = ItemDatabase.get_item(backpack_slot.item_id)
|
||||
if not item or item.item_type != item_type:
|
||||
return false
|
||||
|
||||
var new_item_id: String = backpack_slot.item_id
|
||||
var new_quantity: int = backpack_slot.quantity
|
||||
backpack_slot.item_id = equipped_slot.item_id
|
||||
backpack_slot.quantity = equipped_slot.quantity
|
||||
equipped_slot.item_id = new_item_id
|
||||
equipped_slot.quantity = new_quantity
|
||||
if backpack_slot.is_empty():
|
||||
backpack_slot.clear()
|
||||
return true
|
||||
|
||||
|
||||
func unequip_to_slot(item_type: Item.ItemType, destination_index: int = -1) -> bool:
|
||||
var equipped_slot: InventorySlot = get_equipped_slot(item_type)
|
||||
if not equipped_slot or equipped_slot.is_empty():
|
||||
return false
|
||||
if item_type == Item.ItemType.BACKPACK:
|
||||
return _unequip_backpack_to_slot(equipped_slot, destination_index)
|
||||
|
||||
if destination_index < 0:
|
||||
destination_index = get_first_empty_slot()
|
||||
if not is_slot_active(destination_index):
|
||||
return false
|
||||
var destination: InventorySlot = get_slot(destination_index)
|
||||
if not destination or not destination.is_empty():
|
||||
return false
|
||||
|
||||
destination.item_id = equipped_slot.item_id
|
||||
destination.quantity = equipped_slot.quantity
|
||||
equipped_slot.clear()
|
||||
return true
|
||||
|
||||
|
||||
func _unequip_backpack_to_slot(equipped_slot: InventorySlot, destination_index: int) -> bool:
|
||||
if destination_index < 0:
|
||||
destination_index = _get_first_empty_base_slot()
|
||||
if destination_index < 0 or destination_index >= BASE_INVENTORY_SIZE:
|
||||
return false
|
||||
|
||||
var destination := get_slot(destination_index)
|
||||
if not destination or not destination.is_empty():
|
||||
return false
|
||||
|
||||
var occupied_bonus_slots: Array[int] = []
|
||||
for i in range(BASE_INVENTORY_SIZE, MAX_INVENTORY_SIZE):
|
||||
if not slots[i].is_empty():
|
||||
occupied_bonus_slots.append(i)
|
||||
|
||||
var available_base_slots: Array[int] = []
|
||||
for i in range(BASE_INVENTORY_SIZE):
|
||||
if i != destination_index and slots[i].is_empty():
|
||||
available_base_slots.append(i)
|
||||
if occupied_bonus_slots.size() > available_base_slots.size():
|
||||
return false
|
||||
|
||||
for i in range(occupied_bonus_slots.size()):
|
||||
var source := slots[occupied_bonus_slots[i]]
|
||||
var target := slots[available_base_slots[i]]
|
||||
target.item_id = source.item_id
|
||||
target.quantity = source.quantity
|
||||
source.clear()
|
||||
|
||||
destination.item_id = equipped_slot.item_id
|
||||
destination.quantity = equipped_slot.quantity
|
||||
equipped_slot.clear()
|
||||
return true
|
||||
|
||||
|
||||
func _get_first_empty_base_slot() -> int:
|
||||
for i in range(BASE_INVENTORY_SIZE):
|
||||
if slots[i].is_empty():
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
func add_item(item: Item, quantity: int = 1) -> int:
|
||||
var remaining = quantity
|
||||
var active_slot_count := get_active_slot_count()
|
||||
|
||||
if item.stackable:
|
||||
for i in range(active_slot_count):
|
||||
var slot := slots[i]
|
||||
if slot.item_id == item.id:
|
||||
remaining = slot.add_item(item, remaining)
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
if remaining > 0:
|
||||
for i in range(active_slot_count):
|
||||
var slot := slots[i]
|
||||
if slot.is_empty():
|
||||
remaining = slot.add_item(item, remaining)
|
||||
if remaining <= 0:
|
||||
break
|
||||
|
||||
return remaining
|
||||
|
||||
|
||||
func remove_item(item_id: String, quantity: int = 1) -> int:
|
||||
var removed = 0
|
||||
for i in range(get_active_slot_count()):
|
||||
var slot := slots[i]
|
||||
if slot.item_id == item_id:
|
||||
var slot_removed = slot.remove_item(quantity - removed)
|
||||
removed += slot_removed
|
||||
if removed >= quantity:
|
||||
break
|
||||
return removed
|
||||
|
||||
|
||||
func move_item(from_index: int, to_index: int, quantity: int = -1) -> bool:
|
||||
if (
|
||||
from_index == to_index
|
||||
or not is_slot_active(from_index)
|
||||
or not is_slot_active(to_index)
|
||||
or quantity == 0
|
||||
or quantity < -1
|
||||
):
|
||||
return false
|
||||
|
||||
var from_slot = get_slot(from_index)
|
||||
var to_slot = get_slot(to_index)
|
||||
|
||||
if not from_slot or not to_slot or from_slot.is_empty():
|
||||
return false
|
||||
|
||||
var move_amount = from_slot.quantity if quantity == -1 else quantity
|
||||
move_amount = min(move_amount, from_slot.quantity)
|
||||
|
||||
var item = ItemDatabase.get_item(from_slot.item_id)
|
||||
if not item:
|
||||
return false
|
||||
|
||||
var remaining = to_slot.add_item(item, move_amount)
|
||||
var moved_amount = move_amount - remaining
|
||||
if moved_amount > 0:
|
||||
from_slot.remove_item(moved_amount)
|
||||
|
||||
return moved_amount > 0
|
||||
|
||||
|
||||
func swap_items(from_index: int, to_index: int) -> bool:
|
||||
if not is_slot_active(from_index) or not is_slot_active(to_index):
|
||||
return false
|
||||
|
||||
var from_slot = get_slot(from_index)
|
||||
var to_slot = get_slot(to_index)
|
||||
|
||||
if not from_slot or not to_slot:
|
||||
return false
|
||||
|
||||
if from_slot.item_id == to_slot.item_id and not from_slot.is_empty() and not to_slot.is_empty():
|
||||
var item = ItemDatabase.get_item(from_slot.item_id)
|
||||
if item and item.stackable:
|
||||
var total_quantity = from_slot.quantity + to_slot.quantity
|
||||
if total_quantity <= item.max_stack:
|
||||
to_slot.quantity = total_quantity
|
||||
from_slot.clear()
|
||||
return true
|
||||
var space_available = item.max_stack - to_slot.quantity
|
||||
var amount_to_move = min(space_available, from_slot.quantity)
|
||||
to_slot.quantity += amount_to_move
|
||||
from_slot.quantity -= amount_to_move
|
||||
if from_slot.quantity <= 0:
|
||||
from_slot.clear()
|
||||
return true
|
||||
|
||||
var temp_item_id = from_slot.item_id
|
||||
var temp_quantity = from_slot.quantity
|
||||
|
||||
from_slot.item_id = to_slot.item_id
|
||||
from_slot.quantity = to_slot.quantity
|
||||
|
||||
to_slot.item_id = temp_item_id
|
||||
to_slot.quantity = temp_quantity
|
||||
|
||||
return true
|
||||
|
||||
|
||||
func get_first_empty_slot() -> int:
|
||||
for i in range(get_active_slot_count()):
|
||||
if slots[i].is_empty():
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
func to_dict() -> Dictionary:
|
||||
var data = []
|
||||
for slot in slots:
|
||||
data.append(slot.to_dict())
|
||||
return {
|
||||
"slots": data,
|
||||
"equipped_weapon": equipped_weapon.to_dict(),
|
||||
"equipped_hat": equipped_hat.to_dict(),
|
||||
"equipped_backpack": equipped_backpack.to_dict()
|
||||
}
|
||||
|
||||
|
||||
func from_dict(data: Dictionary) -> void:
|
||||
var slots_data = data.get("slots", [])
|
||||
for i in range(min(slots_data.size(), slots.size())):
|
||||
slots[i].from_dict(slots_data[i])
|
||||
for i in range(slots_data.size(), slots.size()):
|
||||
slots[i].clear()
|
||||
equipped_weapon.from_dict(data.get("equipped_weapon", {}))
|
||||
equipped_hat.from_dict(data.get("equipped_hat", {}))
|
||||
equipped_backpack.from_dict(data.get("equipped_backpack", {}))
|
||||
@@ -1 +0,0 @@
|
||||
uid://dncl3vbyhhee3
|
||||
@@ -1,4 +0,0 @@
|
||||
class_name ItemRigidBody3D
|
||||
extends RigidBody3D
|
||||
|
||||
@export var item_id: String = ""
|
||||
@@ -1 +0,0 @@
|
||||
uid://co2jpexoehl55
|
||||
@@ -1,487 +0,0 @@
|
||||
extends Node3D
|
||||
|
||||
const MAX_CHAT_MESSAGE_LENGTH := 160
|
||||
const MIN_NICKNAME_HEIGHT := 2.0
|
||||
const MAX_NICKNAME_HEIGHT := 8.0
|
||||
|
||||
@export var player_scene: PackedScene
|
||||
|
||||
var chat_visible := false
|
||||
var inventory_visible := false
|
||||
var player_list_visible := false
|
||||
|
||||
var _player_nickname_heights: Dictionary = {}
|
||||
var _nickname_heights_requested := false
|
||||
var _nickname_height_requesters: Dictionary = {}
|
||||
|
||||
@onready var players_container: Node3D = $PlayersContainer
|
||||
@onready var main_menu: MainMenuUI = $MainMenuUI
|
||||
@onready var multiplayer_chat: MultiplayerChatUI = $MultiplayerChatUI
|
||||
@onready var inventory_ui: InventoryUI = $InventoryUI
|
||||
@onready var player_list_ui: PlayerListUI = $PlayerListUI
|
||||
@onready var pause_menu: PauseMenuUI = $PauseMenuUI
|
||||
|
||||
|
||||
func _ready():
|
||||
after_ready()
|
||||
|
||||
if DisplayServer.get_name() == "headless":
|
||||
Network.start_host("", "")
|
||||
|
||||
pause_menu.hide_menu()
|
||||
main_menu.show_menu()
|
||||
multiplayer_chat.set_process_input(true)
|
||||
|
||||
main_menu.host_pressed.connect(_on_host_pressed)
|
||||
main_menu.join_pressed.connect(_on_join_pressed)
|
||||
main_menu.quit_pressed.connect(_on_quit_pressed)
|
||||
pause_menu.resume_pressed.connect(_on_pause_resume_pressed)
|
||||
pause_menu.main_menu_pressed.connect(_on_pause_main_menu_pressed)
|
||||
pause_menu.quit_pressed.connect(_on_pause_quit_pressed)
|
||||
|
||||
if inventory_ui:
|
||||
inventory_ui.inventory_closed.connect(_on_inventory_closed)
|
||||
|
||||
if multiplayer_chat:
|
||||
multiplayer_chat.message_sent.connect(_on_chat_message_sent)
|
||||
|
||||
Network.server_disconnected.connect(_on_server_disconnected)
|
||||
Network.connect("player_connected", Callable(self, "_on_player_connected"))
|
||||
multiplayer.peer_disconnected.connect(_remove_player)
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
var can_show_player_list := (
|
||||
not main_menu.is_menu_visible() and not pause_menu.is_menu_visible() and multiplayer.has_multiplayer_peer()
|
||||
)
|
||||
if Input.is_key_pressed(KEY_TAB) and can_show_player_list:
|
||||
if not player_list_visible:
|
||||
_show_player_list()
|
||||
elif player_list_visible:
|
||||
_hide_player_list()
|
||||
|
||||
|
||||
func after_ready():
|
||||
var ip_address: String
|
||||
if OS.has_feature("windows"):
|
||||
if OS.has_environment("COMPUTERNAME"):
|
||||
ip_address = IP.resolve_hostname(str(OS.get_environment("COMPUTERNAME")), IP.TYPE_IPV4)
|
||||
elif OS.has_feature("x11"):
|
||||
if OS.has_environment("HOSTNAME"):
|
||||
ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4)
|
||||
elif OS.has_feature("OSX"):
|
||||
if OS.has_environment("HOSTNAME"):
|
||||
ip_address = IP.resolve_hostname(str(OS.get_environment("HOSTNAME")), IP.TYPE_IPV4)
|
||||
main_menu.address_input.text = ip_address
|
||||
|
||||
|
||||
func _on_server_disconnected():
|
||||
_reset_session_ui()
|
||||
|
||||
|
||||
func _reset_session_ui() -> void:
|
||||
for child in players_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
var loot_root := get_node_or_null("Environment/ItemContainer")
|
||||
if loot_root:
|
||||
for child in loot_root.get_children():
|
||||
child.queue_free()
|
||||
|
||||
chat_visible = false
|
||||
inventory_visible = false
|
||||
_hide_player_list()
|
||||
_player_nickname_heights.clear()
|
||||
_nickname_heights_requested = false
|
||||
_nickname_height_requesters.clear()
|
||||
multiplayer_chat.close_chat()
|
||||
multiplayer_chat.clear_chat()
|
||||
if inventory_ui:
|
||||
inventory_ui.close_inventory()
|
||||
inventory_ui.current_player = null
|
||||
_hide_pause_menu(false)
|
||||
main_menu.show_menu()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _on_player_connected(peer_id, player_info):
|
||||
var player = _add_player(peer_id, player_info)
|
||||
if multiplayer.is_server() and player:
|
||||
player.call_deferred("_sync_inventory_to_owner")
|
||||
if multiplayer.is_server():
|
||||
if peer_id != 1:
|
||||
call_deferred("_sync_nickname_heights_to_peer", peer_id)
|
||||
elif not _nickname_heights_requested:
|
||||
_nickname_heights_requested = true
|
||||
call_deferred("_request_nickname_heights")
|
||||
_refresh_player_list()
|
||||
|
||||
|
||||
func _on_host_pressed(nickname: String, skin: String):
|
||||
var error = Network.start_host(nickname, skin)
|
||||
if error:
|
||||
push_warning("Failed to host game. Error: " + str(error))
|
||||
main_menu.show_menu()
|
||||
_update_mouse_mode()
|
||||
return
|
||||
main_menu.hide_menu()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _on_join_pressed(nickname: String, skin: String, address: String):
|
||||
var error = Network.join_game(nickname, skin, address)
|
||||
if error:
|
||||
push_warning("Failed to join game. Error: " + str(error))
|
||||
main_menu.show_menu()
|
||||
_update_mouse_mode()
|
||||
return
|
||||
main_menu.hide_menu()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _add_player(id: int, player_info: Dictionary) -> Character:
|
||||
if DisplayServer.get_name() == "headless" and id == 1:
|
||||
return null
|
||||
|
||||
if players_container.has_node(str(id)):
|
||||
return players_container.get_node(str(id)) as Character
|
||||
|
||||
var player = player_scene.instantiate()
|
||||
player.name = str(id)
|
||||
player.position = get_spawn_point(id)
|
||||
players_container.add_child(player, true)
|
||||
|
||||
var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(id))
|
||||
player.nickname.text = nick
|
||||
|
||||
var skin_enum = Network.sanitize_skin_value(player_info.get("skin", Character.SkinColor.BLUE))
|
||||
player.set_player_skin(skin_enum)
|
||||
_apply_player_nickname_height(id)
|
||||
return player
|
||||
|
||||
|
||||
func get_spawn_point(id: int) -> Vector3:
|
||||
var spawn_angle := fmod(float(id) * 2.399963229728653, 2.0 * PI)
|
||||
var spawn_point := Vector2.from_angle(spawn_angle) * 10
|
||||
return Vector3(spawn_point.x, 0, spawn_point.y)
|
||||
|
||||
|
||||
func _remove_player(id):
|
||||
_player_nickname_heights.erase(id)
|
||||
if not players_container.has_node(str(id)):
|
||||
call_deferred("_refresh_player_list")
|
||||
return
|
||||
var player_node = players_container.get_node(str(id))
|
||||
if player_node:
|
||||
player_node.queue_free()
|
||||
call_deferred("_refresh_player_list")
|
||||
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
Network.leave_game()
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func toggle_chat():
|
||||
if main_menu.is_menu_visible() or is_gameplay_input_blocked():
|
||||
return
|
||||
|
||||
multiplayer_chat.toggle_chat()
|
||||
chat_visible = multiplayer_chat.is_chat_visible()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func is_chat_visible() -> bool:
|
||||
return multiplayer_chat.is_chat_visible()
|
||||
|
||||
|
||||
func _input(event):
|
||||
if event.is_action_pressed("pause"):
|
||||
_handle_pause_action()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
||||
if is_gameplay_input_blocked():
|
||||
return
|
||||
|
||||
if event.is_action_pressed("toggle_chat"):
|
||||
toggle_chat()
|
||||
elif chat_visible and multiplayer_chat.message.has_focus():
|
||||
pass
|
||||
elif event.is_action_pressed("inventory"):
|
||||
toggle_inventory()
|
||||
elif event is InputEventKey and event.pressed and event.keycode == KEY_F1:
|
||||
_debug_add_item()
|
||||
elif event is InputEventKey and event.pressed and event.keycode == KEY_F2:
|
||||
_debug_print_inventory()
|
||||
|
||||
|
||||
func _on_chat_message_sent(message_text: String) -> void:
|
||||
chat_visible = multiplayer_chat.is_chat_visible()
|
||||
_update_mouse_mode()
|
||||
|
||||
var trimmed_message = _sanitize_chat_message(message_text)
|
||||
if trimmed_message.is_empty():
|
||||
return
|
||||
|
||||
if multiplayer.is_server():
|
||||
_broadcast_chat_message(multiplayer.get_unique_id(), trimmed_message)
|
||||
else:
|
||||
submit_chat_message.rpc_id(1, trimmed_message)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func submit_chat_message(message_text: String):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var sender_id = multiplayer.get_remote_sender_id()
|
||||
var trimmed_message = _sanitize_chat_message(message_text)
|
||||
if trimmed_message.is_empty():
|
||||
return
|
||||
_broadcast_chat_message(sender_id, trimmed_message)
|
||||
|
||||
|
||||
func _broadcast_chat_message(sender_id: int, message_text: String):
|
||||
var player_info = Network.players.get(sender_id, {})
|
||||
var nick = Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(sender_id))
|
||||
show_chat_message.rpc(nick, message_text)
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func show_chat_message(nick: String, msg: String):
|
||||
multiplayer_chat.add_message(nick, msg)
|
||||
|
||||
|
||||
func _sanitize_chat_message(message_text: String) -> String:
|
||||
var clean = message_text.strip_edges()
|
||||
if clean.length() > MAX_CHAT_MESSAGE_LENGTH:
|
||||
clean = clean.substr(0, MAX_CHAT_MESSAGE_LENGTH)
|
||||
return clean
|
||||
|
||||
|
||||
func toggle_inventory():
|
||||
if main_menu.is_menu_visible() or is_gameplay_input_blocked():
|
||||
return
|
||||
|
||||
var local_player = _get_local_player()
|
||||
if not local_player:
|
||||
return
|
||||
|
||||
inventory_visible = !inventory_visible
|
||||
if inventory_visible:
|
||||
inventory_ui.open_inventory(local_player)
|
||||
else:
|
||||
inventory_ui.close_inventory()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func is_inventory_visible() -> bool:
|
||||
return inventory_visible
|
||||
|
||||
|
||||
func _show_player_list() -> void:
|
||||
if main_menu.is_menu_visible() or pause_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
player_list_visible = true
|
||||
player_list_ui.show_players(Network.players, multiplayer.get_unique_id())
|
||||
|
||||
|
||||
func _hide_player_list() -> void:
|
||||
player_list_visible = false
|
||||
if player_list_ui:
|
||||
player_list_ui.hide_players()
|
||||
|
||||
|
||||
func _refresh_player_list() -> void:
|
||||
if player_list_visible and player_list_ui:
|
||||
player_list_ui.refresh_players(Network.players, multiplayer.get_unique_id())
|
||||
|
||||
|
||||
func register_player_nickname_height(player_id: int, height: float) -> void:
|
||||
if not multiplayer.is_server() or player_id <= 0:
|
||||
return
|
||||
var normalized_height := _normalize_nickname_height(height)
|
||||
_store_and_apply_player_nickname_height(player_id, normalized_height)
|
||||
sync_player_nickname_height.rpc(player_id, normalized_height)
|
||||
|
||||
|
||||
@rpc("authority", "reliable")
|
||||
func sync_player_nickname_height(player_id: int, height: float) -> void:
|
||||
if multiplayer.get_remote_sender_id() != 1:
|
||||
return
|
||||
_store_and_apply_player_nickname_height(player_id, _normalize_nickname_height(height))
|
||||
|
||||
|
||||
func _request_nickname_heights() -> void:
|
||||
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
request_nickname_heights.rpc_id(1)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_nickname_heights() -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var requester_id := multiplayer.get_remote_sender_id()
|
||||
if requester_id <= 0 or _nickname_height_requesters.has(requester_id):
|
||||
return
|
||||
_nickname_height_requesters[requester_id] = true
|
||||
_sync_nickname_heights_to_peer(requester_id)
|
||||
|
||||
|
||||
func _sync_nickname_heights_to_peer(peer_id: int) -> void:
|
||||
if not multiplayer.is_server() or peer_id <= 0:
|
||||
return
|
||||
for child in players_container.get_children():
|
||||
var player := child as Character
|
||||
if not player:
|
||||
continue
|
||||
var player_id := str(player.name).to_int()
|
||||
var height := float(_player_nickname_heights.get(player_id, player.get_current_nickname_height()))
|
||||
sync_player_nickname_height.rpc_id(peer_id, player_id, _normalize_nickname_height(height))
|
||||
|
||||
|
||||
func _store_and_apply_player_nickname_height(player_id: int, height: float) -> void:
|
||||
if player_id <= 0:
|
||||
return
|
||||
_player_nickname_heights[player_id] = height
|
||||
_apply_player_nickname_height(player_id)
|
||||
|
||||
|
||||
func _apply_player_nickname_height(player_id: int) -> void:
|
||||
if not _player_nickname_heights.has(player_id):
|
||||
return
|
||||
var player := players_container.get_node_or_null(str(player_id)) as Character
|
||||
if player:
|
||||
player.apply_synced_nickname_height(float(_player_nickname_heights[player_id]))
|
||||
|
||||
|
||||
func _normalize_nickname_height(height: float) -> float:
|
||||
if is_nan(height) or is_inf(height):
|
||||
return MIN_NICKNAME_HEIGHT
|
||||
return clampf(height, MIN_NICKNAME_HEIGHT, MAX_NICKNAME_HEIGHT)
|
||||
|
||||
|
||||
func _on_inventory_closed():
|
||||
inventory_visible = false
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func is_gameplay_input_blocked() -> bool:
|
||||
return pause_menu.is_menu_visible()
|
||||
|
||||
|
||||
func is_camera_input_blocked() -> bool:
|
||||
return is_gameplay_input_blocked() or inventory_visible or multiplayer_chat.is_chat_visible()
|
||||
|
||||
|
||||
func _handle_pause_action() -> void:
|
||||
if inventory_visible:
|
||||
_close_inventory()
|
||||
return
|
||||
if multiplayer_chat.is_chat_visible():
|
||||
_close_chat()
|
||||
return
|
||||
if pause_menu.is_menu_visible():
|
||||
_hide_pause_menu()
|
||||
return
|
||||
if main_menu.is_menu_visible() or not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
_show_pause_menu()
|
||||
|
||||
|
||||
func _show_pause_menu() -> void:
|
||||
pause_menu.show_menu()
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _hide_pause_menu(restore_mouse_mode: bool = true) -> void:
|
||||
if not pause_menu.is_menu_visible():
|
||||
return
|
||||
pause_menu.hide_menu()
|
||||
if restore_mouse_mode:
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _close_chat() -> void:
|
||||
multiplayer_chat.close_chat()
|
||||
chat_visible = false
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _close_inventory() -> void:
|
||||
inventory_ui.close_inventory()
|
||||
inventory_visible = false
|
||||
_update_mouse_mode()
|
||||
|
||||
|
||||
func _update_mouse_mode() -> void:
|
||||
if DisplayServer.get_name() == "headless":
|
||||
return
|
||||
var ui_requires_cursor := (
|
||||
main_menu.is_menu_visible()
|
||||
or pause_menu.is_menu_visible()
|
||||
or inventory_visible
|
||||
or not multiplayer.has_multiplayer_peer()
|
||||
)
|
||||
if ui_requires_cursor:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
|
||||
else:
|
||||
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
|
||||
|
||||
|
||||
func _on_pause_resume_pressed() -> void:
|
||||
_hide_pause_menu()
|
||||
|
||||
|
||||
func _on_pause_main_menu_pressed() -> void:
|
||||
_hide_pause_menu(false)
|
||||
Network.leave_game()
|
||||
if not main_menu.is_menu_visible():
|
||||
_reset_session_ui()
|
||||
|
||||
|
||||
func _on_pause_quit_pressed() -> void:
|
||||
Network.leave_game()
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func update_local_inventory_display():
|
||||
if inventory_ui:
|
||||
inventory_ui.refresh_display()
|
||||
|
||||
|
||||
func _get_local_player() -> Character:
|
||||
var local_player_id = multiplayer.get_unique_id()
|
||||
if players_container.has_node(str(local_player_id)):
|
||||
return players_container.get_node(str(local_player_id)) as Character
|
||||
return null
|
||||
|
||||
|
||||
func _debug_add_item():
|
||||
if not OS.is_debug_build() or not multiplayer.is_server():
|
||||
return
|
||||
var local_player = _get_local_player()
|
||||
if local_player:
|
||||
var test_items: Array[String] = []
|
||||
for item_id in ItemDatabase.get_all_items():
|
||||
test_items.append(str(item_id))
|
||||
if test_items.is_empty():
|
||||
return
|
||||
var random_item = test_items[randi() % test_items.size()]
|
||||
local_player.request_add_item.rpc_id(1, random_item, 1)
|
||||
|
||||
|
||||
func _debug_print_inventory():
|
||||
var local_player = _get_local_player()
|
||||
if local_player and local_player.get_inventory():
|
||||
var inventory = local_player.get_inventory()
|
||||
print("=== Inventory Debug ===")
|
||||
for i in range(inventory.slots.size()):
|
||||
var slot = inventory.get_slot(i)
|
||||
if slot and not slot.is_empty():
|
||||
print("Slot ", i, ": ", slot.item_id, " x", slot.quantity)
|
||||
print("=====================")
|
||||
else:
|
||||
print("No inventory found for local player")
|
||||
@@ -1 +0,0 @@
|
||||
uid://d0dgljwwl463n
|
||||
@@ -1,43 +0,0 @@
|
||||
#class_name Body
|
||||
extends Node3D
|
||||
|
||||
# 线性插值速度
|
||||
const LERP_VELOCITY: float = 0.15
|
||||
|
||||
@export_category("Objects")
|
||||
@export var _character: CharacterBody3D = null
|
||||
@export var animation_player: AnimationPlayer = null
|
||||
|
||||
var _current_state: StringName = &""
|
||||
|
||||
|
||||
func apply_rotation(_velocity: Vector3) -> void:
|
||||
var new_rotation_y = lerp_angle(rotation.y, atan2(-_velocity.x, -_velocity.z), LERP_VELOCITY)
|
||||
rotation.y = new_rotation_y
|
||||
|
||||
|
||||
func get_movement_animation(_velocity: Vector3) -> StringName:
|
||||
if not _character.is_on_floor():
|
||||
if _velocity.y < 0:
|
||||
return &"Fall"
|
||||
if _current_state == &"Jump2":
|
||||
return &"Jump2"
|
||||
return &"Jump"
|
||||
|
||||
if _velocity:
|
||||
if _character._is_running() and _character.is_on_floor():
|
||||
return &"Sprint"
|
||||
return &"Run"
|
||||
|
||||
return &"Idle"
|
||||
|
||||
|
||||
func play_animation_state(state: StringName, restart: bool = false) -> void:
|
||||
if not animation_player:
|
||||
return
|
||||
if not animation_player.has_animation(state):
|
||||
return
|
||||
if not restart and _current_state == state and animation_player.is_playing():
|
||||
return
|
||||
_current_state = state
|
||||
animation_player.play(state)
|
||||
@@ -1 +0,0 @@
|
||||
uid://v8j54rbc0ik
|
||||
@@ -1,872 +0,0 @@
|
||||
class_name Character
|
||||
extends CharacterBody3D
|
||||
|
||||
enum SkinColor { BLUE, YELLOW, GREEN, RED }
|
||||
|
||||
@export var 正常速度 = 6.0
|
||||
@export var 冲刺速度 = 10.0
|
||||
@export var 跳跃初速度 = 7.5
|
||||
@export var 下落重力倍率 = 1.6
|
||||
@export var 昵称基准高度 := 2.0
|
||||
@export var 服务器每秒动画请求数 := 10.0
|
||||
@export var 服务器动画请求突发上限 := 6.0
|
||||
@export var 拾取动画延迟毫秒 := 1000
|
||||
@export var 拾取动画时间窗口毫秒 := 2500
|
||||
@export var 拾取请求冷却毫秒 := 1000
|
||||
const 允许动画状态 := {
|
||||
&"Idle": true,
|
||||
&"Run": true,
|
||||
&"Sprint": true,
|
||||
&"Jump": true,
|
||||
&"Jump2": true,
|
||||
&"Fall": true,
|
||||
&"Attack1": true,
|
||||
&"Emote2": true
|
||||
}
|
||||
const HAT_NODES_BY_ITEM := {
|
||||
"fedora": "Fedora",
|
||||
"headphones": "Headphones",
|
||||
"pirate_hat": "PirateHat",
|
||||
"sheriff_hat": "SheriffHat",
|
||||
"sombrero": "Sombrero",
|
||||
"wizard_hat": "WizardHat"
|
||||
}
|
||||
const WEAPON_NODES_BY_ITEM := {"sword": "Sword", "sword_big": "SwordBig", "axe": "Axe"}
|
||||
const BACKPACK_NODES_BY_ITEM := {"backpack": "Backpack"}
|
||||
const HEAD_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/HeadAttach/"
|
||||
const HAND_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/LeftHandAttach/"
|
||||
const BACK_EQUIPMENT_PATH := "GodotRobot3D/RobotArmature/Skeleton3D/BackAttach/"
|
||||
const FIRST_PERSON_HIDDEN_BONES: Array[StringName] = [&"Head", &"HeadTop"]
|
||||
|
||||
@export var skin_color: SkinColor = SkinColor.BLUE
|
||||
|
||||
@export_category("Nickname")
|
||||
@export_range(0.0, 1.0, 0.01) var nickname_clearance: float = 0.2
|
||||
|
||||
@export_category("Objects")
|
||||
@export var _body: Node3D = null
|
||||
@export var _spring_arm_offset: SpringArmCharacter = null
|
||||
|
||||
@export_category("Skin Colors")
|
||||
@export var blue_texture: CompressedTexture2D
|
||||
@export var yellow_texture: CompressedTexture2D
|
||||
@export var green_texture: CompressedTexture2D
|
||||
@export var red_texture: CompressedTexture2D
|
||||
|
||||
var player_inventory: PlayerInventory
|
||||
var gravity = ProjectSettings.get_setting("physics/3d/default_gravity")
|
||||
var can_double_jump = true
|
||||
var has_double_jumped = false
|
||||
var is_attacking := false
|
||||
var is_collecting := false
|
||||
|
||||
var _current_speed: float
|
||||
var _spawn_point = Vector3(0, 5, 0)
|
||||
var _animation_sequence := 0
|
||||
var _last_applied_animation_sequence := 0
|
||||
var _last_requested_animation: StringName = &""
|
||||
var _appearance_sync_requesters: Dictionary = {}
|
||||
var _server_animation_request_tokens := 服务器动画请求突发上限
|
||||
var _last_server_animation_token_update_msec := 0
|
||||
var _server_pickup_animation_started_msec := -1
|
||||
var _last_server_pickup_request_msec := -拾取请求冷却毫秒
|
||||
var _pickup_area_camera_yaw_offset := 0.0
|
||||
var _equipped_hat_visual_id := ""
|
||||
|
||||
@onready var nickname: Label3D = $PlayerNick/Nickname
|
||||
|
||||
@onready var _bottom_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Bottom")
|
||||
@onready var _chest_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Chest")
|
||||
@onready var _face_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/Face")
|
||||
@onready var _limbs_head_mesh: MeshInstance3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D/LimbsAndHead")
|
||||
@onready var _skeleton: Skeleton3D = get_node("GodotRobot3D/RobotArmature/Skeleton3D")
|
||||
@onready var _pickup_area: Area3D = $GodotRobot3D/InfrontArea3D
|
||||
@onready var _first_person_hud: CanvasLayer = $FirstPersonHUD
|
||||
|
||||
|
||||
func _enter_tree():
|
||||
set_multiplayer_authority(str(name).to_int())
|
||||
$SpringArmOffset/SpringArm3D/Camera3D.current = is_multiplayer_authority()
|
||||
|
||||
|
||||
func _ready():
|
||||
if multiplayer.is_server():
|
||||
player_inventory = PlayerInventory.new()
|
||||
_add_starting_items()
|
||||
call_deferred("_sync_equipment_appearance")
|
||||
if not is_multiplayer_authority():
|
||||
call_deferred("_sync_inventory_to_owner")
|
||||
|
||||
set_player_skin(skin_color)
|
||||
var animation_player := get_node_or_null("GodotRobot3D/AnimationPlayer") as AnimationPlayer
|
||||
if animation_player:
|
||||
animation_player.animation_finished.connect(_on_animation_finished)
|
||||
_body.play_animation_state(&"Idle")
|
||||
nickname.visible = true
|
||||
_set_nickname_height(昵称基准高度)
|
||||
call_deferred("_update_nickname_height")
|
||||
if not multiplayer.is_server():
|
||||
call_deferred("_request_equipment_appearance")
|
||||
if is_multiplayer_authority() and _spring_arm_offset:
|
||||
_pickup_area_camera_yaw_offset = wrapf(
|
||||
_pickup_area.global_rotation.y - _spring_arm_offset.global_rotation.y, -PI, PI
|
||||
)
|
||||
_spring_arm_offset.perspective_changed.connect(_on_camera_perspective_changed)
|
||||
_on_camera_perspective_changed(_spring_arm_offset.is_first_person)
|
||||
|
||||
|
||||
func _on_camera_perspective_changed(first_person: bool) -> void:
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
_body.visible = true
|
||||
_bottom_mesh.visible = not first_person
|
||||
_chest_mesh.visible = not first_person
|
||||
_face_mesh.visible = not first_person
|
||||
_limbs_head_mesh.visible = true
|
||||
nickname.visible = not first_person
|
||||
_first_person_hud.visible = first_person
|
||||
for bone_name in FIRST_PERSON_HIDDEN_BONES:
|
||||
var bone_index := _skeleton.find_bone(bone_name)
|
||||
if bone_index >= 0:
|
||||
_skeleton.set_bone_pose_scale(bone_index, Vector3.ZERO if first_person else Vector3.ONE)
|
||||
if first_person:
|
||||
_align_pickup_area_with_camera()
|
||||
else:
|
||||
_pickup_area.rotation = Vector3.ZERO
|
||||
call_deferred("_refresh_nickname_height_after_perspective_change")
|
||||
|
||||
|
||||
func _refresh_nickname_height_after_perspective_change() -> void:
|
||||
if not is_multiplayer_authority() or _is_local_first_person():
|
||||
return
|
||||
var height := _calculate_nickname_height(_equipped_hat_visual_id)
|
||||
_set_nickname_height(height)
|
||||
nickname.visible = true
|
||||
if multiplayer.is_server():
|
||||
_broadcast_nickname_height(height)
|
||||
|
||||
|
||||
func _align_pickup_area_with_camera() -> void:
|
||||
var pickup_rotation := _pickup_area.global_rotation
|
||||
pickup_rotation.y = wrapf(_spring_arm_offset.global_rotation.y + _pickup_area_camera_yaw_offset, -PI, PI)
|
||||
_pickup_area.global_rotation = pickup_rotation
|
||||
|
||||
|
||||
func _physics_process(delta):
|
||||
if not multiplayer.has_multiplayer_peer() or not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
var current_scene = get_tree().get_current_scene()
|
||||
var should_freeze = false
|
||||
if current_scene:
|
||||
if current_scene.has_method("is_gameplay_input_blocked") and current_scene.is_gameplay_input_blocked():
|
||||
should_freeze = true
|
||||
elif current_scene.has_method("is_chat_visible") and current_scene.is_chat_visible():
|
||||
should_freeze = true
|
||||
elif current_scene.has_method("is_inventory_visible") and current_scene.is_inventory_visible():
|
||||
should_freeze = true
|
||||
|
||||
if is_attacking or is_collecting:
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
_apply_gravity(delta)
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
if should_freeze:
|
||||
_freeze()
|
||||
_apply_gravity(delta)
|
||||
move_and_slide()
|
||||
_request_animation(_body.get_movement_animation(velocity))
|
||||
return
|
||||
|
||||
if Input.is_action_just_pressed("pickup") and is_on_floor() and _has_collectible_item_in_front():
|
||||
is_collecting = true
|
||||
_request_animation(&"Emote2", true)
|
||||
return
|
||||
|
||||
if Input.is_action_just_pressed("attack") and is_on_floor():
|
||||
_start_attack()
|
||||
return
|
||||
|
||||
if is_on_floor():
|
||||
can_double_jump = true
|
||||
has_double_jumped = false
|
||||
|
||||
if Input.is_action_just_pressed("jump"):
|
||||
velocity.y = 跳跃初速度
|
||||
can_double_jump = true
|
||||
_request_animation(&"Jump", true)
|
||||
else:
|
||||
_apply_gravity(delta)
|
||||
|
||||
if can_double_jump and not has_double_jumped and Input.is_action_just_pressed("jump"):
|
||||
velocity.y = 跳跃初速度
|
||||
has_double_jumped = true
|
||||
can_double_jump = false
|
||||
_request_animation(&"Jump2", true)
|
||||
|
||||
_move()
|
||||
var collided = move_and_slide()
|
||||
if collided:
|
||||
_push_collided_items()
|
||||
|
||||
_request_animation(_body.get_movement_animation(velocity))
|
||||
|
||||
|
||||
func _apply_gravity(delta: float) -> void:
|
||||
if is_on_floor():
|
||||
return
|
||||
var gravity_multiplier = 下落重力倍率 if velocity.y < 0 else 1.0
|
||||
velocity.y -= gravity * gravity_multiplier * delta
|
||||
|
||||
|
||||
func _start_attack() -> void:
|
||||
if is_attacking or is_collecting or not is_on_floor():
|
||||
return
|
||||
is_attacking = true
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
_request_animation(&"Attack1", true)
|
||||
|
||||
|
||||
func _on_animation_finished(animation_name: StringName) -> void:
|
||||
match animation_name:
|
||||
&"Attack1":
|
||||
is_attacking = false
|
||||
&"Emote2":
|
||||
is_collecting = false
|
||||
|
||||
|
||||
func _request_animation(state: StringName, restart: bool = false) -> void:
|
||||
if not 允许动画状态.has(state):
|
||||
return
|
||||
if not restart and _last_requested_animation == state:
|
||||
return
|
||||
_last_requested_animation = state
|
||||
_body.play_animation_state(state, restart)
|
||||
if multiplayer.is_server():
|
||||
request_animation_state(state)
|
||||
else:
|
||||
request_animation_state.rpc_id(1, state)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_animation_state(state: StringName) -> void:
|
||||
if not multiplayer.is_server() or not _is_owner_request():
|
||||
return
|
||||
if not 允许动画状态.has(state):
|
||||
return
|
||||
if not _server_consume_animation_request_token():
|
||||
return
|
||||
if state == &"Emote2":
|
||||
if not _is_grounded_on_server() or not _has_collectible_item_in_front():
|
||||
return
|
||||
_server_pickup_animation_started_msec = Time.get_ticks_msec()
|
||||
else:
|
||||
_server_pickup_animation_started_msec = -1
|
||||
_server_publish_animation(state)
|
||||
|
||||
|
||||
func _server_consume_animation_request_token() -> bool:
|
||||
var now := Time.get_ticks_msec()
|
||||
if _last_server_animation_token_update_msec == 0:
|
||||
_last_server_animation_token_update_msec = now
|
||||
else:
|
||||
var elapsed_seconds := (now - _last_server_animation_token_update_msec) / 1000.0
|
||||
_server_animation_request_tokens = minf(
|
||||
服务器动画请求突发上限,
|
||||
_server_animation_request_tokens + elapsed_seconds * 服务器每秒动画请求数
|
||||
)
|
||||
_last_server_animation_token_update_msec = now
|
||||
|
||||
if _server_animation_request_tokens < 1.0:
|
||||
return false
|
||||
_server_animation_request_tokens -= 1.0
|
||||
return true
|
||||
|
||||
|
||||
func _server_publish_animation(state: StringName) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
_animation_sequence += 1
|
||||
sync_animation_state.rpc(state, _animation_sequence)
|
||||
sync_animation_state(state, _animation_sequence)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_animation_state(state: StringName, sequence: int) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
|
||||
return
|
||||
if sequence <= _last_applied_animation_sequence:
|
||||
return
|
||||
_last_applied_animation_sequence = sequence
|
||||
|
||||
if not 允许动画状态.has(state):
|
||||
return
|
||||
|
||||
# The owner already played the input-driven state immediately.
|
||||
if is_multiplayer_authority():
|
||||
return
|
||||
_body.play_animation_state(state, true)
|
||||
|
||||
|
||||
func _push_collided_items() -> void:
|
||||
for i in get_slide_collision_count():
|
||||
var c = get_slide_collision(i)
|
||||
if c.get_collider() is RigidBody3D:
|
||||
apply_force_to_server_object.rpc_id(1, c.get_collider().name, -c.get_normal())
|
||||
|
||||
|
||||
func _process(_delta):
|
||||
if not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
var first_person := _spring_arm_offset != null and _spring_arm_offset.is_first_person
|
||||
if first_person:
|
||||
_align_pickup_area_with_camera()
|
||||
var camera_input_blocked := false
|
||||
var current_scene := get_tree().get_current_scene()
|
||||
if current_scene and current_scene.has_method("is_camera_input_blocked"):
|
||||
camera_input_blocked = current_scene.is_camera_input_blocked()
|
||||
_first_person_hud.visible = (
|
||||
first_person and not camera_input_blocked and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED
|
||||
)
|
||||
_check_out_of_bounds()
|
||||
|
||||
|
||||
func _freeze():
|
||||
velocity.x = 0
|
||||
velocity.z = 0
|
||||
_current_speed = 0
|
||||
_request_animation(&"Idle")
|
||||
|
||||
|
||||
func _move() -> void:
|
||||
var input_direction: Vector2 = Vector2.ZERO
|
||||
if is_multiplayer_authority():
|
||||
input_direction = Input.get_vector("move_left", "move_right", "move_forward", "move_backward")
|
||||
|
||||
var direction: Vector3 = transform.basis * Vector3(input_direction.x, 0, input_direction.y).normalized()
|
||||
|
||||
_is_running()
|
||||
direction = direction.rotated(Vector3.UP, _spring_arm_offset.rotation.y)
|
||||
|
||||
if direction:
|
||||
velocity.x = direction.x * _current_speed
|
||||
velocity.z = direction.z * _current_speed
|
||||
_body.apply_rotation(velocity)
|
||||
return
|
||||
|
||||
velocity.x = move_toward(velocity.x, 0, _current_speed)
|
||||
velocity.z = move_toward(velocity.z, 0, _current_speed)
|
||||
|
||||
|
||||
func _is_running() -> bool:
|
||||
if Input.is_action_pressed("shift"):
|
||||
_current_speed = 冲刺速度
|
||||
return true
|
||||
_current_speed = 正常速度
|
||||
return false
|
||||
|
||||
|
||||
func _check_out_of_bounds():
|
||||
if global_transform.origin.y < -15.0:
|
||||
_reset_position_after_fall()
|
||||
|
||||
|
||||
func _reset_position_after_fall():
|
||||
global_transform.origin = _spawn_point
|
||||
velocity = Vector3.ZERO
|
||||
|
||||
|
||||
func _get_texture_from_name(color: SkinColor) -> CompressedTexture2D:
|
||||
match color:
|
||||
SkinColor.BLUE:
|
||||
return blue_texture
|
||||
SkinColor.GREEN:
|
||||
return green_texture
|
||||
SkinColor.RED:
|
||||
return red_texture
|
||||
SkinColor.YELLOW:
|
||||
return yellow_texture
|
||||
_:
|
||||
return blue_texture
|
||||
|
||||
|
||||
func set_player_skin(skin_name: SkinColor) -> void:
|
||||
var texture = _get_texture_from_name(skin_name)
|
||||
|
||||
_set_mesh_texture(_bottom_mesh, texture)
|
||||
_set_mesh_texture(_chest_mesh, texture)
|
||||
_set_mesh_texture(_face_mesh, texture)
|
||||
_set_mesh_texture(_limbs_head_mesh, texture)
|
||||
|
||||
|
||||
func _set_mesh_texture(mesh_instance: MeshInstance3D, texture: CompressedTexture2D) -> void:
|
||||
if mesh_instance:
|
||||
var new_material := StandardMaterial3D.new()
|
||||
new_material.albedo_texture = texture
|
||||
mesh_instance.set_surface_override_material(0, new_material)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func sync_inventory_to_owner(inventory_data: Dictionary):
|
||||
var sender_id = multiplayer.get_remote_sender_id()
|
||||
if sender_id != 1 and not (sender_id == 0 and multiplayer.is_server()):
|
||||
return
|
||||
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
if not player_inventory:
|
||||
player_inventory = PlayerInventory.new()
|
||||
player_inventory.from_dict(inventory_data)
|
||||
|
||||
var level_scene = get_tree().get_current_scene()
|
||||
if (
|
||||
level_scene
|
||||
and get_multiplayer_authority() == multiplayer.get_unique_id()
|
||||
and level_scene.has_method("update_local_inventory_display")
|
||||
):
|
||||
level_scene.update_local_inventory_display()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_move_item(from_slot: int, to_slot: int, quantity: int = -1):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
var requesting_client = multiplayer.get_remote_sender_id()
|
||||
if not _is_owner_request():
|
||||
push_warning(
|
||||
(
|
||||
"Client "
|
||||
+ str(requesting_client)
|
||||
+ " tried to modify inventory for player "
|
||||
+ str(get_multiplayer_authority())
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not player_inventory:
|
||||
return
|
||||
|
||||
if not player_inventory.is_slot_active(from_slot) or not player_inventory.is_slot_active(to_slot):
|
||||
push_warning("Invalid slot indices: from=" + str(from_slot) + " to=" + str(to_slot))
|
||||
return
|
||||
|
||||
if quantity != -1 and quantity <= 0:
|
||||
push_warning("Invalid move quantity: " + str(quantity))
|
||||
return
|
||||
|
||||
var success = false
|
||||
if quantity == -1:
|
||||
success = player_inventory.move_item(from_slot, to_slot)
|
||||
if not success:
|
||||
success = player_inventory.swap_items(from_slot, to_slot)
|
||||
else:
|
||||
success = player_inventory.move_item(from_slot, to_slot, quantity)
|
||||
|
||||
if success:
|
||||
_sync_inventory_to_owner()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_add_item(item_id: String, quantity: int = 1):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
var requesting_client = multiplayer.get_remote_sender_id()
|
||||
var is_local_server_call = requesting_client == 0 and multiplayer.get_unique_id() == 1
|
||||
if requesting_client != 1 and not is_local_server_call:
|
||||
push_warning(
|
||||
"Client " + str(requesting_client) + " tried to add items to player " + str(get_multiplayer_authority())
|
||||
)
|
||||
return
|
||||
|
||||
if not player_inventory:
|
||||
return
|
||||
|
||||
if quantity <= 0:
|
||||
push_warning("Invalid quantity: " + str(quantity))
|
||||
return
|
||||
|
||||
var item = ItemDatabase.get_item(item_id)
|
||||
if not item:
|
||||
push_warning("Item not found: " + item_id)
|
||||
return
|
||||
|
||||
var remaining = player_inventory.add_item(item, quantity)
|
||||
var added = quantity - remaining
|
||||
|
||||
if added > 0:
|
||||
_sync_inventory_to_owner()
|
||||
|
||||
|
||||
func request_add_single_item(item_id: String) -> bool:
|
||||
if not multiplayer.is_server():
|
||||
return false
|
||||
|
||||
if player_inventory == null:
|
||||
return false
|
||||
|
||||
var item = ItemDatabase.get_item(item_id)
|
||||
if not item:
|
||||
push_warning("Item not found: " + item_id)
|
||||
return false
|
||||
|
||||
var remaining = player_inventory.add_item(item, 1)
|
||||
|
||||
if remaining == 0:
|
||||
_sync_inventory_to_owner()
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_remove_item(item_id: String, quantity: int = 1):
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
|
||||
var requesting_client = multiplayer.get_remote_sender_id()
|
||||
if not _is_owner_request():
|
||||
push_warning(
|
||||
(
|
||||
"Client "
|
||||
+ str(requesting_client)
|
||||
+ " tried to remove items from player "
|
||||
+ str(get_multiplayer_authority())
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not player_inventory:
|
||||
return
|
||||
|
||||
if quantity <= 0:
|
||||
push_warning("Invalid quantity: " + str(quantity))
|
||||
return
|
||||
|
||||
var removed = player_inventory.remove_item(item_id, quantity)
|
||||
|
||||
if removed > 0:
|
||||
_sync_inventory_to_owner()
|
||||
|
||||
|
||||
@rpc("authority", "call_local", "reliable")
|
||||
func add_world_item(scene_path: String, player_position: Vector3) -> void:
|
||||
var item_container = get_node_or_null("/root/Level/Environment/ItemContainer")
|
||||
if not item_container:
|
||||
push_warning("ItemContainer not found at /root/Level/Environment/ItemContainer")
|
||||
return
|
||||
if scene_path.is_empty() or not ResourceLoader.exists(scene_path):
|
||||
push_warning("Cannot add world item: invalid scene path '" + scene_path + "'")
|
||||
return
|
||||
var packed_scene = load(scene_path) as PackedScene
|
||||
if not packed_scene:
|
||||
push_warning("Cannot add world item: scene path is not a PackedScene '" + scene_path + "'")
|
||||
return
|
||||
var instance_item := packed_scene.instantiate() as Node3D
|
||||
if not instance_item:
|
||||
push_warning("Cannot add world item: scene root is not a Node3D '" + scene_path + "'")
|
||||
return
|
||||
item_container.add_child(instance_item, true)
|
||||
instance_item.global_position = player_position
|
||||
|
||||
|
||||
func get_inventory() -> PlayerInventory:
|
||||
return player_inventory
|
||||
|
||||
|
||||
func _sync_inventory_to_owner() -> void:
|
||||
if not multiplayer.is_server() or not player_inventory:
|
||||
return
|
||||
var owner_id = get_multiplayer_authority()
|
||||
if owner_id == 1:
|
||||
sync_inventory_to_owner(player_inventory.to_dict())
|
||||
else:
|
||||
sync_inventory_to_owner.rpc_id(owner_id, player_inventory.to_dict())
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_equip_item(from_slot: int, item_type: Item.ItemType) -> void:
|
||||
if not multiplayer.is_server() or not _is_owner_request():
|
||||
return
|
||||
if not player_inventory or not player_inventory.is_slot_active(from_slot):
|
||||
return
|
||||
if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
|
||||
return
|
||||
if player_inventory.equip_from_slot(from_slot, item_type):
|
||||
_sync_inventory_to_owner()
|
||||
_sync_equipment_appearance()
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_unequip_item(item_type: Item.ItemType, destination_slot: int = -1) -> void:
|
||||
if not multiplayer.is_server() or not _is_owner_request():
|
||||
return
|
||||
if not player_inventory:
|
||||
return
|
||||
if item_type != Item.ItemType.WEAPON and item_type != Item.ItemType.HAT and item_type != Item.ItemType.BACKPACK:
|
||||
return
|
||||
if destination_slot < -1 or destination_slot >= PlayerInventory.MAX_INVENTORY_SIZE:
|
||||
return
|
||||
if destination_slot >= 0 and not player_inventory.is_slot_active(destination_slot):
|
||||
return
|
||||
if player_inventory.unequip_to_slot(item_type, destination_slot):
|
||||
_sync_inventory_to_owner()
|
||||
_sync_equipment_appearance()
|
||||
|
||||
|
||||
func _is_owner_request() -> bool:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
return sender == get_multiplayer_authority() or (sender == 0 and multiplayer.is_server())
|
||||
|
||||
|
||||
func _sync_equipment_appearance() -> void:
|
||||
if not multiplayer.is_server() or not player_inventory:
|
||||
return
|
||||
var weapon_id := player_inventory.equipped_weapon.item_id
|
||||
var hat_id := player_inventory.equipped_hat.item_id
|
||||
var backpack_id := player_inventory.equipped_backpack.item_id
|
||||
var nickname_height := _calculate_nickname_height(hat_id)
|
||||
_broadcast_nickname_height(nickname_height)
|
||||
sync_equipment_appearance.rpc(weapon_id, hat_id, backpack_id)
|
||||
sync_equipment_appearance(weapon_id, hat_id, backpack_id)
|
||||
|
||||
|
||||
func _request_equipment_appearance() -> void:
|
||||
if multiplayer.is_server() or not multiplayer.has_multiplayer_peer():
|
||||
return
|
||||
request_equipment_appearance.rpc_id(1)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func request_equipment_appearance() -> void:
|
||||
if not multiplayer.is_server() or not player_inventory:
|
||||
return
|
||||
var requester_id := multiplayer.get_remote_sender_id()
|
||||
if requester_id <= 0:
|
||||
return
|
||||
if _appearance_sync_requesters.has(requester_id):
|
||||
return
|
||||
_appearance_sync_requesters[requester_id] = true
|
||||
_sync_equipment_appearance_to_peer(requester_id)
|
||||
|
||||
|
||||
func _sync_equipment_appearance_to_peer(peer_id: int) -> void:
|
||||
if not multiplayer.is_server() or not player_inventory or peer_id <= 0:
|
||||
return
|
||||
var weapon_id := player_inventory.equipped_weapon.item_id
|
||||
var hat_id := player_inventory.equipped_hat.item_id
|
||||
var backpack_id := player_inventory.equipped_backpack.item_id
|
||||
sync_equipment_appearance.rpc_id(peer_id, weapon_id, hat_id, backpack_id)
|
||||
|
||||
|
||||
@rpc("any_peer", "reliable")
|
||||
func sync_equipment_appearance(weapon_id: String, hat_id: String, backpack_id: String) -> void:
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
if sender != 1 and not (sender == 0 and multiplayer.is_server()):
|
||||
return
|
||||
_set_equipment_visibility(weapon_id, hat_id, backpack_id)
|
||||
|
||||
|
||||
func _set_equipment_visibility(weapon_id: String, hat_id: String, backpack_id: String) -> void:
|
||||
_equipped_hat_visual_id = hat_id
|
||||
_set_equipment_nodes_visibility(HEAD_EQUIPMENT_PATH, HAT_NODES_BY_ITEM, hat_id)
|
||||
_set_equipment_nodes_visibility(HAND_EQUIPMENT_PATH, WEAPON_NODES_BY_ITEM, weapon_id)
|
||||
_set_equipment_nodes_visibility(BACK_EQUIPMENT_PATH, BACKPACK_NODES_BY_ITEM, backpack_id)
|
||||
|
||||
|
||||
func _broadcast_nickname_height(height: float) -> void:
|
||||
if not multiplayer.is_server():
|
||||
return
|
||||
var level_scene := get_tree().get_current_scene()
|
||||
if level_scene and level_scene.has_method("register_player_nickname_height"):
|
||||
level_scene.register_player_nickname_height(get_multiplayer_authority(), height)
|
||||
|
||||
|
||||
func _set_equipment_nodes_visibility(parent_path: String, nodes_by_item: Dictionary, equipped_item_id: String) -> void:
|
||||
for item_id in nodes_by_item:
|
||||
var equipment := get_node_or_null(parent_path + str(nodes_by_item[item_id])) as Node3D
|
||||
if equipment:
|
||||
equipment.visible = item_id == equipped_item_id
|
||||
|
||||
|
||||
func _update_nickname_height(hat_id: String = "") -> void:
|
||||
if not nickname:
|
||||
return
|
||||
nickname.visible = not _is_local_first_person()
|
||||
_set_nickname_height(_calculate_nickname_height(hat_id))
|
||||
|
||||
|
||||
func _calculate_nickname_height(hat_id: String) -> float:
|
||||
var target_height := 拾取动画延迟毫秒
|
||||
var equipped_hat := _get_hat_node(hat_id)
|
||||
if equipped_hat:
|
||||
var hat_top := _get_visual_top(equipped_hat)
|
||||
if hat_top > -INF and hat_top < INF:
|
||||
target_height = max(拾取动画延迟毫秒, hat_top + nickname_clearance)
|
||||
return target_height
|
||||
|
||||
|
||||
func _set_nickname_height(height: float) -> void:
|
||||
var nickname_position := nickname.position
|
||||
nickname_position.y = height
|
||||
nickname.position = nickname_position
|
||||
|
||||
|
||||
func apply_synced_nickname_height(height: float) -> void:
|
||||
if not nickname:
|
||||
return
|
||||
nickname.visible = not _is_local_first_person()
|
||||
if height > -INF and height < INF:
|
||||
_set_nickname_height(maxf(昵称基准高度, height))
|
||||
else:
|
||||
_set_nickname_height(昵称基准高度)
|
||||
|
||||
|
||||
func _is_local_first_person() -> bool:
|
||||
return is_multiplayer_authority() and _spring_arm_offset != null and _spring_arm_offset.is_first_person
|
||||
|
||||
|
||||
func get_current_nickname_height() -> float:
|
||||
return nickname.position.y if nickname else 昵称基准高度
|
||||
|
||||
|
||||
func _get_hat_node(hat_id: String) -> Node3D:
|
||||
if HAT_NODES_BY_ITEM.has(hat_id):
|
||||
var requested_hat_path := HEAD_EQUIPMENT_PATH + str(HAT_NODES_BY_ITEM[hat_id])
|
||||
var requested_hat := get_node_or_null(requested_hat_path) as Node3D
|
||||
if requested_hat:
|
||||
return requested_hat
|
||||
return null
|
||||
|
||||
|
||||
func _get_visual_top(root: Node3D) -> float:
|
||||
var visual_top := -INF
|
||||
if root is MeshInstance3D:
|
||||
visual_top = _get_mesh_top(root as MeshInstance3D)
|
||||
|
||||
for child in root.find_children("*", "MeshInstance3D", true, false):
|
||||
var mesh_instance := child as MeshInstance3D
|
||||
if mesh_instance:
|
||||
visual_top = max(visual_top, _get_mesh_top(mesh_instance))
|
||||
return visual_top
|
||||
|
||||
|
||||
func _get_mesh_top(mesh_instance: MeshInstance3D) -> float:
|
||||
var visual_top := -INF
|
||||
var mesh_bounds := mesh_instance.get_aabb()
|
||||
for endpoint_index in range(8):
|
||||
var endpoint_global := mesh_instance.to_global(mesh_bounds.get_endpoint(endpoint_index))
|
||||
visual_top = max(visual_top, to_local(endpoint_global).y)
|
||||
return visual_top
|
||||
|
||||
|
||||
func _add_starting_items():
|
||||
if not player_inventory:
|
||||
return
|
||||
|
||||
var backpack := ItemDatabase.get_item("backpack")
|
||||
if backpack:
|
||||
player_inventory.add_item(backpack, 1)
|
||||
|
||||
var starting_item_ids: Array[String] = [
|
||||
"fedora",
|
||||
"headphones",
|
||||
"pirate_hat",
|
||||
"sheriff_hat",
|
||||
"sombrero",
|
||||
"wizard_hat",
|
||||
"sword",
|
||||
"sword_big",
|
||||
"axe",
|
||||
"chicken_leg",
|
||||
"bone",
|
||||
"chalice"
|
||||
]
|
||||
|
||||
for item_id in starting_item_ids:
|
||||
var item = ItemDatabase.get_item(item_id)
|
||||
if item:
|
||||
player_inventory.add_item(item, 1)
|
||||
|
||||
|
||||
func pickup() -> void:
|
||||
if multiplayer.is_server():
|
||||
request_pickup()
|
||||
else:
|
||||
request_pickup.rpc_id(1)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func request_pickup() -> void:
|
||||
if not multiplayer.is_server() or not _is_owner_request():
|
||||
return
|
||||
var now := Time.get_ticks_msec()
|
||||
if now - _last_server_pickup_request_msec < 拾取请求冷却毫秒:
|
||||
return
|
||||
_last_server_pickup_request_msec = now
|
||||
|
||||
var animation_elapsed := now - _server_pickup_animation_started_msec
|
||||
if (
|
||||
_server_pickup_animation_started_msec < 0
|
||||
or animation_elapsed < 拾取动画时间窗口毫秒
|
||||
or animation_elapsed > 拾取动画时间窗口毫秒
|
||||
):
|
||||
return
|
||||
if not _is_grounded_on_server():
|
||||
return
|
||||
if not _has_collectible_item_in_front():
|
||||
return
|
||||
_server_pickup_animation_started_msec = -1
|
||||
_server_pickup()
|
||||
|
||||
|
||||
func _server_pickup() -> void:
|
||||
for item in _get_collectible_items_in_front():
|
||||
var result := request_add_single_item(item.item_id)
|
||||
if result and item.is_inside_tree():
|
||||
item.queue_free()
|
||||
|
||||
|
||||
func _has_collectible_item_in_front() -> bool:
|
||||
return not _get_collectible_items_in_front().is_empty()
|
||||
|
||||
|
||||
func _get_collectible_items_in_front() -> Array[ItemRigidBody3D]:
|
||||
var collectible_items: Array[ItemRigidBody3D] = []
|
||||
var pickup_area := get_node_or_null("GodotRobot3D/InfrontArea3D") as Area3D
|
||||
if not pickup_area:
|
||||
return collectible_items
|
||||
|
||||
for body in pickup_area.get_overlapping_bodies():
|
||||
var item := body as ItemRigidBody3D
|
||||
if item and not item.item_id.is_empty() and ItemDatabase.get_item(item.item_id):
|
||||
collectible_items.append(item)
|
||||
return collectible_items
|
||||
|
||||
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func apply_force_to_server_object(object_name: String, normal: Vector3) -> void:
|
||||
var object_node = get_node_or_null("/root/Level/Environment/ItemContainer")
|
||||
if object_node:
|
||||
for n in object_node.get_children():
|
||||
if n.name == object_name and n is RigidBody3D:
|
||||
n.apply_force(normal * 100)
|
||||
|
||||
|
||||
func _is_grounded_on_server() -> bool:
|
||||
if not multiplayer.is_server() or not is_inside_tree():
|
||||
return false
|
||||
var query := PhysicsRayQueryParameters3D.new()
|
||||
query.from = global_position + Vector3.UP * 0.15
|
||||
query.to = global_position + Vector3.DOWN * 0.3
|
||||
query.collision_mask = 2
|
||||
query.exclude = [get_rid()]
|
||||
query.collide_with_areas = false
|
||||
return not get_world_3d().direct_space_state.intersect_ray(query).is_empty()
|
||||
@@ -1 +0,0 @@
|
||||
uid://c2si8gkbnde0c
|
||||
@@ -1,92 +0,0 @@
|
||||
class_name SpringArmCharacter
|
||||
extends Node3D
|
||||
|
||||
signal perspective_changed(first_person: bool)
|
||||
|
||||
const MOUSE_SENSIBILITY: float = 0.005
|
||||
|
||||
@export_category("Objects")
|
||||
@export var _spring_arm: SpringArm3D = null
|
||||
@export var _first_person_anchor: Node3D = null
|
||||
|
||||
@export_category("Camera Perspective")
|
||||
@export_range(0.0, 10.0, 0.05) var third_person_distance := 5.0
|
||||
@export_range(0.0, 1.0, 0.05) var first_person_distance := 0.0
|
||||
@export_range(0.0, 3.0, 0.05) var third_person_height := 2.0
|
||||
@export_range(1.0, 179.0, 1.0) var third_person_fov := 75.0
|
||||
@export_range(1.0, 179.0, 1.0) var first_person_fov := 90.0
|
||||
|
||||
var is_first_person := false
|
||||
var _yaw := 0.0
|
||||
var _pitch := 0.0
|
||||
|
||||
@onready var _camera: Camera3D = _spring_arm.get_node_or_null("Camera3D") as Camera3D
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_yaw = rotation.y
|
||||
_pitch = _spring_arm.rotation.x if _spring_arm else 0.0
|
||||
_apply_axis_lock()
|
||||
_apply_perspective()
|
||||
|
||||
# 进入直接切换第一人称
|
||||
is_first_person = not is_first_person
|
||||
_apply_perspective()
|
||||
perspective_changed.emit(is_first_person)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_apply_axis_lock()
|
||||
if is_first_person and _spring_arm and _first_person_anchor:
|
||||
_spring_arm.global_position = _first_person_anchor.global_position
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not is_multiplayer_authority():
|
||||
return
|
||||
|
||||
var current_scene := get_tree().get_current_scene()
|
||||
if current_scene and current_scene.has_method("is_camera_input_blocked"):
|
||||
if current_scene.is_camera_input_blocked():
|
||||
return
|
||||
|
||||
#if event.is_action_pressed("toggle_camera"):
|
||||
#is_first_person = not is_first_person
|
||||
#_apply_perspective()
|
||||
#perspective_changed.emit(is_first_person)
|
||||
#get_viewport().set_input_as_handled()
|
||||
#return
|
||||
|
||||
if not (event is InputEventMouseMotion):
|
||||
return
|
||||
if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED:
|
||||
return
|
||||
|
||||
_yaw = wrapf(_yaw - event.relative.x * MOUSE_SENSIBILITY, -PI, PI)
|
||||
_pitch = clampf(_pitch - event.relative.y * MOUSE_SENSIBILITY, -PI / 4.0, PI / 24.0)
|
||||
_apply_axis_lock()
|
||||
|
||||
|
||||
func _apply_axis_lock() -> void:
|
||||
rotation = Vector3(0.0, _yaw, 0.0)
|
||||
if _spring_arm:
|
||||
_spring_arm.rotation = Vector3(_pitch, 0.0, 0.0)
|
||||
if _camera:
|
||||
_camera.rotation = Vector3.ZERO
|
||||
|
||||
|
||||
func _apply_perspective() -> void:
|
||||
if not _spring_arm:
|
||||
return
|
||||
if is_first_person:
|
||||
_spring_arm.spring_length = first_person_distance
|
||||
if _first_person_anchor:
|
||||
_spring_arm.global_position = _first_person_anchor.global_position
|
||||
if _camera:
|
||||
_camera.fov = first_person_fov
|
||||
else:
|
||||
_spring_arm.spring_length = third_person_distance
|
||||
_spring_arm.position = Vector3(0.0, third_person_height, 0.0)
|
||||
if _camera:
|
||||
_camera.fov = third_person_fov
|
||||
@@ -1 +0,0 @@
|
||||
uid://bj7yrijm7bppq
|
||||
@@ -1,55 +0,0 @@
|
||||
extends Node
|
||||
## 最简近距离语音:按住 C(talk)采集麦克风 → 广播给所有对端 →
|
||||
## 在说话者位置的 RaytracedAudioPlayer3D 回放。
|
||||
## 距离衰减/声像由 3D 音频完成,墙体闷音、环境混响由 raytraced_audio 插件完成。
|
||||
## 依赖:default_bus_layout.tres 的 mic 总线(静音 + AudioEffectCapture)、
|
||||
## RaytracedReverb/RaytracedAmbient 总线(插件用)、player.tscn 根节点下的 Voice 节点。
|
||||
|
||||
@export var max_distance := 25.0
|
||||
|
||||
var _capture: AudioEffectCapture
|
||||
var _voice_3d: RaytracedAudioPlayer3D
|
||||
var _playback: AudioStreamGeneratorPlayback
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_capture = AudioServer.get_bus_effect(AudioServer.get_bus_index("mic"), 0)
|
||||
_voice_3d = RaytracedAudioPlayer3D.new()
|
||||
var gen := AudioStreamGenerator.new()
|
||||
gen.mix_rate = 48000.0 # 与 audio/driver/mix_rate 一致,否则变速变调
|
||||
_voice_3d.stream = gen
|
||||
_voice_3d.position = Vector3(0, 1.5, 0) # 嘴部高度
|
||||
_voice_3d.max_distance = max_distance
|
||||
add_child(_voice_3d)
|
||||
if is_multiplayer_authority() and DisplayServer.get_name() != "headless":
|
||||
var mic := AudioStreamPlayer.new()
|
||||
mic.stream = AudioStreamMicrophone.new()
|
||||
mic.bus = "mic"
|
||||
add_child(mic)
|
||||
mic.play()
|
||||
# 监听器只建在本地玩家身上(多个监听器会互相抢 current)
|
||||
var listener := RaytracedAudioListener.new()
|
||||
listener.position = Vector3(0, 1.6, 0) # 头部高度
|
||||
add_child(listener)
|
||||
listener.owner = get_parent() # 插件的 ambient 计算会读 owner,代码创建的节点必须手动指定
|
||||
else:
|
||||
_voice_3d.play()
|
||||
_playback = _voice_3d.get_stream_playback()
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_multiplayer_authority() or _capture == null:
|
||||
return
|
||||
if not Input.is_action_pressed("talk"):
|
||||
_capture.clear_buffer() # 松开时丢弃积压,避免下次开口带旧音频
|
||||
return
|
||||
var frames := _capture.get_buffer(_capture.get_frames_available())
|
||||
if not frames.is_empty():
|
||||
receive_voice.rpc(frames)
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "unreliable_ordered", 1)
|
||||
func receive_voice(frames: PackedVector2Array) -> void:
|
||||
if not _playback:
|
||||
_playback = _voice_3d.get_stream_playback()
|
||||
_playback.push_buffer(frames)
|
||||
@@ -1 +0,0 @@
|
||||
uid://3i8mgirdlpli
|
||||
@@ -1,147 +0,0 @@
|
||||
class_name InventorySlotUI
|
||||
extends Control
|
||||
|
||||
signal slot_clicked(slot_index: int, button: int)
|
||||
signal slot_double_clicked(slot_index: int)
|
||||
signal item_hovered(slot_index: int, item: Item)
|
||||
signal item_unhovered
|
||||
|
||||
enum TYPE {
|
||||
INVENTORY,
|
||||
WEAPON,
|
||||
HAT,
|
||||
BACKPACK,
|
||||
}
|
||||
var slot_index: int = 0
|
||||
var slot_type: TYPE = TYPE.INVENTORY
|
||||
var inventory_data: InventorySlot
|
||||
var parent_inventory: Control
|
||||
|
||||
var _is_mouse_hovering := false
|
||||
|
||||
@onready var background: Panel = $Background
|
||||
@onready var item_icon: TextureRect = $ItemIcon
|
||||
@onready var quantity_label: Label = $QuantityLabel
|
||||
|
||||
|
||||
func _ready():
|
||||
gui_input.connect(_on_gui_input)
|
||||
mouse_entered.connect(_on_mouse_entered)
|
||||
mouse_exited.connect(_on_mouse_exited)
|
||||
|
||||
update_display()
|
||||
|
||||
|
||||
func set_slot_data(slot_data: InventorySlot, index: int):
|
||||
inventory_data = slot_data
|
||||
slot_index = index
|
||||
update_display()
|
||||
if is_node_ready() and _is_mouse_hovering:
|
||||
_emit_current_hover_state()
|
||||
|
||||
|
||||
func update_display():
|
||||
if not inventory_data or inventory_data.is_empty():
|
||||
_show_empty_slot()
|
||||
else:
|
||||
_show_item_slot()
|
||||
|
||||
|
||||
func _show_empty_slot():
|
||||
if item_icon:
|
||||
item_icon.texture = null
|
||||
if quantity_label:
|
||||
quantity_label.visible = false
|
||||
if background:
|
||||
background.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _show_item_slot():
|
||||
var item = ItemDatabase.get_item(inventory_data.item_id)
|
||||
if not item:
|
||||
_show_empty_slot()
|
||||
return
|
||||
|
||||
item_icon.texture = item.icon
|
||||
|
||||
if item.stackable and inventory_data.quantity > 1:
|
||||
quantity_label.text = str(inventory_data.quantity)
|
||||
quantity_label.visible = true
|
||||
else:
|
||||
quantity_label.visible = false
|
||||
|
||||
|
||||
func _on_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
if event.pressed:
|
||||
if event.button_index == MOUSE_BUTTON_LEFT and event.double_click:
|
||||
slot_double_clicked.emit(slot_index)
|
||||
accept_event()
|
||||
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
slot_clicked.emit(slot_index, event.button_index)
|
||||
accept_event()
|
||||
|
||||
|
||||
func _on_mouse_entered():
|
||||
_is_mouse_hovering = true
|
||||
_emit_current_hover_state()
|
||||
background.modulate = Color(1.2, 1.2, 1.2)
|
||||
|
||||
|
||||
func _on_mouse_exited():
|
||||
_is_mouse_hovering = false
|
||||
item_unhovered.emit()
|
||||
|
||||
background.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _emit_current_hover_state() -> void:
|
||||
if inventory_data and not inventory_data.is_empty():
|
||||
var item := ItemDatabase.get_item(inventory_data.item_id)
|
||||
if item:
|
||||
item_hovered.emit(slot_index, item)
|
||||
return
|
||||
item_unhovered.emit()
|
||||
|
||||
|
||||
func _can_drop_data(_position: Vector2, data) -> bool:
|
||||
return data is Dictionary and data.has("slot_index") and data.has("inventory_type")
|
||||
|
||||
|
||||
func _drop_data(_position: Vector2, data):
|
||||
if parent_inventory and parent_inventory.has_method("handle_item_drop") and slot_type == TYPE.INVENTORY:
|
||||
parent_inventory.handle_item_drop(data.slot_index, slot_index)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_weapon_equip") and slot_type == TYPE.WEAPON:
|
||||
parent_inventory.handle_weapon_equip(data.slot_index, data)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_hat_equip") and slot_type == TYPE.HAT:
|
||||
parent_inventory.handle_hat_equip(data.slot_index, data)
|
||||
elif parent_inventory and parent_inventory.has_method("handle_backpack_equip") and slot_type == TYPE.BACKPACK:
|
||||
parent_inventory.handle_backpack_equip(data.slot_index, data)
|
||||
|
||||
|
||||
func _get_drag_data(_position: Vector2):
|
||||
if not inventory_data or inventory_data.is_empty():
|
||||
return null
|
||||
|
||||
var item = ItemDatabase.get_item(inventory_data.item_id)
|
||||
if not item:
|
||||
return null
|
||||
|
||||
var preview = Control.new()
|
||||
var preview_icon = TextureRect.new()
|
||||
preview_icon.texture = item.icon
|
||||
preview_icon.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||
preview_icon.size = Vector2(32, 32)
|
||||
preview.add_child(preview_icon)
|
||||
|
||||
preview.modulate = Color(1, 1, 1, 0.8)
|
||||
set_drag_preview(preview)
|
||||
|
||||
item_icon.modulate = Color(0.5, 0.5, 0.5)
|
||||
|
||||
return {"slot_index": slot_index, "inventory_type": item.item_type}
|
||||
|
||||
|
||||
func _notification(what):
|
||||
if what == NOTIFICATION_DRAG_END:
|
||||
item_icon.modulate = Color.WHITE
|
||||
@@ -1 +0,0 @@
|
||||
uid://bi0mlil0a0e5i
|
||||
@@ -1,422 +0,0 @@
|
||||
class_name InventoryUI
|
||||
extends Control
|
||||
|
||||
signal inventory_closed
|
||||
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
const TOOLTIP_MAX_WIDTH := 280.0
|
||||
const TOOLTIP_MAX_HEIGHT := 240.0
|
||||
const TOOLTIP_SCREEN_MARGIN := 10.0
|
||||
const TOOLTIP_HORIZONTAL_PADDING := 16.0
|
||||
const TOOLTIP_VERTICAL_PADDING := 16.0
|
||||
|
||||
const SLOT_INDEX_WEAPON := -1
|
||||
const SLOT_INDEX_HAT := -2
|
||||
const SLOT_INDEX_BACKPACK := -3
|
||||
|
||||
var current_player: Character
|
||||
var slot_ui_scene: PackedScene
|
||||
var slot_uis: Array[InventorySlotUI] = []
|
||||
var current_item: Item
|
||||
var current_slot_index: int
|
||||
var weapon_slot_ui: InventorySlotUI
|
||||
var hat_slot_ui: InventorySlotUI
|
||||
var backpack_slot_ui: InventorySlotUI
|
||||
var _tooltip_layout_request := 0
|
||||
var _tooltip_should_be_visible := false
|
||||
|
||||
@onready var grid_container: GridContainer = get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/GridContainer"
|
||||
)
|
||||
@onready var close_button: Button = get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/InventoryPanel/MarginContainer/VBoxContainer/TitleBar/CloseButton"
|
||||
)
|
||||
@onready var tooltip: Control = $ItemTooltip
|
||||
@onready var tooltip_label: RichTextLabel = $ItemTooltip/Panel/MarginContainer/TooltipText
|
||||
@onready var inventory_panels: HBoxContainer = $SafeArea/CenterContainer/InventoryPanels
|
||||
|
||||
|
||||
func _ready():
|
||||
slot_ui_scene = preload("res://scenes/ui/inventory_slot_ui.tscn")
|
||||
grid_container.columns = 4
|
||||
close_button.pressed.connect(_on_close_pressed)
|
||||
tooltip.visible = false
|
||||
_create_slot_uis()
|
||||
_create_hat_slot_ui()
|
||||
_create_weapon_slot_ui()
|
||||
_create_backpack_slot_ui()
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _create_hat_slot_ui():
|
||||
hat_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
hat_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
hat_slot_ui.parent_inventory = self
|
||||
hat_slot_ui.slot_type = InventorySlotUI.TYPE.HAT
|
||||
hat_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
hat_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
hat_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/HatSlotContainer"
|
||||
)
|
||||
. add_child(hat_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_weapon_slot_ui():
|
||||
weapon_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
weapon_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
weapon_slot_ui.parent_inventory = self
|
||||
weapon_slot_ui.slot_type = InventorySlotUI.TYPE.WEAPON
|
||||
weapon_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
weapon_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
weapon_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/WeaponSlotContainer"
|
||||
)
|
||||
. add_child(weapon_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_backpack_slot_ui() -> void:
|
||||
backpack_slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
backpack_slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
backpack_slot_ui.parent_inventory = self
|
||||
backpack_slot_ui.slot_type = InventorySlotUI.TYPE.BACKPACK
|
||||
backpack_slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
backpack_slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
backpack_slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
(
|
||||
get_node(
|
||||
"SafeArea/CenterContainer/InventoryPanels/EquipmentPanel/MarginContainer/VBoxContainer/BackpackSlotContainer"
|
||||
)
|
||||
. add_child(backpack_slot_ui)
|
||||
)
|
||||
|
||||
|
||||
func _create_slot_uis():
|
||||
for child in grid_container.get_children():
|
||||
child.queue_free()
|
||||
slot_uis.clear()
|
||||
|
||||
for i in range(PlayerInventory.MAX_INVENTORY_SIZE):
|
||||
var slot_ui = slot_ui_scene.instantiate() as InventorySlotUI
|
||||
slot_ui.custom_minimum_size = Vector2(64, 64)
|
||||
slot_ui.parent_inventory = self
|
||||
slot_ui.visible = i < PlayerInventory.BASE_INVENTORY_SIZE
|
||||
|
||||
slot_ui.slot_clicked.connect(_on_slot_clicked)
|
||||
slot_ui.slot_double_clicked.connect(_on_slot_double_clicked)
|
||||
slot_ui.item_hovered.connect(_on_item_hovered)
|
||||
slot_ui.item_unhovered.connect(_on_item_unhovered)
|
||||
|
||||
slot_ui.set_slot_data(null, i)
|
||||
|
||||
grid_container.add_child(slot_ui)
|
||||
slot_uis.append(slot_ui)
|
||||
|
||||
|
||||
func update_inventory_display():
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
var player_inventory = current_player.get_inventory()
|
||||
var active_slot_count := player_inventory.get_active_slot_count()
|
||||
for i in range(slot_uis.size()):
|
||||
slot_uis[i].visible = i < active_slot_count
|
||||
if i < active_slot_count:
|
||||
slot_uis[i].set_slot_data(player_inventory.get_slot(i), i)
|
||||
else:
|
||||
slot_uis[i].set_slot_data(null, i)
|
||||
weapon_slot_ui.set_slot_data(player_inventory.equipped_weapon, SLOT_INDEX_WEAPON)
|
||||
hat_slot_ui.set_slot_data(player_inventory.equipped_hat, SLOT_INDEX_HAT)
|
||||
backpack_slot_ui.set_slot_data(player_inventory.equipped_backpack, SLOT_INDEX_BACKPACK)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _on_slot_clicked(slot_index: int, button: int):
|
||||
if button == MOUSE_BUTTON_RIGHT:
|
||||
_handle_right_click(slot_index)
|
||||
|
||||
|
||||
func _on_slot_double_clicked(slot_index: int) -> void:
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
if slot_index < 0 or not current_player.get_inventory().is_slot_active(slot_index):
|
||||
return
|
||||
|
||||
var slot := current_player.get_inventory().get_slot(slot_index)
|
||||
if not slot or slot.is_empty():
|
||||
return
|
||||
|
||||
var item := ItemDatabase.get_item(slot.item_id)
|
||||
if not item or not item.context_options.has(Item.ContextOptions.EQUIP):
|
||||
return
|
||||
if (
|
||||
item.item_type != Item.ItemType.WEAPON
|
||||
and item.item_type != Item.ItemType.HAT
|
||||
and item.item_type != Item.ItemType.BACKPACK
|
||||
):
|
||||
return
|
||||
|
||||
current_player.request_equip_item.rpc_id(1, slot_index, item.item_type)
|
||||
|
||||
|
||||
func _handle_right_click(slot_index: int):
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
var player_inventory = current_player.get_inventory()
|
||||
current_slot_index = slot_index
|
||||
var slot: InventorySlot
|
||||
if slot_index == SLOT_INDEX_WEAPON:
|
||||
slot = player_inventory.equipped_weapon
|
||||
elif slot_index == SLOT_INDEX_HAT:
|
||||
slot = player_inventory.equipped_hat
|
||||
elif slot_index == SLOT_INDEX_BACKPACK:
|
||||
slot = player_inventory.equipped_backpack
|
||||
else:
|
||||
slot = player_inventory.get_slot(slot_index)
|
||||
if slot and not slot.is_empty():
|
||||
current_item = ItemDatabase.get_item(slot.item_id)
|
||||
if current_item:
|
||||
_hide_tooltip()
|
||||
var context_menu = PopupMenu.new()
|
||||
add_child(context_menu)
|
||||
context_menu.popup_hide.connect(context_menu.queue_free)
|
||||
context_menu.id_pressed.connect(_on_item_selected)
|
||||
if slot_index == SLOT_INDEX_WEAPON or slot_index == SLOT_INDEX_HAT or slot_index == SLOT_INDEX_BACKPACK:
|
||||
context_menu.add_item(
|
||||
_get_context_menu_string(Item.ContextOptions.UNEQUIP), Item.ContextOptions.UNEQUIP
|
||||
)
|
||||
else:
|
||||
for item_option in current_item.context_options:
|
||||
context_menu.add_item(_get_context_menu_string(item_option), item_option)
|
||||
|
||||
context_menu.set_position(get_viewport().get_mouse_position())
|
||||
context_menu.popup()
|
||||
|
||||
|
||||
func _on_item_selected(index: int):
|
||||
if not current_player or not current_player.get_inventory():
|
||||
return
|
||||
|
||||
if index == Item.ContextOptions.EQUIP:
|
||||
current_player.request_equip_item.rpc_id(1, current_slot_index, current_item.item_type)
|
||||
elif index == Item.ContextOptions.UNEQUIP:
|
||||
current_player.request_unequip_item.rpc_id(1, current_item.item_type)
|
||||
elif index == Item.ContextOptions.DROP:
|
||||
if current_item.scene_path.is_empty() or not ResourceLoader.exists(current_item.scene_path):
|
||||
push_warning(
|
||||
"Cannot drop item '" + current_item.id + "': invalid scene path '" + current_item.scene_path + "'"
|
||||
)
|
||||
return
|
||||
current_player.add_world_item.rpc_id(
|
||||
1, current_item.scene_path, current_player.get_node("GodotRobot3D/InfrontArea3D").global_position
|
||||
)
|
||||
current_player.request_remove_item.rpc_id(1, current_item.id, 1)
|
||||
refresh_display()
|
||||
|
||||
|
||||
func _on_item_hovered(_slot_index: int, item: Item):
|
||||
_show_tooltip(item)
|
||||
|
||||
|
||||
func _on_item_unhovered():
|
||||
_hide_tooltip()
|
||||
|
||||
|
||||
func _show_tooltip(item: Item):
|
||||
if not item:
|
||||
return
|
||||
|
||||
var tooltip_content = "[font_size=18][b][color=#FFD700]" + item.name + "[/color][/b][/font_size]\n"
|
||||
tooltip_content += "[color=#CCCCCC]" + item.description + "[/color]\n"
|
||||
tooltip_content += "[color=#87CEEB]Type:[/color] " + _get_item_type_string(item.item_type) + "\n"
|
||||
tooltip_content += "[color=#FF69B4]Rarity:[/color] " + _get_rarity_string(item.rarity) + "\n"
|
||||
tooltip_content += "[color=#FFD700]Value:[/color] " + str(item.value) + " gold"
|
||||
|
||||
if item.stackable:
|
||||
tooltip_content += "\n[color=#98FB98]Max Stack:[/color] " + str(item.max_stack)
|
||||
|
||||
tooltip_label.text = tooltip_content
|
||||
_tooltip_should_be_visible = true
|
||||
_queue_tooltip_layout()
|
||||
|
||||
|
||||
func _hide_tooltip():
|
||||
_tooltip_should_be_visible = false
|
||||
_tooltip_layout_request += 1
|
||||
tooltip.visible = false
|
||||
tooltip.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _queue_tooltip_layout() -> void:
|
||||
_set_tooltip_width()
|
||||
_tooltip_layout_request += 1
|
||||
var request_id := _tooltip_layout_request
|
||||
tooltip.modulate = Color(1.0, 1.0, 1.0, 0.0)
|
||||
tooltip.visible = true
|
||||
_apply_tooltip_layout_after_frame(request_id)
|
||||
|
||||
|
||||
func _apply_tooltip_layout_after_frame(request_id: int) -> void:
|
||||
await get_tree().process_frame
|
||||
if request_id != _tooltip_layout_request or not _tooltip_should_be_visible or not tooltip or not tooltip_label:
|
||||
return
|
||||
|
||||
var tooltip_size := tooltip.size
|
||||
var available_height := minf(TOOLTIP_MAX_HEIGHT, maxf(1.0, size.y - TOOLTIP_SCREEN_MARGIN * 2.0))
|
||||
tooltip_size.y = minf(float(tooltip_label.get_content_height()) + TOOLTIP_VERTICAL_PADDING, available_height)
|
||||
tooltip.size = tooltip_size
|
||||
_position_tooltip_smartly()
|
||||
tooltip.modulate = Color.WHITE
|
||||
|
||||
|
||||
func _set_tooltip_width() -> void:
|
||||
var tooltip_size := tooltip.size
|
||||
var available_width := maxf(1.0, size.x - TOOLTIP_SCREEN_MARGIN * 2.0)
|
||||
tooltip_size.x = minf(TOOLTIP_MAX_WIDTH, available_width)
|
||||
tooltip.size = tooltip_size
|
||||
var label_minimum_size := tooltip_label.custom_minimum_size
|
||||
label_minimum_size.x = maxf(1.0, tooltip_size.x - TOOLTIP_HORIZONTAL_PADDING)
|
||||
tooltip_label.custom_minimum_size = label_minimum_size
|
||||
var label_size := tooltip_label.size
|
||||
label_size.x = label_minimum_size.x
|
||||
tooltip_label.size = label_size
|
||||
|
||||
|
||||
func _position_tooltip_smartly():
|
||||
var mouse_pos = get_global_mouse_position()
|
||||
var tooltip_size = tooltip.size
|
||||
|
||||
var viewport_size = get_viewport().get_visible_rect().size
|
||||
var tooltip_pos = mouse_pos + Vector2.ONE * TOOLTIP_SCREEN_MARGIN
|
||||
if tooltip_pos.x + tooltip_size.x > viewport_size.x - TOOLTIP_SCREEN_MARGIN:
|
||||
tooltip_pos.x = mouse_pos.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN
|
||||
if tooltip_pos.y + tooltip_size.y > viewport_size.y - TOOLTIP_SCREEN_MARGIN:
|
||||
tooltip_pos.y = mouse_pos.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN
|
||||
|
||||
var max_x := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.x - tooltip_size.x - TOOLTIP_SCREEN_MARGIN)
|
||||
var max_y := maxf(TOOLTIP_SCREEN_MARGIN, viewport_size.y - tooltip_size.y - TOOLTIP_SCREEN_MARGIN)
|
||||
tooltip_pos.x = clampf(tooltip_pos.x, TOOLTIP_SCREEN_MARGIN, max_x)
|
||||
tooltip_pos.y = clampf(tooltip_pos.y, TOOLTIP_SCREEN_MARGIN, max_y)
|
||||
|
||||
tooltip.global_position = tooltip_pos
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not inventory_panels or not tooltip:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var content_size := inventory_panels.size
|
||||
if content_size.x > 0.0 and content_size.y > 0.0:
|
||||
var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
|
||||
inventory_panels.pivot_offset = content_size * 0.5
|
||||
inventory_panels.scale = Vector2.ONE * scale_factor
|
||||
|
||||
_set_tooltip_width()
|
||||
if _tooltip_should_be_visible:
|
||||
_queue_tooltip_layout()
|
||||
|
||||
|
||||
func _get_item_type_string(type: Item.ItemType) -> String:
|
||||
var type_name := "Unknown"
|
||||
match type:
|
||||
Item.ItemType.WEAPON:
|
||||
type_name = "Weapon"
|
||||
Item.ItemType.HAT:
|
||||
type_name = "Hat"
|
||||
Item.ItemType.BACKPACK:
|
||||
type_name = "Backpack"
|
||||
Item.ItemType.CONSUMABLE:
|
||||
type_name = "Consumable"
|
||||
Item.ItemType.TOOL:
|
||||
type_name = "Tool"
|
||||
Item.ItemType.MISC:
|
||||
type_name = "Miscellaneous"
|
||||
return type_name
|
||||
|
||||
|
||||
func _get_rarity_string(rarity: Item.ItemRarity) -> String:
|
||||
match rarity:
|
||||
Item.ItemRarity.COMMON:
|
||||
return "Common"
|
||||
Item.ItemRarity.UNCOMMON:
|
||||
return "Uncommon"
|
||||
Item.ItemRarity.RARE:
|
||||
return "Rare"
|
||||
Item.ItemRarity.EPIC:
|
||||
return "Epic"
|
||||
Item.ItemRarity.LEGENDARY:
|
||||
return "Legendary"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func _get_context_menu_string(context: Item.ContextOptions) -> String:
|
||||
match context:
|
||||
Item.ContextOptions.DROP:
|
||||
return "Drop"
|
||||
Item.ContextOptions.EQUIP:
|
||||
return "Equip"
|
||||
Item.ContextOptions.UNEQUIP:
|
||||
return "Unequip"
|
||||
_:
|
||||
return "Unknown"
|
||||
|
||||
|
||||
func handle_item_drop(from_slot: int, to_slot: int):
|
||||
if not current_player:
|
||||
return
|
||||
if from_slot == SLOT_INDEX_WEAPON:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.WEAPON, to_slot)
|
||||
elif from_slot == SLOT_INDEX_HAT:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.HAT, to_slot)
|
||||
elif from_slot == SLOT_INDEX_BACKPACK:
|
||||
current_player.request_unequip_item.rpc_id(1, Item.ItemType.BACKPACK, to_slot)
|
||||
else:
|
||||
current_player.request_move_item.rpc_id(1, from_slot, to_slot)
|
||||
|
||||
|
||||
func _on_close_pressed():
|
||||
_hide_tooltip()
|
||||
inventory_closed.emit()
|
||||
visible = false
|
||||
|
||||
|
||||
func open_inventory(player: Character = null):
|
||||
_hide_tooltip()
|
||||
if player:
|
||||
current_player = player
|
||||
update_inventory_display()
|
||||
visible = true
|
||||
|
||||
|
||||
func close_inventory():
|
||||
_hide_tooltip()
|
||||
visible = false
|
||||
|
||||
|
||||
func refresh_display():
|
||||
update_inventory_display()
|
||||
|
||||
|
||||
func handle_weapon_equip(from_slot: int, item: Dictionary):
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.WEAPON:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.WEAPON)
|
||||
|
||||
|
||||
func handle_hat_equip(from_slot: int, item: Dictionary):
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.HAT:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.HAT)
|
||||
|
||||
|
||||
func handle_backpack_equip(from_slot: int, item: Dictionary) -> void:
|
||||
if current_player and from_slot >= 0 and item.inventory_type == Item.ItemType.BACKPACK:
|
||||
current_player.request_equip_item.rpc_id(1, from_slot, Item.ItemType.BACKPACK)
|
||||
@@ -1 +0,0 @@
|
||||
uid://c2w12kgcvtww4
|
||||
@@ -1,71 +0,0 @@
|
||||
class_name MainMenuUI
|
||||
extends Control
|
||||
|
||||
signal host_pressed(nickname: String, skin: String)
|
||||
signal join_pressed(nickname: String, skin: String, address: String)
|
||||
signal quit_pressed
|
||||
|
||||
const SKIN_OPTIONS: Array[String] = ["Blue", "Yellow", "Green", "Red"]
|
||||
const SAFE_AREA_MARGIN := 24.0
|
||||
|
||||
@onready var skin_input: OptionButton = $MainContainer/MainMenu/Option2/SkinInput
|
||||
@onready var nick_input: LineEdit = $MainContainer/MainMenu/Option1/NickInput
|
||||
@onready var address_input: LineEdit = $MainContainer/MainMenu/Option3/AddressInput
|
||||
@onready var main_container: VBoxContainer = $MainContainer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
skin_input.clear()
|
||||
for skin_name in SKIN_OPTIONS:
|
||||
skin_input.add_item(skin_name)
|
||||
skin_input.select(0)
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func _on_host_pressed() -> void:
|
||||
var nickname = nick_input.text.strip_edges()
|
||||
var skin = get_skin()
|
||||
host_pressed.emit(nickname, skin)
|
||||
|
||||
|
||||
func _on_join_pressed() -> void:
|
||||
var nickname = nick_input.text.strip_edges()
|
||||
var skin = get_skin()
|
||||
var address = address_input.text.strip_edges()
|
||||
join_pressed.emit(nickname, skin, address)
|
||||
|
||||
|
||||
func _on_quit_pressed():
|
||||
quit_pressed.emit()
|
||||
|
||||
|
||||
func show_menu():
|
||||
show()
|
||||
call_deferred("_update_responsive_layout")
|
||||
|
||||
|
||||
func hide_menu():
|
||||
hide()
|
||||
|
||||
|
||||
func is_menu_visible() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not main_container:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var content_size := main_container.get_combined_minimum_size()
|
||||
if content_size.x <= 0.0 or content_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / content_size.x, available_size.y / content_size.y))
|
||||
main_container.pivot_offset = main_container.size * 0.5
|
||||
main_container.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func get_skin() -> String:
|
||||
return skin_input.get_item_text(skin_input.selected).to_lower()
|
||||
@@ -1 +0,0 @@
|
||||
uid://c9x8y7z6a5b4c3d2
|
||||
@@ -1,155 +0,0 @@
|
||||
class_name MultiplayerChatUI
|
||||
extends Control
|
||||
|
||||
signal message_sent(message_text: String)
|
||||
|
||||
const MAX_CHAT_MESSAGES: int = 100
|
||||
const MAX_VISIBLE_MESSAGES: int = 8
|
||||
const FEED_VISIBLE_SECONDS: float = 6.0
|
||||
const FEED_FADE_SECONDS: float = 0.35
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
const MIN_CHAT_WIDTH := 280.0
|
||||
const MAX_CHAT_WIDTH := 480.0
|
||||
const MAX_CHAT_HEIGHT := 268.0
|
||||
|
||||
var chat_visible: bool = false
|
||||
var chat_history: Array[String] = []
|
||||
var fade_tween: Tween
|
||||
|
||||
@onready var chat: RichTextLabel = $ChatLayout/Chat
|
||||
@onready var message: LineEdit = $ChatLayout/Message
|
||||
@onready var feed_timer: Timer = $FeedTimer
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
message.text_submitted.connect(_on_message_submitted)
|
||||
feed_timer.timeout.connect(_on_feed_timer_timeout)
|
||||
feed_timer.wait_time = FEED_VISIBLE_SECONDS
|
||||
get_viewport().size_changed.connect(_update_responsive_layout)
|
||||
_update_responsive_layout()
|
||||
show()
|
||||
clear_chat()
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
var viewport_size := get_viewport().get_visible_rect().size
|
||||
var available_width := maxf(1.0, viewport_size.x - SAFE_AREA_MARGIN * 2.0)
|
||||
var preferred_width := clampf(viewport_size.x * 0.42, MIN_CHAT_WIDTH, MAX_CHAT_WIDTH)
|
||||
var target_width := minf(available_width, preferred_width)
|
||||
var target_height := minf(MAX_CHAT_HEIGHT, maxf(1.0, viewport_size.y - SAFE_AREA_MARGIN * 2.0))
|
||||
|
||||
offset_left = SAFE_AREA_MARGIN
|
||||
offset_right = SAFE_AREA_MARGIN + target_width
|
||||
offset_bottom = -SAFE_AREA_MARGIN
|
||||
offset_top = -SAFE_AREA_MARGIN - target_height
|
||||
|
||||
|
||||
func toggle_chat() -> void:
|
||||
if chat_visible:
|
||||
close_chat()
|
||||
return
|
||||
|
||||
chat_visible = true
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
message.show()
|
||||
await get_tree().process_frame
|
||||
if not chat_visible:
|
||||
return
|
||||
message.grab_focus()
|
||||
|
||||
|
||||
func close_chat() -> void:
|
||||
chat_visible = false
|
||||
message.text = ""
|
||||
message.release_focus()
|
||||
message.hide()
|
||||
|
||||
if chat_history.is_empty():
|
||||
chat.hide()
|
||||
else:
|
||||
_show_feed_temporarily()
|
||||
|
||||
|
||||
func is_chat_visible() -> bool:
|
||||
return chat_visible
|
||||
|
||||
|
||||
func _on_message_submitted(_submitted_text: String) -> void:
|
||||
var message_text := message.text.strip_edges()
|
||||
if message_text.is_empty():
|
||||
return
|
||||
|
||||
close_chat()
|
||||
message_sent.emit(message_text)
|
||||
|
||||
|
||||
func add_message(nick: String, msg: String) -> void:
|
||||
var formatted_message := _escape_bbcode("%s: %s" % [nick, msg])
|
||||
chat_history.append(formatted_message)
|
||||
while chat_history.size() > MAX_CHAT_MESSAGES:
|
||||
chat_history.pop_front()
|
||||
|
||||
_render_chat_history()
|
||||
if chat_visible:
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
else:
|
||||
_show_feed_temporarily()
|
||||
|
||||
|
||||
func _render_chat_history() -> void:
|
||||
chat.clear()
|
||||
var first_message := maxi(0, chat_history.size() - MAX_VISIBLE_MESSAGES)
|
||||
for index in range(first_message, chat_history.size()):
|
||||
chat.append_text(chat_history[index])
|
||||
if index < chat_history.size() - 1:
|
||||
chat.append_text("\n")
|
||||
|
||||
|
||||
func _show_feed_temporarily() -> void:
|
||||
_stop_feed_animation()
|
||||
_show_feed()
|
||||
feed_timer.start()
|
||||
|
||||
|
||||
func _show_feed() -> void:
|
||||
chat.modulate.a = 1.0
|
||||
chat.show()
|
||||
|
||||
|
||||
func _on_feed_timer_timeout() -> void:
|
||||
if chat_visible:
|
||||
return
|
||||
|
||||
_stop_fade_tween()
|
||||
fade_tween = create_tween()
|
||||
fade_tween.tween_property(chat, "modulate:a", 0.0, FEED_FADE_SECONDS)
|
||||
fade_tween.tween_callback(chat.hide)
|
||||
|
||||
|
||||
func _stop_feed_animation() -> void:
|
||||
feed_timer.stop()
|
||||
_stop_fade_tween()
|
||||
|
||||
|
||||
func _stop_fade_tween() -> void:
|
||||
if fade_tween and fade_tween.is_valid():
|
||||
fade_tween.kill()
|
||||
fade_tween = null
|
||||
|
||||
|
||||
func _escape_bbcode(text: String) -> String:
|
||||
return text.replace("[", "[lb]")
|
||||
|
||||
|
||||
func clear_chat() -> void:
|
||||
_stop_feed_animation()
|
||||
chat_history.clear()
|
||||
chat.clear()
|
||||
chat.modulate.a = 1.0
|
||||
chat.hide()
|
||||
message.text = ""
|
||||
message.release_focus()
|
||||
message.hide()
|
||||
chat_visible = false
|
||||
@@ -1 +0,0 @@
|
||||
uid://c8x9y2z1a3b4c5d6
|
||||
@@ -1,57 +0,0 @@
|
||||
class_name PauseMenuUI
|
||||
extends Control
|
||||
|
||||
signal resume_pressed
|
||||
signal main_menu_pressed
|
||||
signal quit_pressed
|
||||
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
|
||||
@onready var resume_button: Button = $Overlay/Panel/MarginContainer/Buttons/Resume
|
||||
@onready var panel: PanelContainer = $Overlay/Panel
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resized.connect(_update_responsive_layout)
|
||||
call_deferred("_update_responsive_layout")
|
||||
hide()
|
||||
|
||||
|
||||
func show_menu() -> void:
|
||||
show()
|
||||
call_deferred("_update_responsive_layout")
|
||||
resume_button.grab_focus()
|
||||
|
||||
|
||||
func hide_menu() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func is_menu_visible() -> bool:
|
||||
return visible
|
||||
|
||||
|
||||
func _update_responsive_layout() -> void:
|
||||
if not panel:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var panel_size := panel.size
|
||||
if panel_size.x <= 0.0 or panel_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
|
||||
panel.pivot_offset = panel_size * 0.5
|
||||
panel.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func _on_resume_pressed() -> void:
|
||||
resume_pressed.emit()
|
||||
|
||||
|
||||
func _on_main_menu_pressed() -> void:
|
||||
main_menu_pressed.emit()
|
||||
|
||||
|
||||
func _on_quit_pressed() -> void:
|
||||
quit_pressed.emit()
|
||||
@@ -1 +0,0 @@
|
||||
uid://wbiqr2q6c47q
|
||||
@@ -1,109 +0,0 @@
|
||||
class_name PlayerListUI
|
||||
extends Control
|
||||
|
||||
const PLAYER_ROW_HEIGHT := 28.0
|
||||
const MAX_VISIBLE_ROWS := 8
|
||||
const RESERVED_VERTICAL_SPACE := 190.0
|
||||
const SAFE_AREA_MARGIN := 16.0
|
||||
|
||||
var _player_count := 0
|
||||
|
||||
@onready var panel: PanelContainer = $SafeArea/PositionContainer/Panel
|
||||
@onready var count_label: Label = $SafeArea/PositionContainer/Panel/MarginContainer/Content/Count
|
||||
@onready var player_scroll: ScrollContainer = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll
|
||||
@onready
|
||||
var players_label: RichTextLabel = $SafeArea/PositionContainer/Panel/MarginContainer/Content/PlayerScroll/Players
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
resized.connect(_update_layout)
|
||||
_update_layout()
|
||||
hide()
|
||||
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible or not (event is InputEventMouseButton):
|
||||
return
|
||||
var mouse_event := event as InputEventMouseButton
|
||||
if not mouse_event.pressed:
|
||||
return
|
||||
if mouse_event.button_index == MOUSE_BUTTON_WHEEL_UP:
|
||||
player_scroll.scroll_vertical -= int(PLAYER_ROW_HEIGHT)
|
||||
elif mouse_event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
player_scroll.scroll_vertical += int(PLAYER_ROW_HEIGHT)
|
||||
|
||||
|
||||
func show_players(players: Dictionary, local_peer_id: int) -> void:
|
||||
refresh_players(players, local_peer_id)
|
||||
player_scroll.scroll_vertical = 0
|
||||
show()
|
||||
call_deferred("_apply_panel_scale")
|
||||
|
||||
|
||||
func hide_players() -> void:
|
||||
hide()
|
||||
|
||||
|
||||
func refresh_players(players: Dictionary, local_peer_id: int) -> void:
|
||||
var peer_ids: Array[int] = []
|
||||
for peer_id_value in players:
|
||||
var peer_id := int(peer_id_value)
|
||||
if peer_id > 0:
|
||||
peer_ids.append(peer_id)
|
||||
peer_ids.sort()
|
||||
_player_count = peer_ids.size()
|
||||
|
||||
count_label.text = "%d online" % _player_count
|
||||
players_label.clear()
|
||||
for index in range(peer_ids.size()):
|
||||
var peer_id := peer_ids[index]
|
||||
var player_info: Dictionary = players.get(peer_id, {})
|
||||
var nickname := Network.sanitize_nickname(str(player_info.get("nick", "")), "Player_" + str(peer_id))
|
||||
var local_marker := " [color=#c7ccd4](you)[/color]" if peer_id == local_peer_id else ""
|
||||
players_label.append_text(
|
||||
(
|
||||
"[color=#a8adb5]>[/color] [b]%s[/b]%s [color=#9aa0a8]#%d[/color]"
|
||||
% [_escape_bbcode(nickname), local_marker, peer_id]
|
||||
)
|
||||
)
|
||||
if index < peer_ids.size() - 1:
|
||||
players_label.append_text("\n")
|
||||
_update_layout()
|
||||
|
||||
|
||||
func _update_layout() -> void:
|
||||
if not panel or not player_scroll:
|
||||
return
|
||||
var panel_minimum_size := panel.custom_minimum_size
|
||||
panel_minimum_size.x = maxf(0.0, minf(420.0, size.x - 32.0))
|
||||
panel.custom_minimum_size = panel_minimum_size
|
||||
|
||||
var visible_rows := maxi(1, mini(_player_count, MAX_VISIBLE_ROWS))
|
||||
var desired_height := float(visible_rows) * PLAYER_ROW_HEIGHT
|
||||
var viewport_limit := maxf(30.0, size.y - RESERVED_VERTICAL_SPACE)
|
||||
var scroll_minimum_size := player_scroll.custom_minimum_size
|
||||
scroll_minimum_size.y = minf(desired_height, viewport_limit)
|
||||
player_scroll.custom_minimum_size = scroll_minimum_size
|
||||
|
||||
var players_minimum_size := players_label.custom_minimum_size
|
||||
players_minimum_size.y = float(maxi(1, _player_count)) * PLAYER_ROW_HEIGHT
|
||||
players_label.custom_minimum_size = players_minimum_size
|
||||
call_deferred("_apply_panel_scale")
|
||||
|
||||
|
||||
func _apply_panel_scale() -> void:
|
||||
if not panel:
|
||||
return
|
||||
var available_size := Vector2(
|
||||
maxf(1.0, size.x - SAFE_AREA_MARGIN * 2.0), maxf(1.0, size.y - SAFE_AREA_MARGIN * 2.0)
|
||||
)
|
||||
var panel_size := panel.size
|
||||
if panel_size.x <= 0.0 or panel_size.y <= 0.0:
|
||||
return
|
||||
var scale_factor := minf(1.0, minf(available_size.x / panel_size.x, available_size.y / panel_size.y))
|
||||
panel.pivot_offset = panel_size * 0.5
|
||||
panel.scale = Vector2.ONE * scale_factor
|
||||
|
||||
|
||||
func _escape_bbcode(text: String) -> String:
|
||||
return text.replace("[", "[lb]")
|
||||
@@ -1 +0,0 @@
|
||||
uid://dpxhd38sedg8u
|
||||
Reference in New Issue
Block a user