Skip to content

Commit

Permalink
Lamia & Segmented Entity System (#11)
Browse files Browse the repository at this point in the history
## 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**

:cl: 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 <[email protected]>
Co-authored-by: Rane <[email protected]>
Co-authored-by: Aiden <[email protected]>
  • Loading branch information
3 people authored Jan 4, 2025
1 parent b958425 commit 1e356fb
Show file tree
Hide file tree
Showing 225 changed files with 2,110 additions and 320 deletions.
172 changes: 172 additions & 0 deletions Content.Client/DeltaV/Lamiae/SnakeOverlay.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
using Content.Shared.SegmentedEntity;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Client.Resources;
using Robust.Client.ResourceManagement;
using Robust.Client.Graphics;
using Robust.Shared.Enums;
using System.Numerics;
using System.Linq;


namespace Content.Client.Lamiae;

/// <summary>
/// This draws lamia segments directly from polygons instead of sprites. This is a very novel approach as of the time this is being written (August 2024) but it wouldn't surprise me
/// if there's a better way to do this at some point. Currently we have a very heavy restriction on the tools we can make, forcing me to make several helpers that may be redundant later.
/// This will be overcommented because I know you haven't seen code like this before and you might want to copy it.
/// This is an expansion on some techniques I discovered in (https://github.com/Elijahrane/Delta-v/blob/49d76c437740eab79fc622ab50d628b926e6ddcb/Content.Client/DeltaV/Arcade/S3D/Renderer/S3DRenderer.cs)
/// </summary>
public sealed class SnakeOverlay : Overlay
{
private readonly IResourceCache _resourceCache;
private readonly IEntityManager _entManager;
private readonly SharedTransformSystem _transform;
private readonly SharedHumanoidAppearanceSystem _humanoid = default!;

// Look through these carefully. WorldSpace is useful for debugging. Note that this defaults to "screen space" which breaks when you try and get the world handle.
public override OverlaySpace Space => OverlaySpace.WorldSpaceEntities;

// Overlays are strange and you need this pattern where you define readonly deps above, and then make a constructor with this pattern. Anything that creates this overlay will then
// have to provide all the deps.
public SnakeOverlay(IEntityManager entManager, IResourceCache resourceCache)
{
_resourceCache = resourceCache;
// we get ent manager from SnakeOverlaySystem turning this on and passing it
_entManager = entManager;
// with ent manager we can fetch our other entity systems
_transform = _entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
_humanoid = _entManager.EntitySysManager.GetEntitySystem<SharedHumanoidAppearanceSystem>();

// draw at drawdepth 3
ZIndex = 3;
}

// This step occurs each frame. For some overlays you may want to conisder limiting how often they update, but for player entities that move around fast we'll just do it every frame.
protected override void Draw(in OverlayDrawArgs args)
{
// load the handle, the "pen" we draw with
var handle = args.WorldHandle;

// Get all lamiae the client knows of and their transform in a way we can enumerate over
var enumerator = _entManager.AllEntityQueryEnumerator<SegmentedEntityComponent, TransformComponent>();

// I go over the collection above, pulling out an EntityUid and the two components I need for each.
while (enumerator.MoveNext(out var uid, out var lamia, out var xform))
{
// Skip ones that are off-map. "Map" in this context means interconnected stuff you can travel between by moving, rather than needing e.g. FTL to load a new map.
if (xform.MapID != args.MapId)
continue;

// Skip ones where they are not loaded properly, uninitialized, or w/e
if (lamia.Segments.Count < lamia.NumberOfSegments)
{
_entManager.Dirty(uid, lamia); // pls give me an update...
continue;
}

// By the way, there's a hack to mitigate overdraw somewhat. Check out whatever is going on with the variable called "bounds" in DoAfterOverlay.
// I won't do it here because (1) it's ugly and (2) theoretically these entities can be fucking huge and you'll see the tail end of them when they are way off screen.
// On a PVS level I think segmented entities should be all-or-nothing when it comes to PVS range, that is you either load all of their segments or none.

// Color.White is drawing without modifying color. For clothed tails, we should use White. For skin, we should use the color of the marking.
// TODO: Better way to cache this
Color? col = null;
if (_entManager.TryGetComponent<HumanoidAppearanceComponent>(uid, out var humanoid))
if (humanoid.MarkingSet.TryGetCategory(MarkingCategories.Tail, out var tailMarkings))
col = tailMarkings.First().MarkingColors.First();

DrawLamia(handle, lamia, col ?? Color.White);
}
}

// This is where we do the actual drawing.
private void DrawLamia(DrawingHandleWorld handle, SegmentedEntityComponent lamia, Color color)
{
// We're going to store all our verticies in here and then draw them
List<DrawVertexUV2D> verts = new List<DrawVertexUV2D>();

// Radius of the initial segment
float radius = lamia.InitialRadius;

// We're storing the left and right verticies of the last segment so we can start drawing from there without gaps
Vector2? lastPtCW = null;
Vector2? lastPtCCW = null;

var tex = _resourceCache.GetTexture(lamia.TexturePath);

int i = 1;
// do each segment except the last one normally
while (i < lamia.Segments.Count - 1)
{
// get centerpoints of last segment and this one
var origin = _transform.GetWorldPosition(_entManager.GetEntity(lamia.Segments[i - 1]));
var destination = _transform.GetWorldPosition(_entManager.GetEntity(lamia.Segments[i]));

// get direction between the two points and normalize it
var connectorVec = destination - origin;
connectorVec = connectorVec.Normalized();

//get one rotated 90 degrees clockwise
var offsetVecCW = new Vector2(connectorVec.Y, 0 - connectorVec.X);

//and counterclockwise
var offsetVecCCW = new Vector2(0 - connectorVec.Y, connectorVec.X);

/// tri 1: line across first segment and corner of second
if (lastPtCW == null)
{
verts.Add(new DrawVertexUV2D(origin + offsetVecCW * radius, Vector2.Zero));
}
else
{
verts.Add(new DrawVertexUV2D((Vector2) lastPtCW, Vector2.Zero));
}

if (lastPtCCW == null)
{
verts.Add(new DrawVertexUV2D(origin + offsetVecCCW * radius, new Vector2(1, 0)));
}
else
{
verts.Add(new DrawVertexUV2D((Vector2) lastPtCCW, new Vector2(1, 0)));
}

verts.Add(new DrawVertexUV2D(destination + offsetVecCW * radius, new Vector2(0, 1)));

// tri 2: line across second segment and corner of first
if (lastPtCCW == null)
{
verts.Add(new DrawVertexUV2D(origin + offsetVecCCW * radius, new Vector2(1, 0)));
}
else
{
verts.Add(new DrawVertexUV2D((Vector2) lastPtCCW, new Vector2(1, 0)));
}

lastPtCW = destination + offsetVecCW * radius;
verts.Add(new DrawVertexUV2D((Vector2) lastPtCW, new Vector2(0, 1)));
lastPtCCW = destination + offsetVecCCW * radius;
verts.Add(new DrawVertexUV2D((Vector2) lastPtCCW, new Vector2(1, 1)));

// slim down a bit for next segment
radius *= lamia.SlimFactor;

i++;
}

// draw tail (1 tri)
if (lastPtCW != null && lastPtCCW != null)
{
verts.Add(new DrawVertexUV2D((Vector2) lastPtCW, new Vector2(0, 0)));
verts.Add(new DrawVertexUV2D((Vector2) lastPtCCW, new Vector2(1, 0)));

var destination = _transform.GetWorldPosition(_entManager.GetEntity(lamia.Segments.Last()));

verts.Add(new DrawVertexUV2D(destination, new Vector2(0.5f, 1f)));
}

// Draw all of the triangles we just pit in at once
handle.DrawPrimitives(DrawPrimitiveTopology.TriangleList, texture: tex, verts.ToArray().AsSpan(), color);
}
}
26 changes: 26 additions & 0 deletions Content.Client/DeltaV/Lamiae/SnakeOverlaySystem.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;

namespace Content.Client.Lamiae;

/// <summary>
/// This system turns on our always-on overlay. I have no opinion on this design pattern or the existence of this file.
/// It also fetches the deps it needs.
/// </summary>
public sealed class SnakeOverlaySystem : EntitySystem
{
[Dependency] private readonly IOverlayManager _overlay = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;

public override void Initialize()
{
base.Initialize();
_overlay.AddOverlay(new SnakeOverlay(EntityManager, _resourceCache));
}

public override void Shutdown()
{
base.Shutdown();
_overlay.RemoveOverlay<SnakeOverlay>();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using Content.Shared.Ghost;
using Content.Shared.Maps;
using Content.Shared.Parallax;
using Content.Shared.SegmentedEntity;
using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems;
using Content.Shared.StatusEffect;
Expand Down Expand Up @@ -643,7 +644,9 @@ private void KnockOverKids(TransformComponent xform, ref ValueList<EntityUid> to
var childEnumerator = xform.ChildEnumerator;
while (childEnumerator.MoveNext(out var child))
{
if (!_buckleQuery.TryGetComponent(child, out var buckle) || buckle.Buckled)
if (!_buckleQuery.TryGetComponent(child, out var buckle) || buckle.Buckled
|| HasComp<SegmentedEntityComponent>(child)
|| HasComp<SegmentedEntitySegmentComponent>(child))
continue;

toKnock.Add(child);
Expand Down
2 changes: 1 addition & 1 deletion Content.Server/Teleportation/PortalSystem.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Ghost;
using Content.Shared.Mind.Components;
Expand Down
8 changes: 7 additions & 1 deletion Content.Server/Weapons/Ranged/Systems/GunSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,13 @@ public override void Shoot(EntityUid gunUid, GunComponent gun, List<(EntityUid?
if (!rayCastResults.Any())
break;

var result = rayCastResults[0];
var raycastEvent = new HitScanAfterRayCastEvent(rayCastResults);
RaiseLocalEvent(lastUser, ref raycastEvent);

if (raycastEvent.RayCastResults == null)
break;

var result = raycastEvent.RayCastResults[0];

// Check if laser is shot from in a container
if (!_container.IsEntityOrParentInContainer(lastUser))
Expand Down
1 change: 1 addition & 0 deletions Content.Shared/Roles/StartingGearPrototype.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public string GetGear(string slot, HumanoidCharacterProfile? profile)
{
case "jumpsuit" when profile.Clothing == ClothingPreference.Jumpskirt && !string.IsNullOrEmpty(InnerClothingSkirt):
case "jumpsuit" when profile.Species == "Harpy" && !string.IsNullOrEmpty(InnerClothingSkirt):
case "jumpsuit" when profile.Species == "Lamia" && !string.IsNullOrEmpty(InnerClothingSkirt):
return InnerClothingSkirt;
case "back" when profile.Backpack == BackpackPreference.Satchel && !string.IsNullOrEmpty(Satchel):
return Satchel;
Expand Down
11 changes: 11 additions & 0 deletions Content.Shared/SegmentedEntity/SegmentSpawnedEvent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Content.Shared.SegmentedEntity;

public sealed class SegmentSpawnedEvent : EntityEventArgs
{
public EntityUid Lamia = default!;

public SegmentSpawnedEvent(EntityUid lamia)
{
Lamia = lamia;
}
}
101 changes: 101 additions & 0 deletions Content.Shared/SegmentedEntity/SegmentedEntityComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
using Robust.Shared.GameStates;

namespace Content.Shared.SegmentedEntity;

/// <summary>
/// Controls initialization of any Multi-segmented entity
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState]
public sealed partial class SegmentedEntityComponent : Component
{
/// <summary>
/// A list of each UID attached to the Lamia, in order of spawn
/// </summary>
[DataField, AutoNetworkedField]
public List<NetEntity> Segments = new();

/// <summary>
/// A clamped variable that represents the number of segments to be spawned
/// </summary>
[DataField]
public int NumberOfSegments = 18;

/// <summary>
/// How wide the initial segment should be.
/// </summary>
[DataField]
public float InitialRadius = 0.3f;

/// <summary>
/// Texture of the segment.
/// </summary>
[DataField(required: true)]
public string TexturePath;

/// <summary>
/// If UseTaperSystem is true, this constant represents the rate at which a segmented entity will taper towards the tip. Tapering is on a logarithmic scale, and will asymptotically approach 0.
/// </summary>
[DataField]
public float OffsetConstant = 1.03f;

/// <summary>
/// Represents the prototype used to parent all segments
/// </summary>
[DataField]
public string InitialSegmentId = "LamiaInitialSegment";

/// <summary>
/// Represents the segment prototype to be spawned
/// </summary>
[DataField]
public string SegmentId = "LamiaSegment";

/// <summary>
/// How much to slim each successive segment.
/// </summary>
[DataField]
public float SlimFactor = 0.93f;

/// <summary>
/// Set to false for constant width
/// </summary>
[DataField]
public bool UseTaperSystem = true;

/// <summary>
/// The standard distance between the centerpoint of each segment.
/// </summary>
[DataField]
public float StaticOffset = 0.15f;

/// <summary>
/// The standard sprite scale of each segment.
/// </summary>
[DataField]
public float StaticScale = 1f;

/// <summary>
/// Used to more finely tune how much damage should be transfered from tail to body.
/// </summary>
[DataField]
public float DamageModifierOffset = 0.4f;

/// <summary>
/// A clamped variable that represents how far from the tip should tapering begin.
/// </summary>
[DataField]
public int TaperOffset = 18;

/// <summary>
/// Coefficient used to finely tune how much explosion damage should be transfered to the body. This is calculated multiplicatively with the derived damage modifier set.
/// </summary>
[DataField]
public float ExplosiveModifierOffset = 0.1f;

/// <summary>
/// Controls whether or not lamia segments always block bullets, or use the bullet passover system for laying down bodies.
/// </summary>
[DataField]
public bool BulletPassover = true;
}
Loading

0 comments on commit 1e356fb

Please sign in to comment.