From 235d5ad90a19d8f24f9a3d416f1a418ec88d7655 Mon Sep 17 00:00:00 2001 From: Oscar Date: Sun, 29 Jun 2025 15:06:54 +0300 Subject: [PATCH] tst --- Code/Inventory/Inventar.cs | 139 +++++++++++++++++++++++++++-- Code/Player/Dedugan.Camera.cs | 3 - Code/Player/Dedugan.Hit.cs | 37 ++++++++ Code/Player/Dedugan.cs | 32 ++++--- Code/UI/GUI.razor | 161 ++++++++++++++++------------------ Code/UI/GUI.razor.scss | 65 ++++++++++++++ README.md | 97 +++++++++++++++++++- 7 files changed, 423 insertions(+), 111 deletions(-) create mode 100644 Code/UI/GUI.razor.scss diff --git a/Code/Inventory/Inventar.cs b/Code/Inventory/Inventar.cs index f0682dc..c96bda8 100644 --- a/Code/Inventory/Inventar.cs +++ b/Code/Inventory/Inventar.cs @@ -83,6 +83,7 @@ public class Inventar : Component _itemCache[item.Definition] = item; } } + _cacheDirty = false; } @@ -116,7 +117,10 @@ public class Inventar : Component while ( toAdd > 0 && (UnlimitedSlots || Items.Count < MaxInventorySlots) ) { int stackCount = Math.Min( toAdd, item.Definition.MaxCount ); - var newStack = new InventoryItem { Definition = item.Definition, Count = stackCount, MagazineAmmo = item.MagazineAmmo }; + var newStack = new InventoryItem + { + Definition = item.Definition, Count = stackCount, MagazineAmmo = item.MagazineAmmo + }; Items.Add( newStack ); toAdd -= stackCount; _cacheDirty = true; // Помечаем кэш как устаревший @@ -193,22 +197,89 @@ public class Inventar : Component if ( item == null || !Items.Contains( item ) ) return; - GameObject gO = item.Definition.Prefab.Clone( position ); - - if ( gO.Components.TryGet( out var inventoryItem ) ) + // Проверяем, является ли предмет одеждой + if ( item.Definition is ClothingItemDefinition clothingDef ) { - inventoryItem.Count = item.Count; - inventoryItem.Definition = item.Definition; - // Копируем патроны из оригинального предмета - inventoryItem.MagazineAmmo = item.MagazineAmmo; + // Для одежды создаем специальный физический объект + DropClothingItem( item, position, clothingDef ); } + else + { + // Для остальных предметов используем стандартный префаб + if ( item.Definition.Prefab != null ) + { + GameObject gO = item.Definition.Prefab.Clone( position ); - gO.NetworkSpawn(); + if ( gO.Components.TryGet( out var inventoryItem ) ) + { + inventoryItem.Count = item.Count; + inventoryItem.Definition = item.Definition; + // Копируем патроны из оригинального предмета + inventoryItem.MagazineAmmo = item.MagazineAmmo; + } + + gO.NetworkSpawn(); + } + else + { + Log.Warning( $"Префаб не найден для предмета: {item.Definition.Name}" ); + } + } // Удаляем весь предмет из инвентаря RemoveItem( item, item.Count ); } + /// + /// Выбрасывает предмет одежды как физический объект + /// + private void DropClothingItem( InventoryItem item, Vector3 position, ClothingItemDefinition clothingDef ) + { + // Пытаемся найти подходящий префаб для одежды + GameObject clothingPrefab = GameObject.GetPrefab( "prefabs/item_parcel.prefab" ); + GameObject clothingObject = null; + + clothingObject = clothingPrefab.Clone( position ); + + // Добавляем компонент InventoryItem + if ( clothingObject.Components.TryGet( out var inventoryItem ) ) + { + inventoryItem.Count = item.Count; + inventoryItem.Definition = item.Definition; + } + + // Добавляем компонент PickupItem для подбора + if ( clothingObject.Components.TryGet( out var pickupItem ) ) + { + // Устанавливаем правильную метку для одежды + var slotName = GetSlotDisplayName( clothingDef.Slot ); + pickupItem.Label = $"{clothingDef.Name} ({slotName})"; + } + + clothingObject.NetworkSpawn(); + + Log.Info( $"Выброшена одежда: {clothingDef.Name} ({clothingDef.Slot})" ); + } + + + /// + /// Получает отображаемое название слота + /// + private string GetSlotDisplayName( InventorySlot slot ) + { + return slot switch + { + InventorySlot.LeftHand => "Л.Рука", + InventorySlot.RightHand => "П.Рука", + InventorySlot.Head => "Голова", + InventorySlot.Body => "Тело", + InventorySlot.Hands => "Руки", + InventorySlot.Bottom => "Ноги", + InventorySlot.Feet => "Обувь", + _ => "Неизвестно" + }; + } + public void UnEquipItem( InventoryItem item ) { if ( item == null ) @@ -334,5 +405,55 @@ public class Inventar : Component } } + /// + /// Выбрасывает все предметы из инвентаря + /// + /// Базовая позиция для выбрасывания + /// Радиус разброса предметов + /// Количество выброшенных предметов + public int DropAllItems( Vector3 dropPosition, float scatterRadius = 50f ) + { + if ( !Network.IsOwner ) return 0; + + Log.Info( "Выбрасываем все предметы из инвентаря..." ); + + // Создаем копию списка предметов для безопасной итерации + var itemsToDrop = Items.ToList(); + var equippedItemsToDrop = EquippedItems.Values.ToList(); + + int droppedCount = 0; + + // Сначала снимаем все экипированные предметы и добавляем их в список для выбрасывания + foreach ( var equippedItem in equippedItemsToDrop ) + { + if ( equippedItem != null ) + { + // Снимаем предмет + UnEquipItem( equippedItem ); + + // Добавляем в список для выбрасывания, если его там еще нет + if ( !itemsToDrop.Contains( equippedItem ) ) + { + itemsToDrop.Add( equippedItem ); + } + } + } + + // Затем выбрасываем все предметы + foreach ( var item in itemsToDrop ) + { + if ( item != null && item.Count > 0 ) + { + // Выбираем случайную позицию в радиусе разброса + var scatteredPosition = dropPosition + Vector3.Random * scatterRadius + Vector3.Up * 10f; + DropItem( item, scatteredPosition ); + droppedCount++; + } + } + + Log.Info( $"Выброшено {droppedCount} предметов из инвентаря" ); + return droppedCount; + } + #endregion } diff --git a/Code/Player/Dedugan.Camera.cs b/Code/Player/Dedugan.Camera.cs index 95875b5..1f54365 100644 --- a/Code/Player/Dedugan.Camera.cs +++ b/Code/Player/Dedugan.Camera.cs @@ -20,9 +20,6 @@ public sealed partial class Dedugan // Плавная интерполяция позиции пивота для устранения колебаний CameraPivot.WorldPosition = Vector3.Lerp( CameraPivot.WorldPosition, Eyes.WorldPosition, Time.Delta * 25f ); - - // Компенсируем поворот тела, чтобы камера вращалась правильно - // Используем только yaw от EyeAngles, но pitch оставляем как есть var bodyYaw = Renderer.LocalRotation.Yaw(); var cameraYaw = EyeAngles.yaw; var compensatedYaw = cameraYaw - bodyYaw; diff --git a/Code/Player/Dedugan.Hit.cs b/Code/Player/Dedugan.Hit.cs index c508bec..3242948 100644 --- a/Code/Player/Dedugan.Hit.cs +++ b/Code/Player/Dedugan.Hit.cs @@ -22,5 +22,42 @@ public sealed partial class Dedugan { await GameTask.DelaySeconds( 0.1f ); RagdollController.Enabled = true; + + // Выбрасываем все предметы при смерти + DropAllItemsOnDeath(); + } + + /// + /// Выбрасывает все предметы из инвентаря при смерти игрока + /// + private void DropAllItemsOnDeath() + { + if ( Inventory == null || !Network.IsOwner ) return; + + Log.Info( $"Игрок {Name} умер. Выбрасываем все предметы..." ); + + // Используем метод из инвентаря для выбрасывания всех предметов + int droppedCount = Inventory.DropAllItems( WorldPosition, 50f ); + + Log.Info( $"Выброшено {droppedCount} предметов при смерти игрока {Name}" ); + } + + /// + /// Публичный метод для принудительного выбрасывания всех предметов + /// Можно вызывать из других мест (например, из команд или UI) + /// + /// Позиция для выбрасывания предметов (если null, используется позиция игрока) + /// Радиус разброса предметов + public void ForceDropAllItems( Vector3? dropPosition = null, float scatterRadius = 50f ) + { + if ( Inventory == null || !Network.IsOwner ) return; + + Log.Info( $"Принудительно выбрасываем все предметы игрока {Name}..." ); + + // Используем метод из инвентаря для выбрасывания всех предметов + var finalDropPosition = dropPosition ?? WorldPosition; + int droppedCount = Inventory.DropAllItems( finalDropPosition, scatterRadius ); + + Log.Info( $"Принудительно выброшено {droppedCount} предметов игрока {Name}" ); } } diff --git a/Code/Player/Dedugan.cs b/Code/Player/Dedugan.cs index a1c7890..4dd77b6 100644 --- a/Code/Player/Dedugan.cs +++ b/Code/Player/Dedugan.cs @@ -73,10 +73,10 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork protected override void OnUpdate() { _isOwner = Network.IsOwner; - + UpdateCustomAnimations(); InventoryUpdate(); - + if ( _isOwner ) { EyeAngles += Input.AnalogLook; @@ -94,15 +94,19 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork UpdateBodyRotation(); - Camera.LocalRotation = EyeAngles.ToRotation(); + CameraPivot.WorldPosition = + Vector3.Lerp( CameraPivot.WorldPosition, Eyes.WorldPosition, Time.Delta * 25f ); + var bodyYaw = Renderer.LocalRotation.Yaw(); + var cameraYaw = EyeAngles.yaw; + var compensatedYaw = cameraYaw - bodyYaw; + CameraPivot.LocalRotation = Rotation.FromYaw( compensatedYaw ) * Rotation.FromPitch( EyeAngles.pitch ); - var pivotOffset = CameraPivot.LocalRotation.Right * CamOffset.y + - CameraPivot.LocalRotation.Forward * CamOffset.x + - CameraPivot.LocalRotation.Up * CamOffset.z; - - var rotatedOffset = pivotOffset * EyeAngles.ToRotation(); - - Camera.WorldPosition = CameraPivot.WorldPosition + rotatedOffset; + // Camera.LocalRotation = EyeAngles.ToRotation(); + // var pivotOffset = CameraPivot.LocalRotation.Right * CamOffset.y + + // CameraPivot.LocalRotation.Forward * CamOffset.x + + // CameraPivot.LocalRotation.Up * CamOffset.z; + // var rotatedOffset = pivotOffset * EyeAngles.ToRotation(); + // Camera.WorldPosition = CameraPivot.WorldPosition + rotatedOffset; } } @@ -145,15 +149,15 @@ public sealed partial class Dedugan : Component, IUseContext, Component.INetwork { var currentBodyYaw = Renderer.LocalRotation.Yaw(); var currentCameraYaw = EyeAngles.yaw; - + // Проверяем, изменились ли углы - if ( Math.Abs( currentBodyYaw - _lastBodyYaw ) < 0.1f && - Math.Abs( currentCameraYaw - _lastCameraYaw ) < 0.1f && + if ( Math.Abs( currentBodyYaw - _lastBodyYaw ) < 0.1f && + Math.Abs( currentCameraYaw - _lastCameraYaw ) < 0.1f && !_bodyRotationDirty ) { return; // Пропускаем обновление, если углы не изменились } - + _lastBodyYaw = currentBodyYaw; _lastCameraYaw = currentCameraYaw; _bodyRotationDirty = false; diff --git a/Code/UI/GUI.razor b/Code/UI/GUI.razor index 64b85ba..07cfc79 100644 --- a/Code/UI/GUI.razor +++ b/Code/UI/GUI.razor @@ -25,29 +25,32 @@ } - @* *@ - @*
*@ - @*
*@ - @*
@( HasSaveFile ? "Сохранение есть" : "Нет сохранения" )
*@ + @*
*@ + @*
Сохранение инвентаря
*@ + @*
*@ + @* *@ + @* *@ + @* *@ + @* *@ + @*
*@ + @*
*@ + @* @if ( HasSaveFile ) *@ + @* { *@ + @* ✓ Файл сохранения найден *@ + @* } *@ + @* else *@ + @* { *@ + @* ✗ Файл сохранения не найден *@ + @* } *@ @*
*@ - @* *@ - @* @if ( HasSaveFile ) *@ - @* { *@ - @* *@ - @* } *@ - @* *@ - @* *@ - @* *@ - @* @if ( HasSaveFile ) *@ - @* { *@ - @* *@ - @* } *@ @*
*@ @@ -125,7 +128,7 @@ display: none; } - .save-status { + .save-panel { position: absolute; top: 20px; right: 20px; @@ -137,79 +140,61 @@ gap: 8px; min-width: 200px; - .save-indicator { - display: flex; - align-items: center; - gap: 8px; - padding: 8px; - border-radius: 4px; - font-size: 14px; + .save-title { + font-size: 18px; font-weight: 500; + color: white; + } + + .save-buttons { + display: flex; + justify-content: space-between; + gap: 8px; - &.has-save { - background: rgba(76, 175, 80, 0.2); - color: #4CAF50; - border: 1px solid #4CAF50; - } - - &.no-save { - background: rgba(244, 67, 54, 0.2); - color: #F44336; - border: 1px solid #F44336; - } - - .save-text { + button { + padding: 8px 12px; + border-radius: 4px; + border: none; + cursor: pointer; font-size: 14px; font-weight: 500; + transition: all 0.2s ease; + + &:hover { + transform: translateY(-1px); + } + + &.save-button { + background: #2196F3; + color: white; + + &:hover { + background: #1976D2; + } + } + + &.danger { + background: #F44336; + color: white; + + &:hover { + background: #D32F2F; + } + } } } - button { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 12px; - border-radius: 4px; - border: none; - cursor: pointer; + .save-status { font-size: 14px; font-weight: 500; - transition: all 0.2s ease; + color: white; - &:hover { - transform: translateY(-1px); + &.save-status-ok { + color: #4CAF50; } - &.save-button { - background: #2196F3; - color: white; - - &:hover { - background: #1976D2; - } - } - - &.load-button { - background: #4CAF50; - color: white; - - &:hover { - background: #388E3C; - } - } - - &.clear-button { - background: #F44336; - color: white; - - &:hover { - background: #D32F2F; - } - } - - .button-text { - font-size: 14px; - font-weight: 500; + &.save-status-none { + color: #F44336; } } } @@ -284,6 +269,14 @@ CheckSaveFile(); } + private void DropAllItems() + { + if ( Dedugan.Local != null ) + { + Dedugan.Local.ForceDropAllItems(); + } + } + protected override int BuildHash() { if ( Dedugan.Local == null || !HasWeapon ) diff --git a/Code/UI/GUI.razor.scss b/Code/UI/GUI.razor.scss new file mode 100644 index 0000000..2ee6ee7 --- /dev/null +++ b/Code/UI/GUI.razor.scss @@ -0,0 +1,65 @@ + .save-panel { + position: absolute; + top: 20px; + right: 20px; + background: rgba(0, 0, 0, 0.8); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 8px; + padding: 16px; + min-width: 200px; + + .save-title { + font-size: 18px; + font-weight: 500; + color: white; + margin-bottom: 12px; + text-align: center; + } + + .save-buttons { + display: flex; + flex-direction: column; + gap: 8px; + + button { + padding: 8px 12px; + border-radius: 4px; + border: none; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + background: #2196F3; + color: white; + + &:hover { + background: #1976D2; + transform: translateY(-1px); + } + + &.danger { + background: #F44336; + color: white; + + &:hover { + background: #D32F2F; + } + } + } + } + + .save-status { + margin-top: 12px; + font-size: 14px; + font-weight: 500; + text-align: center; + + .save-status-ok { + color: #4CAF50; + } + + .save-status-none { + color: #F44336; + } + } + } \ No newline at end of file diff --git a/README.md b/README.md index cbf5777..5368a2d 100644 --- a/README.md +++ b/README.md @@ -75,4 +75,99 @@ ### Оптимизации сети - Кэширование проверок владельца - Уменьшение частоты обновлений -- Эффективная синхронизация состояний \ No newline at end of file +- Эффективная синхронизация состояний + +## Система выбрасывания предметов при смерти + +### Автоматическое выбрасывание при смерти + +При смерти игрока (когда здоровье достигает 0) автоматически выбрасываются все предметы из инвентаря: + +- **Все экипированные предметы** автоматически снимаются +- **Все предметы в инвентаре** выбрасываются на землю рядом с игроком +- **Предметы разбрасываются** в радиусе 50 единиц от позиции игрока +- **Логирование** процесса выбрасывания в консоль + +### Специальная обработка одежды + +Одежда обрабатывается особым образом при выбрасывании: + +- **Создание физического объекта** - для одежды создается специальный физический объект +- **Fallback система** - если префаб не найден, создается простой объект с коллайдером +- **Правильная метка** - отображается название одежды и слот (например, "Куртка (Тело)") +- **Поддержка подбора** - выброшенная одежда может быть подобрана обратно + +### Методы для выбрасывания предметов + +#### `DropAllItemsOnDeath()` (приватный) +Автоматически вызывается при смерти игрока: +```csharp +private void DropAllItemsOnDeath() +{ + if ( Inventory == null || !Network.IsOwner ) return; + + int droppedCount = Inventory.DropAllItems( WorldPosition, 50f ); + Log.Info( $"Выброшено {droppedCount} предметов при смерти игрока {Name}" ); +} +``` + +#### `ForceDropAllItems()` (публичный) +Можно вызывать принудительно из других мест: +```csharp +public void ForceDropAllItems( Vector3? dropPosition = null, float scatterRadius = 50f ) +{ + if ( Inventory == null || !Network.IsOwner ) return; + + var finalDropPosition = dropPosition ?? WorldPosition; + int droppedCount = Inventory.DropAllItems( finalDropPosition, scatterRadius ); +} +``` + +#### `Inventory.DropAllItems()` (публичный) +Метод в классе инвентаря для массового выбрасывания: +```csharp +public int DropAllItems( Vector3 dropPosition, float scatterRadius = 50f ) +{ + // Снимает все экипированные предметы + // Выбрасывает все предметы в указанной позиции с разбросом + // Возвращает количество выброшенных предметов +} +``` + +#### `DropClothingItem()` (приватный) +Специальный метод для выбрасывания одежды: +```csharp +private void DropClothingItem( InventoryItem item, Vector3 position, ClothingItemDefinition clothingDef ) +{ + // Создает физический объект для одежды + // Добавляет правильную метку и компоненты + // Поддерживает fallback если префаб не найден +} +``` + +### UI для тестирования + +В интерфейсе добавлена кнопка "📦 Выбросить все" для тестирования системы выбрасывания предметов. + +### Особенности реализации + +1. **Безопасная итерация** - создаются копии списков предметов для избежания ошибок при изменении коллекций +2. **Сетевая синхронизация** - выбрасывание происходит только на владельце объекта +3. **Снятие экипировки** - все экипированные предметы сначала снимаются, затем выбрасываются +4. **Случайный разброс** - предметы разбрасываются случайным образом в указанном радиусе +5. **Специальная обработка одежды** - одежда создает физические объекты с правильными метками +6. **Fallback система** - если префаб не найден, создается простой объект с базовыми компонентами +7. **Логирование** - все действия логируются для отладки + +### Использование + +```csharp +// Принудительно выбросить все предметы игрока +Dedugan.Local.ForceDropAllItems(); + +// Выбросить предметы в определенной позиции +Dedugan.Local.ForceDropAllItems( new Vector3(100, 0, 100), 30f ); + +// Выбросить все предметы из инвентаря напрямую +player.Inventory.DropAllItems( player.WorldPosition, 25f ); +``` \ No newline at end of file