Files
wwdpublic/Content.Server/Body/Systems/MetabolizerSystem.cs
WarMechanic 7916f07ddd Heavyweight Drunk Trait + Drunk Traits Rework (#512)
<!--
This is a semi-strict format, you can add/remove sections as needed but
the order/format should be kept the same
Remove these comments before submitting
-->

# Description
<!--
Explain this PR in as much detail as applicable

Some example prompts to consider:
How might this affect the game? The codebase?
What might be some alternatives to this?
How/Who does this benefit/hurt [the game/codebase]?
-->

Adds the inverse of lightweight drunk, which makes you less susceptible
to the effects of ethanol.
(more specifically, it halves the damage of alcohol and you need to
drink twice as much to feel the effects in the first place)

To make the change happen, `LightweightDrunk` component was reworked to:
- A) no longer change any drunk effects (including non-alcohol related
drunkness like bloodloss)
- B) instead multiply the effects of ethanol in your bloodstream

I chose this route in particular, because the other option of
multiplying the amount of ethanol gained from alcohols is nonsense.

---

# TODO

<!--
A list of everything you have to do before this PR is "complete"
You probably won't have to complete everything before merging but it's
good to leave future references
-->

- [X] Add a `TryMetabolizeReagent` event to `MetabolizerSystem.cs` so a
`LightweightDrunkSystem.cs` can listen to the event and multiply the
effects of "Ethanol" specifically.
- [X] Add the Heavyweight Drunk trait
- ~~Fix a minor spelling mistake that caused the trait condition to not
show up~~
- [ ] Would we be able to name trait files after categories? I don't
want to put every positive trait in a file named 'skills.yml'

---

<!--
This is default collapsed, readers click to expand it and see all your
media
The PR media section can get very large at times, so this is a good way
to keep it clean
The title is written using HTML tags
The title must be within the <summary> tags or you won't see it
-->

<details><summary><h1>Media</h1></summary>
<p>

![Example Media Embed](https://example.com/thisimageisntreal.png)

</p>
</details>

---

# Changelog

<!--
You can add an author after the `🆑` to change the name that appears
in the changelog (ex: `🆑 Death`)
Leaving it blank will default to your GitHub display name
This includes all available types for the changelog
-->

🆑
- add: Added the Heavyweight Drunk trait, which doubles your alcoholism
potential.

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: VMSolidus <evilexecutive@gmail.com>
2024-07-12 15:02:15 -04:00

260 lines
9.8 KiB
C#

using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Administration.Logs;
using Content.Shared.Body.Organ;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Body.Systems
{
public sealed class MetabolizerSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
private EntityQuery<OrganComponent> _organQuery;
private EntityQuery<SolutionContainerManagerComponent> _solutionQuery;
public override void Initialize()
{
base.Initialize();
_organQuery = GetEntityQuery<OrganComponent>();
_solutionQuery = GetEntityQuery<SolutionContainerManagerComponent>();
SubscribeLocalEvent<MetabolizerComponent, ComponentInit>(OnMetabolizerInit);
SubscribeLocalEvent<MetabolizerComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<MetabolizerComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<MetabolizerComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
}
private void OnMapInit(Entity<MetabolizerComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdate = _gameTiming.CurTime + ent.Comp.UpdateInterval;
}
private void OnUnpaused(Entity<MetabolizerComponent> ent, ref EntityUnpausedEvent args)
{
ent.Comp.NextUpdate += args.PausedTime;
}
private void OnMetabolizerInit(Entity<MetabolizerComponent> entity, ref ComponentInit args)
{
if (!entity.Comp.SolutionOnBody)
{
_solutionContainerSystem.EnsureSolution(entity.Owner, entity.Comp.SolutionName);
}
else if (_organQuery.CompOrNull(entity)?.Body is { } body)
{
_solutionContainerSystem.EnsureSolution(body, entity.Comp.SolutionName);
}
}
private void OnApplyMetabolicMultiplier(
Entity<MetabolizerComponent> ent,
ref ApplyMetabolicMultiplierEvent args)
{
if (args.Apply)
{
ent.Comp.UpdateInterval *= args.Multiplier;
return;
}
ent.Comp.UpdateInterval /= args.Multiplier;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var metabolizers = new ValueList<(EntityUid Uid, MetabolizerComponent Component)>(Count<MetabolizerComponent>());
var query = EntityQueryEnumerator<MetabolizerComponent>();
while (query.MoveNext(out var uid, out var comp))
{
metabolizers.Add((uid, comp));
}
foreach (var (uid, metab) in metabolizers)
{
// Only update as frequently as it should
if (_gameTiming.CurTime < metab.NextUpdate)
continue;
metab.NextUpdate += metab.UpdateInterval;
TryMetabolize((uid, metab));
}
}
private void TryMetabolize(Entity<MetabolizerComponent, OrganComponent?, SolutionContainerManagerComponent?> ent)
{
_organQuery.Resolve(ent, ref ent.Comp2, logMissing: false);
// First step is get the solution we actually care about
var solutionName = ent.Comp1.SolutionName;
Solution? solution = null;
Entity<SolutionComponent>? soln = default!;
EntityUid? solutionEntityUid = null;
if (ent.Comp1.SolutionOnBody)
{
if (ent.Comp2?.Body is { } body)
{
if (!_solutionQuery.Resolve(body, ref ent.Comp3, logMissing: false))
return;
_solutionContainerSystem.TryGetSolution((body, ent.Comp3), solutionName, out soln, out solution);
solutionEntityUid = body;
}
}
else
{
if (!_solutionQuery.Resolve(ent, ref ent.Comp3, logMissing: false))
return;
_solutionContainerSystem.TryGetSolution((ent, ent), solutionName, out soln, out solution);
solutionEntityUid = ent;
}
if (solutionEntityUid is null
|| soln is null
|| solution is null
|| solution.Contents.Count == 0)
{
return;
}
// randomize the reagent list so we don't have any weird quirks
// like alphabetical order or insertion order mattering for processing
var list = solution.Contents.ToArray();
_random.Shuffle(list);
int reagents = 0;
foreach (var (reagent, quantity) in list)
{
if (!_prototypeManager.TryIndex<ReagentPrototype>(reagent.Prototype, out var proto))
continue;
var mostToRemove = FixedPoint2.Zero;
if (proto.Metabolisms is null)
{
if (ent.Comp1.RemoveEmpty)
{
solution.RemoveReagent(reagent, FixedPoint2.New(1));
}
continue;
}
// we're done here entirely if this is true
if (reagents >= ent.Comp1.MaxReagentsProcessable)
return;
// loop over all our groups and see which ones apply
if (ent.Comp1.MetabolismGroups is null)
continue;
foreach (var group in ent.Comp1.MetabolismGroups)
{
if (!proto.Metabolisms.TryGetValue(group.Id, out var entry))
continue;
var rate = entry.MetabolismRate * group.MetabolismRateModifier;
// Remove $rate, as long as there's enough reagent there to actually remove that much
mostToRemove = FixedPoint2.Clamp(rate, 0, quantity);
float scale = (float) mostToRemove / (float) rate;
// if it's possible for them to be dead, and they are,
// then we shouldn't process any effects, but should probably
// still remove reagents
if (TryComp<MobStateComponent>(solutionEntityUid.Value, out var state))
{
if (!proto.WorksOnTheDead && _mobStateSystem.IsDead(solutionEntityUid.Value, state))
continue;
}
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
var ev = new TryMetabolizeReagent(reagent, proto, quantity);
RaiseLocalEvent(actualEntity, ref ev);
var args = new ReagentEffectArgs(actualEntity, ent, solution, proto, mostToRemove,
EntityManager, null, scale * ev.Scale, ev.QuantityMultiplier);
// do all effects, if conditions apply
foreach (var effect in entry.Effects)
{
if (!effect.ShouldApply(args, _random))
continue;
if (effect.ShouldLog)
{
_adminLogger.Add(
LogType.ReagentEffect,
effect.LogImpact,
$"Metabolism effect {effect.GetType().Name:effect}"
+ $" of reagent {proto.LocalizedName:reagent}"
+ $" applied on entity {actualEntity:entity}"
+ $" at {Transform(actualEntity).Coordinates:coordinates}"
);
}
effect.Effect(args);
}
}
// remove a certain amount of reagent
if (mostToRemove > FixedPoint2.Zero)
{
solution.RemoveReagent(reagent, mostToRemove);
// We have processed a reagant, so count it towards the cap
reagents += 1;
}
}
_solutionContainerSystem.UpdateChemicals(soln.Value);
}
}
[ByRefEvent]
public readonly record struct ApplyMetabolicMultiplierEvent(
EntityUid Uid,
float Multiplier,
bool Apply)
{
/// <summary>
/// The entity whose metabolism is being modified.
/// </summary>
public readonly EntityUid Uid = Uid;
/// <summary>
/// What the metabolism's update rate will be multiplied by.
/// </summary>
public readonly float Multiplier = Multiplier;
/// <summary>
/// If true, apply the multiplier. If false, revert it.
/// </summary>
public readonly bool Apply = Apply;
}
}
[ByRefEvent]
public record struct TryMetabolizeReagent(ReagentId Reagent, ReagentPrototype Prototype, FixedPoint2 Quantity, float Scale = 1f, float QuantityMultiplier = 1f);