Skip to content

Commit

Permalink
The Throwing Update (Simple-Station#1307)
Browse files Browse the repository at this point in the history
# Description

Turns a plethora of items into throwing weapons that deal damage when
thrown. Throwing weapons cost stamina to throw.

## Technical/Balance Details

To make a melee weapon also a throwing weapon, just add `- type:
DamageOtherOnHit`, and it will automatically inherit the damage from a
light melee attack and the melee sound effect as the thrown hit sound
effect. You can set a custom damage value with the `damage` field
(necessary when the item is not a `MeleeWeapon`) and stamina cost with
`staminaCost`.

To make the throwing weapon embed and deal damage over time when
embedded, add `- type: EmbeddableProjectile` and `- type:
EmbedPassiveDamage`. By default, the embed damage per second is 5% of
the throwing damage, but it can be modified on `EmbedPassiveDamage` with
`throwingDamageMultiplier`.

The default stamina cost for throwing is 3.5 stamina. The baseline cost
for almost all DoT embeddables is 5 stamina, because of the extra damage
the DoT brings.

When a thrown item hits a target with body parts, it will randomly
select a body part and only deal throwing damage to that body part. It
will also embed to the same body part and only deal passive embed damage
to it.

## TODO

The unchecked checkmarks are best addressed in another PR but they will
stay here for now.

<details><summary>Show Todo</summary>

- [ ] Deal with prediction issue on embeddable projectile removal
- [ ] This happens even before this PR so not really a big issue, maybe
in a separate PR
- [x] Add embeddable damage numbers to embeddables
- [x] Fix throwing angle for surgery tools after the surgical tools
sprite update
- [ ] Try to make the throw knockback function as if it hit a wall 
- [x] Esword/desword/e-dagger toggle embed damage
- [x] Don't start passive embed damage if EmbedPassiveDamageComponent
has no damage
- [x] Make DamageOtherOnHit.Damage not nullable
- [x] Throwing damage only to a specific body part

### Traits

- [ ] **Enraged Throw** (Oni)
  - [ ] Oni/trait damage bonus applied to throwing weapon too
- [ ] Can throw carried bodies, which will do a MassContest between the
thrown body and the hit body to determine blunt damage, and stun
duration for each party
  - [ ] 15% resistance to thrown/embed damage
- This helps when their enemy uses the items they throw against them.

- [ ] **Sharpthrower** (Human)
  - [ ] 10% more Brute thrown damage
  - [ ] 50% chance of throw hitting targetted body part
  - [ ] 40% throwing stamina cost reduction
  - [ ] 15% resistance to thrown/embed damage

### Embeds

- [x] Adjust embed damage per second to be like /tg/ (in /tg/ spear has
around ~1.2 embed DPS, adjust for ~45% embed chance since we're not
implementing embed chance and its 0.54)
- [ ] Merge EmbeddableProjectileComponent and
EmbeddablePassiveDamageComponent
- [ ] Split SharedProjectileSystem into EmbeddableProjectileSystem
- [x] Embed to a specific body part and deal damage only to that part,
for now can randomly select body parts on embed
- [ ] ~~Normal passive damage becomes x0.2 when lying down~~
- [ ] Increased damage when moving, more bonus damage for running
(Jostle DPS on /tg/ is 0.2 running and 0.1 when walking/crawling)
- [x] All embeddables have a fall out time (30 or 45 secs)
- [ ] - [x] On damage examine, can see that an object is embeddable "It
can embed on a target if thrown."
- [ ] Negative moodlet for attached harmful embeddables
- [ ] On health examine target with embeds, can see embedded objects "He
has a spear embedded in his left arm."
- [x] On examine item that is embedded, can see to which body part the
item is embedded "The spear is embedded on Urist McHands's left arm."
- [ ] An embeddable removed outside of surgery deals a lot of damage (x2
thrown damage)
- [ ] Lying down prevents natural falling out and thus the damage with
non-surgical removal
- [ ] Surgical procedure on a body part to remove all embeds on it,
using hemostat for removal
- [x] Allow anyone to remove embedded cultist weapons even if they're
not a cultist

</details>

## Media

**Throwing Toolbox Tools**


https://github.com/user-attachments/assets/4e20568f-adf0-4be8-ac38-fc6b21fed03c

**Examine**


![image](https://github.com/user-attachments/assets/ef95e653-1491-4d9b-8f84-785c3df22763)

**Examine After Embedding**

![image](https://github.com/user-attachments/assets/edc79c8f-db23-4bd3-9fa7-3b47f79c5881)



## Changelog

:cl: Skubman
- add: The Throwing Update is here. You can throw most melee weapons at
the cost of stamina to deal damage from afar.
- add: Dozens of throwable weapons, mainly sharp weapons will now embed
on throw and deal damage every second until they're manually removed or
naturally fall off after some time.
- add: Examining the damage values of an item now shows its throwing
damage, throwing stamina cost, whether or not it embeds on a throw, and
if the embed deals damage over time.
- add: Examining an embedded item now shows what body part it's embedded
in.
- tweak: The traits High Adrenaline, Adrenal Dysfunction, Masochism and
Low Pain Tolerance now affect throwing attacks just like melee attacks.
- tweak: The default time to remove embedded items has been increased
from 3 to 5 seconds, and both the remover and the target with the
embedded item need to stand still during the removal.
- tweak: The time to pry up a floor tile with a crowbar and other tools
has been decreased from 1 second to 0.5 seconds. The throwing damage of
floor tiles has been increased. Go figure.
- fix: Attempting to throw a Blood Cultist item without being a cultist
will stun you and drop the item you're holding properly.

---------

Co-authored-by: sleepyyapril <[email protected]>
  • Loading branch information
angelofallars and sleepyyapril authored Dec 8, 2024
1 parent 3ae4370 commit c40af73
Show file tree
Hide file tree
Showing 105 changed files with 1,773 additions and 160 deletions.
5 changes: 5 additions & 0 deletions Content.Client/Damage/DamageOtherOnHitSystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using Content.Shared.Damage.Systems;

namespace Content.Client.Damage;

public sealed class DamageOtherOnHitSystem : SharedDamageOtherOnHitSystem;
19 changes: 0 additions & 19 deletions Content.Server/Damage/Components/DamageOtherOnHitComponent.cs

This file was deleted.

65 changes: 33 additions & 32 deletions Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
Original file line number Diff line number Diff line change
@@ -1,65 +1,66 @@
using Content.Server.Administration.Logs;
using Content.Server.Damage.Components;
using Content.Server.Weapons.Ranged.Systems;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Events;
using Content.Shared.Damage.Systems;
using Content.Shared.Database;
using Content.Shared.Effects;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Projectiles;
using Content.Shared.Popups;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Melee;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;

namespace Content.Server.Damage.Systems
{
public sealed class DamageOtherOnHitSystem : EntitySystem
public sealed class DamageOtherOnHitSystem : SharedDamageOtherOnHitSystem
{
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly GunSystem _guns = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly DamageExamineSystem _damageExamine = default!;
[Dependency] private readonly SharedCameraRecoilSystem _sharedCameraRecoil = default!;
[Dependency] private readonly SharedColorFlashEffectSystem _color = default!;
[Dependency] private readonly ThrownItemSystem _thrownItem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly StaminaSystem _stamina = default!;

public override void Initialize()
{
SubscribeLocalEvent<DamageOtherOnHitComponent, ThrowDoHitEvent>(OnDoHit);
base.Initialize();

SubscribeLocalEvent<StaminaComponent, BeforeThrowEvent>(OnBeforeThrow);
SubscribeLocalEvent<DamageOtherOnHitComponent, DamageExamineEvent>(OnDamageExamine);
}

private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args)
private void OnBeforeThrow(EntityUid uid, StaminaComponent component, ref BeforeThrowEvent args)
{
var dmg = _damageable.TryChangeDamage(args.Target, component.Damage, component.IgnoreResistances, origin: args.Component.Thrower);

// Log damage only for mobs. Useful for when people throw spears at each other, but also avoids log-spam when explosions send glass shards flying.
if (dmg != null && HasComp<MobStateComponent>(args.Target))
_adminLogger.Add(LogType.ThrowHit, $"{ToPrettyString(args.Target):target} received {dmg.GetTotal():damage} damage from collision");

if (dmg is { Empty: false })
{
_color.RaiseEffect(Color.Red, new List<EntityUid>() { args.Target }, Filter.Pvs(args.Target, entityManager: EntityManager));
}
if (!TryComp<DamageOtherOnHitComponent>(args.ItemUid, out var damage))
return;

_guns.PlayImpactSound(args.Target, dmg, null, false);
if (TryComp<PhysicsComponent>(uid, out var body) && body.LinearVelocity.LengthSquared() > 0f)
if (component.CritThreshold - component.StaminaDamage <= damage.StaminaCost)
{
var direction = body.LinearVelocity.Normalized();
_sharedCameraRecoil.KickCamera(args.Target, direction);
args.Cancelled = true;
_popup.PopupEntity(Loc.GetString("throw-no-stamina", ("item", args.ItemUid)), uid, uid);
return;
}

// TODO: If more stuff touches this then handle it after.
if (TryComp<PhysicsComponent>(uid, out var physics))
{
_thrownItem.LandComponent(args.Thrown, args.Component, physics, false);
}
_stamina.TakeStaminaDamage(uid, damage.StaminaCost, component, visual: false);
}

private void OnDamageExamine(EntityUid uid, DamageOtherOnHitComponent component, ref DamageExamineEvent args)
{
_damageExamine.AddDamageExamine(args.Message, component.Damage, Loc.GetString("damage-throw"));
_damageExamine.AddDamageExamine(args.Message, GetDamage(uid, component, args.User), Loc.GetString("damage-throw"));

if (component.StaminaCost == 0)
return;

var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(
Loc.GetString("damage-stamina-cost",
("type", Loc.GetString("damage-throw")), ("cost", component.StaminaCost)));
args.Message.PushNewline();
args.Message.AddMessage(staminaCostMarkup);
}
}
}
20 changes: 14 additions & 6 deletions Content.Server/Flash/FlashSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using Content.Shared.Inventory;
using Content.Shared.Physics;
using Content.Shared.Tag;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Melee.Events;
using Robust.Server.Audio;
using Robust.Server.GameObjects;
Expand Down Expand Up @@ -50,6 +51,7 @@ public override void Initialize()
SubscribeLocalEvent<FlashComponent, MeleeHitEvent>(OnFlashMeleeHit);
// ran before toggling light for extra-bright lantern
SubscribeLocalEvent<FlashComponent, UseInHandEvent>(OnFlashUseInHand, before: new []{ typeof(HandheldLightSystem) });
SubscribeLocalEvent<FlashComponent, ThrowDoHitEvent>(OnFlashThrowHitEvent);
SubscribeLocalEvent<InventoryComponent, FlashAttemptEvent>(OnInventoryFlashAttempt);
SubscribeLocalEvent<FlashImmunityComponent, FlashAttemptEvent>(OnFlashImmunityFlashAttempt);
SubscribeLocalEvent<PermanentBlindnessComponent, FlashAttemptEvent>(OnPermanentBlindnessFlashAttempt);
Expand All @@ -60,10 +62,8 @@ private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent a
{
if (!args.IsHit ||
!args.HitEntities.Any() ||
!UseFlash(uid, comp, args.User))
{
!UseFlash(uid, comp))
return;
}

args.Handled = true;
foreach (var e in args.HitEntities)
Expand All @@ -74,14 +74,22 @@ private void OnFlashMeleeHit(EntityUid uid, FlashComponent comp, MeleeHitEvent a

private void OnFlashUseInHand(EntityUid uid, FlashComponent comp, UseInHandEvent args)
{
if (args.Handled || !UseFlash(uid, comp, args.User))
if (args.Handled || !UseFlash(uid, comp))
return;

args.Handled = true;
FlashArea(uid, args.User, comp.Range, comp.AoeFlashDuration, comp.SlowTo, true, comp.Probability);
}

private bool UseFlash(EntityUid uid, FlashComponent comp, EntityUid user)
private void OnFlashThrowHitEvent(EntityUid uid, FlashComponent comp, ThrowDoHitEvent args)
{
if (!UseFlash(uid, comp))
return;

FlashArea(uid, args.User, comp.Range, comp.AoeFlashDuration, comp.SlowTo, false, comp.Probability);
}

private bool UseFlash(EntityUid uid, FlashComponent comp)
{
if (comp.Flashing)
return false;
Expand All @@ -99,7 +107,7 @@ private bool UseFlash(EntityUid uid, FlashComponent comp, EntityUid user)
{
_appearance.SetData(uid, FlashVisuals.Burnt, true);
_tag.AddTag(uid, "Trash");
_popup.PopupEntity(Loc.GetString("flash-component-becomes-empty"), user);
_popup.PopupEntity(Loc.GetString("flash-component-becomes-empty"), uid);
}

uid.SpawnTimer(400, () =>
Expand Down
6 changes: 6 additions & 0 deletions Content.Server/Hands/Systems/HandsSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,12 @@ hands.ActiveHandEntity is not { } throwEnt ||

// Let other systems change the thrown entity (useful for virtual items)
// or the throw strength.
var itemEv = new BeforeGettingThrownEvent(throwEnt, direction, throwStrength, player);
RaiseLocalEvent(throwEnt, ref itemEv);

if (itemEv.Cancelled)
return true;

var ev = new BeforeThrowEvent(throwEnt, direction, throwStrength, player);
RaiseLocalEvent(player, ref ev);

Expand Down
20 changes: 20 additions & 0 deletions Content.Server/Projectiles/ProjectileSystem.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Content.Shared.Damage.Events;
using Content.Server.Administration.Logs;
using Content.Server.Effects;
using Content.Server.Weapons.Ranged.Systems;
Expand All @@ -7,6 +8,7 @@
using Content.Shared.Projectiles;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Utility;

namespace Content.Server.Projectiles;

Expand All @@ -22,6 +24,7 @@ public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ProjectileComponent, StartCollideEvent>(OnStartCollide);
SubscribeLocalEvent<EmbeddableProjectileComponent, DamageExamineEvent>(OnDamageExamine);
}

private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref StartCollideEvent args)
Expand Down Expand Up @@ -77,4 +80,21 @@ private void OnStartCollide(EntityUid uid, ProjectileComponent component, ref St
RaiseNetworkEvent(new ImpactEffectEvent(component.ImpactEffect, GetNetCoordinates(xform.Coordinates)), Filter.Pvs(xform.Coordinates, entityMan: EntityManager));
}
}

private void OnDamageExamine(EntityUid uid, EmbeddableProjectileComponent component, ref DamageExamineEvent args)
{
if (!component.EmbedOnThrow)
return;

if (!args.Message.IsEmpty)
args.Message.PushNewline();

var isHarmful = TryComp<EmbedPassiveDamageComponent>(uid, out var passiveDamage) && passiveDamage.Damage.Any();
var loc = isHarmful
? "damage-examine-embeddable-harmful"
: "damage-examine-embeddable";

var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(Loc.GetString(loc));
args.Message.AddMessage(staminaCostMarkup);
}
}
2 changes: 1 addition & 1 deletion Content.Server/Weapons/Melee/MeleeWeaponSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private void OnMeleeExamineDamage(EntityUid uid, MeleeWeaponComponent component,
if (component.HeavyStaminaCost != 0)
{
var staminaCostMarkup = FormattedMessage.FromMarkupOrThrow(
Loc.GetString("damage-melee-heavy-stamina-cost",
Loc.GetString("damage-stamina-cost",
("type", Loc.GetString("damage-melee-heavy")), ("cost", component.HeavyStaminaCost)));
args.Message.PushNewline();
args.Message.AddMessage(staminaCostMarkup);
Expand Down
2 changes: 1 addition & 1 deletion Content.Shared/Body/Systems/SharedBodySystem.Targeting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ private static TargetBodyPart GetRandomPartSpread(IRobustRandom random, ushort t

public TargetBodyPart? GetRandomBodyPart(EntityUid uid, TargetingComponent? target = null)
{
if (!Resolve(uid, ref target))
if (!Resolve(uid, ref target, false))
return null;

var totalWeight = target.TargetOdds.Values.Sum();
Expand Down
75 changes: 75 additions & 0 deletions Content.Shared/Damage/Components/DamageOtherOnHitComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using Content.Shared.Contests;
using Content.Shared.Damage.Systems;
using Content.Shared.Damage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;

namespace Content.Shared.Damage.Components;

/// <summary>
/// Deals damage when thrown.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class DamageOtherOnHitComponent : Component
{
[DataField, AutoNetworkedField]
public bool IgnoreResistances = false;

/// <summary>
/// The damage that a throw deals.
/// </summary>
[DataField, AutoNetworkedField]
public DamageSpecifier Damage = new();

/// <summary>
/// The stamina cost of throwing this entity.
/// </summary>
[DataField, AutoNetworkedField]
public float StaminaCost = 3.5f;

/// <summary>
/// The maximum amount of hits per throw before landing on the floor.
/// </summary>
[DataField, AutoNetworkedField]
public int MaxHitQuantity = 1;

/// <summary>
/// The tracked amount of hits in a single throw.
/// </summary>
[DataField, AutoNetworkedField]
public int HitQuantity = 0;

/// <summary>
/// The multiplier to apply to the entity's light attack damage to calculate the throwing damage.
/// Only used if this component has a MeleeWeaponComponent and Damage is not set on the prototype.
/// </summary>
[DataField, AutoNetworkedField]
public float MeleeDamageMultiplier = 1f;

/// <summary>
/// The sound to play when this entity hits on a throw.
/// If null, attempts to retrieve the SoundHit from MeleeWeaponComponent.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier? SoundHit;

/// <summary>
/// Arguments for modifying the throwing weapon damage with contests.
/// These are the same ContestArgs in MeleeWeaponComponent.
/// </summary>
[DataField]
public ContestArgs ContestArgs = new ContestArgs
{
DoStaminaInteraction = true,
StaminaDisadvantage = true,
DoHealthInteraction = true,
};

/// <summary>
/// Plays if no damage is done to the target entity.
/// If null, attempts to retrieve the SoundNoDamage from MeleeWeaponComponent.
/// </summary>
[DataField, AutoNetworkedField]
public SoundSpecifier SoundNoDamage { get; set; } = new SoundCollectionSpecifier("WeakHit");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Shared.Damage.Components;

/// <summary>
/// This is used for entities that are immune to getting hit by DamageOtherOnHit, and getting embedded from EmbeddableProjectile.
/// </summary>
[RegisterComponent]
public sealed partial class DamageOtherOnHitImmuneComponent : Component {}
21 changes: 21 additions & 0 deletions Content.Shared/Damage/Events/DamageOtherOnHitEvents.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using Content.Shared.Damage;
using Content.Shared.Damage.Components;
using Content.Shared.Item.ItemToggle.Components;

namespace Content.Shared.Damage.Events;

/// <summary>
/// Raised on a throwing weapon to calculate potential damage bonuses or decreases.
/// </summary>
[ByRefEvent]
public record struct GetThrowingDamageEvent(EntityUid Weapon, DamageSpecifier Damage, List<DamageModifierSet> Modifiers, EntityUid? User);

/// <summary>
/// Raised on a throwing weapon when DamageOtherOnHit has been successfully initialized.
/// </summary>
public record struct DamageOtherOnHitStartupEvent(Entity<DamageOtherOnHitComponent> Weapon);

/// <summary>
/// Raised on a throwing weapon when ItemToggleDamageOtherOnHit has been successfully initialized.
/// </summary>
public record struct ItemToggleDamageOtherOnHitStartupEvent(Entity<ItemToggleDamageOtherOnHitComponent> Weapon);
Loading

0 comments on commit c40af73

Please sign in to comment.