diff --git a/Content.Server/_CP14/Workbench/CP14WorkbenchComponent.cs b/Content.Server/_CP14/Workbench/CP14WorkbenchComponent.cs new file mode 100644 index 0000000000..87c2669cb8 --- /dev/null +++ b/Content.Server/_CP14/Workbench/CP14WorkbenchComponent.cs @@ -0,0 +1,15 @@ +using Content.Shared._CP14.Workbench.Prototypes; +using Robust.Shared.Prototypes; + +namespace Content.Server._CP14.Workbench; + +[RegisterComponent] +[Access(typeof(CP14WorkbenchSystem))] +public sealed partial class CP14WorkbenchComponent : Component +{ + [DataField] + public float CraftSpeed = 1f; + + [DataField] + public List> Recipes = new(); +} diff --git a/Content.Server/_CP14/Workbench/CP14WorkbenchSystem.cs b/Content.Server/_CP14/Workbench/CP14WorkbenchSystem.cs new file mode 100644 index 0000000000..21cfc98225 --- /dev/null +++ b/Content.Server/_CP14/Workbench/CP14WorkbenchSystem.cs @@ -0,0 +1,229 @@ +using Content.Server.Popups; +using Content.Shared._CP14.Workbench; +using Content.Shared._CP14.Workbench.Prototypes; +using Content.Shared.DoAfter; +using Content.Shared.Stacks; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Prototypes; + +namespace Content.Server._CP14.Workbench; + +public sealed class CP14WorkbenchSystem : SharedCP14WorkbenchSystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedStackSystem _stack = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; + + private EntityQuery _metaQuery; + private EntityQuery _stackQuery; + + private const float WorkbenchRadius = 0.5f; + + public override void Initialize() + { + base.Initialize(); + + _metaQuery = GetEntityQuery(); + _stackQuery = GetEntityQuery(); + + SubscribeLocalEvent>(OnInteractionVerb); + SubscribeLocalEvent(OnCraftFinished); + } + + private void OnInteractionVerb(Entity ent, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands is null) + return; + + var placedEntities = _lookup.GetEntitiesInRange(Transform(ent).Coordinates, WorkbenchRadius); + + var user = args.User; + foreach (var craftProto in ent.Comp.Recipes) + { + if (!_proto.TryIndex(craftProto, out var craft)) + continue; + + if (!_proto.TryIndex(craft.Result, out var result)) + continue; + + args.Verbs.Add(new() + { + Act = () => + { + StartCraft(ent, user, craft); + }, + Text = result.Name, + Message = GetCraftRecipeMessage(result.Description, craft), + Category = VerbCategory.CP14Craft, + Disabled = !CanCraftRecipe(craft, placedEntities), + }); + } + } + + private void OnCraftFinished(Entity ent, ref CP14CraftDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + if (!_proto.TryIndex(args.Recipe, out var recipe)) + return; + + var placedEntities = _lookup.GetEntitiesInRange(Transform(ent).Coordinates, WorkbenchRadius); + + if (!CanCraftRecipe(recipe, placedEntities)) + { + _popup.PopupEntity(Loc.GetString("cp14-workbench-no-resource"), ent, args.User); + return; + } + + foreach (var requiredIngredient in recipe.Entities) + { + var requiredCount = requiredIngredient.Value; + foreach (var placedEntity in placedEntities) + { + var placedProto = MetaData(placedEntity).EntityPrototype?.ID; + if (placedProto != null && placedProto == requiredIngredient.Key && requiredCount > 0) + { + requiredCount--; + QueueDel(placedEntity); + } + } + } + + foreach (var requiredStack in recipe.Stacks) + { + var requiredCount = requiredStack.Value; + foreach (var placedEntity in placedEntities) + { + if (!_stackQuery.TryGetComponent(placedEntity, out var stack)) + continue; + + if (stack.StackTypeId != requiredStack.Key) + continue; + + var count = (int)MathF.Min(requiredCount, stack.Count); + _stack.SetCount(placedEntity, stack.Count - count, stack); + + requiredCount -= count; + } + } + + Spawn(_proto.Index(args.Recipe).Result, Transform(ent).Coordinates); + + args.Handled = true; + } + + private void StartCraft(Entity workbench, EntityUid user, CP14WorkbenchRecipePrototype recipe) + { + var craftDoAfter = new CP14CraftDoAfterEvent + { + Recipe = recipe.ID, + }; + + var doAfterArgs = new DoAfterArgs(EntityManager, + user, + recipe.CraftTime * workbench.Comp.CraftSpeed, + craftDoAfter, + workbench, + workbench) + { + BreakOnMove = true, + BreakOnDamage = true, + NeedHand = true, + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + _audio.PlayPvs(recipe.CraftSound, workbench); + } + + private List GetPossibleCrafts(Entity workbench, HashSet ingrediEnts) + { + List result = new(); + + if (ingrediEnts.Count == 0) + return result; + + foreach (var recipeProto in workbench.Comp.Recipes) + { + var recipe = _proto.Index(recipeProto); + + if (CanCraftRecipe(recipe, ingrediEnts)) + { + result.Add(recipe); + } + } + + return result; + } + + private bool CanCraftRecipe(CP14WorkbenchRecipePrototype recipe, HashSet entities) + { + var indexedIngredients = IndexIngredients(entities); + foreach (var requiredIngredient in recipe.Entities) + { + if (!indexedIngredients.TryGetValue(requiredIngredient.Key, out var availableQuantity) || + availableQuantity < requiredIngredient.Value) + return false; + } + + foreach (var (key, value) in recipe.Stacks) + { + var count = 0; + foreach (var ent in entities) + { + if (_stackQuery.TryGetComponent(ent, out var stack)) + { + if (stack.StackTypeId != key) + continue; + + count += stack.Count; + } + } + + if (count < value) + return false; + } + return true; + } + + private string GetCraftRecipeMessage(string desc, CP14WorkbenchRecipePrototype recipe) + { + var result = desc + "\n \n" + Loc.GetString("cp14-workbench-recipe-list")+ "\n"; + + foreach (var pair in recipe.Entities) + { + var proto = _proto.Index(pair.Key); + result += $"{proto.Name} x{pair.Value}\n"; + } + + foreach (var pair in recipe.Stacks) + { + var proto = _proto.Index(pair.Key); + result += $"{proto.Name} x{pair.Value}\n"; + } + + return result; + } + + private Dictionary IndexIngredients(HashSet ingredients) + { + var indexedIngredients = new Dictionary(); + + foreach (var ingredient in ingredients) + { + var protoId = _metaQuery.GetComponent(ingredient).EntityPrototype?.ID; + if (protoId == null) + continue; + + if (indexedIngredients.ContainsKey(protoId)) + indexedIngredients[protoId]++; + else + indexedIngredients[protoId] = 1; + } + return indexedIngredients; + } +} diff --git a/Content.Shared/Verbs/VerbCategory.cs b/Content.Shared/Verbs/VerbCategory.cs index f8ef906e6a..308d6e91b5 100644 --- a/Content.Shared/Verbs/VerbCategory.cs +++ b/Content.Shared/Verbs/VerbCategory.cs @@ -88,5 +88,7 @@ namespace Content.Shared.Verbs public static readonly VerbCategory SelectType = new("verb-categories-select-type", null); public static readonly VerbCategory PowerLevel = new("verb-categories-power-level", null); + + public static readonly VerbCategory CP14Craft = new("cp14-verb-categories-craft", null); //CP14 } } diff --git a/Content.Shared/_CP14/Skills/Prototypes/CP14SkillPrototype.cs b/Content.Shared/_CP14/Skills/Prototypes/CP14SkillPrototype.cs index a5f5bb514e..a7cd16d168 100644 --- a/Content.Shared/_CP14/Skills/Prototypes/CP14SkillPrototype.cs +++ b/Content.Shared/_CP14/Skills/Prototypes/CP14SkillPrototype.cs @@ -3,7 +3,7 @@ using Robust.Shared.Prototypes; namespace Content.Shared._CP14.Skills.Prototypes; /// -/// A prototype of the lock category. Need a roundstart mapping to ensure that keys and locks will fit together despite randomization. +/// /// [Prototype("CP14Skill")] public sealed partial class CP14SkillPrototype : IPrototype diff --git a/Content.Shared/_CP14/Workbench/Prototypes/CP14WorkbenchRecipePrototype.cs b/Content.Shared/_CP14/Workbench/Prototypes/CP14WorkbenchRecipePrototype.cs new file mode 100644 index 0000000000..a05e08db12 --- /dev/null +++ b/Content.Shared/_CP14/Workbench/Prototypes/CP14WorkbenchRecipePrototype.cs @@ -0,0 +1,31 @@ +using Content.Shared.Stacks; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; + +namespace Content.Shared._CP14.Workbench.Prototypes; + +/// +/// +/// +[Prototype("CP14Recipe")] +public sealed partial class CP14WorkbenchRecipePrototype : IPrototype +{ + [ViewVariables] + [IdDataField] + public string ID { get; private set; } = default!; + + [DataField] + public TimeSpan CraftTime = TimeSpan.FromSeconds(1f); + + [DataField] + public SoundSpecifier CraftSound = new SoundCollectionSpecifier("CP14Hammering"); + + [DataField] + public Dictionary Entities = new(); + + [DataField] + public Dictionary, int> Stacks = new(); + + [DataField(required: true)] + public EntProtoId Result = default!; +} diff --git a/Content.Shared/_CP14/Workbench/SharedCP14WorkbenchSystem.cs b/Content.Shared/_CP14/Workbench/SharedCP14WorkbenchSystem.cs new file mode 100644 index 0000000000..6177f742f0 --- /dev/null +++ b/Content.Shared/_CP14/Workbench/SharedCP14WorkbenchSystem.cs @@ -0,0 +1,19 @@ +using Content.Shared._CP14.Workbench.Prototypes; +using Content.Shared.DoAfter; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared._CP14.Workbench; + +public class SharedCP14WorkbenchSystem : EntitySystem +{ +} + +[Serializable, NetSerializable] +public sealed partial class CP14CraftDoAfterEvent : DoAfterEvent +{ + [DataField(required: true)] + public ProtoId Recipe = default!; + + public override DoAfterEvent Clone() => this; +} diff --git a/Resources/Locale/en-US/_CP14/workbench/workbench.ftl b/Resources/Locale/en-US/_CP14/workbench/workbench.ftl new file mode 100644 index 0000000000..e78f40c4cb --- /dev/null +++ b/Resources/Locale/en-US/_CP14/workbench/workbench.ftl @@ -0,0 +1,5 @@ +cp14-verb-categories-craft = Select recipe: + +cp14-workbench-recipe-list = Recipe: + +cp14-workbench-no-resource = There aren't enough ingredients! \ No newline at end of file diff --git a/Resources/Locale/ru-RU/_CP14/workbench/workbench.ftl b/Resources/Locale/ru-RU/_CP14/workbench/workbench.ftl new file mode 100644 index 0000000000..ef2308935d --- /dev/null +++ b/Resources/Locale/ru-RU/_CP14/workbench/workbench.ftl @@ -0,0 +1,5 @@ +cp14-verb-categories-craft = Выберите крафт: + +cp14-workbench-recipe-list = Рецепт: + +cp14-workbench-no-resource = Не хватает ингредиентов! \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Entities/Structures/Furniture/tables.yml b/Resources/Prototypes/_CP14/Entities/Structures/Furniture/tables.yml index 83b0de4c70..223cdfce62 100644 --- a/Resources/Prototypes/_CP14/Entities/Structures/Furniture/tables.yml +++ b/Resources/Prototypes/_CP14/Entities/Structures/Furniture/tables.yml @@ -18,6 +18,7 @@ layer: - TableLayer - type: Sprite + snapCardinals: true sprite: _CP14/Structures/Furniture/Tables/wood.rsi state: frame - type: Icon diff --git a/Resources/Prototypes/_CP14/Entities/Structures/Furniture/workbenchs.yml b/Resources/Prototypes/_CP14/Entities/Structures/Furniture/workbenchs.yml new file mode 100644 index 0000000000..521a007050 --- /dev/null +++ b/Resources/Prototypes/_CP14/Entities/Structures/Furniture/workbenchs.yml @@ -0,0 +1,92 @@ +- type: entity + parent: + - BaseStructure + id: CP14BaseWorkbench + abstract: true + components: + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + density: 55 + mask: + - TabletopMachineMask + layer: + - Impassable + - MidImpassable + - LowImpassable + hard: false + fix2: + shape: + !type:PhysShapeAabb + bounds: "-0.45,-0.45,0.45,0.45" + density: 55 + mask: + - TableMask + layer: + - TableLayer + - type: Climbable + - type: Clickable + - type: PlaceableSurface + - type: CP14Workbench + craftSpeed: 1 + +- type: entity + parent: + - CP14BaseWorkbench + - CP14BaseWooden + id: CP14Workbench + name: workbench + description: Table for the production of various basic tools. + components: + - type: Sprite + snapCardinals: true + sprite: _CP14/Structures/Furniture/workbench.rsi + state: icon + - type: Icon + sprite: _CP14/Structures/Furniture/workbench.rsi + state: icon + - type: Damageable + damageContainer: Inorganic + damageModifierSet: Wood + - type: Destructible + thresholds: + - trigger: + !type:DamageTypeTrigger + damageType: Heat + damage: 40 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: WoodDestroy + - trigger: + !type:DamageTrigger + damage: 60 + behaviors: + - !type:DoActsBehavior + acts: ["Destruction"] + - !type:PlaySoundBehavior + sound: + collection: WoodDestroy + - !type:SpawnEntitiesBehavior + spawn: + CP14WoodenPlanks1: + min: 1 + max: 1 + - type: FootstepModifier + footstepSoundCollection: + collection: FootstepWood + - type: FireVisuals + sprite: _CP14/Effects/fire.rsi + normalState: full + - type: Construction + graph: CP14TableWooden + node: CP14Workbench + - type: CP14Workbench + recipes: + - CP14Bucket + - CP14BaseBarrel \ No newline at end of file diff --git a/Resources/Prototypes/_CP14/Recipes/Construction/Graphs/Furniture/tables.yml b/Resources/Prototypes/_CP14/Recipes/Construction/Graphs/Furniture/tables.yml index 9e7f806739..5f3e6ee579 100644 --- a/Resources/Prototypes/_CP14/Recipes/Construction/Graphs/Furniture/tables.yml +++ b/Resources/Prototypes/_CP14/Recipes/Construction/Graphs/Furniture/tables.yml @@ -25,6 +25,10 @@ - tool: Screwing #TODO - new tool doAfter: 1 - to: CP14TableWooden + steps: + - tool: CP14Hammering + doAfter: 1 + - to: CP14Workbench steps: - material: CP14Nail amount: 1 @@ -34,6 +38,14 @@ - node: CP14TableWooden entity: CP14TableWooden + edges: + - to: CP14TableWoodenFrame + steps: + - tool: Screwing #TODO - new tool + doAfter: 1 + + - node: CP14Workbench + entity: CP14Workbench edges: - to: CP14TableWoodenFrame steps: diff --git a/Resources/Prototypes/_CP14/Recipes/Construction/furniture.yml b/Resources/Prototypes/_CP14/Recipes/Construction/furniture.yml index 3800bba5d7..78470ca3e0 100644 --- a/Resources/Prototypes/_CP14/Recipes/Construction/furniture.yml +++ b/Resources/Prototypes/_CP14/Recipes/Construction/furniture.yml @@ -90,6 +90,24 @@ conditions: - !type:TileNotBlocked +- type: construction + crystallPunkAllowed: true + name: Workbench + description: Table for the production of various basic tools. + id: CP14Workbench + graph: CP14TableWooden + startNode: start + targetNode: CP14Workbench + category: construction-category-furniture + icon: + sprite: _CP14/Structures/Furniture/workbench.rsi + state: icon + objectType: Structure + placementMode: SnapgridCenter + canBuildInImpassable: false + conditions: + - !type:TileNotBlocked + - type: construction crystallPunkAllowed: true id: CP14Mannequin diff --git a/Resources/Prototypes/_CP14/Recipes/Workbench/workbench.yml b/Resources/Prototypes/_CP14/Recipes/Workbench/workbench.yml new file mode 100644 index 0000000000..45304d6805 --- /dev/null +++ b/Resources/Prototypes/_CP14/Recipes/Workbench/workbench.yml @@ -0,0 +1,16 @@ +- type: CP14Recipe + id: CP14Bucket + craftTime: 2 + entities: + CP14Rope: 1 + stacks: + CP14WoodenPlanks: 3 + result: CP14Bucket + +- type: CP14Recipe + id: CP14BaseBarrel + craftTime: 3 + stacks: + CP14WoodenPlanks: 5 + CP14Nail: 2 + result: CP14BaseBarrel \ No newline at end of file diff --git a/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/icon.png b/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/icon.png new file mode 100644 index 0000000000..67b5171331 Binary files /dev/null and b/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/icon.png differ diff --git a/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/meta.json b/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/meta.json new file mode 100644 index 0000000000..94345c710f --- /dev/null +++ b/Resources/Textures/_CP14/Structures/Furniture/workbench.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "All rights reserved for the CrystallPunk14 project only", + "copyright": "Created by jaraten(discord) for CrystallPunk14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + } + ] +}