This commit is contained in:
Oscar 2025-06-29 15:06:54 +03:00
parent 495c672818
commit 235d5ad90a
7 changed files with 423 additions and 111 deletions

View File

@ -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<InventoryItem>( 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<InventoryItem>( 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 );
}
/// <summary>
/// Выбрасывает предмет одежды как физический объект
/// </summary>
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<InventoryItem>( out var inventoryItem ) )
{
inventoryItem.Count = item.Count;
inventoryItem.Definition = item.Definition;
}
// Добавляем компонент PickupItem для подбора
if ( clothingObject.Components.TryGet<PickupItem>( out var pickupItem ) )
{
// Устанавливаем правильную метку для одежды
var slotName = GetSlotDisplayName( clothingDef.Slot );
pickupItem.Label = $"{clothingDef.Name} ({slotName})";
}
clothingObject.NetworkSpawn();
Log.Info( $"Выброшена одежда: {clothingDef.Name} ({clothingDef.Slot})" );
}
/// <summary>
/// Получает отображаемое название слота
/// </summary>
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
}
}
/// <summary>
/// Выбрасывает все предметы из инвентаря
/// </summary>
/// <param name="dropPosition">Базовая позиция для выбрасывания</param>
/// <param name="scatterRadius">Радиус разброса предметов</param>
/// <returns>Количество выброшенных предметов</returns>
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
}

View File

@ -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;

View File

@ -22,5 +22,42 @@ public sealed partial class Dedugan
{
await GameTask.DelaySeconds( 0.1f );
RagdollController.Enabled = true;
// Выбрасываем все предметы при смерти
DropAllItemsOnDeath();
}
/// <summary>
/// Выбрасывает все предметы из инвентаря при смерти игрока
/// </summary>
private void DropAllItemsOnDeath()
{
if ( Inventory == null || !Network.IsOwner ) return;
Log.Info( $"Игрок {Name} умер. Выбрасываем все предметы..." );
// Используем метод из инвентаря для выбрасывания всех предметов
int droppedCount = Inventory.DropAllItems( WorldPosition, 50f );
Log.Info( $"Выброшено {droppedCount} предметов при смерти игрока {Name}" );
}
/// <summary>
/// Публичный метод для принудительного выбрасывания всех предметов
/// Можно вызывать из других мест (например, из команд или UI)
/// </summary>
/// <param name="dropPosition">Позиция для выбрасывания предметов (если null, используется позиция игрока)</param>
/// <param name="scatterRadius">Радиус разброса предметов</param>
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}" );
}
}

View File

@ -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;

View File

@ -25,29 +25,32 @@
}
</div>
@* <!-- Панель управления сохранением инвентаря --> *@
@* <div class="save-status"> *@
@* <div class="save-indicator @( HasSaveFile ? "has-save" : "no-save" )"> *@
@* <div class="save-text">@( HasSaveFile ? "Сохранение есть" : "Нет сохранения" )</div> *@
@* <div class="save-panel"> *@
@* <div class="save-title">Сохранение инвентаря</div> *@
@* <div class="save-buttons"> *@
@* <button class="save-button" @onclick="SaveInventory"> *@
@* 💾 Сохранить *@
@* </button> *@
@* <button class="save-button" @onclick="LoadInventory"> *@
@* 📂 Загрузить *@
@* </button> *@
@* <button class="save-button danger" @onclick="ClearSave"> *@
@* 🗑️ Очистить *@
@* </button> *@
@* <button class="save-button danger" @onclick="DropAllItems"> *@
@* 📦 Выбросить все *@
@* </button> *@
@* </div> *@
@* <div class="save-status"> *@
@* @if ( HasSaveFile ) *@
@* { *@
@* <span class="save-status-ok">✓ Файл сохранения найден</span> *@
@* } *@
@* else *@
@* { *@
@* <span class="save-status-none">✗ Файл сохранения не найден</span> *@
@* } *@
@* </div> *@
@* *@
@* @if ( HasSaveFile ) *@
@* { *@
@* <button class="load-button" onmousedown="LoadInventory"> *@
@* <div class="button-text">Загрузить</div> *@
@* </button> *@
@* } *@
@* *@
@* <button class="save-button" onmousedown="SaveInventory"> *@
@* <div class="button-text">Сохранить</div> *@
@* </button> *@
@* *@
@* @if ( HasSaveFile ) *@
@* { *@
@* <button class="clear-button" onmousedown="ClearSave"> *@
@* <div class="button-text">Очистить</div> *@
@* </button> *@
@* } *@
@* </div> *@
</root>
@ -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 )

65
Code/UI/GUI.razor.scss Normal file
View File

@ -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;
}
}
}

View File

@ -75,4 +75,99 @@
### Оптимизации сети
- Кэширование проверок владельца
- Уменьшение частоты обновлений
- Эффективная синхронизация состояний
- Эффективная синхронизация состояний
## Система выбрасывания предметов при смерти
### Автоматическое выбрасывание при смерти
При смерти игрока (когда здоровье достигает 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 );
```