diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 997bffad24..48256031b0 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -7,7 +7,6 @@ namespace Content.Client.Entry
{
"AirlockPainter",
"AmmoBox",
- "Pickaxe",
"IngestionBlocker",
"Charger",
"CloningPod",
@@ -49,7 +48,6 @@ namespace Content.Client.Entry
"DiseaseZombie",
"DiseaseBuildup",
"ZombieTransfer",
- "Mineable",
"RangedMagazine",
"RandomMetadata",
"Ammo",
@@ -307,6 +305,7 @@ namespace Content.Client.Entry
"ExtensionCableReceiver",
"ExtensionCableProvider",
"ApcNetworkConnection",
+ "Gatherable",
"SuitSensor",
"CrewMonitoringConsole",
"ApcNetSwitch",
@@ -321,6 +320,7 @@ namespace Content.Client.Entry
"FireAlarm",
"AirAlarm",
"RadarConsole",
+ "GatheringTool",
"Guardian",
"GuardianCreator",
"GuardianHost",
diff --git a/Content.Server/Gatherable/Components/GatherableComponent.cs b/Content.Server/Gatherable/Components/GatherableComponent.cs
new file mode 100644
index 0000000000..bb9b0be9c1
--- /dev/null
+++ b/Content.Server/Gatherable/Components/GatherableComponent.cs
@@ -0,0 +1,33 @@
+using Content.Shared.EntityList;
+using Content.Shared.Whitelist;
+
+namespace Content.Server.Gatherable.Components;
+
+[RegisterComponent]
+[Friend(typeof(GatherableSystem))]
+public sealed class GatherableComponent : Component
+{
+ ///
+ /// Whitelist for specifying the kind of tools can be used on a resource
+ /// Supports multiple tags.
+ ///
+ [ViewVariables]
+ [DataField("whitelist", required: true)]
+ public EntityWhitelist? ToolWhitelist;
+
+ ///
+ /// YAML example below
+ /// (Tag1, Tag2, LootTableID1, LootTableID2 are placeholders for example)
+ /// --------------------
+ /// useMappedLoot: true
+ /// whitelist:
+ /// tags:
+ /// - Tag1
+ /// - Tag2
+ /// mappedLoot:
+ /// Tag1: LootTableID1
+ /// Tag2: LootTableID2
+ ///
+ [DataField("loot")]
+ public Dictionary? MappedLoot = new();
+}
diff --git a/Content.Server/Gatherable/Components/GatheringToolComponent.cs b/Content.Server/Gatherable/Components/GatheringToolComponent.cs
new file mode 100644
index 0000000000..14e9888d41
--- /dev/null
+++ b/Content.Server/Gatherable/Components/GatheringToolComponent.cs
@@ -0,0 +1,44 @@
+using System.Threading;
+using Content.Shared.Damage;
+using Content.Shared.Sound;
+
+namespace Content.Server.Gatherable.Components
+{
+ ///
+ /// When interacting with an allows it to spawn entities.
+ ///
+ [RegisterComponent]
+ public sealed class GatheringToolComponent : Component
+ {
+ ///
+ /// Sound that is made once you completed gathering
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("sound")]
+ public SoundSpecifier GatheringSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Mining/pickaxe.ogg");
+
+ ///
+ /// This directly plugs into the time delay for gathering.
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("gatheringTime")]
+ public float GatheringTime { get; set; } = 1f;
+
+ ///
+ /// What damage should be given to objects when
+ /// gathered using this tool? (0 for infinite gathering)
+ ///
+ [DataField("damage", required: true)]
+ public DamageSpecifier Damage { get; set; } = default!;
+
+ ///
+ /// How many entities can this tool gather from at once?
+ ///
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("maxEntities")]
+ public int MaxGatheringEntities = 1;
+
+ [ViewVariables]
+ public readonly Dictionary GatheringEntities = new();
+ }
+}
diff --git a/Content.Server/Gatherable/GatherableSystem.cs b/Content.Server/Gatherable/GatherableSystem.cs
new file mode 100644
index 0000000000..e55667b374
--- /dev/null
+++ b/Content.Server/Gatherable/GatherableSystem.cs
@@ -0,0 +1,111 @@
+using System.Threading;
+using Content.Server.DoAfter;
+using Content.Server.Gatherable.Components;
+using Content.Shared.Damage;
+using Content.Shared.EntityList;
+using Content.Shared.Interaction;
+using Content.Shared.Tag;
+using Robust.Shared.Audio;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+
+namespace Content.Server.Gatherable;
+
+public sealed class GatherableSystem : EntitySystem
+{
+ [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = null!;
+ [Dependency] private readonly TagSystem _tagSystem = Get();
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInteractUsing);
+ SubscribeLocalEvent(OnDoafterCancel);
+ SubscribeLocalEvent(OnDoafterSuccess);
+ }
+
+ private void OnInteractUsing(EntityUid uid, GatherableComponent component, InteractUsingEvent args)
+ {
+ if (!TryComp(args.Used, out var tool) ||
+ component.ToolWhitelist?.IsValid(args.Used) == false ||
+ tool.GatheringEntities.TryGetValue(uid, out var cancelToken))
+ return;
+
+ // Can't gather too many entities at once.
+ if (tool.MaxGatheringEntities < tool.GatheringEntities.Count + 1)
+ return;
+
+ cancelToken = new CancellationTokenSource();
+ tool.GatheringEntities[uid] = cancelToken;
+
+ var doAfter = new DoAfterEventArgs(args.User, tool.GatheringTime, cancelToken.Token, uid)
+ {
+ BreakOnDamage = true,
+ BreakOnStun = true,
+ BreakOnTargetMove = true,
+ BreakOnUserMove = true,
+ MovementThreshold = 0.25f,
+ BroadcastCancelledEvent = new GatheringDoafterCancel { Tool = args.Used, Resource = uid },
+ TargetFinishedEvent = new GatheringDoafterSuccess { Tool = args.Used, Resource = uid, Player = args.User }
+ };
+
+ _doAfterSystem.DoAfter(doAfter);
+ }
+
+ private void OnDoafterSuccess(EntityUid uid, GatherableComponent component, GatheringDoafterSuccess ev)
+ {
+ if (!TryComp(ev.Tool, out GatheringToolComponent? tool))
+ return;
+
+ // Complete the gathering process
+ _damageableSystem.TryChangeDamage(ev.Resource, tool.Damage);
+ SoundSystem.Play(Filter.Pvs(ev.Resource, entityManager: EntityManager), tool.GatheringSound.GetSound(), ev.Resource);
+ tool.GatheringEntities.Remove(ev.Resource);
+
+ // Spawn the loot!
+ if (component.MappedLoot == null) return;
+
+ var playerPos = Transform(ev.Player).MapPosition;
+
+ foreach (var (tag, table) in component.MappedLoot)
+ {
+ if (tag != "All")
+ {
+ if (!_tagSystem.HasTag(tool.Owner, tag)) continue;
+ }
+ var getLoot = _prototypeManager.Index(table);
+ var spawnLoot = getLoot.GetSpawns();
+ var spawnPos = playerPos.Offset(_random.NextVector2(0.3f));
+ Spawn(spawnLoot[0], spawnPos);
+ }
+ }
+
+ private void OnDoafterCancel(GatheringDoafterCancel ev)
+ {
+ if (!TryComp(ev.Tool, out var tool))
+ return;
+
+ tool.GatheringEntities.Remove(ev.Resource);
+ }
+
+ private sealed class GatheringDoafterCancel : EntityEventArgs
+ {
+ public EntityUid Tool;
+ public EntityUid Resource;
+ }
+
+ private sealed class GatheringDoafterSuccess : EntityEventArgs
+ {
+ public EntityUid Tool;
+ public EntityUid Resource;
+ public EntityUid Player;
+ }
+}
+
+
+
diff --git a/Content.Server/Mining/Components/MineableComponent.cs b/Content.Server/Mining/Components/MineableComponent.cs
deleted file mode 100644
index 0368bcc9e1..0000000000
--- a/Content.Server/Mining/Components/MineableComponent.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System.Threading;
-using Content.Shared.Storage;
-
-namespace Content.Server.Mining.Components;
-
-[RegisterComponent]
-[Friend(typeof(MineableSystem))]
-public sealed class MineableComponent : Component
-{
- [DataField("ores")] public List Ores = new();
- public float BaseMineTime = 1.0f;
-}
diff --git a/Content.Server/Mining/Components/PickaxeComponent.cs b/Content.Server/Mining/Components/PickaxeComponent.cs
deleted file mode 100644
index 181ed2757a..0000000000
--- a/Content.Server/Mining/Components/PickaxeComponent.cs
+++ /dev/null
@@ -1,38 +0,0 @@
-using System.Threading;
-using Content.Shared.Damage;
-using Content.Shared.Sound;
-
-namespace Content.Server.Mining.Components
-{
- ///
- /// When interacting with an allows it to spawn entities.
- ///
- [RegisterComponent]
- public sealed class PickaxeComponent : Component
- {
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("sound")]
- public SoundSpecifier MiningSound { get; set; } = new SoundPathSpecifier("/Audio/Items/Mining/pickaxe.ogg");
-
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("timeMultiplier")]
- public float MiningTimeMultiplier { get; set; } = 1f;
-
- ///
- /// What damage should be given to objects when
- /// mined using a pickaxe?
- ///
- [DataField("damage", required: true)]
- public DamageSpecifier Damage { get; set; } = default!;
-
- ///
- /// How many entities can this pickaxe mine at once?
- ///
- [ViewVariables(VVAccess.ReadWrite)]
- [DataField("maxEntities")]
- public int MaxMiningEntities = 1;
-
- [ViewVariables]
- public readonly Dictionary MiningEntities = new();
- }
-}
diff --git a/Content.Server/Mining/MineableSystem.cs b/Content.Server/Mining/MineableSystem.cs
deleted file mode 100644
index 7b25f1e9f5..0000000000
--- a/Content.Server/Mining/MineableSystem.cs
+++ /dev/null
@@ -1,99 +0,0 @@
-using System.Threading;
-using Content.Server.DoAfter;
-using Content.Server.Mining.Components;
-using Content.Shared.Damage;
-using Content.Shared.Interaction;
-using Content.Shared.Storage;
-using Robust.Shared.Audio;
-using Robust.Shared.Player;
-using Robust.Shared.Random;
-
-namespace Content.Server.Mining;
-
-public sealed class MineableSystem : EntitySystem
-{
- [Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
- [Dependency] private readonly DamageableSystem _damageableSystem = default!;
- [Dependency] private readonly IRobustRandom _random = null!;
-
- public override void Initialize()
- {
- base.Initialize();
-
- SubscribeLocalEvent(OnInteractUsing);
- SubscribeLocalEvent(OnDoafterCancel);
- SubscribeLocalEvent(OnDoafterSuccess);
- }
-
- private void OnInteractUsing(EntityUid uid, MineableComponent component, InteractUsingEvent args)
- {
- if (!TryComp(args.Used, out var pickaxe))
- return;
-
- if (pickaxe.MiningEntities.TryGetValue(uid, out var cancelToken))
- {
- cancelToken.Cancel();
- pickaxe.MiningEntities.Remove(uid);
- return;
- }
-
- // Can't mine too many entities at once.
- if (pickaxe.MaxMiningEntities < pickaxe.MiningEntities.Count + 1)
- return;
-
- cancelToken = new CancellationTokenSource();
- pickaxe.MiningEntities[uid] = cancelToken;
-
- var doAfter = new DoAfterEventArgs(args.User, component.BaseMineTime * pickaxe.MiningTimeMultiplier, cancelToken.Token, uid)
- {
- BreakOnDamage = true,
- BreakOnStun = true,
- BreakOnTargetMove = true,
- BreakOnUserMove = true,
- MovementThreshold = 0.25f,
- BroadcastCancelledEvent = new MiningDoafterCancel { Pickaxe = args.Used, Rock = uid },
- TargetFinishedEvent = new MiningDoafterSuccess { Pickaxe = args.Used, Rock = uid, Player = args.User }
- };
-
- _doAfterSystem.DoAfter(doAfter);
- }
-
- private void OnDoafterSuccess(EntityUid uid, MineableComponent component, MiningDoafterSuccess ev)
- {
- if (!TryComp(ev.Pickaxe, out PickaxeComponent? pickaxe))
- return;
-
- _damageableSystem.TryChangeDamage(ev.Rock, pickaxe.Damage);
- SoundSystem.Play(Filter.Pvs(ev.Rock, entityManager: EntityManager), pickaxe.MiningSound.GetSound(), ev.Rock);
- pickaxe.MiningEntities.Remove(ev.Rock);
-
- var spawnOre = EntitySpawnCollection.GetSpawns(component.Ores, _random);
- var playerPos = Transform(ev.Player).MapPosition;
- var spawnPos = playerPos.Offset(_random.NextVector2(0.3f));
- EntityManager.SpawnEntity(spawnOre[0], spawnPos);
- pickaxe.MiningEntities.Remove(uid);
- }
-
- private void OnDoafterCancel(MiningDoafterCancel ev)
- {
- if (!TryComp(ev.Pickaxe, out var pickaxe))
- return;
-
- pickaxe.MiningEntities.Remove(ev.Rock);
- }
-
- private sealed class MiningDoafterCancel : EntityEventArgs
- {
- public EntityUid Pickaxe;
- public EntityUid Rock;
- }
-}
-
-// grumble grumble
-public sealed class MiningDoafterSuccess : EntityEventArgs
-{
- public EntityUid Pickaxe;
- public EntityUid Rock;
- public EntityUid Player;
-}
-
diff --git a/Resources/Prototypes/Entities/Objects/Misc/paper.yml b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
index ac588ce999..5ffeb25795 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/paper.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/paper.yml
@@ -85,14 +85,11 @@
- type: Tag
tags:
- Write
+ - Pickaxe
- type: Sprite
sprite: Objects/Misc/bureaucracy.rsi
state: overpriced_pen
netsync: false
- - type: Pickaxe
- damage:
- types:
- Piercing: 5
- type: ItemCooldown
- type: MeleeWeapon
damage:
diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml
index 6a6993e02a..f3a8036caf 100644
--- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml
+++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml
@@ -4,10 +4,13 @@
id: Pickaxe
description: Notched to perfection, for jamming it into rocks
components:
+ - type: Tag
+ tags:
+ - Pickaxe
- type: Sprite
sprite: Objects/Weapons/Melee/pickaxe.rsi
state: pickaxe
- - type: Pickaxe
+ - type: GatheringTool
damage:
types:
Piercing: 25
@@ -29,18 +32,21 @@
id: MiningDrill
description: Powerful tool used to quickly drill through rocks
components:
- - type: Sprite
- sprite: Objects/Tools/handdrill.rsi
- state: handdrill
- - type: Pickaxe
- damage:
- types:
- Piercing: 25
- timeMultiplier: 0.75
- - type: ItemCooldown
- - type: MeleeWeapon
- damage:
- types:
- Piercing: 10
- Blunt: 4
- arcCooldownTime: 3
+ - type: Tag
+ tags:
+ - Pickaxe
+ - type: Sprite
+ sprite: Objects/Tools/handdrill.rsi
+ state: handdrill
+ - type: GatheringTool
+ damage:
+ types:
+ Piercing: 25
+ gatheringTime: 0.75
+ - type: ItemCooldown
+ - type: MeleeWeapon
+ damage:
+ types:
+ Piercing: 10
+ Blunt: 4
+ arcCooldownTime: 3
diff --git a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
index f3eaad3fd5..7e0a25cfad 100644
--- a/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
+++ b/Resources/Prototypes/Entities/Structures/Walls/asteroid.yml
@@ -4,26 +4,12 @@
name: asteroid rock
description: An asteroid.
components:
- - type: Mineable
- ores:
- - id: SteelOre1
- prob: 0.25
- orGroup: Asteroid
- - id: GoldOre1
- prob: 0.05
- orGroup: Asteroid
- - id: SpaceQuartz1
- prob: 0.20
- orGroup: Asteroid
- - id: PlasmaOre1
- prob: 0.10
- orGroup: Asteroid
- - id: SilverOre1
- prob: 0.025
- orGroup: Asteroid
- - id: UraniumOre1
- prob: 0.025
- orGroup: Asteroid
+ - type: Gatherable
+ whitelist:
+ tags:
+ - Pickaxe
+ loot:
+ Pickaxe: MiningLootTable
- type: Sprite
sprite: Structures/Walls/asteroid_rock.rsi
state: full
diff --git a/Resources/Prototypes/LootTables/mining_loot_table.yml b/Resources/Prototypes/LootTables/mining_loot_table.yml
new file mode 100644
index 0000000000..34eb4192eb
--- /dev/null
+++ b/Resources/Prototypes/LootTables/mining_loot_table.yml
@@ -0,0 +1,21 @@
+- type: entityLootTable
+ id: MiningLootTable
+ entries:
+ - id: SteelOre1
+ prob: 0.25
+ orGroup: Asteroid
+ - id: GoldOre1
+ prob: 0.05
+ orGroup: Asteroid
+ - id: SpaceQuartz1
+ prob: 0.20
+ orGroup: Asteroid
+ - id: PlasmaOre1
+ prob: 0.10
+ orGroup: Asteroid
+ - id: SilverOre1
+ prob: 0.025
+ orGroup: Asteroid
+ - id: UraniumOre1
+ prob: 0.025
+ orGroup: Asteroid
\ No newline at end of file
diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml
index a7fd26174b..3d8add8cc6 100644
--- a/Resources/Prototypes/tags.yml
+++ b/Resources/Prototypes/tags.yml
@@ -244,7 +244,7 @@
id: PercussionInstrument
- type: Tag
- id: Plastic
+ id: Pickaxe
- type: Tag
id: Pill
@@ -266,6 +266,9 @@
- type: Tag
id: PlantSampleTaker
+
+- type: Tag
+ id: Plastic
- type: Tag
id: Powerdrill