Files
LuaCsForBarotraumaEP/Barotrauma/BarotraumaShared/Source/Map/Structure.cs
T
Joonas Rikkonen 77a07a95af 1473f77...ece6ead
commit ece6ead54c021d084f406f4f99daa5a0a7ef4b19
Author: Regalis11 <poe.regalis@gmail.com>
Date:   Fri Mar 22 21:52:56 2019 +0200

    v0.8.9.7

commit c10dd821ca1a89b4ae62046cf8e558589ff8e6af
Author: Regalis11 <poe.regalis@gmail.com>
Date:   Fri Mar 22 21:00:02 2019 +0200

    Fixed release builds crashing due to simulatedlatency etc commands not existing in release builds

commit dee0dded80cbbf30d484232b6706dd705a577eb7
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Fri Mar 22 15:24:44 2019 +0200

    Fixed nullref exception if a client disconnects while netstats is enabled

commit c45d5bf0c5a4a68200c9eca461cd04090a5de23a
Author: Joonas Rikkonen <poe.regalis@gmail.com>
Date:   Fri Mar 22 15:24:26 2019 +0200

    Togglehud, toggleupperhud and togglecharacternames don't require a permission to use, made simulatedlatency, simulatedloss and simulatedduplicateschange usable to clients

commit cec1ac6bccac058bc12ddf18c8e060a7a47c9301
Merge: 411cd9726 1473f77ba
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Fri Mar 22 14:01:58 2019 +0200

    Merge branch 'dev' into enemy-ai

commit 411cd9726979668764eea782b515c7510ec4f5a8
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Fri Mar 22 14:01:09 2019 +0200

    If the target has changed, re-evaluate the attacking limb. Fixes Hammerhead getting stuck next to the sub, because it treats the claw as the attacking limb when targeting characters inside the sub. It should use the head, because it has a wall target.

commit 2522bec262f9cb1dea9df75e1d2c22307be5254c
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Fri Mar 22 13:58:18 2019 +0200

    Enemy ai/steering fixes:
    - Store both sim and world positions. Offset the simposition with the subposition and use it for steering. Fixes enemy indoorsteering, which was broken.
    - Use head or torso for steering instead of always using the main limb. Fixes characters like Mudraptor overshooting their targets badly.

commit c667ff9e4edf8af8f95278fbad42e0c8dd37d84c
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Fri Mar 22 13:54:21 2019 +0200

    Improve the ai debug graphics.

commit 4c6c13e07e43a4e3ce2a11dfc4064961c442044a
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Thu Mar 21 17:11:17 2019 +0200

    Refactor, fix and adjust the enemy targeting logic:
    - Change the logic for fading the memories
    - When attacking a wall target, set it as the currently selected ai target so that we adjust the right memory
    - Significantly reduce the value of character targets that are not in the same submarine
    - In aggressive boarding, double the priority of walls when outside. Set the priority to 0 when inside. Reduce the attractiveness of  doors, but still keep the values high.
    - Redefine priorities for Mudraptor and Crawler (wip)

commit b085a95cff6bcf5e3f13e90dd1b71ac15e5ec1ab
Author: itchyOwl <lauri.harkanen@gmail.com>
Date:   Thu Mar 21 13:16:22 2019 +0200

    Allow enemies to target walls by decision (not only when they happen to be on their way). TODO: target only outer walls and only when outside of the sub.
2019-03-23 19:22:13 +02:00

1209 lines
44 KiB
C#

using Barotrauma.Networking;
using Barotrauma.Extensions;
using FarseerPhysics;
using FarseerPhysics.Dynamics;
using FarseerPhysics.Dynamics.Contacts;
using FarseerPhysics.Factories;
using Lidgren.Network;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
namespace Barotrauma
{
partial class WallSection
{
public Rectangle rect;
public float damage;
public Gap gap;
public WallSection(Rectangle rect)
{
System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
this.rect = rect;
damage = 0.0f;
}
public WallSection(Rectangle rect, float damage)
{
System.Diagnostics.Debug.Assert(rect.Width > 0 && rect.Height > 0);
this.rect = rect;
this.damage = 0.0f;
}
}
partial class Structure : MapEntity, IDamageable, IServerSerializable, ISerializableEntity
{
public const int WallSectionSize = 96;
public static List<Structure> WallList = new List<Structure>();
//how much mechanic skill increases per damage removed from the wall by welding
public const float SkillIncreaseMultiplier = 0.005f;
const float LeakThreshold = 0.1f;
private SpriteEffects SpriteEffects = SpriteEffects.None;
//dimensions of the wall sections' physics bodies (only used for debug rendering)
private List<Vector2> bodyDebugDimensions = new List<Vector2>();
public bool Indestructible;
//sections of the wall that are supposed to be rendered
public WallSection[] Sections
{
get;
private set;
}
public override Sprite Sprite
{
get { return prefab.sprite; }
}
public bool IsPlatform
{
get { return Prefab.Platform; }
}
public Direction StairDirection
{
get;
private set;
}
public override string Name
{
get { return prefab.Name; }
}
public bool HasBody
{
get { return Prefab.Body; }
}
public List<Body> Bodies { get; private set; }
public bool CastShadow
{
get { return Prefab.CastShadow; }
}
public bool IsHorizontal { get; private set; }
public int SectionCount
{
get { return Sections.Length; }
}
public float Health
{
get { return Prefab.Health; }
}
public override bool DrawBelowWater
{
get
{
return base.DrawBelowWater || Prefab.BackgroundSprite != null;
}
}
public override bool DrawOverWater
{
get
{
return !DrawDamageEffect;
}
}
public override bool DrawDamageEffect
{
get
{
return Prefab.Body;
}
}
public StructurePrefab Prefab => prefab as StructurePrefab;
public HashSet<string> Tags
{
get { return prefab.Tags; }
}
protected Color spriteColor;
[Editable, Serialize("1.0,1.0,1.0,1.0", true)]
public Color SpriteColor
{
get { return spriteColor; }
set { spriteColor = value; }
}
public override Rectangle Rect
{
get
{
return base.Rect;
}
set
{
Rectangle oldRect = Rect;
base.Rect = value;
if (Prefab.Body) CreateSections();
else
{
foreach (WallSection sec in Sections)
{
Rectangle secRect = sec.rect;
secRect.X -= oldRect.X; secRect.Y -= oldRect.Y;
secRect.X *= value.Width; secRect.X /= oldRect.Width;
secRect.Y *= value.Height; secRect.Y /= oldRect.Height;
secRect.Width *= value.Width; secRect.Width /= oldRect.Width;
secRect.Height *= value.Height; secRect.Height /= oldRect.Height;
secRect.X += value.X; secRect.Y += value.Y;
sec.rect = secRect;
}
}
}
}
public float BodyWidth
{
get { return Prefab.BodyWidth > 0.0f ? Prefab.BodyWidth : rect.Width; }
}
public float BodyHeight
{
get { return Prefab.BodyHeight > 0.0f ? Prefab.BodyHeight : rect.Height; }
}
/// <summary>
/// In radians, takes flipping into account
/// </summary>
public float BodyRotation
{
get
{
float rotation = MathHelper.ToRadians(Prefab.BodyRotation);
if (FlippedX) rotation = -MathHelper.Pi - rotation;
if (FlippedY) rotation = -rotation;
return rotation;
}
}
/// <summary>
/// Offset of the physics body from the center of the structure. Takes flipping into account.
/// </summary>
public Vector2 BodyOffset
{
get
{
Vector2 bodyOffset = Prefab.BodyOffset;
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
return bodyOffset;
}
}
public Dictionary<string, SerializableProperty> SerializableProperties
{
get;
private set;
}
public override void Move(Vector2 amount)
{
base.Move(amount);
for (int i = 0; i < Sections.Length; i++)
{
Rectangle r = Sections[i].rect;
r.X += (int)amount.X;
r.Y += (int)amount.Y;
Sections[i].rect = r;
}
if (Bodies != null)
{
Vector2 simAmount = ConvertUnits.ToSimUnits(amount);
foreach (Body b in Bodies)
{
b.SetTransform(b.Position + simAmount, b.Rotation);
}
}
#if CLIENT
if (convexHulls!=null)
{
convexHulls.ForEach(x => x.Move(amount));
}
#endif
}
public Structure(Rectangle rectangle, StructurePrefab sp, Submarine submarine)
: base(sp, submarine)
{
if (rectangle.Width == 0 || rectangle.Height == 0) return;
System.Diagnostics.Debug.Assert(rectangle.Width > 0 && rectangle.Height > 0);
rect = rectangle;
#if CLIENT
TextureScale = sp.TextureScale;
#endif
spriteColor = prefab.SpriteColor;
if (ResizeHorizontal && !ResizeVertical)
{
IsHorizontal = true;
}
else if (ResizeVertical && !ResizeHorizontal)
{
IsHorizontal = false;
}
else
{
if (BodyWidth > 0.0f && BodyHeight > 0.0f)
{
IsHorizontal = BodyWidth > BodyHeight;
}
else
{
IsHorizontal = (rect.Width > rect.Height);
}
}
StairDirection = Prefab.StairDirection;
SerializableProperties = SerializableProperty.GetProperties(this);
if (Prefab.Body)
{
Bodies = new List<Body>();
WallList.Add(this);
CreateSections();
UpdateSections();
}
else
{
Sections = new WallSection[1];
Sections[0] = new WallSection(rect);
if (StairDirection != Direction.None)
{
CreateStairBodies();
}
}
// Only add ai targets automatically to walls
if (aiTarget == null && HasBody && Tags.Contains("wall"))
{
aiTarget = new AITarget(this);
}
InsertToList();
}
public override string ToString()
{
return Name;
}
public override MapEntity Clone()
{
var clone = new Structure(rect, Prefab, Submarine);
foreach (KeyValuePair<string, SerializableProperty> property in SerializableProperties)
{
if (!property.Value.Attributes.OfType<Editable>().Any()) continue;
clone.SerializableProperties[property.Key].TrySetValue(clone, property.Value.GetValue(this));
}
if (FlippedX) clone.FlipX(false);
if (FlippedY) clone.FlipY(false);
return clone;
}
private void CreateStairBodies()
{
Bodies = new List<Body>();
float bodyWidth = ConvertUnits.ToSimUnits(rect.Width * Math.Sqrt(2.0));
float bodyHeight = ConvertUnits.ToSimUnits(10);
Body newBody = BodyFactory.CreateRectangle(GameMain.World,
bodyWidth, bodyHeight, 1.5f);
newBody.BodyType = BodyType.Static;
Vector2 stairPos = new Vector2(Position.X, rect.Y - rect.Height + rect.Width / 2.0f);
/*stairPos += new Vector2(
(StairDirection == Direction.Right) ? -Submarine.GridSize.X * 1.5f : Submarine.GridSize.X * 1.5f,
-Submarine.GridSize.Y * 2.0f);*/
newBody.Rotation = (StairDirection == Direction.Right) ? MathHelper.PiOver4 : -MathHelper.PiOver4;
newBody.CollisionCategories = Physics.CollisionStairs;
newBody.Friction = 0.8f;
newBody.UserData = this;
newBody.Position = ConvertUnits.ToSimUnits(stairPos) + BodyOffset;
bodyDebugDimensions.Add(new Vector2(bodyWidth, bodyHeight));
Bodies.Add(newBody);
}
private void CreateSections()
{
int xsections = 1, ysections = 1;
int width = rect.Width, height = rect.Height;
if (!HasBody)
{
if (FlippedX && IsHorizontal)
{
xsections = (int)Math.Ceiling((float)rect.Width / prefab.sprite.SourceRect.Width);
width = prefab.sprite.SourceRect.Width;
}
else if (FlippedY && !IsHorizontal)
{
ysections = (int)Math.Ceiling((float)rect.Height / prefab.sprite.SourceRect.Height);
width = prefab.sprite.SourceRect.Height;
}
else
{
xsections = 1;
ysections = 1;
}
Sections = new WallSection[xsections];
}
else
{
if (IsHorizontal)
{
xsections = (int)Math.Ceiling((float)rect.Width / WallSectionSize);
Sections = new WallSection[xsections];
width = (int)WallSectionSize;
}
else
{
ysections = (int)Math.Ceiling((float)rect.Height / WallSectionSize);
Sections = new WallSection[ysections];
height = (int)WallSectionSize;
}
}
for (int x = 0; x < xsections; x++)
{
for (int y = 0; y < ysections; y++)
{
if (FlippedX || FlippedY)
{
Rectangle sectionRect = new Rectangle(
FlippedX ? rect.Right - (x + 1) * width : rect.X + x * width,
FlippedY ? rect.Y - rect.Height + (y + 1) * height : rect.Y - y * height,
width, height);
if (FlippedX)
{
int over = Math.Max(rect.X - sectionRect.X, 0);
sectionRect.X += over;
sectionRect.Width -= over;
}
else
{
sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f);
}
if (FlippedY)
{
int over = Math.Max(sectionRect.Y - rect.Y, 0);
sectionRect.Y -= over;
sectionRect.Height -= over;
}
else
{
sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
}
//sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
int xIndex = FlippedX && IsHorizontal ? (xsections - 1 - x) : x;
int yIndex = FlippedY && !IsHorizontal ? (ysections - 1 - y) : y;
Sections[xIndex + yIndex] = new WallSection(sectionRect);
}
else
{
Rectangle sectionRect = new Rectangle(rect.X + x * width, rect.Y - y * height, width, height);
sectionRect.Width -= (int)Math.Max(sectionRect.Right - rect.Right, 0.0f);
sectionRect.Height -= (int)Math.Max((rect.Y - rect.Height) - (sectionRect.Y - sectionRect.Height), 0.0f);
Sections[x + y] = new WallSection(sectionRect);
}
}
}
}
private Rectangle GenerateMergedRect(List<WallSection> mergedSections)
{
if (IsHorizontal)
return new Rectangle(mergedSections.Min(x => x.rect.Left), mergedSections.Max(x => x.rect.Top),
mergedSections.Sum(x => x.rect.Width), mergedSections.First().rect.Height);
else
{
return new Rectangle(mergedSections.Min(x => x.rect.Left), mergedSections.Max(x => x.rect.Top),
mergedSections.First().rect.Width, mergedSections.Sum(x => x.rect.Height));
}
}
private static Vector2[] CalculateExtremes(Rectangle sectionRect)
{
Vector2[] corners = new Vector2[4];
corners[0] = new Vector2(sectionRect.X, sectionRect.Y - sectionRect.Height);
corners[1] = new Vector2(sectionRect.X, sectionRect.Y);
corners[2] = new Vector2(sectionRect.Right, sectionRect.Y);
corners[3] = new Vector2(sectionRect.Right, sectionRect.Y - sectionRect.Height);
return corners;
}
/// <summary>
/// Checks if there's a structure items can be attached to at the given position and returns it.
/// </summary>
public static Structure GetAttachTarget(Vector2 worldPosition)
{
foreach (MapEntity mapEntity in mapEntityList)
{
if (!(mapEntity is Structure structure)) continue;
if (!structure.Prefab.AllowAttachItems) continue;
if (structure.Bodies != null && structure.Bodies.Count > 0) continue;
Rectangle worldRect = mapEntity.WorldRect;
if (worldPosition.X < worldRect.X || worldPosition.X > worldRect.Right) continue;
if (worldPosition.Y > worldRect.Y || worldPosition.Y < worldRect.Y - worldRect.Height) continue;
return structure;
}
return null;
}
public override bool IsMouseOn(Vector2 position)
{
if (!base.IsMouseOn(position)) { return false; }
if (StairDirection == Direction.None)
{
Vector2 rectSize = rect.Size.ToVector2();
if (BodyWidth > 0.0f) { rectSize.X = BodyWidth; }
if (BodyHeight > 0.0f) { rectSize.Y = BodyHeight; }
Vector2 bodyPos = WorldPosition + BodyOffset;
Vector2 transformedMousePos = MathUtils.RotatePointAroundTarget(position, bodyPos, MathHelper.ToDegrees(BodyRotation));
return
Math.Abs(transformedMousePos.X - bodyPos.X) < rectSize.X / 2.0f &&
Math.Abs(transformedMousePos.Y - bodyPos.Y) < rectSize.Y / 2.0f;
}
else
{
if (StairDirection == Direction.Left)
{
return MathUtils.LineToPointDistance(new Vector2(WorldRect.X, WorldRect.Y), new Vector2(WorldRect.Right, WorldRect.Y - WorldRect.Height), position) < 40.0f;
}
else
{
return MathUtils.LineToPointDistance(new Vector2(WorldRect.X, WorldRect.Y - rect.Height), new Vector2(WorldRect.Right, WorldRect.Y), position) < 40.0f;
}
}
}
public override void ShallowRemove()
{
base.ShallowRemove();
if (WallList.Contains(this)) WallList.Remove(this);
if (Bodies != null)
{
foreach (Body b in Bodies)
GameMain.World.RemoveBody(b);
}
if (Sections != null)
{
foreach (WallSection s in Sections)
{
if (s.gap != null)
{
s.gap.Remove();
s.gap = null;
}
}
}
#if CLIENT
if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
#endif
}
public override void Remove()
{
base.Remove();
if (WallList.Contains(this)) WallList.Remove(this);
if (Bodies != null)
{
foreach (Body b in Bodies)
GameMain.World.RemoveBody(b);
}
if (Sections != null)
{
foreach (WallSection s in Sections)
{
if (s.gap != null)
{
s.gap.Remove();
s.gap = null;
}
}
}
#if CLIENT
if (convexHulls != null) convexHulls.ForEach(x => x.Remove());
#endif
}
private bool OnWallCollision(Fixture f1, Fixture f2, Contact contact)
{
if (Prefab.Platform)
{
if (f2.Body.UserData is Limb limb)
{
if (limb.character.AnimController.IgnorePlatforms) return false;
}
}
if (f2.Body.UserData is Limb)
{
var character = ((Limb)f2.Body.UserData).character;
if (character.DisableImpactDamageTimer > 0.0f || ((Limb)f2.Body.UserData).Mass < 100.0f) return true;
}
if (!Prefab.Platform && Prefab.StairDirection == Direction.None)
{
Vector2 pos = ConvertUnits.ToDisplayUnits(f2.Body.Position);
int section = FindSectionIndex(pos);
if (section > -1)
{
Vector2 normal = contact.Manifold.LocalNormal;
float impact = Vector2.Dot(f2.Body.LinearVelocity, -normal) * f2.Body.Mass * 0.1f;
if (impact < 10.0f) return true;
#if CLIENT
SoundPlayer.PlayDamageSound("StructureBlunt", impact, SectionPosition(section, true), tags: Tags);
#endif
AddDamage(section, impact);
}
}
return true;
}
public WallSection GetSection(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return null;
return Sections[sectionIndex];
}
public bool SectionBodyDisabled(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false;
return (Sections[sectionIndex].damage >= Prefab.Health);
}
/// <summary>
/// Sections that are leaking have a gap placed on them
/// </summary>
public bool SectionIsLeaking(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return false;
return (Sections[sectionIndex].damage >= Prefab.Health * LeakThreshold);
}
public int SectionLength(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0;
return (IsHorizontal ? Sections[sectionIndex].rect.Width : Sections[sectionIndex].rect.Height);
}
public void AddDamage(int sectionIndex, float damage, Character attacker = null)
{
if (!Prefab.Body || Prefab.Platform || Indestructible) return;
if (sectionIndex < 0 || sectionIndex > Sections.Length - 1) return;
var section = Sections[sectionIndex];
#if CLIENT
float particleAmount = Math.Min(Health - section.damage, damage) * Rand.Range(0.01f, 1.0f);
particleAmount = Math.Min(particleAmount + Rand.Range(-5, 1), 5);
for (int i = 0; i < particleAmount; i++)
{
Vector2 particlePos = new Vector2(
Rand.Range(section.rect.X, section.rect.Right),
Rand.Range(section.rect.Y - section.rect.Height, section.rect.Y));
if (Submarine != null) particlePos += Submarine.DrawPosition;
var particle = GameMain.ParticleManager.CreateParticle("shrapnel", particlePos, Rand.Vector(Rand.Range(1.0f, 50.0f)));
if (particle == null) break;
}
#endif
#if CLIENT
if (GameMain.Client == null)
{
#endif
SetDamage(sectionIndex, section.damage + damage, attacker);
#if CLIENT
}
#endif
}
public int FindSectionIndex(Vector2 displayPos, bool world = false, bool clamp = false)
{
if (!Sections.Any()) return -1;
if (world && Submarine != null)
{
displayPos -= Submarine.Position;
}
//if the sub has been flipped horizontally, the first section may be smaller than wallSectionSize
//and we need to adjust the position accordingly
if (Sections[0].rect.Width < WallSectionSize)
{
displayPos.X += WallSectionSize - Sections[0].rect.Width;
}
int index = (IsHorizontal) ?
(int)Math.Floor((displayPos.X - rect.X) / WallSectionSize) :
(int)Math.Floor((rect.Y - displayPos.Y) / WallSectionSize);
if (clamp)
{
index = MathHelper.Clamp(index, 0, Sections.Length - 1);
}
else if (index < 0 || index > Sections.Length - 1)
{
return -1;
}
return index;
}
public float SectionDamage(int sectionIndex)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return 0.0f;
return Sections[sectionIndex].damage;
}
public Vector2 SectionPosition(int sectionIndex, bool world = false)
{
if (sectionIndex < 0 || sectionIndex >= Sections.Length) return Vector2.Zero;
if (Prefab.BodyRotation == 0.0f)
{
Vector2 sectionPos = new Vector2(
Sections[sectionIndex].rect.X + Sections[sectionIndex].rect.Width / 2.0f,
Sections[sectionIndex].rect.Y - Sections[sectionIndex].rect.Height / 2.0f);
if (world && Submarine != null) sectionPos += Submarine.Position;
return sectionPos;
}
else
{
Rectangle sectionRect = Sections[sectionIndex].rect;
float diffFromCenter;
if (IsHorizontal)
{
diffFromCenter = (sectionRect.Center.X - rect.Center.X) / (float)rect.Width * BodyWidth;
}
else
{
diffFromCenter = ((sectionRect.Y - sectionRect.Height / 2) - (rect.Y - rect.Height / 2)) / (float)rect.Height * BodyHeight;
}
if (FlippedX) diffFromCenter = -diffFromCenter;
Vector2 sectionPos = Position + new Vector2(
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
(float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter;
if (world && Submarine != null) sectionPos += Submarine.Position;
return sectionPos;
}
}
partial void AdjustKarma(IDamageable attacker, float amount);
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false)
{
if (Submarine != null && Submarine.GodMode) return new AttackResult(0.0f, null);
if (!Prefab.Body || Prefab.Platform || Indestructible) return new AttackResult(0.0f, null);
Vector2 transformedPos = worldPosition;
if (Submarine != null) transformedPos -= Submarine.Position;
float damageAmount = 0.0f;
for (int i = 0; i < SectionCount; i++)
{
Rectangle sectionRect = Sections[i].rect;
sectionRect.Y -= Sections[i].rect.Height;
if (MathUtils.CircleIntersectsRectangle(transformedPos, attack.DamageRange, sectionRect))
{
damageAmount = attack.GetStructureDamage(deltaTime);
AddDamage(i, damageAmount, attacker);
#if CLIENT
GameMain.ParticleManager.CreateParticle("dustcloud", SectionPosition(i), 0.0f, 0.0f);
#endif
}
}
#if CLIENT
if (playSound)
{
SoundPlayer.PlayDamageSound(attack.StructureSoundType, damageAmount, worldPosition, tags: Tags);
}
#endif
return new AttackResult(damageAmount, null);
}
private void SetDamage(int sectionIndex, float damage, Character attacker = null, bool createNetworkEvent = true)
{
if (Submarine != null && Submarine.GodMode || Indestructible) return;
if (!Prefab.Body) return;
if (!MathUtils.IsValid(damage)) return;
damage = MathHelper.Clamp(damage, 0.0f, Prefab.Health);
#if SERVER
if (GameMain.Server != null && createNetworkEvent && damage != Sections[sectionIndex].damage)
{
GameMain.Server.CreateEntityEvent(this);
}
bool noGaps = true;
for (int i = 0; i < Sections.Length; i++)
{
if (i != sectionIndex && SectionIsLeaking(i))
{
noGaps = false;
break;
}
}
#endif
if (damage < Prefab.Health * LeakThreshold)
{
if (Sections[sectionIndex].gap != null)
{
#if SERVER
//the structure doesn't have any other gap, log the structure being fixed
if (noGaps && attacker != null)
{
GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall repaired by " + attacker.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
DebugConsole.Log("Removing gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") from wall " + ID);
//remove existing gap if damage is below leak threshold
Sections[sectionIndex].gap.Open = 0.0f;
Sections[sectionIndex].gap.Remove();
Sections[sectionIndex].gap = null;
}
}
else
{
if (Sections[sectionIndex].gap == null)
{
Rectangle gapRect = Sections[sectionIndex].rect;
float diffFromCenter;
if (IsHorizontal)
{
diffFromCenter = (gapRect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
if (BodyWidth > 0.0f) gapRect.Width = (int)(BodyWidth * (gapRect.Width / (float)this.rect.Width));
if (BodyHeight > 0.0f) gapRect.Height = (int)BodyHeight;
}
else
{
diffFromCenter = ((gapRect.Y - gapRect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight;
if (BodyWidth > 0.0f) gapRect.Width = (int)BodyWidth;
if (BodyHeight > 0.0f) gapRect.Height = (int)(BodyHeight * (gapRect.Height / (float)this.rect.Height));
}
if (FlippedX) diffFromCenter = -diffFromCenter;
if (BodyRotation != 0.0f)
{
Vector2 structureCenter = Position;
Vector2 gapPos = structureCenter + new Vector2(
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
(float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * diffFromCenter;
gapRect = new Rectangle((int)(gapPos.X - gapRect.Width / 2), (int)(gapPos.Y + gapRect.Height / 2), gapRect.Width, gapRect.Height);
}
gapRect.X -= 10;
gapRect.Y += 10;
gapRect.Width += 20;
gapRect.Height += 20;
bool horizontalGap = !IsHorizontal;
if (Prefab.BodyRotation != 0.0f)
{
//rotation within a 90 deg sector (e.g. 100 -> 10, 190 -> 10, -10 -> 80)
float sectorizedRotation = MathUtils.WrapAngleTwoPi(BodyRotation) % MathHelper.PiOver2;
//diagonal if 30 < angle < 60
bool diagonal = sectorizedRotation > MathHelper.Pi / 6 && sectorizedRotation < MathHelper.Pi / 3;
//gaps on the lower half of a diagonal wall are horizontal, ones on the upper half are vertical
if (diagonal)
{
horizontalGap = gapRect.Y - gapRect.Height / 2 < Position.Y;
}
}
Sections[sectionIndex].gap = new Gap(gapRect, horizontalGap, Submarine);
//free the ID, because if we give gaps IDs we have to make sure they always match between the clients and the server and
//that clients create them in the correct order along with every other entity created/removed during the round
//which COULD be done via entityspawner, but it's unnecessary because we never access these gaps by ID
Sections[sectionIndex].gap.FreeID();
Sections[sectionIndex].gap.ShouldBeSaved = false;
Sections[sectionIndex].gap.ConnectedWall = this;
DebugConsole.Log("Created gap (ID " + Sections[sectionIndex].gap.ID + ", section: " + sectionIndex + ") on wall " + ID);
//AdjustKarma(attacker, 300);
#if SERVER
//the structure didn't have any other gaps yet, log the breach
if (noGaps && attacker != null)
{
GameServer.Log((Sections[sectionIndex].gap.IsRoomToRoom ? "Inner" : "Outer") + " wall breached by " + attacker.Name, ServerLog.MessageType.ItemInteraction);
}
#endif
}
float gapOpen = (damage / Prefab.Health - LeakThreshold) * (1.0f / (1.0f - LeakThreshold));
Sections[sectionIndex].gap.Open = gapOpen;
}
float damageDiff = damage - Sections[sectionIndex].damage;
bool hadHole = SectionBodyDisabled(sectionIndex);
Sections[sectionIndex].damage = MathHelper.Clamp(damage, 0.0f, Prefab.Health);
//otherwise it's possible to infinitely gain karma by welding fixed things
if (attacker != null && damageDiff != 0.0f)
{
AdjustKarma(attacker, damageDiff);
#if CLIENT
if (GameMain.Client == null)
{
#endif
if (damageDiff < 0.0f)
{
attacker.Info.IncreaseSkillLevel("mechanical",
-damageDiff * SkillIncreaseMultiplier / Math.Max(attacker.GetSkillLevel("mechanical"), 1.0f),
SectionPosition(sectionIndex, true));
}
#if CLIENT
}
#endif
}
bool hasHole = SectionBodyDisabled(sectionIndex);
if (hadHole == hasHole) return;
UpdateSections();
}
public void SetCollisionCategory(Category collisionCategory)
{
if (Bodies == null) return;
foreach (Body body in Bodies)
{
body.CollisionCategories = collisionCategory;
}
}
private void UpdateSections()
{
foreach (Body b in Bodies)
{
GameMain.World.RemoveBody(b);
}
Bodies.Clear();
bodyDebugDimensions.Clear();
#if CLIENT
convexHulls?.ForEach(ch => ch.Remove());
convexHulls?.Clear();
#endif
bool hasHoles = false;
var mergedSections = new List<WallSection>();
for (int i = 0; i < Sections.Length; i++ )
{
// if there is a gap and we have sections to merge, do it.
if (SectionBodyDisabled(i))
{
hasHoles = true;
if (!mergedSections.Any()) continue;
var mergedRect = GenerateMergedRect(mergedSections);
mergedSections.Clear();
CreateRectBody(mergedRect, createConvexHull: true);
}
else
{
mergedSections.Add(Sections[i]);
}
}
// take care of any leftover pieces
if (mergedSections.Count > 0)
{
var mergedRect = GenerateMergedRect(mergedSections);
CreateRectBody(mergedRect, createConvexHull: true);
}
//if the section has holes (or is just one big hole with no bodies),
//we need a sensor for repairtools to be able to target the structure
if (hasHoles || !Bodies.Any())
{
Body sensorBody = CreateRectBody(rect, createConvexHull: false);
sensorBody.CollisionCategories = Physics.CollisionRepair;
sensorBody.IsSensor = true;
}
}
private Body CreateRectBody(Rectangle rect, bool createConvexHull)
{
float diffFromCenter;
if (IsHorizontal)
{
diffFromCenter = (rect.Center.X - this.rect.Center.X) / (float)this.rect.Width * BodyWidth;
if (BodyWidth > 0.0f) rect.Width = Math.Max((int)Math.Round(BodyWidth * (rect.Width / (float)this.rect.Width)), 1);
if (BodyHeight > 0.0f) rect.Height = (int)BodyHeight;
}
else
{
diffFromCenter = ((rect.Y - rect.Height / 2) - (this.rect.Y - this.rect.Height / 2)) / (float)this.rect.Height * BodyHeight;
if (BodyWidth > 0.0f) rect.Width = (int)BodyWidth;
if (BodyHeight > 0.0f) rect.Height = Math.Max((int)Math.Round(BodyHeight * (rect.Height / (float)this.rect.Height)), 1);
}
if (FlippedX) diffFromCenter = -diffFromCenter;
Vector2 bodyOffset = ConvertUnits.ToSimUnits(Prefab.BodyOffset);
if (FlippedX) { bodyOffset.X = -bodyOffset.X; }
if (FlippedY) { bodyOffset.Y = -bodyOffset.Y; }
Body newBody = BodyFactory.CreateRectangle(GameMain.World,
ConvertUnits.ToSimUnits(rect.Width),
ConvertUnits.ToSimUnits(rect.Height),
1.5f);
newBody.BodyType = BodyType.Static;
//newBody.Position = ConvertUnits.ToSimUnits(new Vector2(rect.X + rect.Width / 2.0f, rect.Y - rect.Height / 2.0f));
newBody.Friction = 0.5f;
newBody.OnCollision += OnWallCollision;
newBody.CollisionCategories = (Prefab.Platform) ? Physics.CollisionPlatform : Physics.CollisionWall;
newBody.UserData = this;
Vector2 structureCenter = ConvertUnits.ToSimUnits(Position);
if (BodyRotation != 0.0f)
{
newBody.Position = structureCenter + bodyOffset + new Vector2(
(float)Math.Cos(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation),
(float)Math.Sin(IsHorizontal ? -BodyRotation : MathHelper.PiOver2 - BodyRotation)) * ConvertUnits.ToSimUnits(diffFromCenter);
newBody.Rotation = -BodyRotation;
}
else
{
newBody.Position = structureCenter + (IsHorizontal ? Vector2.UnitX : Vector2.UnitY) * ConvertUnits.ToSimUnits(diffFromCenter) + bodyOffset;
}
if (createConvexHull)
{
CreateConvexHull(ConvertUnits.ToDisplayUnits(newBody.Position), rect.Size.ToVector2(), newBody.Rotation);
}
Bodies.Add(newBody);
bodyDebugDimensions.Add(new Vector2(ConvertUnits.ToSimUnits(rect.Width), ConvertUnits.ToSimUnits(rect.Height)));
return newBody;
}
partial void CreateConvexHull(Vector2 position, Vector2 size, float rotation);
public override void FlipX(bool relativeToSub)
{
base.FlipX(relativeToSub);
if (Prefab.CanSpriteFlipX)
{
SpriteEffects ^= SpriteEffects.FlipHorizontally;
}
if (StairDirection != Direction.None)
{
StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
Bodies.ForEach(b => GameMain.World.RemoveBody(b));
Bodies.Clear();
bodyDebugDimensions.Clear();
CreateStairBodies();
}
if (HasBody)
{
CreateSections();
UpdateSections();
}
}
public override void FlipY(bool relativeToSub)
{
base.FlipY(relativeToSub);
if (Prefab.CanSpriteFlipY)
{
SpriteEffects ^= SpriteEffects.FlipVertically;
}
if (StairDirection != Direction.None)
{
StairDirection = StairDirection == Direction.Left ? Direction.Right : Direction.Left;
Bodies.ForEach(b => GameMain.World.RemoveBody(b));
Bodies.Clear();
bodyDebugDimensions.Clear();
CreateStairBodies();
}
if (HasBody)
{
CreateSections();
UpdateSections();
}
}
public static Structure Load(XElement element, Submarine submarine)
{
string name = element.Attribute("name").Value;
string identifier = element.GetAttributeString("identifier", "");
StructurePrefab prefab = FindPrefab(name, identifier);
if (prefab == null)
{
DebugConsole.ThrowError("Error loading structure - structure prefab \"" + name + "\" (identifier \"" + identifier + "\") not found.");
return null;
}
Rectangle rect = element.GetAttributeRect("rect", Rectangle.Empty);
Structure s = new Structure(rect, prefab, submarine)
{
Submarine = submarine,
ID = (ushort)int.Parse(element.Attribute("ID").Value)
};
foreach (XElement subElement in element.Elements())
{
switch (subElement.Name.ToString())
{
case "section":
int index = subElement.GetAttributeInt("i", -1);
if (index == -1) continue;
s.Sections[index].damage = subElement.GetAttributeFloat("damage", 0.0f);
break;
}
}
if (element.GetAttributeBool("flippedx", false)) s.FlipX(false);
if (element.GetAttributeBool("flippedy", false)) s.FlipY(false);
SerializableProperty.DeserializeProperties(s, element);
return s;
}
public static StructurePrefab FindPrefab(string name, string identifier)
{
StructurePrefab prefab = null;
if (string.IsNullOrEmpty(identifier))
{
//legacy support:
//1. attempt to find a prefab with an empty identifier and a matching name
prefab = MapEntityPrefab.Find(name, "") as StructurePrefab;
//2. not found, attempt to find a prefab with a matching name
if (prefab == null) prefab = MapEntityPrefab.Find(name) as StructurePrefab;
//3. not found, attempt to find a prefab that uses the previous name as an identifier
if (prefab == null) prefab = MapEntityPrefab.Find(null, name) as StructurePrefab;
}
else
{
prefab = MapEntityPrefab.Find(null, identifier) as StructurePrefab;
}
return prefab;
}
public override XElement Save(XElement parentElement)
{
XElement element = new XElement("Structure");
element.Add(
new XAttribute("name", prefab.Name),
new XAttribute("identifier", prefab.Identifier),
new XAttribute("ID", ID),
new XAttribute("rect",
(int)(rect.X - Submarine.HiddenSubPosition.X) + "," +
(int)(rect.Y - Submarine.HiddenSubPosition.Y) + "," +
rect.Width + "," + rect.Height));
if (FlippedX) element.Add(new XAttribute("flippedx", true));
if (FlippedY) element.Add(new XAttribute("flippedy", true));
for (int i = 0; i < Sections.Length; i++)
{
if (Sections[i].damage == 0.0f) continue;
var sectionElement =
new XElement("section",
new XAttribute("i", i),
new XAttribute("damage", Sections[i].damage));
element.Add(sectionElement);
}
SerializableProperty.SerializeProperties(this, element);
parentElement.Add(element);
return element;
}
public override void OnMapLoaded()
{
for (int i = 0; i < Sections.Length; i++)
{
SetDamage(i, Sections[i].damage, createNetworkEvent: false);
}
}
public virtual void Reset()
{
SerializableProperties = SerializableProperty.DeserializeProperties(this, Prefab.ConfigElement);
}
}
}