Files
wwdpublic/Content.Shared/Teleportation/Systems/SharedPortalSystem.cs
VMSolidus f25c7f4181 Lamia & Segmented Entity System (#11)
## About the PR
This is a draft PR for an upcoming new playable species, the Lamia.
Lamia are an older species from the 2022 era of Nyanotrasen, and were
previously abandoned code that was dropped from the game on October
13th, 2022. I was able to locate what I believe to be the last remaining
branch containing Lamia, from a defunct server repository that ran an
October 12th, 2022 build of Nyanotrasen. Thus I began a project to
refurbish this code for use on modern SS14.

The Lamia I wish to PR are only recognizable from the original Lamia in
that they share the Tail Segment System. That is, they prominently
feature a completely unique mechanic whereby their body is composed of
multiple entities linked together in a chain. The original version of
this code had a great many bugs related to the game's physics system,
and it was severely limited by the Robust Toolbox engine at the time. In
the year since Lamia's abandonment, Robust Toolbox has gone through many
iterations and improvements, and has deprecated large parts of the
physics system that old Lamia utilized.

## Redesigns from the original 2022 Lamia, AKA Nyanotrasen Lamia. Vs.
DeltaV Lamia
The original Nyanotrasen Lamia were made with several limitations in
mind. Their size was heavily restricted by the physics engine at the
time, we aren't beholden to that same limitation anymore. Thus DeltaV
Lamia are vastly larger than the Nyanotrasen Lamia, featuring a tail
that is 5 tiles in length, with fully functional physics collisions.
They were also not able to wear Hardsuits due to limitations of the
SpriteComponent, and thus were instead designed around having a
"Barotrauma resistance". On DeltaV code, we can arbitrarily state that
species use different optional sprites for items, therefore its possible
to have for example a Nukie Hardsuit, with its equipped-outerwear state,
equipped-outerwear-lamia, equipped-outerwear-lamiainitialsegment,
equipped-outerwear-lamiasegment, and so on. The Lamia Segments can
simply state that if they equip a hardsuit, they utilize the
-lamiasegment sprite option. Therefore its no longer necessary to create
an entire new item solely so that snakes can wear a hardsuit.

**Positive Traits**
- Extreme Size. Lamia are 5 tiles long, and weigh as much as a car. They
make a mockery of mass contests, and they can push physics objects
around simply by slithering into them.
- Significantly larger health bar. A Lamia is put into critical
condition at 200 damage, and dies at 300 damage. This is offset by the
Lamia having a certain percentage of damage taken by the tail
transferred to the main body.
- High resistance to forced movement. Space Wind at standard pressure
cannot move them.
- Unusual hybrid damage melee via their Hypo-Fangs. Lamia bite attacks
deal 1 point of armor piercing, 2 points of poison, 2 points of
asphyxiation, and inject 3u of Space Drugs. Planned traitor items exist
that add a fillable chemical reservoir that they can inject into people
with their attacks.

**Negative Traits**
- Extreme Size. Lamia are literally the size of a, "Broad side of a
barn". A blind person could throw a rock, and still hit the Lamia. By
extension its essentially impossible for a Lamia to evade attacks. Yea
you can take hits, but you're also going to *take* hits. All of them.
- Paramedics WORST ENEMY. Since they weigh as much as a car, even Oni
struggle to drag them. Even a rollerbed only slightly helps drag a Lamia
around.
- Vulnerability to AOE damage. An explosion that simultaneously strikes
a Lamia's entire tail, plus their body, will deal double damage.
- Cannot wear shoes. Although not being able to wear magboots is also
offset to their natural high resistance to space wind.
- Extreme size also means Lamia are functionally uncloneable. They
require 770 units of Biomass, the equivalent of 5.5 Onis, in order to be
cloned.

## Why / Balance
This PR is part of an ongoing project to add exciting new content to the
DeltaV repository, with a focus on keeping the theme of "Monster People"
species, per request by admins.

## Technical details
The code regarding Tail Segments is actually unfinished, and still needs
significant overhauling before this PR can be undrafted. Here's a few
concerns:

- [x] Implement "Marking Parity"
- [x] Make/Commission/Request new markings for the Lamia and her tail
segments
- [x] Implement "Hardsuit Appearance Parity"
- [x] ServerLamiaSystem now utilizes new physics engine options.
- [x] "We need to be able to spawn 80 Lamias without slowing down the
server -Debug", this is a hard requirement. Having 32 tail segments is
not required. I would prefer that we have 32 tail segments, but if we
optimize their code for performance and still find out we aren't meeting
the 80 Lamia hard requirement, I am willing to reduce them to as low as
16 to 20 segments.
- [x] Implement Wizden's upcoming "Shoot Over Corpses unless they're
targetted" for Lamia Tails.
- [x] Reimplement the mechanic for Segments sharing their healthbar with
the Lamia.
- [x] Possibly make it so that Lamia can only wear Jumpskirts? I'd want
to outright get rid of the layer mask if possible.

## Non-Technical TODO list
These are all the TODO's that don't necessarily involve C#, and
primarily live in the YAML side of things.

- [x] Implement marking customization for Lamia. They should have
marking variations for More/Less humanlike versions. Such as a Snake
Head(We can re-use the Lizard snake head), Medusa Head, changing how far
up the scales go, and if the Lamia has human skin or full scales. I'd
like to have tail pattern variations that can be set in the character
customization, but that is also pending the VisualizerSystem for tail
segments.
- [x] Make their hardsuit variants. Not actually difficult, just takes
some time.
- [x] Finely tune their numerical values. Basically nothing on the YAML
side of things is final, and is subject to change pending beta feedback
and/or testing.

## Media

![spacenoodle](https://github.com/DeltaV-Station/Delta-v/assets/16548818/a97de084-5fd3-4b27-b8ea-69786d1dbdcc)

One of the downsides of having extreme mass.

![image](https://github.com/DeltaV-Station/Delta-v/assets/16548818/840f694a-8898-4ada-b5dd-df7f2fc1299e)

Working Collision physics:
![Noodle
movement](https://github.com/DeltaV-Station/Delta-v/assets/16548818/70ccebcc-5446-4bda-9bb5-40edc65f55f6)

Finalized version of the damage system, also featuring significant
improvements to the tail systems.
![damage
system](https://github.com/Simple-Station/Einstein-Engines/assets/16548818/42918aab-f40d-4da2-bfc2-c70055facee0)

**Changelog**

🆑 VMSolidus, @Elijahrane, and @noctyrnal
- add: Lamia have been added to the game as a new playable species! They
are currently extremely buggy, and so are by default disabled as a
roundstart species. To enable them for (Buggy) playtesting, go to
/Species/lamia.yml, and set roundstart to true.

---------

Signed-off-by: VMSolidus <evilexecutive@gmail.com>
Co-authored-by: Rane <60792108+Elijahrane@users.noreply.github.com>
Co-authored-by: Aiden <aiden@djkraz.com>

(cherry picked from commit 1e356fbb38120a850fe5abc8a9ecef57f8c049f1)
2025-01-14 01:02:39 +03:00

248 lines
9.4 KiB
C#

using System.Linq;
using Content.Shared.Ghost;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Teleportation.Components;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Physics.Dynamics;
using Robust.Shared.Physics.Events;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Shared.Teleportation.Systems;
/// <summary>
/// This handles teleporting entities through portals, and creating new linked portals.
/// </summary>
public abstract class SharedPortalSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly INetManager _netMan = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly PullingSystem _pulling = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
private const string PortalFixture = "portalFixture";
private const string ProjectileFixture = "projectile";
private const int MaxRandomTeleportAttempts = 20;
/// <inheritdoc/>
public override void Initialize()
{
SubscribeLocalEvent<PortalComponent, StartCollideEvent>(OnCollide);
SubscribeLocalEvent<PortalComponent, EndCollideEvent>(OnEndCollide);
SubscribeLocalEvent<PortalComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
}
private void OnGetVerbs(EntityUid uid, PortalComponent component, GetVerbsEvent<AlternativeVerb> args)
{
// Traversal altverb for ghosts to use that bypasses normal functionality
if (!args.CanAccess || !HasComp<GhostComponent>(args.User))
return;
// Don't use the verb with unlinked or with multi-output portals
// (this is only intended to be useful for ghosts to see where a linked portal leads)
var disabled = !TryComp<LinkedEntityComponent>(uid, out var link) || link.LinkedEntities.Count != 1;
args.Verbs.Add(new AlternativeVerb
{
Priority = 11,
Act = () =>
{
if (link == null || disabled)
return;
var ent = link.LinkedEntities.First();
TeleportEntity(uid, args.User, Transform(ent).Coordinates, ent, false);
},
Disabled = disabled,
Text = Loc.GetString("portal-component-ghost-traverse"),
Message = disabled
? Loc.GetString("portal-component-no-linked-entities")
: Loc.GetString("portal-component-can-ghost-traverse"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/open.svg.192dpi.png"))
});
}
private bool ShouldCollide(string ourId, string otherId, Fixture our, Fixture other)
{
// most non-hard fixtures shouldn't pass through portals, but projectiles are non-hard as well
// and they should still pass through
return ourId == PortalFixture && (other.Hard || otherId == ProjectileFixture);
}
private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollideEvent args)
{
if (HasComp<PortalExemptComponent>(args.OtherEntity))
return;
if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId, args.OurFixture, args.OtherFixture))
return;
var subject = args.OtherEntity;
// best not.
if (Transform(subject).Anchored)
return;
// break pulls before portal enter so we dont break shit
if (TryComp<PullableComponent>(subject, out var pullable) && pullable.BeingPulled)
{
_pulling.TryStopPull(subject, pullable);
}
if (TryComp<PullerComponent>(subject, out var pullerComp)
&& TryComp<PullableComponent>(pullerComp.Pulling, out var subjectPulling))
{
_pulling.TryStopPull(subject, subjectPulling);
}
// if they came from another portal, just return and wait for them to exit the portal
if (HasComp<PortalTimeoutComponent>(subject))
{
return;
}
if (TryComp<LinkedEntityComponent>(uid, out var link))
{
if (!link.LinkedEntities.Any())
return;
// client can't predict outside of simple portal-to-portal interactions due to randomness involved
// --also can't predict if the target doesn't exist on the client / is outside of PVS
if (_netMan.IsClient)
{
var first = link.LinkedEntities.First();
var exists = Exists(first);
if (link.LinkedEntities.Count != 1 || !exists || (exists && Transform(first).MapID == MapId.Nullspace))
return;
}
// pick a target and teleport there
var target = _random.Pick(link.LinkedEntities);
if (HasComp<PortalComponent>(target))
{
// if target is a portal, signal that they shouldn't be immediately portaled back
var timeout = EnsureComp<PortalTimeoutComponent>(subject);
timeout.EnteredPortal = uid;
Dirty(subject, timeout);
}
TeleportEntity(uid, subject, Transform(target).Coordinates, target);
return;
}
if (_netMan.IsClient)
return;
// no linked entity--teleport randomly
if (component.RandomTeleport)
TeleportRandomly(uid, subject, component);
}
private void OnEndCollide(EntityUid uid, PortalComponent component, ref EndCollideEvent args)
{
if (!ShouldCollide(args.OurFixtureId, args.OtherFixtureId,args.OurFixture, args.OtherFixture))
return;
var subject = args.OtherEntity;
// if they came from (not us), remove the timeout
if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != uid)
{
RemCompDeferred<PortalTimeoutComponent>(subject);
}
}
private void TeleportEntity(EntityUid portal, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity=null, bool playSound=true,
PortalComponent? portalComponent = null)
{
if (!Resolve(portal, ref portalComponent))
return;
var ourCoords = Transform(portal).Coordinates;
var onSameMap = ourCoords.GetMapId(EntityManager) == target.GetMapId(EntityManager);
var distanceInvalid = portalComponent.MaxTeleportRadius != null
&& ourCoords.TryDistance(EntityManager, target, out var distance)
&& distance > portalComponent.MaxTeleportRadius;
if (!onSameMap && !portalComponent.CanTeleportToOtherMaps || distanceInvalid)
{
if (!_netMan.IsServer)
return;
// Early out if this is an invalid configuration
_popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
ourCoords, Filter.Pvs(ourCoords, entityMan: EntityManager), true);
_popup.PopupCoordinates(Loc.GetString("portal-component-invalid-configuration-fizzle"),
target, Filter.Pvs(target, entityMan: EntityManager), true);
QueueDel(portal);
if (targetEntity != null)
QueueDel(targetEntity.Value);
return;
}
var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? portalComponent.ArrivalSound;
var departureSound = portalComponent.DepartureSound;
// Some special cased stuff: projectiles should stop ignoring shooter when they enter a portal, to avoid
// stacking 500 bullets in between 2 portals and instakilling people--you'll just hit yourself instead
// (as expected)
if (TryComp<ProjectileComponent>(subject, out var projectile))
{
projectile.IgnoreShooter = false;
}
LogTeleport(portal, subject, Transform(subject).Coordinates, target);
_transform.SetCoordinates(subject, target);
if (!playSound)
return;
_audio.PlayPredicted(departureSound, portal, subject);
_audio.PlayPredicted(arrivalSound, subject, subject);
}
private void TeleportRandomly(EntityUid portal, EntityUid subject, PortalComponent? component = null)
{
if (!Resolve(portal, ref component))
return;
var xform = Transform(portal);
var coords = xform.Coordinates;
var newCoords = coords.Offset(_random.NextVector2(component.MaxRandomRadius));
for (var i = 0; i < MaxRandomTeleportAttempts; i++)
{
var randVector = _random.NextVector2(component.MaxRandomRadius);
newCoords = coords.Offset(randVector);
if (!_lookup.GetEntitiesIntersecting(newCoords.ToMap(EntityManager, _transform), LookupFlags.Static).Any())
{
break;
}
}
TeleportEntity(portal, subject, newCoords);
}
protected virtual void LogTeleport(EntityUid portal, EntityUid subject, EntityCoordinates source,
EntityCoordinates target)
{
}
}