AI захватит мир

This commit is contained in:
Oscar 2025-06-26 23:24:52 +03:00
parent 793165bb03
commit 875d594038
28 changed files with 1263 additions and 266 deletions

File diff suppressed because one or more lines are too long

24
Assets/Items/pistol.ammo Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
{
"Slot": "RightHand",
"HoldType": "Pistol",
"AmmoType": "Pistol",
"WeaponDefinition": {
"Position": "-1.108,0.38,-2.367",
"Rotation": "0.002790701,0.01173679,0.006057173,0.9999089"

View File

@ -49,7 +49,7 @@
"OnComponentUpdate": null
},
{
"__type": "Sandbox.UI.PickupItem",
"__type": "Sasalka.PickupItem",
"__guid": "a90f059e-5c0a-4122-b742-9dd12e5d8494",
"__enabled": true,
"Label": "E",

View File

@ -61,7 +61,7 @@
"SurfaceVelocity": "0,0,0"
},
{
"__type": "Sandbox.UI.PickupItem",
"__type": "Sasalka.PickupItem",
"__guid": "8546ae27-6c60-4e32-b613-760bc20bd651",
"__enabled": true,
"Label": "E",
@ -106,8 +106,7 @@
"__guid": "beaec0c2-ccc3-48b0-8459-5a3f3fe679f1",
"__enabled": true,
"Count": 120,
"Definition": "items/pistol_ammo.inv",
"Equipped": false,
"Definition": "items/pistol.ammo",
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
@ -179,7 +178,7 @@
"SurfaceVelocity": "0,0,0"
},
{
"__type": "Sandbox.UI.PickupItem",
"__type": "Sasalka.PickupItem",
"__guid": "edaf7116-38ed-4e2f-91d6-b20aa9a1a009",
"__enabled": true,
"Label": "E",
@ -223,9 +222,8 @@
"__type": "Sasalka.InventoryItem",
"__guid": "0b67d1c5-594a-49fa-8c23-e7a3ed9edb2c",
"__enabled": true,
"Count": 120,
"Count": 1,
"Definition": "items/pistol_ammo.inv",
"Equipped": false,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,
@ -274,7 +272,7 @@
"SurfaceVelocity": "0,0,0"
},
{
"__type": "Sandbox.UI.PickupItem",
"__type": "Sasalka.PickupItem",
"__guid": "a8bc6a6d-dc7b-441a-8191-79e83a998981",
"__enabled": true,
"Label": "E",
@ -318,9 +316,8 @@
"__type": "Sasalka.InventoryItem",
"__guid": "50b368f3-d27b-4408-a66b-a9d852e6fbae",
"__enabled": true,
"Count": 120,
"Count": 1,
"Definition": "items/pistol_ammo.inv",
"Equipped": false,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
"OnComponentEnabled": null,

View File

@ -136,10 +136,11 @@
"particlePrefab": {
"_type": "gameobject",
"prefab": "prefabs/impacts/impact.metal.prefab"
}
},
"WeaponDefinition": "items/pistol_test.weapon"
},
{
"__type": "Sandbox.UI.PickupItem",
"__type": "Sasalka.PickupItem",
"__guid": "7350112e-dea3-4c22-b9fc-76eaed3d2e57",
"__enabled": true,
"Label": "E",

View File

@ -483,18 +483,18 @@
"e5e018f2-9cad-4ece-b160-ab2aa3aeb7ec": "7cdbb78c-19c3-4f0a-9662-91609da57955",
"ee9d15a8-ffe9-43f1-b6b4-2f04ce55c908": "5fbe67ec-6b54-4560-87ef-8515fce7d33d",
"5830c995-d747-4c8c-abd7-b0366fa2a2aa": "81bd6292-744c-4e33-8c6a-3904a13ee646",
"8546ae27-6c60-4e32-b613-760bc20bd651": "9e0bc531-e476-492f-912c-53859daf1dea",
"eeca4029-990c-43e6-aa16-1df8fc69fa1c": "cd79147f-c09a-4b76-94a0-b46b730a9ff1",
"4d97a077-f187-44f4-967b-d346530c38d9": "7fb3b2db-8dd8-4611-b79b-18529c4a2081",
"beaec0c2-ccc3-48b0-8459-5a3f3fe679f1": "823f8251-0a51-4513-9995-178c44e63469",
"e428ecbc-40fd-4ec5-b1af-373bce265f3e": "766cbde5-8f5c-4660-8608-100ffefbfa29",
"ce6aaed8-3ae5-41a4-a21c-1386b86a158a": "a8918404-1c91-48c8-ba49-f878516a2565",
"1ef0f21d-1147-4a46-97e8-97c587a99040": "6151c1b8-ff4e-4cc8-99dd-1eff15bb641d",
"edaf7116-38ed-4e2f-91d6-b20aa9a1a009": "8ea1e4c3-5543-4367-b3e2-7ed11559b437",
"038bd275-df09-422f-9f02-8448b02706f8": "bc2008a9-f206-4008-89d1-beee5d1051bd",
"1bb4075f-849c-4761-93bc-38f31cd11650": "811e4d93-ba7e-4edd-9ac4-ccb89bc87fb3",
"0b67d1c5-594a-49fa-8c23-e7a3ed9edb2c": "90d493ea-1b5c-4a93-ba8b-a8b538fb4ade",
"3cd5251b-38a8-4f0e-8b33-4b2e5f7041ab": "2ccacb25-f30f-42ea-982d-bc521e4aefac",
"8b94182b-9ac1-4646-8f8c-d3188a804946": "15f06e09-733f-4885-9a95-f7dd23d93657",
"a8bc6a6d-dc7b-441a-8191-79e83a998981": "fea348c1-39df-4974-9b74-857163bd976c",
"c4a3df64-7ff1-483a-9f9e-e181152d7645": "f5ed5bad-5418-4b0d-a6f6-3c0cdfbcef5f",
"064ba569-c0b4-48ac-8ab4-a5c4d63b1a30": "73756613-cabc-413b-8fec-024b6e6c0246",
"50b368f3-d27b-4408-a66b-a9d852e6fbae": "dd02755a-000b-440f-9e55-7b4b0b7bac75"
}
@ -530,18 +530,18 @@
"e5e018f2-9cad-4ece-b160-ab2aa3aeb7ec": "2fe45086-5076-4fdc-a2c7-29ec92c23810",
"ee9d15a8-ffe9-43f1-b6b4-2f04ce55c908": "c7e24a6f-de41-4d1e-be96-0348b347119b",
"5830c995-d747-4c8c-abd7-b0366fa2a2aa": "d7c8e6d4-4f33-4929-9600-91bab058201d",
"8546ae27-6c60-4e32-b613-760bc20bd651": "cd546fdd-9e2c-4e21-b068-6802c17eac4b",
"eeca4029-990c-43e6-aa16-1df8fc69fa1c": "30727755-3261-4208-b2cd-604ea5f3110e",
"4d97a077-f187-44f4-967b-d346530c38d9": "6d89a94c-3924-4534-822a-8199fcc273c6",
"beaec0c2-ccc3-48b0-8459-5a3f3fe679f1": "7f1321e3-4e18-431a-ac36-4c006ff42ca2",
"e428ecbc-40fd-4ec5-b1af-373bce265f3e": "dd9de019-e850-4b70-9f23-9c7606b71258",
"ce6aaed8-3ae5-41a4-a21c-1386b86a158a": "6835aaa4-31e7-44d3-bcf7-21435bfc30a8",
"1ef0f21d-1147-4a46-97e8-97c587a99040": "24907222-1cef-4d26-aae2-55cde0ce8506",
"edaf7116-38ed-4e2f-91d6-b20aa9a1a009": "9a64d9dd-30d7-49d1-b1f8-f06602183892",
"038bd275-df09-422f-9f02-8448b02706f8": "f23c909a-e080-418f-8bf4-4812459b17b4",
"1bb4075f-849c-4761-93bc-38f31cd11650": "60d7794d-069e-4533-a002-5a0596f4a52d",
"0b67d1c5-594a-49fa-8c23-e7a3ed9edb2c": "f87dd0c6-58c1-4d94-9799-7ad2e8a247f8",
"3cd5251b-38a8-4f0e-8b33-4b2e5f7041ab": "abb38bd9-332e-45fc-8929-165de4a4cbde",
"8b94182b-9ac1-4646-8f8c-d3188a804946": "2f496a29-ba4e-4878-abdb-85a9b2af2748",
"a8bc6a6d-dc7b-441a-8191-79e83a998981": "8252f8f4-c89c-4f50-b3d1-09912957e3de",
"c4a3df64-7ff1-483a-9f9e-e181152d7645": "89cb0618-4fb3-4ccf-b9d3-63abe5bd67ed",
"064ba569-c0b4-48ac-8ab4-a5c4d63b1a30": "f291bdc2-16d4-496b-9cfc-2c1900649bf0",
"50b368f3-d27b-4408-a66b-a9d852e6fbae": "a2165c8e-f32c-4bba-a482-c0a1c52bfe1f"
}
@ -577,18 +577,18 @@
"e5e018f2-9cad-4ece-b160-ab2aa3aeb7ec": "ef158b83-fa9f-4774-9eb1-36651c8b08a9",
"ee9d15a8-ffe9-43f1-b6b4-2f04ce55c908": "c1b4d1db-34b2-4dd2-986c-0ca47ee367c6",
"5830c995-d747-4c8c-abd7-b0366fa2a2aa": "688d5b7a-3628-411c-bce4-7e71c36b65c8",
"8546ae27-6c60-4e32-b613-760bc20bd651": "e6b39fb1-4f30-45a7-8ba4-fb6d40910258",
"eeca4029-990c-43e6-aa16-1df8fc69fa1c": "32cb01e1-fc77-431b-ae65-96f807c4580e",
"4d97a077-f187-44f4-967b-d346530c38d9": "621b7ca2-7ae7-4867-9251-30e27db49025",
"beaec0c2-ccc3-48b0-8459-5a3f3fe679f1": "57855c6f-9f2c-45a8-a493-b4144dabbdae",
"e428ecbc-40fd-4ec5-b1af-373bce265f3e": "f421713f-ba53-49ef-9299-e43dfbafcb96",
"ce6aaed8-3ae5-41a4-a21c-1386b86a158a": "3d8d56a5-3400-4f27-bfd6-ea9bdca3d1fc",
"1ef0f21d-1147-4a46-97e8-97c587a99040": "75e8850c-7f68-42f8-b708-5cee771c0712",
"edaf7116-38ed-4e2f-91d6-b20aa9a1a009": "adc87480-58e1-4d52-ba22-ec0ba98e72d7",
"038bd275-df09-422f-9f02-8448b02706f8": "248d9927-b022-4daa-aa13-222b1e5a4a96",
"1bb4075f-849c-4761-93bc-38f31cd11650": "72218084-be9b-4bf0-986a-166ca09da287",
"0b67d1c5-594a-49fa-8c23-e7a3ed9edb2c": "7f607022-8e99-4761-9e9b-72a71ae4d5a8",
"3cd5251b-38a8-4f0e-8b33-4b2e5f7041ab": "a766afdb-5b02-4407-96f7-0a0e345ba3c1",
"8b94182b-9ac1-4646-8f8c-d3188a804946": "67c84159-9ad0-43fb-880c-3ad680d09cd3",
"a8bc6a6d-dc7b-441a-8191-79e83a998981": "b2510e96-331a-4ab3-9734-69bb46501c1f",
"c4a3df64-7ff1-483a-9f9e-e181152d7645": "deaf34f3-b848-4556-ab94-f9c93cadf1bc",
"064ba569-c0b4-48ac-8ab4-a5c4d63b1a30": "bd311d99-63b5-4a80-8fc6-7debe7012e8d",
"50b368f3-d27b-4408-a66b-a9d852e6fbae": "a5a33b69-7390-4492-8dfd-a52766b99b88"
}
@ -625,7 +625,7 @@
"6017d24d-39d0-4acf-bf9f-010c345fd13d": "4a6feb0c-3798-41fd-b68f-9f43df293833",
"be95c906-5f2f-4239-8fd6-a42a148dea6e": "7844d416-a403-43e6-b473-c337d1be6fc5",
"fc38d078-995b-443a-b409-e877618fcf09": "428482a2-31e0-4f7b-a587-eb91a6027627",
"7350112e-dea3-4c22-b9fc-76eaed3d2e57": "d900697b-2fde-4916-b9ac-14ebedc2797b",
"f5968c60-ed2e-47d1-a2bf-353e0398f234": "257dce97-2921-4cf9-95de-e06525bd0492",
"ce60c131-01f0-4aa3-a84c-c93eac1d1c2d": "cb13417e-2527-40b7-9ddf-2c017f9a8a49",
"8a705640-2966-489e-a41a-2bf6f2cc62f0": "a5f8a313-cccc-4c11-98c0-e82b09badcd3",
"8b4ca11f-8cd7-4bb5-bb63-df86dac15ab9": "43445fd1-090e-49c9-8ed7-80ee31b7e783",
@ -680,7 +680,7 @@
"6017d24d-39d0-4acf-bf9f-010c345fd13d": "a04677d0-af09-4eb5-bda6-9019c17389d2",
"be95c906-5f2f-4239-8fd6-a42a148dea6e": "204255d7-7330-4b3d-b0fd-e648a4b8c728",
"fc38d078-995b-443a-b409-e877618fcf09": "ff3f86ae-660f-4a43-a0b0-1c7e87d1e642",
"7350112e-dea3-4c22-b9fc-76eaed3d2e57": "804bd67d-499b-4fbb-89ac-b59184fd434f",
"f5968c60-ed2e-47d1-a2bf-353e0398f234": "2c56a4c8-eb8d-44aa-95fa-6c27d6391e4d",
"ce60c131-01f0-4aa3-a84c-c93eac1d1c2d": "4157870e-5b90-40cb-a0ba-3c61a7805d06",
"8a705640-2966-489e-a41a-2bf6f2cc62f0": "1eb22b1c-ab75-4553-9470-f5e3e4eef369",
"8b4ca11f-8cd7-4bb5-bb63-df86dac15ab9": "f9cec441-fa67-4932-a804-41c9beebf07e",
@ -735,7 +735,7 @@
"6017d24d-39d0-4acf-bf9f-010c345fd13d": "b2d698cd-6aee-497b-bfa9-163d21d2e0b5",
"be95c906-5f2f-4239-8fd6-a42a148dea6e": "fa129559-6f5f-40ee-87f0-9ab69ee70743",
"fc38d078-995b-443a-b409-e877618fcf09": "16b190ad-d52f-403d-882b-bff9261232ae",
"7350112e-dea3-4c22-b9fc-76eaed3d2e57": "ef492981-3649-401a-aa0f-7b12d77873bc",
"f5968c60-ed2e-47d1-a2bf-353e0398f234": "7f88a632-e787-4609-8a8b-ecd3f555872a",
"ce60c131-01f0-4aa3-a84c-c93eac1d1c2d": "e59c9852-cd4c-4475-9385-4fd32ad4b876",
"8a705640-2966-489e-a41a-2bf6f2cc62f0": "aa7217af-d2b3-440f-9852-c8c047c096c6",
"8b4ca11f-8cd7-4bb5-bb63-df86dac15ab9": "1ad59142-d2a2-47c6-88ee-4ed5817cb5c6",
@ -845,7 +845,6 @@
"__type": "WorlModelClothSpawner",
"__guid": "a6297e3c-aaa4-4051-8850-d577d90f7640",
"__enabled": true,
"CenterPosition": "0,0,0",
"Height": 5,
"OnComponentDestroy": null,
"OnComponentDisabled": null,
@ -5095,6 +5094,32 @@
}
],
"Children": []
},
{
"__guid": "b593d08e-1bf7-4ebb-a2ed-bd8ee901d09e",
"__version": 1,
"__Prefab": "prefabs/item_parcel.prefab",
"__PrefabInstancePatch": {
"AddedObjects": [],
"RemovedObjects": [],
"PropertyOverrides": [
{
"Target": {
"Type": "GameObject",
"IdValue": "0b13f26c-c198-4efc-88df-260d896af36a"
},
"Property": "Position",
"Value": "160,16,4.069437"
}
],
"MovedObjects": []
},
"__PrefabIdToInstanceId": {
"0b13f26c-c198-4efc-88df-260d896af36a": "b593d08e-1bf7-4ebb-a2ed-bd8ee901d09e",
"80b52707-c81d-47fa-bab7-4be48f2d75a4": "2150cd49-a27c-462f-a7ea-ead17e42f6d6",
"c888eaab-cb05-4469-bf5f-7c23ede5c25f": "799f4a0c-f42b-46e6-ba2b-44de0a85255e",
"a90f059e-5c0a-4122-b742-9dd12e5d8494": "d14d3f67-52e9-4ff1-9348-66990b170c0f"
}
}
],
"SceneProperties": {

View File

@ -3,6 +3,7 @@
public class AttachmentSlotResolver
{
private readonly Func<string, GameObject> _attachmentGetter;
private readonly Dictionary<Inventar.InventorySlot, GameObject> _slotCache = new();
public AttachmentSlotResolver( Func<string, GameObject> attachmentGetter )
{
@ -11,12 +12,27 @@ public class AttachmentSlotResolver
public GameObject GetSlotObject( Inventar.InventorySlot slot )
{
return slot switch
// Проверяем кэш
if (_slotCache.TryGetValue(slot, out var cachedObject))
{
return cachedObject;
}
// Получаем объект и кэшируем его
var slotObject = slot switch
{
Inventar.InventorySlot.LeftHand => _attachmentGetter.Invoke( "hold_L" ),
Inventar.InventorySlot.RightHand => _attachmentGetter.Invoke( "hold_R" ),
Inventar.InventorySlot.Body => _attachmentGetter.Invoke( "forward_reference_modelspace" ),
_ => _attachmentGetter.Invoke( "forward_reference_modelspace" )
};
_slotCache[slot] = slotObject;
return slotObject;
}
public void ClearCache()
{
_slotCache.Clear();
}
}

View File

@ -0,0 +1,32 @@
namespace Sasalka;
[GameResource( "Ammo Item Definition", "ammo", "", Category = "Sasalka", Icon = "inventory_2" )]
public class AmmoItemDefinition : BaseItemDefinition
{
[Property, Category( "Ammo Properties" )]
public string AmmoType { get; set; } = "Pistol";
[Property, Category( "Ammo Properties" )]
public float Damage { get; set; } = 10f;
[Property, Category( "Ammo Properties" )]
public float Penetration { get; set; } = 0f;
[Property, Category( "Ammo Properties" )]
public bool IsExplosive { get; set; } = false;
[Property, Category( "Ammo Properties" )]
public float ExplosionRadius { get; set; } = 0f;
[Property, Category( "Ammo Properties" )]
public string CompatibleWeapons { get; set; } = "";
public override ItemCategory Category => ItemCategory.Ammo;
public override bool CanUse() => false; // Патроны нельзя использовать напрямую
public bool IsCompatibleWith( string ammoType )
{
return AmmoType == ammoType;
}
}

View File

@ -1,19 +1,92 @@
namespace Sasalka;
[GameResource( "Base Item Definition", "inv", "", Category = "Sasalka", Icon = "inventory_2" )]
public enum ItemCategory
{
Weapon,
Clothing,
Ammo,
Consumable,
Tool,
Misc
}
public enum ItemRarity
{
Common,
Uncommon,
Rare,
Epic,
Legendary
}
[GameResource("Base Item Definition", "inv", "", Category = "Sasalka", Icon = "inventory_2")]
public class BaseItemDefinition : GameResource
{
public string Name { get; set; }
[Property, Title("Basic Info")]
public string Name { get; set; } = "Unknown Item";
public string Description { get; set; }
[Property]
public string Description { get; set; } = "";
[ResourceType( "prefab" )]
public GameObject Prefab { get; set; } = GameObject.GetPrefab( "prefabs/item_parcel.prefab" );
[Property, Category("Visual")]
[ResourceType("prefab")]
public GameObject Prefab { get; set; } = GameObject.GetPrefab("prefabs/item_parcel.prefab");
[Property, Category("Visual")]
public Texture ImageTexture { get; set; }
[Property, Category("Visual")]
public string ImageUrl { get; set; }
[Property, Category("Properties")]
[Range(1, 1000)]
public int MaxCount { get; set; } = 1;
[Property, Category("Properties")]
public virtual ItemCategory Category { get; set; } = ItemCategory.Misc;
[Property, Category("Properties")]
public ItemRarity Rarity { get; set; } = ItemRarity.Common;
[Property, Category("Properties")]
public float Weight { get; set; } = 1.0f;
[Property, Category("Properties")]
public bool IsStackable => MaxCount > 1;
[Property, Category("Properties")]
public bool IsEquipable => this is IEquipable;
public virtual Inventar.InventorySlot? GetSlot() => null;
public virtual bool CanUse() => false;
public virtual void OnUse(InventoryItem item) { }
public string GetRarityColor()
{
return Rarity switch
{
ItemRarity.Common => "#ffffff",
ItemRarity.Uncommon => "#1eff00",
ItemRarity.Rare => "#0070dd",
ItemRarity.Epic => "#a335ee",
ItemRarity.Legendary => "#ff8000",
_ => "#ffffff"
};
}
public string GetCategoryIcon()
{
return Category switch
{
ItemCategory.Weapon => "🔫",
ItemCategory.Clothing => "👕",
ItemCategory.Ammo => "💥",
ItemCategory.Consumable => "🍖",
ItemCategory.Tool => "🔧",
ItemCategory.Misc => "📦",
_ => "📦"
};
}
}

View File

@ -3,7 +3,29 @@
[GameResource( "Clothing Item Definition", "clitem", "", Category = "Sasalka", Icon = "inventory_2" )]
public class ClothingItemDefinition : BaseItemDefinition, IEquipable
{
[Property] public string ClothUrl { get; set; }
[Property, Category( "Clothing Properties" )]
public string ClothUrl { get; set; }
[Property, Category( "Clothing Properties" )]
public Inventar.InventorySlot Slot { get; set; }
[Property, Category( "Clothing Properties" )]
public float ArmorValue { get; set; } = 0f;
[Property, Category( "Clothing Properties" )]
public bool IsVisible { get; set; } = true;
[Property, Category( "Clothing Properties" )]
public string BodyPart { get; set; } = "";
public override Inventar.InventorySlot? GetSlot() => Slot;
public override ItemCategory Category => ItemCategory.Clothing;
public override bool CanUse() => true;
public override void OnUse( InventoryItem item )
{
// Логика экипировки одежды
}
}

View File

@ -5,16 +5,55 @@ namespace Sasalka;
[GameResource( "Weapon Item Definition", "weapon", "", Category = "Sasalka", Icon = "inventory_2" )]
public class WeaponItemDefinition : BaseItemDefinition, IEquipable
{
[Property, Category( "Weapon Properties" )]
public Inventar.InventorySlot Slot { get; set; }
public override Inventar.InventorySlot? GetSlot() => Slot;
[Property, Category( "Weapon Properties" )]
public CitizenAnimationHelper.HoldTypes HoldType { get; set; } = CitizenAnimationHelper.HoldTypes.None;
[InlineEditor, Space] public WeaponDefinition WeaponDefinition { get; set; }
[Property, Category( "Weapon Properties" )]
[InlineEditor, Space]
public WeaponDefinition WeaponDefinition { get; set; }
[Property, Category( "Weapon Properties" )]
public float Damage { get; set; } = 10f;
[Property, Category( "Weapon Properties" )]
public float FireRate { get; set; } = 1f;
[Property, Category( "Weapon Properties" )]
public float Range { get; set; } = 1000f;
[Property, Category( "Weapon Properties" )]
public int MagazineSize { get; set; } = 30;
[Property, Category( "Weapon Properties" )]
public string AmmoType { get; set; } = "Pistol";
public override Inventar.InventorySlot? GetSlot() => Slot;
public override ItemCategory Category => ItemCategory.Weapon;
public override bool CanUse() => true;
public override void OnUse( InventoryItem item )
{
// Логика использования оружия будет в компоненте Weapon
}
}
public struct WeaponDefinition
{
// public CitizenAnimationHelper.Hand Hand { get; set; }
public Vector3 Position { get; set; }
public Rotation Rotation { get; set; }
[Property] public Vector3 Position { get; set; }
[Property] public Rotation Rotation { get; set; }
[Property] public Vector3 Scale { get; set; }
public WeaponDefinition()
{
Position = Vector3.Zero;
Rotation = Rotation.Identity;
Scale = Vector3.One;
}
}

View File

@ -1,6 +1,11 @@
namespace Sasalka;
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
public class Inventar
namespace Sasalka;
public class Inventar : Component
{
[Flags]
public enum InventorySlot
@ -15,84 +20,243 @@ public class Inventar
Feet = 1 << 6 // 64
}
public List<InventoryItem> Items { get; private set; } = new();
[Sync] public IList<InventoryItem> Items { get; set; } = new List<InventoryItem>();
[Sync]
public IDictionary<InventorySlot, InventoryItem> EquippedItems { get; set; } =
new Dictionary<InventorySlot, InventoryItem>();
// Настройка вместимости инвентаря
[Property] public int MaxInventorySlots { get; set; } = 20; // Максимальное количество слотов
[Property] public bool UnlimitedSlots { get; set; } = false; // Безлимитный инвентарь
public static bool IsInventoryOpen = false;
public Dictionary<InventorySlot, InventoryItem> EquippedItems { get; private set; } = new();
public event Action OnChanged;
public event Action<InventoryItem> OnEquipped;
public event Action<InventoryItem> OnUnEquipped;
public event Action<InventoryItem> OnItemAdded;
public event Action<InventoryItem> OnItemRemoved;
public void AddItem( InventoryItem item )
public bool CanAddItem( InventoryItem item )
{
Items.Add( item );
OnChanged?.Invoke();
if ( item == null || item.Definition == null )
return false;
// Проверяем, есть ли уже такой предмет в инвентаре
var existingItem = Items.FirstOrDefault( x => x.Definition == item.Definition );
if ( existingItem != null )
{
// Если предмет уже есть, проверяем, можно ли добавить к нему количество
return existingItem.Count + item.Count <= item.Definition.MaxCount;
}
// Если предмета нет, проверяем вместимость инвентаря
if ( !UnlimitedSlots && Items.Count >= MaxInventorySlots )
{
return false; // Инвентарь полон
}
// Проверяем только MaxCount для нового предмета
return item.Count <= item.Definition.MaxCount;
}
public void RemoveItem( InventoryItem item )
/// <summary>
/// Добавляет предмет в инвентарь, распределяя по существующим и новым стекам. Возвращает остаток, который не удалось добавить (или 0, если всё добавлено).
/// </summary>
public int AddItem(InventoryItem item)
{
UnEquipItem( item );
Items.Remove( item );
OnChanged?.Invoke();
if (item == null || item.Definition == null || item.Count <= 0)
return item.Count;
int toAdd = item.Count;
// 1. Заполняем существующие стаки
foreach (var stack in Items.Where(x => x.Definition == item.Definition && x.Count < x.Definition.MaxCount))
{
int canAdd = Math.Min(toAdd, stack.Definition.MaxCount - stack.Count);
if (canAdd > 0)
{
stack.Count += canAdd;
toAdd -= canAdd;
OnChanged?.Invoke();
OnItemAdded?.Invoke(stack);
}
if (toAdd <= 0) return 0;
}
// 2. Добавляем новые стаки, если есть место
while (toAdd > 0 && (UnlimitedSlots || Items.Count < MaxInventorySlots))
{
int stackCount = Math.Min(toAdd, item.Definition.MaxCount);
var newStack = new InventoryItem { Definition = item.Definition, Count = stackCount };
Items.Add(newStack);
toAdd -= stackCount;
OnChanged?.Invoke();
OnItemAdded?.Invoke(newStack);
}
// 3. Возвращаем остаток, если не всё удалось добавить
return toAdd;
}
public void EquipItem( InventoryItem item )
public bool RemoveItem( InventoryItem item, int count = 1 )
{
if ( item.Definition is not IEquipable equipable )
return;
if ( item == null || !Items.Contains( item ) )
return false;
if ( count >= item.Count )
{
// Удаляем весь предмет
UnEquipItem( item );
Items.Remove( item );
OnChanged?.Invoke();
OnItemRemoved?.Invoke( item );
return true;
}
else
{
// Уменьшаем количество
item.Count -= count;
OnChanged?.Invoke();
return true;
}
}
public bool EquipItem( InventoryItem item )
{
if ( item?.Definition is not IEquipable equipable )
return false;
var slot = item.Definition.GetSlot();
if ( slot == null )
return false;
// Если уже экипирован этот же предмет — снять его
if ( EquippedItems.ContainsValue( item ) )
if ( EquippedItems.Values.Contains( item ) )
{
UnEquipItem( item );
return;
return true;
}
// Если на этом слоте уже что-то есть — снять старый предмет
if ( EquippedItems.TryGetValue( equipable.Slot, out var oldItem ) )
if ( EquippedItems.TryGetValue( slot.Value, out var oldItem ) )
{
UnEquipItem( oldItem );
// Вернуть снятый предмет обратно в инвентарь, если его там нет
if ( !Items.Contains( oldItem ) )
Items.Add( oldItem );
}
// Экипировать новый предмет
EquippedItems[equipable.Slot] = item;
EquippedItems[slot.Value] = item;
item.Equipped = true;
OnEquipped?.Invoke( item );
OnChanged?.Invoke();
return true;
}
public void DropItem( InventoryItem item, Vector3 position )
{
if ( item == null || !Items.Contains( item ) )
return;
// Создаем копию предмета для выбрасывания
var droppedItem = new InventoryItem
{
Definition = item.Definition,
Count = item.Count // Выбрасываем всю стопку
};
GameObject gO = item.Definition.Prefab.Clone( position );
if ( gO.Components.TryGet<InventoryItem>( out var inventoryItem ) )
{
inventoryItem.Count = item.Count;
if ( inventoryItem.Definition == null )
{
inventoryItem.Definition = item.Definition;
}
inventoryItem.Count = droppedItem.Count;
inventoryItem.Definition = droppedItem.Definition;
}
gO.NetworkSpawn( null );
RemoveItem( item );
Items.Remove( item );
OnChanged?.Invoke();
gO.NetworkSpawn();
// Удаляем весь предмет из инвентаря
RemoveItem( item, item.Count );
}
public void UnEquipItem( InventoryItem item )
{
foreach ( var kvp in EquippedItems.Where( kvp => kvp.Value == item ).ToList() )
if ( item == null )
return;
var slotToRemove = EquippedItems.FirstOrDefault( kvp => kvp.Value == item ).Key;
if ( EquippedItems.ContainsKey( slotToRemove ) )
{
EquippedItems.Remove( kvp.Key );
EquippedItems.Remove( slotToRemove );
item.Equipped = false;
OnUnEquipped?.Invoke( item );
OnChanged?.Invoke();
}
}
public InventoryItem GetEquippedItem( InventorySlot slot )
{
return EquippedItems.TryGetValue( slot, out var item ) ? item : null;
}
public bool IsSlotOccupied( InventorySlot slot )
{
return EquippedItems.ContainsKey( slot );
}
public void ClearInventory()
{
// Снимаем все экипированные предметы
foreach ( var item in EquippedItems.Values.ToList() )
{
UnEquipItem( item );
}
item.Equipped = false;
OnUnEquipped?.Invoke( item );
Items.Clear();
OnChanged?.Invoke();
}
// Публичный метод для уведомления об изменениях извне класса
public void NotifyChanged()
{
OnChanged?.Invoke();
}
// Публичный метод для уведомления о добавлении предмета извне класса
public void NotifyItemAdded( InventoryItem item )
{
OnItemAdded?.Invoke( item );
}
// Методы для получения информации о вместимости
public int GetUsedSlots()
{
return Items.Count;
}
public int GetAvailableSlots()
{
if ( UnlimitedSlots )
return int.MaxValue;
return Math.Max( 0, MaxInventorySlots - Items.Count );
}
public bool IsInventoryFull()
{
if ( UnlimitedSlots )
return false;
return Items.Count >= MaxInventorySlots;
}
public float GetInventoryUsagePercentage()
{
if ( UnlimitedSlots )
return 0f;
return (float)Items.Count / MaxInventorySlots * 100f;
}
}

View File

@ -6,14 +6,60 @@ namespace Sasalka;
public class InventoryItem : Component
{
[Property] public BaseItemDefinition Definition { get; set; }
[Property] public int Count { get; set; } = 1;
[Property] public bool Equipped { get; set; } = false;
[Sync, Property] public int Count { get; set; } = 1;
[Sync] public bool Equipped { get; set; } = false;
protected override void OnStart()
{
if ( GameObject.Components.TryGet<PickupItem>( out var item ) ) //FindMode.EverythingInSelf
if ( GameObject.Components.TryGet<PickupItem>( out var item ) )
{
item.Label = Definition.Name;
item.Label = Definition?.Name ?? "Unknown Item";
}
}
public bool CanStackWith( InventoryItem other )
{
if ( other == null || Definition == null || other.Definition == null )
return false;
return Definition == other.Definition && Definition.MaxCount > 1;
}
public bool CanAddCount( int amount )
{
if ( Definition == null )
return false;
return Count + amount <= Definition.MaxCount;
}
public bool TryAddCount( int amount )
{
if ( !CanAddCount( amount ) )
return false;
Count += amount;
return true;
}
public bool TryRemoveCount( int amount )
{
if ( Count < amount )
return false;
Count -= amount;
return true;
}
public InventoryItem Clone()
{
var clone = new InventoryItem { Definition = Definition, Count = Count, Equipped = false };
return clone;
}
public override string ToString()
{
return $"{Definition?.Name ?? "Unknown"} x{Count}";
}
}

View File

@ -1,9 +0,0 @@
using Sandbox;
namespace Sasalka;
public class InventoryItemCountable : InventoryItem
{
// public int Count { get; set; } = 1;
// public int MaxCount { get; set; } = 1;
}

View File

@ -0,0 +1,55 @@
using Sandbox;
namespace Sasalka;
public class ItemSpawner : Component
{
[Property] public bool SpawnTestItems { get; set; } = false;
[Property] public GameObject ItemPrefab { get; set; }
protected override void OnStart()
{
if (!Network.IsOwner || !SpawnTestItems) return;
GameTask.DelaySeconds(2f).ContinueWith(_ => SpawnItems());
}
private void SpawnItems()
{
if (ItemPrefab == null)
{
// Log.Warning("ItemPrefab не установлен!");
return;
}
// Log.Info("Спавним тестовые предметы...");
var player = Dedugan.Local;
if (player?.Transform == null) return;
var playerPos = player.WorldPosition;
var spawnRadius = 5f;
var itemCount = 5;
for (int i = 0; i < itemCount; i++)
{
var angle = (float)i / itemCount * 360f * MathF.PI / 180f;
var offset = new Vector3(
MathF.Cos(angle) * spawnRadius,
0,
MathF.Sin(angle) * spawnRadius
);
var position = playerPos + offset;
var item = ItemPrefab.Clone(position);
// Добавляем случайный поворот
item.WorldRotation = Rotation.Random;
// Спавним в сети
item.NetworkSpawn();
// Log.Info($"Предмет создан в позиции {position}");
}
}
}

172
Code/Inventory/README.md Normal file
View File

@ -0,0 +1,172 @@
# Система инвентаря Sasalka
## Обзор
Новая система инвентаря была полностью переработана для улучшения производительности, сетевой синхронизации и пользовательского опыта.
## Основные изменения
### 1. Inventar (Основной класс инвентаря)
- Теперь наследуется от `Component` для лучшей интеграции с S&box
- Добавлена сетевая синхронизация с атрибутами `[Sync]`
- Улучшена валидация при добавлении предметов
- Добавлена поддержка стакания предметов
- Новые события: `OnItemAdded`, `OnItemRemoved`
### 2. InventoryItem
- Добавлена сетевая синхронизация с `[Sync]`
- Улучшена валидация количества предметов
- Новые методы: `CanStackWith`, `CanAddCount`, `TryAddCount`, `TryRemoveCount`, `Clone`
### 3. Определения предметов
#### BaseItemDefinition
- Добавлены категории предметов (`ItemCategory`)
- Добавлена система редкости (`ItemRarity`)
- Добавлен вес предметов
- Улучшен редактор с группировкой свойств
#### WeaponItemDefinition
- Добавлены свойства оружия (урон, скорострельность, дальность)
- Улучшена структура `WeaponDefinition`
#### ClothingItemDefinition
- Добавлены свойства брони и видимости
- Улучшена интеграция с системой одежды
#### AmmoItemDefinition (новый)
- Специализированное определение для патронов
- Система совместимости с оружием
- Свойства патронов (урон, пробивная способность)
### 4. UI инвентаря
- Добавлена фильтрация по категориям
- Улучшен дизайн с поддержкой редкости
- Отображение экипированных предметов
- Адаптивный интерфейс
## Исправления API
### Сетевая синхронизация
- Заменены атрибуты `[Net]` на `[Sync]` для совместимости с S&box
- Все изменения инвентаря автоматически синхронизируются между клиентами
### Методы коллекций
- Исправлен метод `ContainsValue()` на `Values.Contains()` для `Dictionary`
- Обновлены методы работы с коллекциями для совместимости
### Using директивы
- Добавлены необходимые `using` директивы в UI файлы
- Исправлены namespace для компонентов
### Наследование
- Исправлено наследование `PickupItem` от `Sandbox.UI.InteractionButton`
- Обновлены интерфейсы и базовые классы
## Использование
### Создание предметов
```csharp
// Создание предмета оружия
var weaponItem = new InventoryItem
{
Definition = ResourceLibrary.Get<WeaponItemDefinition>("Items/pistol_test.weapon")
};
inventory.AddItem(weaponItem);
// Создание патронов
var ammoItem = new InventoryItem
{
Definition = ResourceLibrary.Get<AmmoItemDefinition>("Items/pistol_ammo.ammo")
};
ammoItem.Count = 30;
inventory.AddItem(ammoItem);
```
### Экипировка предметов
```csharp
// Экипировка предмета
inventory.EquipItem(item);
// Проверка экипированного предмета
var equippedWeapon = inventory.GetEquippedItem(Inventar.InventorySlot.RightHand);
```
### Валидация
```csharp
// Проверка возможности добавления
if (inventory.CanAddItem(item))
{
inventory.AddItem(item);
}
// Проверка стакания
if (item1.CanStackWith(item2))
{
// Предметы можно объединить
}
```
## Создание определений предметов
### Оружие
1. Создайте ресурс типа "Weapon Item Definition"
2. Укажите слот экипировки
3. Настройте параметры оружия (урон, дальность и т.д.)
4. Укажите тип патронов
### Патроны
1. Создайте ресурс типа "Ammo Item Definition"
2. Укажите тип патронов
3. Настройте совместимость с оружием
### Одежда
1. Создайте ресурс типа "Clothing Item Definition"
2. Укажите слот экипировки
3. Добавьте URL одежды из Workshop
## Сетевая синхронизация
Все изменения инвентаря автоматически синхронизируются между клиентами благодаря атрибутам `[Sync]` на свойствах `Items` и `EquippedItems`.
## Производительность
- Улучшена производительность UI с оптимизированным `BuildHash()`
- Добавлено кэширование для `GetUsables()`
- Оптимизирована работа с коллекциями
## Миграция
Для миграции существующих предметов:
1. Обновите определения предметов с новыми свойствами
2. Измените код создания предметов на новый API
3. Обновите UI для использования новых компонентов
## Тестирование
### InventoryTest
Используйте компонент `InventoryTest` для добавления тестовых предметов:
```csharp
// Добавьте компонент к игроку и установите AddTestItems = true
var testComponent = player.GameObject.Components.GetOrCreate<InventoryTest>();
testComponent.AddTestItems = true;
```
### InventoryCompilationTest
Используйте компонент `InventoryCompilationTest` для проверки компиляции:
```csharp
// Добавьте компонент к игроку и установите RunTest = true
var testComponent = player.GameObject.Components.GetOrCreate<InventoryCompilationTest>();
testComponent.RunTest = true;
```
## Известные проблемы
- Убедитесь, что все необходимые ресурсы (префабы, текстуры) существуют
- Проверьте, что определения предметов правильно настроены в редакторе
- При использовании в сети убедитесь, что все компоненты имеют правильные атрибуты `[Sync]`

View File

@ -4,11 +4,31 @@
<root class="inventory @( Inventar.IsInventoryOpen ? "" : "hidden" )">
<div class="inventory-panel">
@if ( PlayerInventory.Items.Count > 0 )
@if ( PlayerInventory != null )
{
@foreach ( var item in PlayerInventory.Items )
<div class="inventory-header">
<div class="inventory-title">Инвентарь</div>
@if ( !PlayerInventory.UnlimitedSlots )
{
<div class="inventory-slots">
@PlayerInventory.GetUsedSlots() / @PlayerInventory.MaxInventorySlots слотов
<div class="inventory-usage-bar">
<div class="inventory-usage-fill" style="width: @(PlayerInventory.GetInventoryUsagePercentage())%"></div>
</div>
</div>
}
</div>
@if ( PlayerInventory.Items.Count > 0 )
{
<Sasalka.Ui.InventoryItem Item="@item" OnItemClick="@( UseItem )" OnItemRightClick="@( DropItem )"/>
@foreach ( var item in PlayerInventory.Items )
{
<Sasalka.Ui.InventoryItem Item="@item" OnItemClick="@( UseItem )" OnItemRightClick="@( DropItem )"/>
}
}
else
{
<div class="inventory-empty">Инвентарь пуст</div>
}
}
</div>

View File

@ -24,6 +24,59 @@ Inventory {
//background-color: rgba(255, 0, 0, 0.1);
}
.inventory-header {
display: flex;
flex-direction: column;
gap: 8px;
padding-bottom: 16px;
border-bottom: 2px solid #2a3d54;
margin-bottom: 16px;
}
.inventory-title {
font-size: 24px;
font-weight: bold;
color: #ffffff;
text-align: center;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}
.inventory-slots {
display: flex;
flex-direction: column;
gap: 6px;
align-items: center;
}
.inventory-slots {
font-size: 14px;
color: #a0b4c8;
text-align: center;
}
.inventory-usage-bar {
width: 100%;
height: 8px;
background: rgba(42, 61, 84, 0.5);
border-radius: 4px;
overflow: hidden;
border: 1px solid #2a3d54;
}
.inventory-usage-fill {
height: 100%;
background: linear-gradient(90deg, #4CAF50 0%, #8BC34A 100%);
transition: width 0.3s ease;
border-radius: 3px;
}
.inventory-empty {
text-align: center;
color: #a0b4c8;
font-style: italic;
padding: 40px 20px;
font-size: 16px;
}
.hidden {
opacity: 0;

View File

@ -12,25 +12,50 @@
: !string.IsNullOrWhiteSpace( definition?.ImageUrl )
? definition.ImageUrl
: null;
var rarityColor = definition?.GetRarityColor() ?? "#ffffff";
var categoryIcon = definition?.GetCategoryIcon() ?? "📦";
}
<root class="inventory-item @( Item.Equipped ? "equipped" : "" )" @onclick="@(() => OnItemClick?.Invoke( Item ))" @onrightclick=@( () => OnItemRightClick?.Invoke( Item ) )>
@if ( slot is not null )
{
<div class="inventory-item__slot">@slot</div>
}
<root class="inventory-item @( Item.Equipped ? "equipped" : "" )"
@onclick="@(() => OnItemClick?.Invoke( Item ))"
@onrightclick="@( () => OnItemRightClick?.Invoke( Item ) )"
style="border-left-color: @rarityColor;">
@if ( !string.IsNullOrEmpty( imageUrl ) )
{
<img src="@imageUrl" alt="@name"/>
}
<div class="inventory-item__icon">
@if ( !string.IsNullOrEmpty( imageUrl ) )
{
<img src="@imageUrl" alt="@name"/>
}
else
{
<span class="category-icon">@categoryIcon</span>
}
</div>
<div class="inventory-item__name">@name</div>
<div class="inventory-item__info">
<div class="inventory-item__name" style="color: @rarityColor;">@name</div>
@if ( definition?.Category != ItemCategory.Misc )
{
<div class="inventory-item__category">@definition?.Category</div>
}
</div>
@if ( definition?.MaxCount > 1 )
{
<div class="inventory-item__count">@Item?.Count / @definition.MaxCount</div>
}
<div class="inventory-item__meta">
@if ( slot is not null )
{
<div class="inventory-item__slot">@GetSlotName( slot.Value )</div>
}
@if ( definition?.MaxCount > 1 )
{
<div class="inventory-item__count">@Item?.Count / @definition.MaxCount</div>
}
@* @if ( Item?.Equipped == true ) *@
@* { *@
@* <div class="inventory-item__equipped">✓</div> *@
@* } *@
</div>
</root>
@code {
@ -38,11 +63,28 @@
public Action<Sasalka.InventoryItem> OnItemClick { get; set; }
public Action<Sasalka.InventoryItem> OnItemRightClick { get; set; }
string GetSlotName( Inventar.InventorySlot slot )
{
return slot switch
{
Inventar.InventorySlot.LeftHand => "Л.Рука",
Inventar.InventorySlot.RightHand => "П.Рука",
Inventar.InventorySlot.Head => "Голова",
Inventar.InventorySlot.Body => "Тело",
Inventar.InventorySlot.Hands => "Руки",
Inventar.InventorySlot.Bottom => "Ноги",
Inventar.InventorySlot.Feet => "Обувь",
_ => "Неизвестно"
};
}
protected override int BuildHash()
{
base.BuildHash();
var hash = new HashCode();
hash.Add( Item?.Count );
hash.Add( Item?.Equipped );
hash.Add( Item?.Definition?.Name );
return hash.ToHashCode();
}

View File

@ -1,49 +1,154 @@
InventoryItem {
.inventory-item {
flex-shrink: 0;
width: 100%;
background: #2a3d53;
background: linear-gradient(135deg, #2a3d53 0%, #1f2d3f 100%);
display: flex;
gap: 24px;
gap: 16px;
align-items: center;
justify-content: flex-start;
border: 1px solid #666;
border-left: 4px solid #ffffff;
border-radius: 12px;
padding: 12px 24px;
padding: 16px;
cursor: pointer;
transition: background 0.2s ease;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
//box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
&:hover {
background: #444;
background: linear-gradient(135deg, #3a4d63 0%, #2f3d4f 100%);
transform: translateY(-2px);
//box-shadow: 0 6px 16px rgba(0, 0, 0, 0.3);
border-color: #888;
}
img {
width: 32px;
height: 32px;
object-fit: contain;
}
.inventory-item__name {
font-size: 20px;
font-weight: 500;
}
.inventory-item__count {
margin-left: auto;
font-size: 14px;
color: #ccc;
}
.inventory-item__slot {
font-size: 12px;
color: #8fc98f;
background: rgba(0, 0, 0, 0.3);
padding: 2px 6px;
border-radius: 4px;
&:active {
transform: translateY(0);
//box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
&.equipped {
border: 2px solid #4caf50;
background: #2e3e2e;
background: linear-gradient(135deg, #2e3e2e 0%, #1f2d1f 100%);
//box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
&:hover {
background: linear-gradient(135deg, #3e4e3e 0%, #2f3d2f 100%);
//box-shadow: 0 6px 16px rgba(76, 175, 80, 0.4);
}
.inventory-item__name {
color: #4caf50;
}
}
.inventory-item__icon {
flex-shrink: 0;
width: 48px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a2a3a 0%, #0f1a2a 100%);
border-radius: 8px;
border: 1px solid #3a4a5a;
//box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
img {
width: 32px;
height: 32px;
object-fit: contain;
//filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}
.category-icon {
font-size: 24px;
//filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5));
}
}
.inventory-item__info {
flex: 1;
min-width: 0;
gap: 20px;
.inventory-item__name {
font-size: 16px;
font-weight: 600;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
//text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.inventory-item__description {
font-size: 12px;
color: #cccccc;
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
opacity: 0.8;
}
.inventory-item__category {
font-size: 10px;
color: #8fc98f;
background: linear-gradient(135deg, rgba(143, 201, 143, 0.2) 0%, rgba(143, 201, 143, 0.1) 100%);
padding: 2px 8px;
border-radius: 4px;
display: inline-block;
border: 1px solid rgba(143, 201, 143, 0.3);
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 500;
}
}
.inventory-item__meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
.inventory-item__slot {
font-size: 10px;
color: #8fc98f;
background: linear-gradient(135deg, rgba(143, 201, 143, 0.3) 0%, rgba(143, 201, 143, 0.2) 100%);
padding: 2px 8px;
border-radius: 4px;
font-weight: 500;
border: 1px solid rgba(143, 201, 143, 0.4);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.inventory-item__count {
font-size: 12px;
color: #cccccc;
font-weight: 500;
background: rgba(0, 0, 0, 0.2);
padding: 2px 6px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
}
.inventory-item__equipped {
font-size: 14px;
color: #4caf50;
font-weight: bold;
//text-shadow: 0 1px 2px rgba(76, 175, 80, 0.5);
animation: pulse 2s infinite;
}
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}

View File

@ -2,35 +2,83 @@
public abstract class AmmoUseableBase : UseableBase
{
private WeaponItemDefinition _cachedWeaponDef;
private InventoryItem _cachedAmmoItem;
protected InventoryItem AmmoItem => FindAmmoItem();
private InventoryItem FindAmmoItem()
{
//По типу патрон поиск + енум типа патрон
return Dedugan.Local.Inventory.Items.FirstOrDefault( i => i.Definition.Name == "Pistol Ammo" );
var player = Dedugan.Local;
if (player?.Inventory == null)
{
return null;
}
// Кэшируем WeaponDefinition для избежания повторных вызовов
if (_cachedWeaponDef == null)
{
_cachedWeaponDef = GetWeaponDefinition();
if (_cachedWeaponDef == null)
{
return null;
}
}
// Проверяем кэшированный результат
if (_cachedAmmoItem != null && _cachedAmmoItem.Count > 0)
{
return _cachedAmmoItem;
}
// Ищем патроны
foreach (var item in player.Inventory.Items)
{
if (item.Definition is AmmoItemDefinition ammoDef && ammoDef.IsCompatibleWith(_cachedWeaponDef.AmmoType))
{
_cachedAmmoItem = item;
return item;
}
}
_cachedAmmoItem = null;
return null;
}
protected virtual WeaponItemDefinition GetWeaponDefinition()
{
// Переопределите в наследниках для возврата определения оружия
return null;
}
public override bool CanUse()
{
var ammo = AmmoItem;
return base.CanUse() && ammo != null && ammo.Count > 0;
var baseCanUse = base.CanUse();
var hasAmmo = ammo != null && ammo.Count > 0;
return baseCanUse && hasAmmo;
}
public override void Use()
{
if ( !CanUse() )
if (!CanUse())
return;
OnUse();
var ammo = AmmoItem;
if ( ammo != null )
if (ammo != null)
{
ammo.Count--;
if ( ammo.Count <= 0 )
// Уменьшаем количество патронов
if (ammo.TryRemoveCount(1))
{
Dedugan.Local.Inventory.RemoveItem( ammo );
// Если патроны закончились, удаляем предмет из инвентаря
if (ammo.Count <= 0)
{
Dedugan.Local.Inventory.RemoveItem(ammo);
_cachedAmmoItem = null; // Очищаем кэш
}
}
}
}

View File

@ -1,24 +1,54 @@
using Sandbox.Gravity;
using Sandbox.UI;
using Sasalka;
using System;
using System.Linq;
namespace Sandbox.UI;
namespace Sasalka;
[Icon( "skip_next" )]
public sealed class PickupItem : InteractionButton
{
[Property] public override string Label { get; set; } = "E";
protected override void OnStart()
{
base.OnStart();
// Устанавливаем правильную метку для предмета
if ( GameObject.Components.TryGet<InventoryItem>( out var inventoryItem ) )
{
Label = inventoryItem.Definition?.Name ?? "Подобрать";
}
}
public override bool Press( IPressable.Event e )
{
base.Press( e );
if ( e.Source.Components.TryGet<Dedugan>( out var dedugan ) )
{
dedugan.Inventory.AddItem( Components.Get<InventoryItem>() );
RpcDestroy();
var inventoryItem = Components.Get<InventoryItem>();
if ( inventoryItem != null && dedugan.Inventory != null )
{
// Пытаемся добавить предмет в инвентарь, остаток остаётся на земле
int left = dedugan.Inventory.AddItem( inventoryItem );
if ( left <= 0 )
{
RpcDestroy();
return true;
}
else
{
inventoryItem.Count = left;
// Оставляем предмет с новым количеством на земле
return true;
}
}
}
return true;
return false;
}
[Rpc.Broadcast]

View File

@ -4,8 +4,13 @@ public static class UseSystem
{
public static bool TryUse( IUseContext context )
{
foreach ( var useable in context.GetUsables() )
// Получаем все доступные предметы
var usables = context.GetUsables();
// Проверяем каждый предмет на возможность использования
foreach ( var useable in usables )
{
// Раннее прерывание если предмет может быть использован
if ( useable.CanUse() )
{
useable.Use();

View File

@ -37,7 +37,8 @@ public abstract class UseableBase : Component, IUseable
public virtual void OnEquipped()
{
Log.Info( $"OnEquip {this}" );
Equipped = true;
// Log.Info( $"OnEquip {this}" );
}
public virtual bool CanUse()

View File

@ -4,7 +4,7 @@ using Sasalka;
public sealed partial class Dedugan : Component
{
[Property, InlineEditor] public Inventar Inventory { get; private set; } = new();
[Property] public Inventar Inventory { get; private set; }
private Dictionary<Inventar.InventorySlot, (GameObject obj, IUseable useable)> _useableCache = new();
[Sync] private bool InAds { get; set; } = false;
@ -12,65 +12,72 @@ public sealed partial class Dedugan : Component
void InventoryStart()
{
if ( !Network.IsOwner ) return;
if (!Network.IsOwner) return;
_resolver = new AttachmentSlotResolver( Renderer.GetAttachmentObject );
// Создаем инвентарь как компонент
Inventory = GameObject.Components.GetOrCreate<Inventar>();
_resolver = new AttachmentSlotResolver(Renderer.GetAttachmentObject);
// Inventory.AddItem( new InventoryItem
// {
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/Cloth/cloth_pijama.clitem" )
// } );
//
// Inventory.AddItem( new InventoryItem
// {
// Definition = ResourceLibrary.Get<ClothingItemDefinition>( "Items/Cloth/cloth_pijama_bottom.clitem" )
// } );
//
// Inventory.AddItem( new InventoryItem
// {
// Definition = ResourceLibrary.Get<WeaponItemDefinition>( "Items/pistol_test.weapon" )
// } );
//
// var ammo = new InventoryItem
// {
// Definition = ResourceLibrary.Get<BaseItemDefinition>( "Items/pistol_ammo.inv" )
// };
// ammo.Count = 30;
// ammo.MaxCount = 130;
//
// Inventory.AddItem( ammo );
// Добавляем тестовые предметы (раскомментируйте для тестирования)
var clothingItem = new InventoryItem
{
Definition = ResourceLibrary.Get<ClothingItemDefinition>("Items/Cloth/cloth_pijama.clitem")
};
Inventory.AddItem(clothingItem);
var weaponItem = new InventoryItem
{
Definition = ResourceLibrary.Get<WeaponItemDefinition>("Items/pistol_test.weapon")
};
Inventory.AddItem(weaponItem);
var ammoItem = new InventoryItem
{
Definition = ResourceLibrary.Get<AmmoItemDefinition>("Items/pistol_ammo.inv")
};
ammoItem.Count = 30;
Inventory.AddItem(ammoItem);
Inventory.OnEquipped += OnItemEquipped;
Inventory.OnUnEquipped += OnItemUnEquipped;
Inventory.OnItemAdded += OnItemAdded;
Inventory.OnItemRemoved += OnItemRemoved;
}
private void OnItemEquipped( InventoryItem item )
private void OnItemEquipped(InventoryItem item)
{
// Если это оружие
if ( item.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid() )
// Очищаем кэши при экипировке предмета
_useableCache.Clear();
_resolver?.ClearCache();
if (item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid())
{
var go = weaponDef.Prefab.Clone();
AnimationHelper.HoldType = weaponDef.HoldType;
switch ( weaponDef.Slot )
switch (weaponDef.Slot)
{
case Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand:
case Inventar.InventorySlot.RightHand:
go.Parent = Renderer.GetAttachmentObject( "hold_R" );
go.Parent = Renderer.GetAttachmentObject("hold_R");
break;
case Inventar.InventorySlot.LeftHand:
go.Parent = Renderer.GetAttachmentObject( "hold_L" );
go.Parent = Renderer.GetAttachmentObject("hold_L");
break;
default:
go.Parent = Renderer.GetAttachmentObject( "forward_reference_modelspace" );
go.Parent = Renderer.GetAttachmentObject("forward_reference_modelspace");
break;
}
go.LocalPosition = weaponDef.WeaponDefinition.Position;
go.LocalRotation = weaponDef.WeaponDefinition.Rotation;
go.LocalScale = weaponDef.WeaponDefinition.Scale;
go.Components.Get<UseableBase>().Equipped = true;
if (go.Components.TryGet<UseableBase>(out var useable))
{
useable.Equipped = true;
}
go.NetworkSpawn();
@ -83,94 +90,108 @@ public sealed partial class Dedugan : Component
};
AnimationHelper.Handedness = hand;
RpcSetHoldAnimation( weaponDef.HoldType, hand );
RpcSetHoldAnimation(weaponDef.HoldType, hand);
InAds = true;
}
// Если это одежда
else if ( item.Definition is ClothingItemDefinition clothingDef )
else if (item?.Definition is ClothingItemDefinition clothingDef)
{
WearWorkshop( new List<string>() { clothingDef.ClothUrl } );
WearWorkshop(new List<string>() { clothingDef.ClothUrl });
}
}
private void OnItemUnEquipped( InventoryItem item )
private void OnItemUnEquipped(InventoryItem item)
{
if ( item.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid() )
// Очищаем кэши при снятии предмета
_useableCache.Clear();
_resolver?.ClearCache();
if (item?.Definition is WeaponItemDefinition weaponDef && weaponDef.Prefab.IsValid())
{
switch ( weaponDef.Slot )
switch (weaponDef.Slot)
{
case Inventar.InventorySlot.LeftHand | Inventar.InventorySlot.RightHand:
case Inventar.InventorySlot.RightHand:
case Inventar.InventorySlot.LeftHand:
var attachmentName = !weaponDef.Slot.HasFlag( Inventar.InventorySlot.RightHand )
var attachmentName = !weaponDef.Slot.HasFlag(Inventar.InventorySlot.RightHand)
? "hold_L"
: "hold_R";
Renderer.GetAttachmentObject( attachmentName ).Children.ForEach( child => child.Destroy() );
RpcSetHoldAnimation( CitizenAnimationHelper.HoldTypes.None, CitizenAnimationHelper.Hand.Both );
Renderer.GetAttachmentObject(attachmentName).Children.ForEach(child => child.Destroy());
RpcSetHoldAnimation(CitizenAnimationHelper.HoldTypes.None, CitizenAnimationHelper.Hand.Both);
break;
default:
Renderer.GetAttachmentObject( "forward_reference_modelspace" ).Children
.ForEach( child => child.Destroy() );
Renderer.GetAttachmentObject("forward_reference_modelspace").Children
.ForEach(child => child.Destroy());
break;
}
item.Destroy();
InAds = false;
}
else if ( item.Definition is ClothingItemDefinition clothingDef )
else if (item?.Definition is ClothingItemDefinition clothingDef)
{
StripByName( clothingDef.Description );
StripByName(clothingDef.Description);
}
}
private void OnItemAdded(InventoryItem item)
{
// Очищаем кэши при добавлении предмета
_useableCache.Clear();
_resolver?.ClearCache();
}
private void OnItemRemoved(InventoryItem item)
{
// Очищаем кэши при удалении предмета
_useableCache.Clear();
_resolver?.ClearCache();
}
[Rpc.Broadcast]
public void RpcSetHoldAnimation( CitizenAnimationHelper.HoldTypes HoldType, CitizenAnimationHelper.Hand hand )
public void RpcSetHoldAnimation(CitizenAnimationHelper.HoldTypes HoldType, CitizenAnimationHelper.Hand hand)
{
AnimationHelper.HoldType = HoldType;
AnimationHelper.Handedness = hand;
}
// AnimationHelper.HoldType = CitizenAnimationHelper.HoldTypes.None;
// AnimationHelper.Handedness = CitizenAnimationHelper.Hand.Both;
void InventoryUpdate()
{
if ( !Network.IsOwner ) return;
if (!Network.IsOwner) return;
// InAds = Input.Down( "Attack2" );
if ( Input.Pressed( "Attack1" ) )
if (Input.Pressed("Attack1"))
{
if ( UseSystem.TryUse( this ) )
if (UseSystem.TryUse(this))
{
Attack();
}
}
}
public IEnumerable<IUseable>
GetUsables() //Допустим, у джетпака слот Body. Просто дописываешь в GetUsables() Inventar.InventorySlot.Body:
public IEnumerable<IUseable> GetUsables()
{
foreach ( var slot in new[] { Inventar.InventorySlot.LeftHand, Inventar.InventorySlot.RightHand } )
// Кэшируем слоты для избежания повторного создания массива
var slots = new[] { Inventar.InventorySlot.LeftHand, Inventar.InventorySlot.RightHand };
foreach (var slot in slots)
{
if ( !Inventory.EquippedItems.TryGetValue( slot, out var item ) )
continue;
var holder = _resolver.GetSlotObject( slot );
var heldObject = holder?.Children.FirstOrDefault();
if ( heldObject == null )
continue;
if ( _useableCache.TryGetValue( slot, out var cached ) && cached.obj == heldObject )
if (!Inventory.EquippedItems.TryGetValue(slot, out var item))
{
if ( cached.useable != null )
continue;
}
var holder = _resolver.GetSlotObject(slot);
if (holder == null) continue;
var heldObject = holder.Children.FirstOrDefault();
if (heldObject == null)
{
continue;
}
// Проверяем кэш
if (_useableCache.TryGetValue(slot, out var cached) && cached.obj == heldObject)
{
if (cached.useable != null)
yield return cached.useable;
}
else
@ -178,7 +199,7 @@ public sealed partial class Dedugan : Component
var useable = heldObject.Components.Get<IUseable>();
_useableCache[slot] = (heldObject, useable);
if ( useable != null )
if (useable != null)
yield return useable;
}
}
@ -187,7 +208,7 @@ public sealed partial class Dedugan : Component
[Rpc.Broadcast]
void Attack()
{
Renderer.Set( "b_attack", true );
Renderer.Set("b_attack", true);
}
}

View File

@ -10,6 +10,7 @@ public sealed class Weapon : AmmoUseableBase
[Property] public GameObject MuzzleLight { get; private set; }
[Property] public GameObject particlePrefab { get; set; }
[Property] public GameObject bloodParticle { get; set; }
[Property] public WeaponItemDefinition WeaponDefinition { get; set; }
private SoundPointComponent _sound;
private Rigidbody _rigidbody;
@ -17,7 +18,7 @@ public sealed class Weapon : AmmoUseableBase
protected override void OnStart()
{
base.OnStart();
_sound = GameObject.GetComponent<SoundPointComponent>( true );
_sound = GameObject.GetComponent<SoundPointComponent>(true);
}
public override void OnEquipped()
@ -27,66 +28,73 @@ public sealed class Weapon : AmmoUseableBase
GameObject.Components.Get<PickupItem>().Enabled = false;
}
protected override WeaponItemDefinition GetWeaponDefinition()
{
return WeaponDefinition;
}
public void Attack()
{
AttackEffects();
Vector3 startPos = Scene.Camera.WorldPosition;
Vector3 dir = Scene.Camera.WorldRotation.Forward;
float maxDistance = 1000f;
float maxDistance = WeaponDefinition?.Range ?? 1000f;
var tr = Scene.Trace
.Ray( startPos, startPos + dir * maxDistance )
.IgnoreGameObjectHierarchy( Dedugan.Local.GameObject )
.WithoutTags( "weapon" )
.Ray(startPos, startPos + dir * maxDistance)
.IgnoreGameObjectHierarchy(Dedugan.Local.GameObject)
.WithoutTags("weapon")
.UseHitboxes()
.Run();
if ( tr.Hit && tr.Hitbox != null )
if (tr.Hit)
{
var components = tr.GameObject.Components;
var boneIndex = tr.Hitbox.Bone.Index;
// Log.Info($"{tr.GameObject.Name} attacked");
var dedugan = components.Get<Dedugan>();
var enemy = components.Get<Enemy>();
if ( dedugan.IsValid() || enemy.IsValid() )
if (tr.Hitbox != null)
{
CreateHitEffects( tr.EndPosition, tr.Normal, true );
}
var boneIndex = tr.Hitbox.Bone.Index;
var components = tr.GameObject.Components;
// Кэшируем компоненты для избежания повторных поисков
var dedugan = components.Get<Dedugan>();
var enemy = components.Get<Enemy>();
var hasTarget = dedugan.IsValid() || enemy.IsValid();
if ( dedugan.IsValid() )
{
dedugan.ReportHit( dir, boneIndex );
}
if (hasTarget)
{
CreateHitEffects(tr.EndPosition, tr.Normal, true);
}
if ( enemy.IsValid() )
{
enemy.ReportHit( dir, boneIndex );
Log.Info( boneIndex );
if (dedugan.IsValid())
{
dedugan.ReportHit(dir, boneIndex);
}
if (enemy.IsValid())
{
enemy.ReportHit(dir, boneIndex);
}
}
else
{
CreateHitEffects(tr.EndPosition, tr.Normal);
}
}
else if ( tr.Hitbox == null )
{
CreateHitEffects( tr.EndPosition, tr.Normal );
}
}
[Rpc.Broadcast]
private void CreateHitEffects( Vector3 position, Vector3 normal, bool blood = false )
private void CreateHitEffects(Vector3 position, Vector3 normal, bool blood = false)
{
var rot = Rotation.LookAt( normal );
var rot = Rotation.LookAt(normal);
DestroyAsync(
blood
? bloodParticle.Clone( position, rot )
: particlePrefab.Clone( position, rot ), 0.5f );
? bloodParticle.Clone(position, rot)
: particlePrefab.Clone(position, rot), 0.5f);
}
async void DestroyAsync( GameObject go, float delay )
async void DestroyAsync(GameObject go, float delay)
{
await GameTask.DelaySeconds( delay );
await GameTask.DelaySeconds(delay);
go.Destroy();
}
@ -95,12 +103,12 @@ public sealed class Weapon : AmmoUseableBase
{
_sound?.StartSound();
MuzzleLight.Enabled = true;
GunRenderer.Set( "Fire", true );
GunRenderer.Set("Fire", true);
GameTask.DelaySeconds( 0.05f ).ContinueWith( ( _ ) =>
GameTask.DelaySeconds(0.05f).ContinueWith((_) =>
{
MuzzleLight.Enabled = false;
} );
});
}
protected override void OnUse()