38f1ddb...178a853: v0.8.9.1, removed content folder
This commit is contained in:
@@ -2,15 +2,17 @@
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class AIController : ISteerable
|
||||
abstract partial class AIController : ISteerable
|
||||
{
|
||||
public enum AIState { None, Attack, GoTo, Escape, Eat }
|
||||
public enum AIState { Idle, Attack, GoTo, Escape, Eat }
|
||||
|
||||
public bool Enabled;
|
||||
|
||||
public readonly Character Character;
|
||||
|
||||
protected AIState state;
|
||||
|
||||
private AIState state;
|
||||
|
||||
protected AITarget selectedAiTarget;
|
||||
|
||||
protected SteeringManager steeringManager;
|
||||
|
||||
@@ -24,7 +26,7 @@ namespace Barotrauma
|
||||
get { return Character.AnimController.TargetMovement; }
|
||||
set { Character.AnimController.TargetMovement = value; }
|
||||
}
|
||||
|
||||
|
||||
public Vector2 SimPosition
|
||||
{
|
||||
get { return Character.SimPosition; }
|
||||
@@ -40,10 +42,35 @@ namespace Barotrauma
|
||||
get { return Character.AnimController.Collider.LinearVelocity; }
|
||||
}
|
||||
|
||||
public virtual bool CanEnterSubmarine
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual bool CanFlip
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual AIObjectiveManager ObjectiveManager
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public AITarget SelectedAiTarget
|
||||
{
|
||||
get { return selectedAiTarget; }
|
||||
}
|
||||
|
||||
public AIState State
|
||||
{
|
||||
get { return state; }
|
||||
set { state = value; }
|
||||
set
|
||||
{
|
||||
if (state == value) return;
|
||||
OnStateChanged(state, value);
|
||||
state = value;
|
||||
}
|
||||
}
|
||||
|
||||
public AIController (Character c)
|
||||
@@ -53,13 +80,13 @@ namespace Barotrauma
|
||||
Enabled = true;
|
||||
}
|
||||
|
||||
public virtual void OnAttacked(Character attacker, float amount) { }
|
||||
public virtual void OnAttacked(Character attacker, AttackResult attackResult) { }
|
||||
|
||||
public virtual void SelectTarget(AITarget target) { }
|
||||
|
||||
public virtual void Update(float deltaTime) { }
|
||||
|
||||
//protected Structure lastStructurePicked;
|
||||
|
||||
protected virtual void OnStateChanged(AIState from, AIState to) { }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -20,15 +21,45 @@ namespace Barotrauma
|
||||
public float SoundRange
|
||||
{
|
||||
get { return soundRange; }
|
||||
set { soundRange = Math.Max(value, 0.0f); }
|
||||
set { soundRange = Math.Max(value, MinSoundRange); }
|
||||
}
|
||||
|
||||
public float SightRange
|
||||
{
|
||||
get { return sightRange; }
|
||||
set { sightRange = Math.Max(value, 0.0f); }
|
||||
set { sightRange = Math.Max(value, MinSightRange); }
|
||||
}
|
||||
|
||||
private float sectorRad = MathHelper.TwoPi;
|
||||
public float SectorDegrees
|
||||
{
|
||||
get { return MathHelper.ToDegrees(sectorRad); }
|
||||
set { sectorRad = MathHelper.ToRadians(value); }
|
||||
}
|
||||
|
||||
private Vector2 sectorDir;
|
||||
public Vector2 SectorDir
|
||||
{
|
||||
get { return sectorDir; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value))
|
||||
{
|
||||
string errorMsg = "Invalid AITarget sector direction (" + value + ")\n" + Environment.StackTrace;
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("AITarget.SectorDir:" + Entity?.ToString(), GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
return;
|
||||
}
|
||||
sectorDir = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string SonarLabel;
|
||||
|
||||
public bool Enabled = true;
|
||||
|
||||
public float MinSoundRange, MinSightRange;
|
||||
|
||||
public Vector2 WorldPosition
|
||||
{
|
||||
get
|
||||
@@ -67,12 +98,27 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public AITarget(Entity e, XElement element) : this(e)
|
||||
{
|
||||
SightRange = MinSightRange = element.GetAttributeFloat("sightrange", 0.0f);
|
||||
SoundRange = MinSoundRange = element.GetAttributeFloat("soundrange", 0.0f);
|
||||
SonarLabel = element.GetAttributeString("sonarlabel", "");
|
||||
}
|
||||
|
||||
public AITarget(Entity e)
|
||||
{
|
||||
Entity = e;
|
||||
List.Add(this);
|
||||
}
|
||||
|
||||
public bool IsWithinSector(Vector2 worldPosition)
|
||||
{
|
||||
if (sectorRad >= MathHelper.TwoPi) return true;
|
||||
|
||||
Vector2 diff = worldPosition - WorldPosition;
|
||||
return MathUtils.GetShortestAngle(MathUtils.VectorToAngle(diff), MathUtils.VectorToAngle(sectorDir)) <= sectorRad * 0.5f;
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
List.Remove(this);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -11,10 +13,19 @@ namespace Barotrauma
|
||||
|
||||
private AIObjectiveManager objectiveManager;
|
||||
|
||||
private AITarget selectedAiTarget;
|
||||
|
||||
private float updateObjectiveTimer;
|
||||
|
||||
private bool shouldCrouch;
|
||||
private float crouchRaycastTimer;
|
||||
const float CrouchRaycastInterval = 1.0f;
|
||||
|
||||
private SteeringManager outsideSteering, insideSteering;
|
||||
|
||||
public override AIObjectiveManager ObjectiveManager
|
||||
{
|
||||
get { return objectiveManager; }
|
||||
}
|
||||
|
||||
public Order CurrentOrder
|
||||
{
|
||||
get;
|
||||
@@ -29,7 +40,8 @@ namespace Barotrauma
|
||||
|
||||
public HumanAIController(Character c) : base(c)
|
||||
{
|
||||
steeringManager = new IndoorsSteeringManager(this, true);
|
||||
insideSteering = new IndoorsSteeringManager(this, true, false);
|
||||
outsideSteering = new SteeringManager(this);
|
||||
|
||||
objectiveManager = new AIObjectiveManager(c);
|
||||
objectiveManager.AddObjective(new AIObjectiveFindSafety(c));
|
||||
@@ -44,12 +56,23 @@ namespace Barotrauma
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (DisableCrewAI || Character.IsUnconscious) return;
|
||||
|
||||
if (Character.Submarine != null || selectedAiTarget?.Entity?.Submarine != null)
|
||||
{
|
||||
if (steeringManager != insideSteering) insideSteering.Reset();
|
||||
steeringManager = insideSteering;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (steeringManager != outsideSteering) outsideSteering.Reset();
|
||||
steeringManager = outsideSteering;
|
||||
}
|
||||
|
||||
(Character.AnimController as HumanoidAnimController).Crouching = shouldCrouch;
|
||||
CheckCrouching(deltaTime);
|
||||
Character.ClearInputs();
|
||||
|
||||
//steeringManager = Character.AnimController.CurrentHull == null ? outdoorsSteeringManager : indoorsSteeringManager;
|
||||
|
||||
if (updateObjectiveTimer>0.0f)
|
||||
|
||||
if (updateObjectiveTimer > 0.0f)
|
||||
{
|
||||
updateObjectiveTimer -= deltaTime;
|
||||
}
|
||||
@@ -59,22 +82,23 @@ namespace Barotrauma
|
||||
updateObjectiveTimer = UpdateObjectiveInterval;
|
||||
}
|
||||
|
||||
if (Character.SpeechImpediment < 100.0f)
|
||||
{
|
||||
ReportProblems();
|
||||
UpdateSpeaking();
|
||||
}
|
||||
|
||||
objectiveManager.DoCurrentObjective(deltaTime);
|
||||
|
||||
float currObjectivePriority = objectiveManager.GetCurrentPriority(Character);
|
||||
float moveSpeed = 1.0f;
|
||||
float currObjectivePriority = objectiveManager.GetCurrentPriority();
|
||||
|
||||
if (currObjectivePriority > 30.0f)
|
||||
{
|
||||
moveSpeed *= Character.AnimController.InWater ? Character.AnimController.SwimSpeedMultiplier : Character.AnimController.RunSpeedMultiplier;
|
||||
}
|
||||
|
||||
steeringManager.Update(moveSpeed);
|
||||
bool run = currObjectivePriority > 30.0f;
|
||||
steeringManager.Update(Character.AnimController.GetCurrentSpeed(run));
|
||||
|
||||
bool ignorePlatforms = Character.AnimController.TargetMovement.Y < -0.5f &&
|
||||
(-Character.AnimController.TargetMovement.Y > Math.Abs(Character.AnimController.TargetMovement.X));
|
||||
|
||||
var currPath = (steeringManager as IndoorsSteeringManager).CurrentPath;
|
||||
var currPath = (steeringManager as IndoorsSteeringManager)?.CurrentPath;
|
||||
if (currPath != null && currPath.CurrentNode != null)
|
||||
{
|
||||
if (currPath.CurrentNode.SimPosition.Y < Character.AnimController.GetColliderBottom().Y)
|
||||
@@ -84,22 +108,34 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Character.AnimController.IgnorePlatforms = ignorePlatforms;
|
||||
(Character.AnimController as HumanoidAnimController).Crouching = false;
|
||||
|
||||
if (!Character.AnimController.InWater)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2(
|
||||
Vector2 targetMovement = new Vector2(
|
||||
Character.AnimController.TargetMovement.X,
|
||||
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f)) * Character.SpeedMultiplier;
|
||||
MathHelper.Clamp(Character.AnimController.TargetMovement.Y, -1.0f, 1.0f));
|
||||
|
||||
Character.SpeedMultiplier = 1.0f;
|
||||
float maxSpeed = Character.GetCurrentMaxSpeed(run);
|
||||
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
|
||||
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
|
||||
|
||||
//apply speed multiplier if
|
||||
// a. it's boosting the movement speed and the character is trying to move fast (= running)
|
||||
// b. it's a debuff that decreases movement speed
|
||||
if (run || Character.SpeedMultiplier <= 0.0f) targetMovement *= Character.SpeedMultiplier;
|
||||
|
||||
Character.SpeedMultiplier = 1.0f; // Reset, items will set the value before the next update
|
||||
|
||||
Character.AnimController.TargetMovement = targetMovement;
|
||||
}
|
||||
|
||||
if (Character.SelectedConstruction != null && Character.SelectedConstruction.GetComponent<Items.Components.Ladder>()!=null)
|
||||
if (Character.AnimController.Anim == AnimController.Animation.Climbing &&
|
||||
Character.SelectedConstruction != null &&
|
||||
Character.SelectedConstruction.GetComponent<Items.Components.Ladder>() != null)
|
||||
{
|
||||
if (currPath != null && currPath.CurrentNode != null && currPath.CurrentNode.Ladders != null)
|
||||
{
|
||||
Character.AnimController.TargetMovement = new Vector2( 0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
Character.AnimController.TargetMovement = new Vector2(0.0f, Math.Sign(Character.AnimController.TargetMovement.Y));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +148,7 @@ namespace Barotrauma
|
||||
//-> take the suit off
|
||||
if (canTakeOffSuit && (Character.Oxygen < 50.0f || objectiveManager.CurrentObjective is AIObjectiveIdle))
|
||||
{
|
||||
var divingSuit = Character.Inventory.FindItem("Diving Suit");
|
||||
var divingSuit = Character.Inventory.FindItemByIdentifier("divingsuit") ?? Character.Inventory.FindItemByTag("divingsuit");
|
||||
if (divingSuit != null) divingSuit.Drop(Character);
|
||||
}
|
||||
|
||||
@@ -136,27 +172,104 @@ namespace Barotrauma
|
||||
Character.AnimController.TargetDir = Character.AnimController.TargetMovement.X > 0.0f ? Direction.Right : Direction.Left;
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAttacked(Character attacker, float amount)
|
||||
|
||||
private void ReportProblems()
|
||||
{
|
||||
if (amount <= 0.0f) return;
|
||||
if (GameMain.Client != null) return;
|
||||
|
||||
var enemy = attacker as Character;
|
||||
if (enemy == null || enemy == Character) return;
|
||||
Order newOrder = null;
|
||||
if (Character.CurrentHull != null)
|
||||
{
|
||||
if (Character.CurrentHull.FireSources.Count > 0)
|
||||
{
|
||||
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportfire");
|
||||
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
|
||||
}
|
||||
|
||||
objectiveManager.AddObjective(new AIObjectiveCombat(Character, enemy));
|
||||
if (Character.CurrentHull.ConnectedGaps.Any(g => !g.IsRoomToRoom && g.ConnectedDoor == null && g.Open > 0.0f))
|
||||
{
|
||||
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportbreach");
|
||||
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
|
||||
}
|
||||
|
||||
//the objective in the manager is not necessarily the same as the one we just instantiated,
|
||||
//because the objective isn't added if there's already an identical objective in the manager
|
||||
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
|
||||
combatObjective.MaxEnemyDamage = Math.Max(amount, combatObjective.MaxEnemyDamage);
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.CurrentHull == Character.CurrentHull && !c.IsDead &&
|
||||
(c.AIController is EnemyAIController || c.TeamID != Character.TeamID))
|
||||
{
|
||||
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "reportintruders");
|
||||
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Character.CurrentHull != null && (Character.Bleeding > 1.0f || Character.Vitality < Character.MaxVitality * 0.1f))
|
||||
{
|
||||
var orderPrefab = Order.PrefabList.Find(o => o.AITag == "requestfirstaid");
|
||||
newOrder = new Order(orderPrefab, Character.CurrentHull, null);
|
||||
}
|
||||
|
||||
if (newOrder != null)
|
||||
{
|
||||
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.AddOrder(newOrder, newOrder.FadeOutTime))
|
||||
{
|
||||
Character.Speak(
|
||||
newOrder.GetChatMessage("", Character.CurrentHull?.RoomName), ChatMessageType.Order);
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
OrderChatMessage msg = new OrderChatMessage(newOrder, "", Character.CurrentHull, null, Character);
|
||||
GameMain.Server.SendOrderChatMessage(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option)
|
||||
private void UpdateSpeaking()
|
||||
{
|
||||
if (Character.Oxygen < 20.0f)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogLowOxygen"), null, 0, "lowoxygen", 30.0f);
|
||||
}
|
||||
|
||||
if (Character.Bleeding > 2.0f)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogBleeding"), null, 0, "bleeding", 30.0f);
|
||||
}
|
||||
|
||||
if (Character.PressureTimer > 50.0f && Character.CurrentHull != null)
|
||||
{
|
||||
Character.Speak(TextManager.Get("DialogPressure").Replace("[roomname]", Character.CurrentHull.RoomName), null, 0, "pressure", 30.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public override void OnAttacked(Character attacker, AttackResult attackResult)
|
||||
{
|
||||
float totalDamage = attackResult.Damage;
|
||||
if (totalDamage <= 0.0f || attacker == null) return;
|
||||
|
||||
if (attacker.AnimController.Anim == AnimController.Animation.CPR && attacker.SelectedCharacter == Character)
|
||||
{
|
||||
// Don't attack characters that damage you while doing cpr, because let's assume that they are helping you.
|
||||
// Should not cancel any existing ai objectives (so that if the character attacked you and then helped, we still would want to retaliate).
|
||||
return;
|
||||
}
|
||||
|
||||
objectiveManager.AddObjective(new AIObjectiveCombat(Character, attacker), Rand.Range(0.5f, 1, Rand.RandSync.Unsynced), () =>
|
||||
{
|
||||
//the objective in the manager is not necessarily the same as the one we just instantiated,
|
||||
//because the objective isn't added if there's already an identical objective in the manager
|
||||
var combatObjective = objectiveManager.GetObjective<AIObjectiveCombat>();
|
||||
combatObjective.MaxEnemyDamage = Math.Max(totalDamage, combatObjective.MaxEnemyDamage);
|
||||
});
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option, Character orderGiver, bool speak = true)
|
||||
{
|
||||
CurrentOrderOption = option;
|
||||
CurrentOrder = order;
|
||||
objectiveManager.SetOrder(order, option);
|
||||
objectiveManager.SetOrder(order, option, orderGiver);
|
||||
if (speak && Character.SpeechImpediment < 100.0f) Character.Speak(TextManager.Get("DialogAffirmative"), null, 1.0f);
|
||||
|
||||
SetOrderProjSpecific(order);
|
||||
}
|
||||
@@ -166,5 +279,21 @@ namespace Barotrauma
|
||||
{
|
||||
selectedAiTarget = target;
|
||||
}
|
||||
|
||||
private void CheckCrouching(float deltaTime)
|
||||
{
|
||||
crouchRaycastTimer -= deltaTime;
|
||||
if (crouchRaycastTimer > 0.0f) return;
|
||||
|
||||
crouchRaycastTimer = CrouchRaycastInterval;
|
||||
|
||||
//start the raycast in front of the character in the direction it's heading to
|
||||
Vector2 startPos = Character.SimPosition;
|
||||
startPos.X += MathHelper.Clamp(Character.AnimController.TargetMovement.X, -1.0f, 1.0f);
|
||||
|
||||
//do a raycast upwards to find any walls
|
||||
float minCeilingDist = Character.AnimController.Collider.height / 2 + Character.AnimController.Collider.radius + 0.1f;
|
||||
shouldCrouch = Submarine.PickBody(startPos, startPos + Vector2.UnitY * minCeilingDist, null, Physics.CollisionWall) != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace Barotrauma
|
||||
private PathFinder pathFinder;
|
||||
private SteeringPath currentPath;
|
||||
|
||||
private bool canOpenDoors;
|
||||
private bool canOpenDoors, canBreakDoors;
|
||||
|
||||
private Character character;
|
||||
|
||||
@@ -33,20 +33,27 @@ namespace Barotrauma
|
||||
get { return currentTarget; }
|
||||
}
|
||||
|
||||
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors)
|
||||
public bool IsPathDirty
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public IndoorsSteeringManager(ISteerable host, bool canOpenDoors, bool canBreakDoors)
|
||||
: base(host)
|
||||
{
|
||||
pathFinder = new PathFinder(WayPoint.WayPointList.FindAll(wp => wp.SpawnType == SpawnType.Path), true);
|
||||
pathFinder.GetNodePenalty = GetNodePenalty;
|
||||
|
||||
this.canOpenDoors = canOpenDoors;
|
||||
this.canBreakDoors = canBreakDoors;
|
||||
|
||||
character = (host as AIController).Character;
|
||||
|
||||
findPathTimer = Rand.Range(0.0f, 1.0f);
|
||||
}
|
||||
|
||||
public override void Update(float speed = 1)
|
||||
public override void Update(float speed)
|
||||
{
|
||||
base.Update(speed);
|
||||
|
||||
@@ -58,16 +65,18 @@ namespace Barotrauma
|
||||
currentPath = path;
|
||||
if (path.Nodes.Any()) currentTarget = path.Nodes[path.Nodes.Count - 1].SimPosition;
|
||||
findPathTimer = 1.0f;
|
||||
IsPathDirty = false;
|
||||
}
|
||||
|
||||
|
||||
protected override Vector2 DoSteeringSeek(Vector2 target, float speed = 1)
|
||||
protected override Vector2 DoSteeringSeek(Vector2 target, float speed)
|
||||
{
|
||||
//find a new path if one hasn't been found yet or the target is different from the current target
|
||||
if (currentPath == null || Vector2.Distance(target, currentTarget) > 1.0f || findPathTimer < -1.0f)
|
||||
{
|
||||
if (findPathTimer > 0.0f) return Vector2.Zero;
|
||||
IsPathDirty = true;
|
||||
|
||||
if (findPathTimer > 0.0f) return Vector2.Zero;
|
||||
|
||||
currentTarget = target;
|
||||
Vector2 pos = host.SimPosition;
|
||||
if (character != null && character.Submarine == null)
|
||||
@@ -75,15 +84,16 @@ namespace Barotrauma
|
||||
var targetHull = Hull.FindHull(FarseerPhysics.ConvertUnits.ToDisplayUnits(target), null, false);
|
||||
if (targetHull != null && targetHull.Submarine != null)
|
||||
{
|
||||
pos -= targetHull.SimPosition;
|
||||
pos -= targetHull.Submarine.SimPosition;
|
||||
}
|
||||
}
|
||||
|
||||
currentPath = pathFinder.FindPath(pos, target);
|
||||
currentPath = pathFinder.FindPath(pos, target, "(Character: " + character.Name + ")");
|
||||
|
||||
findPathTimer = Rand.Range(1.0f, 1.2f);
|
||||
|
||||
return DiffToCurrentNode();
|
||||
IsPathDirty = false;
|
||||
return DiffToCurrentNode();
|
||||
}
|
||||
|
||||
Vector2 diff = DiffToCurrentNode();
|
||||
@@ -116,8 +126,11 @@ namespace Barotrauma
|
||||
}
|
||||
return currentTarget - pos2;
|
||||
}
|
||||
|
||||
if (canOpenDoors && !character.LockHands) CheckDoorsInPath();
|
||||
|
||||
if (canOpenDoors && !character.LockHands)
|
||||
{
|
||||
CheckDoorsInPath();
|
||||
}
|
||||
|
||||
Vector2 pos = host.SimPosition;
|
||||
|
||||
@@ -134,10 +147,12 @@ namespace Barotrauma
|
||||
pos -= FarseerPhysics.ConvertUnits.ToSimUnits(currentPath.CurrentNode.Submarine.Position-character.Submarine.Position);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (currentPath.CurrentNode != null && currentPath.CurrentNode.Ladders != null)
|
||||
//only humanoids can climb ladders
|
||||
if (currentPath.CurrentNode != null &&
|
||||
currentPath.CurrentNode.Ladders != null &&
|
||||
character.AnimController is HumanoidAnimController)
|
||||
{
|
||||
if (character.SelectedConstruction != currentPath.CurrentNode.Ladders.Item &&
|
||||
currentPath.CurrentNode.Ladders.Item.IsInsideTrigger(character.WorldPosition))
|
||||
@@ -158,9 +173,6 @@ namespace Barotrauma
|
||||
//at the same height as the waypoint
|
||||
if (Math.Abs(collider.SimPosition.Y - currentPath.CurrentNode.SimPosition.Y) < (collider.height / 2 + collider.radius) * 1.25f)
|
||||
{
|
||||
//climbing ladders -> don't move horizontally
|
||||
diff.X = 0.0f;
|
||||
|
||||
float heightFromFloor = character.AnimController.GetColliderBottom().Y - character.AnimController.FloorY;
|
||||
if (heightFromFloor <= 0.0f)
|
||||
{
|
||||
@@ -179,10 +191,29 @@ namespace Barotrauma
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
//if the current node is below the character and the next one is above (or vice versa)
|
||||
//and both are on ladders, we can skip directly to the next one
|
||||
|
||||
//e.g. no point in going down to reach the starting point of a path when we could go directly to the one above
|
||||
if (currentPath.CurrentNode.Ladders != null && currentPath.CurrentNode.Ladders == currentPath.NextNode?.Ladders &&
|
||||
Math.Sign(currentPath.CurrentNode.WorldPosition.Y - character.WorldPosition.Y) != Math.Sign(currentPath.NextNode.WorldPosition.Y - character.WorldPosition.Y))
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
}
|
||||
|
||||
character.AnimController.IgnorePlatforms = false;
|
||||
return diff;
|
||||
}
|
||||
else if (character.AnimController.InWater)
|
||||
{
|
||||
if (Vector2.DistanceSquared(pos, currentPath.CurrentNode.SimPosition) < collider.radius * collider.radius)
|
||||
{
|
||||
currentPath.SkipToNextNode();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 colliderBottom = character.AnimController.GetColliderBottom();
|
||||
@@ -203,42 +234,54 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
WayPoint node = null;
|
||||
WayPoint nextNode = null;
|
||||
|
||||
if (i==0)
|
||||
{
|
||||
node = currentPath.CurrentNode;
|
||||
nextNode = currentPath.NextNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
node = currentPath.PrevNode;
|
||||
nextNode = currentPath.CurrentNode;
|
||||
}
|
||||
|
||||
if (node == null || node.ConnectedGap == null || node.ConnectedGap.ConnectedDoor == null) continue;
|
||||
|
||||
if (nextNode == null) continue;
|
||||
|
||||
var door = node.ConnectedGap.ConnectedDoor;
|
||||
|
||||
Door door = null;
|
||||
bool shouldBeOpen = false;
|
||||
|
||||
if (door.LinkedGap.IsHorizontal)
|
||||
if (currentPath.Nodes.Count == 1)
|
||||
{
|
||||
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
|
||||
|
||||
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
|
||||
door = currentPath.Nodes.First().ConnectedDoor;
|
||||
shouldBeOpen = door != null;
|
||||
}
|
||||
else
|
||||
{
|
||||
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
|
||||
WayPoint node = null;
|
||||
WayPoint nextNode = null;
|
||||
if (i == 0)
|
||||
{
|
||||
node = currentPath.CurrentNode;
|
||||
nextNode = currentPath.NextNode;
|
||||
}
|
||||
else
|
||||
{
|
||||
node = currentPath.PrevNode;
|
||||
nextNode = currentPath.CurrentNode;
|
||||
}
|
||||
if (node?.ConnectedDoor == null) continue;
|
||||
|
||||
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
|
||||
if (nextNode == null)
|
||||
{
|
||||
//the node we're heading towards is the last one in the path, and at a door
|
||||
//the door needs to be open for the character to reach the node
|
||||
shouldBeOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
door = node.ConnectedGap.ConnectedDoor;
|
||||
if (door.LinkedGap.IsHorizontal)
|
||||
{
|
||||
int currentDir = Math.Sign(nextNode.WorldPosition.X - door.Item.WorldPosition.X);
|
||||
shouldBeOpen = (door.Item.WorldPosition.X - character.WorldPosition.X) * currentDir > -50.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
int currentDir = Math.Sign(nextNode.WorldPosition.Y - door.Item.WorldPosition.Y);
|
||||
shouldBeOpen = (door.Item.WorldPosition.Y - character.WorldPosition.Y) * currentDir > -80.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (door == null) return;
|
||||
|
||||
//toggle the door if it's the previous node and open, or if it's current node and closed
|
||||
if (door.IsOpen != shouldBeOpen)
|
||||
{
|
||||
@@ -267,7 +310,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
closestButton.Item.TryInteract(character, false, true, true);
|
||||
closestButton.Item.TryInteract(character, false, true, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -281,23 +324,37 @@ namespace Barotrauma
|
||||
float penalty = 0.0f;
|
||||
if (nextNode.Waypoint.ConnectedGap != null && nextNode.Waypoint.ConnectedGap.Open < 0.9f)
|
||||
{
|
||||
if (nextNode.Waypoint.ConnectedGap.ConnectedDoor == null)
|
||||
if (nextNode.Waypoint.ConnectedDoor == null)
|
||||
{
|
||||
penalty = 100.0f;
|
||||
}
|
||||
|
||||
//door closed and the character can't open doors -> node can't be traversed
|
||||
if (!canOpenDoors || character.LockHands) return null;
|
||||
|
||||
var doorButtons = nextNode.Waypoint.ConnectedGap.ConnectedDoor.Item.GetConnectedComponents<Controller>();
|
||||
if (!doorButtons.Any()) return null;
|
||||
|
||||
foreach (Controller button in doorButtons)
|
||||
if (!canBreakDoors)
|
||||
{
|
||||
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
|
||||
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
|
||||
//door closed and the character can't open doors -> node can't be traversed
|
||||
if (!canOpenDoors || character.LockHands) return null;
|
||||
|
||||
if (!button.HasRequiredItems(character, false)) return null;
|
||||
var doorButtons = nextNode.Waypoint.ConnectedDoor.Item.GetConnectedComponents<Controller>();
|
||||
if (!doorButtons.Any()) return null;
|
||||
|
||||
foreach (Controller button in doorButtons)
|
||||
{
|
||||
if (Math.Sign(button.Item.Position.X - nextNode.Waypoint.Position.X) !=
|
||||
Math.Sign(node.Position.X - nextNode.Position.X)) continue;
|
||||
|
||||
if (!button.HasRequiredItems(character, false)) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//non-humanoids can't climb up ladders
|
||||
if (!(character.AnimController is HumanoidAnimController))
|
||||
{
|
||||
if (node.Waypoint.Ladders != null && nextNode.Waypoint.Ladders != null &&
|
||||
nextNode.Position.Y - node.Position.Y > 1.0f && //more than one sim unit to climb up
|
||||
nextNode.Waypoint.CurrentHull != null && nextNode.Waypoint.CurrentHull.Surface < nextNode.Waypoint.Position.Y) //upper node not underwater
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Common;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class LatchOntoAI
|
||||
{
|
||||
const float RaycastInterval = 5.0f;
|
||||
|
||||
private float raycastTimer;
|
||||
|
||||
private Body attachTargetBody;
|
||||
private Vector2 attachSurfaceNormal;
|
||||
private Submarine attachTargetSubmarine;
|
||||
|
||||
private bool attachToSub;
|
||||
private bool attachToWalls;
|
||||
|
||||
private float minDeattachSpeed = 3.0f, maxDeattachSpeed = 10.0f;
|
||||
private float damageOnDetach = 0.0f, detachStun = 0.0f;
|
||||
private float deattachTimer;
|
||||
|
||||
private Vector2 wallAttachPos;
|
||||
|
||||
private float attachCooldown;
|
||||
|
||||
private Limb attachLimb;
|
||||
private Vector2 localAttachPos;
|
||||
private float attachLimbRotation;
|
||||
|
||||
private float jointDir;
|
||||
|
||||
private List<WeldJoint> attachJoints = new List<WeldJoint>();
|
||||
|
||||
public List<WeldJoint> AttachJoints
|
||||
{
|
||||
get { return attachJoints; }
|
||||
}
|
||||
|
||||
public Vector2? WallAttachPos
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IsAttached
|
||||
{
|
||||
get { return attachJoints.Count > 0; }
|
||||
}
|
||||
|
||||
public LatchOntoAI(XElement element, EnemyAIController enemyAI)
|
||||
{
|
||||
attachToWalls = element.GetAttributeBool("attachtowalls", false);
|
||||
attachToSub = element.GetAttributeBool("attachtosub", false);
|
||||
minDeattachSpeed = element.GetAttributeFloat("mindeattachspeed", 3.0f);
|
||||
maxDeattachSpeed = Math.Max(minDeattachSpeed, element.GetAttributeFloat("maxdeattachspeed", 10.0f));
|
||||
damageOnDetach = element.GetAttributeFloat("damageondetach", 0.0f);
|
||||
detachStun = element.GetAttributeFloat("detachstun", 0.0f);
|
||||
localAttachPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("localattachpos", Vector2.Zero));
|
||||
attachLimbRotation = MathHelper.ToRadians(element.GetAttributeFloat("attachlimbrotation", 0.0f));
|
||||
|
||||
if (Enum.TryParse(element.GetAttributeString("attachlimb", "Head"), out LimbType attachLimbType))
|
||||
{
|
||||
attachLimb = enemyAI.Character.AnimController.GetLimb(attachLimbType);
|
||||
}
|
||||
if (attachLimb == null) attachLimb = enemyAI.Character.AnimController.MainLimb;
|
||||
|
||||
enemyAI.Character.OnDeath += OnCharacterDeath;
|
||||
}
|
||||
|
||||
public void SetAttachTarget(Body attachTarget, Submarine attachTargetSub, Vector2 attachPos, Vector2 attachSurfaceNormal)
|
||||
{
|
||||
attachTargetBody = attachTarget;
|
||||
attachTargetSubmarine = attachTargetSub;
|
||||
this.attachSurfaceNormal = attachSurfaceNormal;
|
||||
wallAttachPos = attachPos;
|
||||
}
|
||||
|
||||
public void Update(EnemyAIController enemyAI, float deltaTime)
|
||||
{
|
||||
Character character = enemyAI.Character;
|
||||
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
DeattachFromBody();
|
||||
WallAttachPos = null;
|
||||
return;
|
||||
}
|
||||
if (attachJoints.Count > 0)
|
||||
{
|
||||
if (Math.Sign(attachLimb.Dir) != Math.Sign(jointDir))
|
||||
{
|
||||
attachJoints[0].LocalAnchorA =
|
||||
new Vector2(-attachJoints[0].LocalAnchorA.X, attachJoints[0].LocalAnchorA.Y);
|
||||
attachJoints[0].ReferenceAngle = -attachJoints[0].ReferenceAngle;
|
||||
jointDir = attachLimb.Dir;
|
||||
}
|
||||
for (int i = 0; i < attachJoints.Count; i++)
|
||||
{
|
||||
//something went wrong, limb body is very far from the joint anchor -> deattach
|
||||
if (Vector2.DistanceSquared(attachJoints[i].WorldAnchorB, attachJoints[i].BodyA.Position) > 10.0f * 10.0f)
|
||||
{
|
||||
DebugConsole.ThrowError("Limb body of the character \"" + character.Name + "\" is very far from the attach joint anchor -> deattach");
|
||||
DeattachFromBody();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
attachCooldown -= deltaTime;
|
||||
deattachTimer -= deltaTime;
|
||||
|
||||
Vector2 transformedAttachPos = wallAttachPos;
|
||||
if (character.Submarine == null && attachTargetSubmarine != null)
|
||||
{
|
||||
transformedAttachPos += ConvertUnits.ToSimUnits(attachTargetSubmarine.Position);
|
||||
}
|
||||
if (transformedAttachPos != Vector2.Zero)
|
||||
{
|
||||
WallAttachPos = transformedAttachPos;
|
||||
}
|
||||
|
||||
switch (enemyAI.State)
|
||||
{
|
||||
case AIController.AIState.Idle:
|
||||
if (attachToWalls && character.Submarine == null && Level.Loaded != null)
|
||||
{
|
||||
raycastTimer -= deltaTime;
|
||||
//check if there are any walls nearby the character could attach to
|
||||
if (raycastTimer < 0.0f)
|
||||
{
|
||||
wallAttachPos = Vector2.Zero;
|
||||
|
||||
var cells = Level.Loaded.GetCells(character.WorldPosition, 1);
|
||||
if (cells.Count > 0)
|
||||
{
|
||||
foreach (Voronoi2.VoronoiCell cell in cells)
|
||||
{
|
||||
foreach (Voronoi2.GraphEdge edge in cell.Edges)
|
||||
{
|
||||
if (MathUtils.GetLineIntersection(edge.Point1, edge.Point2, character.WorldPosition, cell.Center, out Vector2 intersection))
|
||||
{
|
||||
attachSurfaceNormal = edge.GetNormal(cell);
|
||||
attachTargetBody = cell.Body;
|
||||
wallAttachPos = ConvertUnits.ToSimUnits(intersection);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (WallAttachPos != Vector2.Zero) break;
|
||||
}
|
||||
}
|
||||
raycastTimer = RaycastInterval;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
wallAttachPos = Vector2.Zero;
|
||||
}
|
||||
|
||||
if (wallAttachPos == Vector2.Zero)
|
||||
{
|
||||
DeattachFromBody();
|
||||
}
|
||||
else
|
||||
{
|
||||
float dist = Vector2.Distance(character.SimPosition, wallAttachPos);
|
||||
if (dist < Math.Max(Math.Max(character.AnimController.Collider.radius, character.AnimController.Collider.width), character.AnimController.Collider.height) * 1.2f)
|
||||
{
|
||||
//close enough to a wall -> attach
|
||||
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, wallAttachPos);
|
||||
enemyAI.SteeringManager.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
//move closer to the wall
|
||||
DeattachFromBody();
|
||||
enemyAI.SteeringManager.SteeringAvoid(deltaTime, 1.0f, character.AnimController.GetCurrentSpeed(false) * 0.1f);
|
||||
enemyAI.SteeringManager.SteeringSeek(wallAttachPos, character.AnimController.GetCurrentSpeed(true));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case AIController.AIState.Attack:
|
||||
if (enemyAI.AttackingLimb != null)
|
||||
{
|
||||
if (attachToSub && wallAttachPos != Vector2.Zero && attachTargetBody != null)
|
||||
{
|
||||
// is not attached or is attached to something else
|
||||
if (!IsAttached || IsAttached && attachJoints[0].BodyB == attachTargetBody)
|
||||
{
|
||||
if (Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(transformedAttachPos), enemyAI.AttackingLimb.WorldPosition) < enemyAI.AttackingLimb.attack.Range * enemyAI.AttackingLimb.attack.Range)
|
||||
{
|
||||
AttachToBody(character.AnimController.Collider, attachLimb, attachTargetBody, transformedAttachPos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
WallAttachPos = null;
|
||||
DeattachFromBody();
|
||||
break;
|
||||
}
|
||||
|
||||
if (attachTargetBody != null && deattachTimer < 0.0f)
|
||||
{
|
||||
Entity entity = attachTargetBody.UserData as Entity;
|
||||
Submarine attachedSub = entity is Submarine ? (Submarine)entity : entity?.Submarine;
|
||||
if (attachedSub != null)
|
||||
{
|
||||
float velocity = attachedSub.Velocity == Vector2.Zero ? 0.0f : attachedSub.Velocity.Length();
|
||||
float velocityFactor = (maxDeattachSpeed - minDeattachSpeed <= 0.0f) ?
|
||||
Math.Sign(Math.Abs(velocity) - minDeattachSpeed) :
|
||||
(Math.Abs(velocity) - minDeattachSpeed) / (maxDeattachSpeed - minDeattachSpeed);
|
||||
|
||||
if (Rand.Range(0.0f, 1.0f) < velocityFactor)
|
||||
{
|
||||
DeattachFromBody();
|
||||
character.AddDamage(character.WorldPosition, new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(damageOnDetach) }, detachStun, true);
|
||||
attachCooldown = 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
deattachTimer = 5.0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void AttachToBody(PhysicsBody collider, Limb attachLimb, Body targetBody, Vector2 attachPos)
|
||||
{
|
||||
//already attached to something
|
||||
if (attachJoints.Count > 0)
|
||||
{
|
||||
//already attached to the target body, no need to do anything
|
||||
if (attachJoints[0].BodyB == targetBody) return;
|
||||
DeattachFromBody();
|
||||
}
|
||||
|
||||
jointDir = attachLimb.Dir;
|
||||
|
||||
Vector2 transformedLocalAttachPos = localAttachPos * attachLimb.character.AnimController.RagdollParams.LimbScale;
|
||||
if (jointDir < 0.0f) transformedLocalAttachPos.X = -transformedLocalAttachPos.X;
|
||||
|
||||
//transformedLocalAttachPos = Vector2.Transform(transformedLocalAttachPos, Matrix.CreateRotationZ(attachLimb.Rotation));
|
||||
|
||||
float angle = MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2 + attachLimbRotation * attachLimb.Dir;
|
||||
attachLimb.body.SetTransform(attachPos + attachSurfaceNormal * transformedLocalAttachPos.Length(), angle);
|
||||
|
||||
var limbJoint = new WeldJoint(attachLimb.body.FarseerBody, targetBody,
|
||||
transformedLocalAttachPos, targetBody.GetLocalPoint(attachPos), false)
|
||||
{
|
||||
FrequencyHz = 10.0f,
|
||||
DampingRatio = 0.5f,
|
||||
KinematicBodyB = true,
|
||||
CollideConnected = false,
|
||||
};
|
||||
GameMain.World.AddJoint(limbJoint);
|
||||
attachJoints.Add(limbJoint);
|
||||
|
||||
// Limb scale is already taken into account when creating the collider.
|
||||
Vector2 colliderFront = collider.GetLocalFront();
|
||||
if (jointDir < 0.0f) colliderFront.X = -colliderFront.X;
|
||||
collider.SetTransform(attachPos + attachSurfaceNormal * colliderFront.Length(), MathUtils.VectorToAngle(-attachSurfaceNormal) - MathHelper.PiOver2);
|
||||
|
||||
var colliderJoint = new WeldJoint(collider.FarseerBody, targetBody, colliderFront, targetBody.GetLocalPoint(attachPos), false)
|
||||
{
|
||||
FrequencyHz = 10.0f,
|
||||
DampingRatio = 0.5f,
|
||||
KinematicBodyB = true,
|
||||
CollideConnected = false,
|
||||
//Length = 0.1f
|
||||
};
|
||||
GameMain.World.AddJoint(colliderJoint);
|
||||
attachJoints.Add(colliderJoint);
|
||||
}
|
||||
|
||||
public void DeattachFromBody()
|
||||
{
|
||||
foreach (Joint joint in attachJoints)
|
||||
{
|
||||
GameMain.World.RemoveJoint(joint);
|
||||
}
|
||||
attachJoints.Clear();
|
||||
}
|
||||
|
||||
private void OnCharacterDeath(Character character, CauseOfDeath causeOfDeath)
|
||||
{
|
||||
DeattachFromBody();
|
||||
character.OnDeath -= OnCharacterDeath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class NPCConversation
|
||||
{
|
||||
const int MaxPreviousConversations = 20;
|
||||
|
||||
private static List<NPCConversation> list = new List<NPCConversation>();
|
||||
|
||||
public readonly string Line;
|
||||
|
||||
public readonly List<JobPrefab> AllowedJobs;
|
||||
|
||||
public readonly List<string> Flags;
|
||||
|
||||
//The line can only be selected when eventmanager intensity is between these values
|
||||
//null = no restriction
|
||||
public float? maxIntensity, minIntensity;
|
||||
|
||||
public readonly List<NPCConversation> Responses;
|
||||
private readonly int speakerIndex;
|
||||
private readonly List<string> allowedSpeakerTags;
|
||||
public static void LoadAll(IEnumerable<string> filePaths)
|
||||
{
|
||||
//language, identifier, filepath
|
||||
List<Tuple<string, string, string>> contentPackageFiles = new List<Tuple<string, string, string>>();
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
if (doc == null || doc.Root == null) continue;
|
||||
string language = doc.Root.GetAttributeString("Language", "English");
|
||||
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
|
||||
contentPackageFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
|
||||
}
|
||||
|
||||
List<Tuple<string, string, string>> translationFiles = new List<Tuple<string, string, string>>();
|
||||
foreach (string filePath in Directory.GetFiles(Path.Combine("Content", "NPCConversations")))
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
if (doc == null || doc.Root == null) continue;
|
||||
string language = doc.Root.GetAttributeString("Language", "English");
|
||||
string identifier = doc.Root.GetAttributeString("Identifier", "unknown");
|
||||
translationFiles.Add(new Tuple<string, string, string>(language, identifier, filePath));
|
||||
}
|
||||
|
||||
|
||||
//get the languages and identifiers of the files
|
||||
for (int i = 0; i < contentPackageFiles.Count; i++)
|
||||
{
|
||||
var contentPackageFile = contentPackageFiles[i];
|
||||
//correct language, all good
|
||||
if (contentPackageFile.Item1 == TextManager.Language) continue;
|
||||
|
||||
//attempt to find a translation file with the correct language and a matching identifier
|
||||
//if it fails, we'll just use the original file with the incorrect language
|
||||
var translation = translationFiles.Find(t => t.Item1 == TextManager.Language && t.Item2 == contentPackageFile.Item2);
|
||||
if (translation != null) contentPackageFiles[i] = translation; //replace with the translation file
|
||||
}
|
||||
|
||||
foreach (var file in contentPackageFiles)
|
||||
{
|
||||
Load(file.Item3);
|
||||
}
|
||||
}
|
||||
|
||||
private static void Load(string file)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file);
|
||||
if (doc == null || doc.Root == null) return;
|
||||
|
||||
string language = doc.Root.GetAttributeString("Language", "English");
|
||||
if (language != TextManager.Language) return;
|
||||
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "conversation":
|
||||
list.Add(new NPCConversation(subElement));
|
||||
break;
|
||||
case "personalitytrait":
|
||||
new NPCPersonalityTrait(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public NPCConversation(XElement element)
|
||||
{
|
||||
Line = element.GetAttributeString("line", "");
|
||||
|
||||
speakerIndex = element.GetAttributeInt("speaker", 0);
|
||||
|
||||
AllowedJobs = new List<JobPrefab>();
|
||||
string allowedJobsStr = element.GetAttributeString("allowedjobs", "");
|
||||
foreach (string allowedJobIdentifier in allowedJobsStr.Split(','))
|
||||
{
|
||||
var jobPrefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == allowedJobIdentifier.ToLowerInvariant());
|
||||
if (jobPrefab != null) AllowedJobs.Add(jobPrefab);
|
||||
}
|
||||
|
||||
Flags = new List<string>(element.GetAttributeStringArray("flags", new string[0]));
|
||||
|
||||
allowedSpeakerTags = new List<string>();
|
||||
string allowedSpeakerTagsStr = element.GetAttributeString("speakertags", "");
|
||||
foreach (string tag in allowedSpeakerTagsStr.Split(','))
|
||||
{
|
||||
if (string.IsNullOrEmpty(tag)) continue;
|
||||
allowedSpeakerTags.Add(tag.Trim().ToLowerInvariant());
|
||||
}
|
||||
|
||||
if (element.Attribute("minintensity") != null) minIntensity = element.GetAttributeFloat("minintensity", 0.0f);
|
||||
if (element.Attribute("maxintensity") != null) maxIntensity = element.GetAttributeFloat("maxintensity", 1.0f);
|
||||
|
||||
Responses = new List<NPCConversation>();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
Responses.Add(new NPCConversation(subElement));
|
||||
}
|
||||
}
|
||||
|
||||
private static List<string> GetCurrentFlags(Character speaker)
|
||||
{
|
||||
var currentFlags = new List<string>();
|
||||
if (Submarine.MainSub != null && Submarine.MainSub.AtDamageDepth) currentFlags.Add("SubmarineDeep");
|
||||
if (GameMain.GameSession != null && Timing.TotalTime < GameMain.GameSession.RoundStartTime + 30.0f) currentFlags.Add("Initial");
|
||||
if (speaker != null)
|
||||
{
|
||||
if (speaker.AnimController.InWater) currentFlags.Add("Underwater");
|
||||
currentFlags.Add(speaker.CurrentHull == null ? "Outside" : "Inside");
|
||||
|
||||
if (Character.Controlled != null)
|
||||
{
|
||||
if (Character.Controlled.CharacterHealth.GetAffliction("psychosis") != null)
|
||||
{
|
||||
currentFlags.Add(speaker != Character.Controlled ? "Psychosis" : "PsychosisSelf");
|
||||
}
|
||||
}
|
||||
|
||||
var afflictions = speaker.CharacterHealth.GetAllAfflictions();
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
var currentEffect = affliction.Prefab.GetActiveEffect(affliction.Strength);
|
||||
if (currentEffect != null && !string.IsNullOrEmpty(currentEffect.DialogFlag) && !currentFlags.Contains(currentEffect.DialogFlag))
|
||||
{
|
||||
currentFlags.Add(currentEffect.DialogFlag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentFlags;
|
||||
}
|
||||
|
||||
private static List<NPCConversation> previousConversations = new List<NPCConversation>();
|
||||
|
||||
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers)
|
||||
{
|
||||
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
|
||||
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
|
||||
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: list);
|
||||
return lines;
|
||||
}
|
||||
|
||||
public static List<Pair<Character, string>> CreateRandom(List<Character> availableSpeakers, List<string> requiredFlags)
|
||||
{
|
||||
Dictionary<int, Character> assignedSpeakers = new Dictionary<int, Character>();
|
||||
List<Pair<Character, string>> lines = new List<Pair<Character, string>>();
|
||||
var availableConversations = list.FindAll(conversation => requiredFlags.All(f => conversation.Flags.Contains(f)));
|
||||
if (availableConversations.Count > 0)
|
||||
{
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, null, lines, availableConversations: availableConversations, ignoreFlags: true);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
private static void CreateConversation(
|
||||
List<Character> availableSpeakers,
|
||||
Dictionary<int, Character> assignedSpeakers,
|
||||
NPCConversation baseConversation,
|
||||
List<Pair<Character, string>> lineList,
|
||||
List<NPCConversation> availableConversations,
|
||||
bool ignoreFlags = false)
|
||||
{
|
||||
List<NPCConversation> conversations = baseConversation == null ? availableConversations : baseConversation.Responses;
|
||||
if (conversations.Count == 0) return;
|
||||
|
||||
int conversationIndex = Rand.Int(conversations.Count);
|
||||
NPCConversation selectedConversation = conversations[conversationIndex];
|
||||
if (string.IsNullOrEmpty(selectedConversation.Line)) return;
|
||||
|
||||
Character speaker = null;
|
||||
//speaker already assigned for this line
|
||||
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
|
||||
{
|
||||
//check if the character has all required flags to say the line
|
||||
var characterFlags = GetCurrentFlags(assignedSpeakers[selectedConversation.speakerIndex]);
|
||||
if (selectedConversation.Flags.All(flag => characterFlags.Contains(flag)))
|
||||
{
|
||||
speaker = assignedSpeakers[selectedConversation.speakerIndex];
|
||||
}
|
||||
}
|
||||
if (speaker == null)
|
||||
{
|
||||
var allowedSpeakers = new List<Character>();
|
||||
|
||||
List<NPCConversation> potentialLines = new List<NPCConversation>(conversations);
|
||||
|
||||
//remove lines that are not appropriate for the intensity of the current situation
|
||||
if (GameMain.GameSession?.EventManager != null)
|
||||
{
|
||||
potentialLines.RemoveAll(l =>
|
||||
(l.minIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity < l.minIntensity) ||
|
||||
(l.maxIntensity.HasValue && GameMain.GameSession.EventManager.CurrentIntensity > l.maxIntensity));
|
||||
}
|
||||
|
||||
while (potentialLines.Count > 0)
|
||||
{
|
||||
//select a random line and attempt to find a speaker for it
|
||||
// and if no valid speaker is found, choose another random line
|
||||
selectedConversation = GetRandomConversation(potentialLines, baseConversation == null);
|
||||
if (selectedConversation == null || string.IsNullOrEmpty(selectedConversation.Line)) return;
|
||||
|
||||
//speaker already assigned for this line
|
||||
if (assignedSpeakers.ContainsKey(selectedConversation.speakerIndex))
|
||||
{
|
||||
speaker = assignedSpeakers[selectedConversation.speakerIndex];
|
||||
break;
|
||||
}
|
||||
|
||||
foreach (Character potentialSpeaker in availableSpeakers)
|
||||
{
|
||||
//check if the character has an appropriate job to say the line
|
||||
if (selectedConversation.AllowedJobs.Count > 0 && !selectedConversation.AllowedJobs.Contains(potentialSpeaker.Info?.Job.Prefab)) continue;
|
||||
|
||||
//check if the character has all required flags to say the line
|
||||
if (!ignoreFlags)
|
||||
{
|
||||
var characterFlags = GetCurrentFlags(potentialSpeaker);
|
||||
if (!selectedConversation.Flags.All(flag => characterFlags.Contains(flag))) continue;
|
||||
}
|
||||
|
||||
//check if the character has an appropriate personality
|
||||
if (selectedConversation.allowedSpeakerTags.Count > 0)
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait == null) continue;
|
||||
if (!selectedConversation.allowedSpeakerTags.Any(t => potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Any(t2 => t2 == t))) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (potentialSpeaker.Info?.PersonalityTrait != null &&
|
||||
!potentialSpeaker.Info.PersonalityTrait.AllowedDialogTags.Contains("none"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
allowedSpeakers.Add(potentialSpeaker);
|
||||
}
|
||||
|
||||
if (allowedSpeakers.Count == 0)
|
||||
{
|
||||
potentialLines.Remove(selectedConversation);
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allowedSpeakers.Count == 0) return;
|
||||
speaker = allowedSpeakers[Rand.Int(allowedSpeakers.Count)];
|
||||
availableSpeakers.Remove(speaker);
|
||||
assignedSpeakers.Add(selectedConversation.speakerIndex, speaker);
|
||||
}
|
||||
|
||||
if (baseConversation == null)
|
||||
{
|
||||
previousConversations.Insert(0, selectedConversation);
|
||||
if (previousConversations.Count > MaxPreviousConversations) previousConversations.RemoveAt(MaxPreviousConversations);
|
||||
}
|
||||
lineList.Add(new Pair<Character, string>(speaker, selectedConversation.Line));
|
||||
CreateConversation(availableSpeakers, assignedSpeakers, selectedConversation, lineList, availableConversations);
|
||||
}
|
||||
|
||||
private static NPCConversation GetRandomConversation(List<NPCConversation> conversations, bool avoidPreviouslyUsed)
|
||||
{
|
||||
if (!avoidPreviouslyUsed)
|
||||
{
|
||||
return conversations.Count == 0 ? null : conversations[Rand.Int(conversations.Count)];
|
||||
}
|
||||
|
||||
List<float> probabilities = new List<float>();
|
||||
foreach (NPCConversation conversation in conversations)
|
||||
{
|
||||
probabilities.Add(GetConversationProbability(conversation));
|
||||
}
|
||||
return ToolBox.SelectWeightedRandom(conversations, probabilities, Rand.RandSync.Unsynced);
|
||||
}
|
||||
|
||||
private static float GetConversationProbability(NPCConversation conversation)
|
||||
{
|
||||
int index = previousConversations.IndexOf(conversation);
|
||||
if (index < 0) return 10.0f;
|
||||
|
||||
return 1.0f - 1.0f / (index + 1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
public void TryComplete(float deltaTime)
|
||||
{
|
||||
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted);
|
||||
subObjectives.RemoveAll(s => s.IsCompleted() || !s.CanBeCompleted || ShouldInterruptSubObjective(s));
|
||||
|
||||
foreach (AIObjective objective in subObjectives)
|
||||
{
|
||||
@@ -66,6 +66,18 @@ namespace Barotrauma
|
||||
return currentSubObjective;
|
||||
}
|
||||
|
||||
public void SortSubObjectives(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (!subObjectives.Any()) return;
|
||||
subObjectives.Sort((x, y) => y.GetPriority(objectiveManager).CompareTo(x.GetPriority(objectiveManager)));
|
||||
subObjectives[0].SortSubObjectives(objectiveManager);
|
||||
}
|
||||
|
||||
protected virtual bool ShouldInterruptSubObjective(AIObjective subObjective)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
protected abstract void Act(float deltaTime);
|
||||
|
||||
public abstract bool IsCompleted();
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveChargeBatteries : AIObjective
|
||||
{
|
||||
private List<PowerContainer> availableBatteries;
|
||||
|
||||
private string orderOption;
|
||||
|
||||
public AIObjectiveChargeBatteries(Character character, string option)
|
||||
: base(character, option)
|
||||
{
|
||||
orderOption = option;
|
||||
|
||||
availableBatteries = new List<PowerContainer>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.Submarine == null) continue;
|
||||
if (item.Prefab.Identifier != "battery" && !item.HasTag("battery")) continue;
|
||||
|
||||
var powerContainer = item.GetComponent<PowerContainer>();
|
||||
availableBatteries.Add(powerContainer);
|
||||
}
|
||||
|
||||
if (availableBatteries.Count == 0)
|
||||
{
|
||||
character?.Speak(TextManager.Get("DialogNoBatteries"), null, 4.0f, "nobatteries", 10.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
return otherObjective is AIObjectiveChargeBatteries other && other.orderOption == orderOption;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (availableBatteries.Count == 0)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveIdle(character));
|
||||
return;
|
||||
}
|
||||
foreach (PowerContainer battery in availableBatteries)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveOperateItem(battery, character, orderOption, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -17,21 +18,13 @@ namespace Barotrauma
|
||||
|
||||
private AIObjectiveFindSafety escapeObjective;
|
||||
|
||||
float coolDownTimer;
|
||||
private AIObjectiveContainItem reloadWeaponObjective;
|
||||
|
||||
private readonly float enemyStrength;
|
||||
private float coolDownTimer;
|
||||
|
||||
public AIObjectiveCombat(Character character, Character enemy)
|
||||
: base(character, "")
|
||||
public AIObjectiveCombat(Character character, Character enemy) : base(character, "")
|
||||
{
|
||||
this.enemy = enemy;
|
||||
|
||||
foreach (Limb limb in enemy.AnimController.Limbs)
|
||||
{
|
||||
if (limb.attack == null) continue;
|
||||
enemyStrength += limb.attack.GetDamage(1.0f);
|
||||
}
|
||||
|
||||
coolDownTimer = CoolDown;
|
||||
}
|
||||
|
||||
@@ -39,26 +32,66 @@ namespace Barotrauma
|
||||
{
|
||||
coolDownTimer -= deltaTime;
|
||||
|
||||
var weapon = character.Inventory.FindItem("weapon");
|
||||
|
||||
var weapon = character.Inventory.FindItemByTag("weapon");
|
||||
if (weapon == null)
|
||||
{
|
||||
Escape(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
//TODO: make sure the weapon is ready to use (projectiles/batteries loaded)
|
||||
if (!character.SelectedItems.Contains(weapon))
|
||||
{
|
||||
if (character.Inventory.TryPutItem(weapon, 3, false, false, character))
|
||||
if (character.Inventory.TryPutItem(weapon, 3, true, false, character))
|
||||
{
|
||||
weapon.Equip(character);
|
||||
}
|
||||
else
|
||||
{
|
||||
//couldn't equip the item, escape
|
||||
Escape(deltaTime);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//make sure the weapon is loaded
|
||||
var weaponComponent =
|
||||
weapon.GetComponent<RangedWeapon>() as ItemComponent ??
|
||||
weapon.GetComponent<MeleeWeapon>() as ItemComponent ??
|
||||
weapon.GetComponent<RepairTool>() as ItemComponent;
|
||||
if (weaponComponent != null && weaponComponent.requiredItems.ContainsKey(RelatedItem.RelationType.Contained))
|
||||
{
|
||||
Item[] containedItems = weapon.ContainedItems;
|
||||
foreach (RelatedItem requiredItem in weaponComponent.requiredItems[RelatedItem.RelationType.Contained])
|
||||
{
|
||||
Item containedItem = Array.Find(containedItems, it => it != null && it.Condition > 0.0f && requiredItem.MatchesItem(it));
|
||||
if (containedItem == null)
|
||||
{
|
||||
var newReloadWeaponObjective = new AIObjectiveContainItem(character, requiredItem.Identifiers, weapon.GetComponent<ItemContainer>());
|
||||
if (!newReloadWeaponObjective.IsDuplicate(reloadWeaponObjective))
|
||||
{
|
||||
reloadWeaponObjective = newReloadWeaponObjective;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reloadWeaponObjective != null)
|
||||
{
|
||||
if (reloadWeaponObjective.IsCompleted())
|
||||
{
|
||||
reloadWeaponObjective = null;
|
||||
}
|
||||
else if (!reloadWeaponObjective.CanBeCompleted)
|
||||
{
|
||||
Escape(deltaTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
reloadWeaponObjective.TryComplete(deltaTime);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
character.CursorPosition = enemy.Position;
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
|
||||
@@ -116,7 +149,7 @@ namespace Barotrauma
|
||||
//clamp the strength to the health of this character
|
||||
//(it doesn't make a difference whether the enemy does 200 or 600 damage, it's one hit kill anyway)
|
||||
|
||||
float enemyDanger = Math.Min(Math.Max(enemyStrength, MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
|
||||
float enemyDanger = Math.Min(Math.Max(CalculateEnemyStrength(), MaxEnemyDamage), character.Health) + enemy.Health / 10.0f;
|
||||
|
||||
EnemyAIController enemyAI = enemy.AIController as EnemyAIController;
|
||||
if (enemyAI != null)
|
||||
@@ -134,5 +167,19 @@ namespace Barotrauma
|
||||
|
||||
return objective.enemy == enemy;
|
||||
}
|
||||
|
||||
private float CalculateEnemyStrength()
|
||||
{
|
||||
float enemyStrength = 0;
|
||||
AttackContext currentContext = character.GetAttackContext();
|
||||
foreach (Limb limb in enemy.AnimController.Limbs)
|
||||
{
|
||||
if (limb.attack == null) continue;
|
||||
if (!limb.attack.IsValidContext(currentContext)) { continue; }
|
||||
if (!limb.attack.IsValidTarget(AttackTarget.Character)) { continue; }
|
||||
enemyStrength += limb.attack.GetTotalDamage(false);
|
||||
}
|
||||
return enemyStrength;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-15
@@ -9,7 +9,8 @@ namespace Barotrauma
|
||||
{
|
||||
public int MinContainedAmount = 1;
|
||||
|
||||
private string[] itemNames;
|
||||
//can either be a tag or an identifier
|
||||
private string[] itemIdentifiers;
|
||||
|
||||
private ItemContainer container;
|
||||
|
||||
@@ -21,16 +22,21 @@ namespace Barotrauma
|
||||
|
||||
private AIObjectiveGetItem getItemObjective;
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
|
||||
public AIObjectiveContainItem(Character character, string itemName, ItemContainer container)
|
||||
: this(character, new string[] { itemName }, container)
|
||||
|
||||
public AIObjectiveContainItem(Character character, string itemIdentifier, ItemContainer container)
|
||||
: this(character, new string[] { itemIdentifier }, container)
|
||||
{
|
||||
}
|
||||
|
||||
public AIObjectiveContainItem(Character character, string[] itemNames, ItemContainer container)
|
||||
public AIObjectiveContainItem(Character character, string[] itemIdentifiers, ItemContainer container)
|
||||
: base (character, "")
|
||||
{
|
||||
this.itemNames = itemNames;
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
|
||||
this.container = container;
|
||||
}
|
||||
|
||||
@@ -41,7 +47,7 @@ namespace Barotrauma
|
||||
int containedItemCount = 0;
|
||||
foreach (Item item in container.Inventory.Items)
|
||||
{
|
||||
if (item != null && itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) containedItemCount++;
|
||||
if (item != null && itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) containedItemCount++;
|
||||
}
|
||||
|
||||
return containedItemCount >= MinContainedAmount;
|
||||
@@ -56,7 +62,7 @@ namespace Barotrauma
|
||||
return goToObjective.CanBeCompleted;
|
||||
}
|
||||
|
||||
return getItemObjective == null || !getItemObjective.CanBeCompleted;
|
||||
return getItemObjective == null || getItemObjective.CanBeCompleted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,12 +81,20 @@ namespace Barotrauma
|
||||
if (isCompleted) return;
|
||||
|
||||
//get the item that should be contained
|
||||
var itemToContain = character.Inventory.FindItem(itemNames);
|
||||
Item itemToContain = null;
|
||||
foreach (string identifier in itemIdentifiers)
|
||||
{
|
||||
itemToContain = character.Inventory.FindItemByIdentifier(identifier) ?? character.Inventory.FindItemByTag(identifier);
|
||||
if (itemToContain != null) break;
|
||||
}
|
||||
|
||||
if (itemToContain == null)
|
||||
{
|
||||
getItemObjective = new AIObjectiveGetItem(character, itemNames);
|
||||
getItemObjective.GetItemPriority = GetItemPriority;
|
||||
getItemObjective.IgnoreContainedItems = IgnoreAlreadyContainedItems;
|
||||
getItemObjective = new AIObjectiveGetItem(character, itemIdentifiers)
|
||||
{
|
||||
GetItemPriority = GetItemPriority,
|
||||
IgnoreContainedItems = IgnoreAlreadyContainedItems
|
||||
};
|
||||
AddSubObjective(getItemObjective);
|
||||
return;
|
||||
}
|
||||
@@ -116,11 +130,11 @@ namespace Barotrauma
|
||||
AIObjectiveContainItem objective = otherObjective as AIObjectiveContainItem;
|
||||
if (objective == null) return false;
|
||||
if (objective.container != container) return false;
|
||||
if (objective.itemNames.Length != itemNames.Length) return false;
|
||||
if (objective.itemIdentifiers.Length != itemIdentifiers.Length) return false;
|
||||
|
||||
for (int i = 0; i < itemNames.Length; i++)
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
if (objective.itemNames[i] != itemNames[i]) return false;
|
||||
if (objective.itemIdentifiers[i] != itemIdentifiers[i]) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFire : AIObjective
|
||||
{
|
||||
private Hull targetHull;
|
||||
|
||||
private AIObjectiveGetItem getExtinguisherObjective;
|
||||
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
private float useExtinquisherTimer;
|
||||
|
||||
public AIObjectiveExtinguishFire(Character character, Hull targetHull) :
|
||||
base(character, "")
|
||||
{
|
||||
this.targetHull = targetHull;
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
return targetHull.FireSources.Sum(fs => fs.Size.X * 0.1f);
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return targetHull.FireSources.Count == 0;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
var otherExtinguishFire = otherObjective as AIObjectiveExtinguishFire;
|
||||
return otherExtinguishFire != null && otherExtinguishFire.targetHull == targetHull;
|
||||
}
|
||||
|
||||
public override bool CanBeCompleted
|
||||
{
|
||||
get { return getExtinguisherObjective == null || getExtinguisherObjective.CanBeCompleted; }
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var extinguisherItem = character.Inventory.FindItemByIdentifier("extinguisher") ?? character.Inventory.FindItemByTag("extinguisher");
|
||||
if (extinguisherItem == null || extinguisherItem.Condition <= 0.0f || !character.HasEquippedItem(extinguisherItem))
|
||||
{
|
||||
if (getExtinguisherObjective == null)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogFindExtinguisher"), null, 2.0f, "findextinguisher", 30.0f);
|
||||
getExtinguisherObjective = new AIObjectiveGetItem(character, "extinguisher", true);
|
||||
}
|
||||
else
|
||||
{
|
||||
getExtinguisherObjective.TryComplete(deltaTime);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var extinguisher = extinguisherItem.GetComponent<RepairTool>();
|
||||
if (extinguisher == null)
|
||||
{
|
||||
DebugConsole.ThrowError("AIObjectiveExtinguishFire failed - the item \"" + extinguisherItem + "\" has no RepairTool component but is tagged as an extinguisher");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (FireSource fs in targetHull.FireSources.ToList())
|
||||
{
|
||||
if (fs.IsInDamageRange(character, MathHelper.Clamp(fs.DamageRange * 1.5f, extinguisher.Range * 0.5f, extinguisher.Range)) || useExtinquisherTimer > 0.0f)
|
||||
{
|
||||
useExtinquisherTimer += deltaTime;
|
||||
if (useExtinquisherTimer > 2.0f) useExtinquisherTimer = 0.0f;
|
||||
|
||||
character.CursorPosition = fs.Position;
|
||||
character.SetInput(InputType.Aim, false, true);
|
||||
character.AIController.SteeringManager.Reset();
|
||||
extinguisher.Use(deltaTime, character);
|
||||
|
||||
if (!targetHull.FireSources.Contains(fs))
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogPutOutFire").Replace("[roomname]", targetHull.Name), null, 0, "putoutfire", 10.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (FireSource fs in targetHull.FireSources)
|
||||
{
|
||||
//go to the first firesource
|
||||
if (gotoObjective == null || !gotoObjective.CanBeCompleted || gotoObjective.IsCompleted())
|
||||
{
|
||||
gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(fs.Position), character);
|
||||
}
|
||||
else
|
||||
{
|
||||
gotoObjective.TryComplete(deltaTime);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveExtinguishFires : AIObjective
|
||||
{
|
||||
public AIObjectiveExtinguishFires(Character character) :
|
||||
base(character, "")
|
||||
{
|
||||
if (!Hull.hullList.Any(h => h.FireSources.Count > 0))
|
||||
{
|
||||
character?.Speak(TextManager.Get("DialogNoFire"), null, 3.0f, "nofire", 30.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (objectiveManager.CurrentObjective == this)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
|
||||
return Hull.hullList.Count(h => h.FireSources.Count > 0) * 10;
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return !Hull.hullList.Any(h => h.FireSources.Count > 0);
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
return otherObjective is AIObjectiveExtinguishFires;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.FireSources.Count > 0)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveExtinguishFire(character, hull));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -7,19 +7,19 @@ namespace Barotrauma
|
||||
{
|
||||
private AIObjective subObjective;
|
||||
|
||||
private string gearName;
|
||||
private string gearTag;
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
for (int i = 0; i < character.Inventory.Items.Length; i++)
|
||||
{
|
||||
if (CharacterInventory.limbSlots[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
|
||||
if (character.Inventory.Items[i].Prefab.NameMatches(gearName) || character.Inventory.Items[i].HasTag(gearName))
|
||||
if (character.Inventory.SlotTypes[i] == InvSlotType.Any || character.Inventory.Items[i] == null) continue;
|
||||
if (character.Inventory.Items[i].HasTag(gearTag))
|
||||
{
|
||||
var containedItems = character.Inventory.Items[i].ContainedItems;
|
||||
if (containedItems == null) continue;
|
||||
|
||||
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.NameMatches("Oxygen Tank") || it.HasTag("oxygensource")) && it.Condition > 0.0f);
|
||||
var oxygenTank = Array.Find(containedItems, it => (it.Prefab.Identifier == "oxygentank" || it.HasTag("oxygensource")) && it.Condition > 0.0f);
|
||||
if (oxygenTank != null) return true;
|
||||
}
|
||||
}
|
||||
@@ -30,18 +30,19 @@ namespace Barotrauma
|
||||
public AIObjectiveFindDivingGear(Character character, bool needDivingSuit)
|
||||
: base(character, "")
|
||||
{
|
||||
gearName = needDivingSuit ? "Diving Suit" : "diving";
|
||||
gearTag = needDivingSuit ? "divingsuit" : "diving";
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var item = character.Inventory.FindItem(gearName);
|
||||
if (item == null)
|
||||
var item = character.Inventory.FindItemByTag(gearTag);
|
||||
if (item == null || !character.HasEquippedItem(item))
|
||||
{
|
||||
//get a diving mask/suit first
|
||||
if (!(subObjective is AIObjectiveGetItem))
|
||||
{
|
||||
subObjective = new AIObjectiveGetItem(character, gearName, true);
|
||||
character.Speak(TextManager.Get("DialogGetDivingGear"), null, 0.0f, "getdivinggear", 30.0f);
|
||||
subObjective = new AIObjectiveGetItem(character, gearTag, true);
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -57,7 +58,7 @@ namespace Barotrauma
|
||||
{
|
||||
containedItem.Drop();
|
||||
}
|
||||
else if (containedItem.Prefab.NameMatches("Oxygen Tank") || containedItem.HasTag("oxygensource"))
|
||||
else if (containedItem.Prefab.Identifier == "oxygentank" || containedItem.HasTag("oxygensource"))
|
||||
{
|
||||
//we've got an oxygen source inside the mask/suit, all good
|
||||
return;
|
||||
@@ -66,7 +67,8 @@ namespace Barotrauma
|
||||
|
||||
if (!(subObjective is AIObjectiveContainItem) || subObjective.IsCompleted())
|
||||
{
|
||||
subObjective = new AIObjectiveContainItem(character, new string[] { "Oxygen Tank", "oxygensource" }, item.GetComponent<ItemContainer>());
|
||||
character.Speak(TextManager.Get("DialogGetOxygenTank"), null, 0, "getoxygentank", 30.0f);
|
||||
subObjective = new AIObjectiveContainItem(character, new string[] { "oxygentank", "oxygensource" }, item.GetComponent<ItemContainer>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+81
-21
@@ -55,7 +55,10 @@ namespace Barotrauma
|
||||
var bestHull = FindBestHull();
|
||||
if (bestHull != null)
|
||||
{
|
||||
goToObjective = new AIObjectiveGoTo(bestHull, character);
|
||||
goToObjective = new AIObjectiveGoTo(bestHull, character)
|
||||
{
|
||||
AllowGoingOutside = true
|
||||
};
|
||||
}
|
||||
|
||||
searchHullTimer = SearchHullInterval;
|
||||
@@ -63,15 +66,57 @@ namespace Barotrauma
|
||||
|
||||
if (goToObjective != null)
|
||||
{
|
||||
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
|
||||
if (pathSteering != null && pathSteering.CurrentPath != null &&
|
||||
goToObjective.TryComplete(deltaTime);
|
||||
|
||||
if (character.AIController.SteeringManager is IndoorsSteeringManager pathSteering &&
|
||||
pathSteering.CurrentPath != null &&
|
||||
pathSteering.CurrentPath.Unreachable && !unreachable.Contains(goToObjective.Target))
|
||||
{
|
||||
unreachable.Add(goToObjective.Target as Hull);
|
||||
goToObjective = null;
|
||||
}
|
||||
}
|
||||
//goto objective doesn't exist (a safe hull not found, or a path to a safe hull not found)
|
||||
// -> attempt to manually steer away from hazards
|
||||
else if (currentHull != null)
|
||||
{
|
||||
Vector2 escapeVel = Vector2.Zero;
|
||||
foreach (FireSource fireSource in currentHull.FireSources)
|
||||
{
|
||||
int dir = Math.Sign(character.Position.X - fireSource.Position.X);
|
||||
|
||||
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(fireSource.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(dir * distMultiplier, 0.0f);
|
||||
}
|
||||
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (enemy.CurrentHull == currentHull && !enemy.IsDead && !enemy.IsUnconscious &&
|
||||
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
|
||||
{
|
||||
float distMultiplier = MathHelper.Clamp(100.0f / Vector2.Distance(enemy.Position, character.Position), 0.1f, 10.0f);
|
||||
escapeVel += new Vector2(Math.Sign(character.Position.X - enemy.Position.X) * distMultiplier, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
goToObjective.TryComplete(deltaTime);
|
||||
if (escapeVel != Vector2.Zero)
|
||||
{
|
||||
//only move if we haven't reached the edge of the room
|
||||
if ((escapeVel.X < 0 && character.Position.X > currentHull.Rect.X + 50) ||
|
||||
(escapeVel.X > 0 && character.Position.X < currentHull.Rect.Right - 50))
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringManual(deltaTime, escapeVel);
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AnimController.TargetDir = escapeVel.X < 0.0f ? Direction.Right : Direction.Left;
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,26 +182,30 @@ namespace Barotrauma
|
||||
return 150.0f - character.Oxygen;
|
||||
}
|
||||
|
||||
if (character.AnimController.CurrentHull == null) return 5.0f;
|
||||
currenthullSafety = GetHullSafety(character.AnimController.CurrentHull, character);
|
||||
if (character.CurrentHull == null) return 5.0f;
|
||||
currenthullSafety = GetHullSafety(character.CurrentHull, character);
|
||||
priority = 100.0f - currenthullSafety;
|
||||
|
||||
var nearbyHulls = character.AnimController.CurrentHull.GetConnectedHulls(3);
|
||||
//var nearbyHulls = character.CurrentHull.GetConnectedHulls(3);
|
||||
|
||||
foreach (Hull hull in nearbyHulls)
|
||||
//increase priority slightly if there's a fire in the room
|
||||
//(will increase more heavily if near the damage range of the fire)
|
||||
if (character.CurrentHull.FireSources.Count > 0)
|
||||
{
|
||||
priority += 5.0f;
|
||||
}
|
||||
|
||||
/*foreach (Hull hull in nearbyHulls)
|
||||
{
|
||||
foreach (FireSource fireSource in hull.FireSources)
|
||||
{
|
||||
//increase priority if almost within damage range of a fire
|
||||
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
|
||||
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
|
||||
character.Position.Y > hull.Rect.Y - hull.Rect.Height &&
|
||||
character.Position.Y < hull.Rect.Y)
|
||||
//heavily increase priority if almost within damage range of a fire
|
||||
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * 1.25f))
|
||||
{
|
||||
priority += Math.Max(fireSource.Size.X, 50.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
if (NeedsDivingGear())
|
||||
{
|
||||
@@ -189,25 +238,36 @@ namespace Barotrauma
|
||||
if (hull.OxygenPercentage < 30.0f) safety -= (30.0f - hull.OxygenPercentage) * 5.0f;
|
||||
|
||||
if (safety <= 0.0f) return 0.0f;
|
||||
|
||||
|
||||
bool extinguishFires =
|
||||
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFires ||
|
||||
character.AIController.ObjectiveManager?.CurrentOrder is AIObjectiveExtinguishFire;
|
||||
|
||||
float fireAmount = 0.0f;
|
||||
var nearbyHulls = hull.GetConnectedHulls(3);
|
||||
foreach (Hull hull2 in nearbyHulls)
|
||||
{
|
||||
foreach (FireSource fireSource in hull2.FireSources)
|
||||
{
|
||||
//increase priority if almost within damage range of a fire
|
||||
if (character.Position.X > fireSource.Position.X - fireSource.DamageRange * 2 &&
|
||||
character.Position.X < fireSource.Position.X + fireSource.Size.X + fireSource.DamageRange * 2 &&
|
||||
character.Position.Y > hull2.Rect.Y - hull2.Rect.Height &&
|
||||
character.Position.Y < hull2.Rect.Y)
|
||||
//increase priority if near the damage range of a fire
|
||||
//if extinguishing fires, the character can go closer the damage range
|
||||
if (fireSource.IsInDamageRange(character, fireSource.DamageRange * (extinguishFires ? 1.25f : 5.0f)))
|
||||
{
|
||||
fireAmount += Math.Max(fireSource.Size.X, 50.0f);
|
||||
fireAmount += Math.Max(fireSource.Size.X, AIObjectiveManager.OrderPriority + 1.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
safety -= fireAmount;
|
||||
|
||||
foreach (Character enemy in Character.CharacterList)
|
||||
{
|
||||
if (enemy.CurrentHull == hull && !enemy.IsDead && !enemy.IsUnconscious &&
|
||||
(enemy.AIController is EnemyAIController || enemy.TeamID != character.TeamID))
|
||||
{
|
||||
safety -= 10.0f;
|
||||
}
|
||||
}
|
||||
|
||||
return MathHelper.Clamp(safety, 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,7 @@ namespace Barotrauma
|
||||
class AIObjectiveFixLeak : AIObjective
|
||||
{
|
||||
private readonly Gap leak;
|
||||
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
|
||||
public Gap Leak
|
||||
{
|
||||
get { return leak; }
|
||||
@@ -47,11 +45,11 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
var weldingTool = character.Inventory.FindItem("Welding Tool");
|
||||
var weldingTool = character.Inventory.FindItemByTag("weldingtool");
|
||||
|
||||
if (weldingTool == null)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveGetItem(character, "Welding Tool", true));
|
||||
AddSubObjective(new AIObjectiveGetItem(character, "weldingtool", true));
|
||||
return;
|
||||
}
|
||||
else
|
||||
@@ -59,11 +57,10 @@ namespace Barotrauma
|
||||
var containedItems = weldingTool.ContainedItems;
|
||||
if (containedItems == null) return;
|
||||
|
||||
var fuelTank = Array.Find(containedItems, i => i.Prefab.NameMatches("Welding Fuel Tank") && i.Condition > 0.0f);
|
||||
|
||||
var fuelTank = Array.Find(containedItems, i => i.HasTag("weldingfueltank") && i.Condition > 0.0f);
|
||||
if (fuelTank == null)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveContainItem(character, "Welding Fuel Tank", weldingTool.GetComponent<ItemContainer>()));
|
||||
AddSubObjective(new AIObjectiveContainItem(character, "weldingfueltank", weldingTool.GetComponent<ItemContainer>()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +70,15 @@ namespace Barotrauma
|
||||
|
||||
Vector2 standPosition = GetStandPosition();
|
||||
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, leak.WorldPosition) > 100.0f * 100.0f)
|
||||
Vector2 gapDiff = leak.WorldPosition - character.WorldPosition;
|
||||
|
||||
if (!character.AnimController.InWater && character.AnimController is HumanoidAnimController &&
|
||||
Math.Abs(gapDiff.X) < 100.0f && gapDiff.Y < 0.0f && gapDiff.Y > -150.0f)
|
||||
{
|
||||
((HumanoidAnimController)character.AnimController).Crouching = true;
|
||||
}
|
||||
|
||||
if (Math.Abs(gapDiff.X) > 100.0f || Math.Abs(gapDiff.Y) > 150.0f)
|
||||
{
|
||||
var gotoObjective = new AIObjectiveGoTo(ConvertUnits.ToSimUnits(standPosition), character);
|
||||
if (!gotoObjective.IsCompleted())
|
||||
|
||||
@@ -105,9 +105,20 @@ namespace Barotrauma
|
||||
{
|
||||
if (gap.ConnectedWall == null) continue;
|
||||
if (gap.ConnectedDoor != null || gap.Open <= 0.0f) continue;
|
||||
|
||||
if (character.TeamID == 0)
|
||||
{
|
||||
if (gap.Submarine == null) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
//prevent characters from attempting to fix leaks in the enemy sub
|
||||
//team 1 plays in sub 0, team 2 in sub 1
|
||||
Submarine mySub = character.TeamID < 1 || character.TeamID > Submarine.MainSubs.Length ?
|
||||
Submarine.MainSub : Submarine.MainSubs[character.TeamID - 1];
|
||||
|
||||
//TODO: prevent the AI characters from fixing leaks in the enemy sub in sub-vs-sub missions if/when multiplayer bots are implemented
|
||||
if (gap.Submarine == null) continue;
|
||||
if (gap.Submarine != mySub) continue;
|
||||
}
|
||||
|
||||
float gapPriority = GetGapFixPriority(gap);
|
||||
|
||||
|
||||
@@ -10,7 +10,8 @@ namespace Barotrauma
|
||||
{
|
||||
public Func<Item, float> GetItemPriority;
|
||||
|
||||
private string[] itemNames;
|
||||
//can be either tags or identifiers
|
||||
private string[] itemIdentifiers;
|
||||
|
||||
private Item targetItem, moveToTarget;
|
||||
|
||||
@@ -45,29 +46,65 @@ namespace Barotrauma
|
||||
: base(character, "")
|
||||
{
|
||||
canBeCompleted = true;
|
||||
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
|
||||
currSearchIndex = 0;
|
||||
|
||||
this.targetItem = targetItem;
|
||||
}
|
||||
|
||||
public AIObjectiveGetItem(Character character, string itemName, bool equip = false)
|
||||
: this(character, new string[] { itemName }, equip)
|
||||
public AIObjectiveGetItem(Character character, string itemIdentifier, bool equip = false)
|
||||
: this(character, new string[] { itemIdentifier }, equip)
|
||||
{
|
||||
}
|
||||
|
||||
public AIObjectiveGetItem(Character character, string[] itemNames, bool equip = false)
|
||||
public AIObjectiveGetItem(Character character, string[] itemIdentifiers, bool equip = false)
|
||||
: base(character, "")
|
||||
{
|
||||
canBeCompleted = true;
|
||||
|
||||
currSearchIndex = -1;
|
||||
this.equip = equip;
|
||||
this.itemIdentifiers = itemIdentifiers;
|
||||
for (int i = 0; i < itemIdentifiers.Length; i++)
|
||||
{
|
||||
itemIdentifiers[i] = itemIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
|
||||
currSearchIndex = 0;
|
||||
CheckInventory();
|
||||
}
|
||||
|
||||
this.itemNames = itemNames;
|
||||
private void CheckInventory()
|
||||
{
|
||||
if (itemIdentifiers == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < character.Inventory.Items.Length; i++)
|
||||
{
|
||||
if (character.Inventory.Items[i] == null || character.Inventory.Items[i].Condition <= 0.0f) continue;
|
||||
if (itemIdentifiers.Any(id => character.Inventory.Items[i].Prefab.Identifier == id || character.Inventory.Items[i].HasTag(id)))
|
||||
{
|
||||
targetItem = character.Inventory.Items[i];
|
||||
moveToTarget = targetItem;
|
||||
currItemPriority = 100.0f;
|
||||
break;
|
||||
}
|
||||
//check items inside items (tool inside a toolbox etc)
|
||||
var containedItems = character.Inventory.Items[i].ContainedItems;
|
||||
if (containedItems != null)
|
||||
{
|
||||
foreach (Item containedItem in containedItems)
|
||||
{
|
||||
if (containedItem == null || containedItem.Condition <= 0.0f) continue;
|
||||
if (itemIdentifiers.Any(id => containedItem.Prefab.Identifier == id || containedItem.HasTag(id)))
|
||||
{
|
||||
targetItem = containedItem;
|
||||
moveToTarget = character.Inventory.Items[i];
|
||||
currItemPriority = 100.0f;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
@@ -99,7 +136,7 @@ namespace Barotrauma
|
||||
for (int i = 0; i < character.Inventory.Items.Length; i++)
|
||||
{
|
||||
//slot not needed by the item, continue
|
||||
if (!slots.HasFlag(CharacterInventory.limbSlots[i])) continue;
|
||||
if (!slots.HasFlag(character.Inventory.SlotTypes[i])) continue;
|
||||
|
||||
targetSlot = i;
|
||||
|
||||
@@ -117,7 +154,7 @@ namespace Barotrauma
|
||||
|
||||
targetItem.TryInteract(character, false, true);
|
||||
|
||||
if (targetSlot > -1 && character.Inventory.IsInLimbSlot(targetItem, InvSlotType.Any))
|
||||
if (targetSlot > -1 && !character.HasEquippedItem(targetItem))
|
||||
{
|
||||
character.Inventory.TryPutItem(targetItem, targetSlot, false, false, character);
|
||||
}
|
||||
@@ -127,14 +164,15 @@ namespace Barotrauma
|
||||
if (goToObjective == null || moveToTarget != goToObjective.Target)
|
||||
{
|
||||
//check if we're already looking for a diving gear
|
||||
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.NameMatches("Diving Gear") || targetItem.HasTag("diving")) ||
|
||||
(itemNames != null && (itemNames.Contains("diving") || itemNames.Contains("Diving Gear")));
|
||||
bool gettingDivingGear = (targetItem != null && targetItem.Prefab.Identifier == "divingsuit" || targetItem.HasTag("diving")) ||
|
||||
(itemIdentifiers != null && (itemIdentifiers.Contains("diving") || itemIdentifiers.Contains("divingsuit")));
|
||||
|
||||
//don't attempt to get diving gear to reach the destination if the item we're trying to get is diving gear
|
||||
goToObjective = new AIObjectiveGoTo(moveToTarget, character, false, !gettingDivingGear);
|
||||
}
|
||||
|
||||
goToObjective.TryComplete(deltaTime);
|
||||
if (!goToObjective.CanBeCompleted) targetItem = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -144,7 +182,7 @@ namespace Barotrauma
|
||||
/// </summary>
|
||||
private void FindTargetItem()
|
||||
{
|
||||
if (itemNames == null)
|
||||
if (itemIdentifiers == null)
|
||||
{
|
||||
if (targetItem == null) canBeCompleted = false;
|
||||
return;
|
||||
@@ -152,7 +190,7 @@ namespace Barotrauma
|
||||
|
||||
float currDist = moveToTarget == null ? 0.0f : Vector2.DistanceSquared(moveToTarget.Position, character.Position);
|
||||
|
||||
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 2; i++)
|
||||
for (int i = 0; i < 10 && currSearchIndex < Item.ItemList.Count - 1; i++)
|
||||
{
|
||||
currSearchIndex++;
|
||||
|
||||
@@ -160,21 +198,19 @@ namespace Barotrauma
|
||||
|
||||
if (item.CurrentHull == null || item.Condition <= 0.0f) continue;
|
||||
if (IgnoreContainedItems && item.Container != null) continue;
|
||||
if (!itemNames.Any(name => item.Prefab.NameMatches(name) || item.HasTag(name))) continue;
|
||||
if (!itemIdentifiers.Any(id => item.Prefab.Identifier == id || item.HasTag(id))) continue;
|
||||
|
||||
//if the item is inside a character's inventory, don't steal it unless the character is dead
|
||||
if (item.ParentInventory is CharacterInventory)
|
||||
{
|
||||
Character owner = item.ParentInventory.Owner as Character;
|
||||
if (owner != null && !owner.IsDead) continue;
|
||||
if (item.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
|
||||
}
|
||||
|
||||
//if the item is inside an item, which is inside a character's inventory, don't steal it
|
||||
Item rootContainer = item.GetRootContainer();
|
||||
if (rootContainer != null && rootContainer.ParentInventory is CharacterInventory)
|
||||
{
|
||||
Character owner = rootContainer.ParentInventory.Owner as Character;
|
||||
if (owner != null && !owner.IsDead) continue;
|
||||
if (rootContainer.ParentInventory.Owner is Character owner && !owner.IsDead) continue;
|
||||
}
|
||||
|
||||
float itemPriority = 0.0f;
|
||||
@@ -194,10 +230,11 @@ namespace Barotrauma
|
||||
|
||||
targetItem = item;
|
||||
moveToTarget = rootContainer ?? item;
|
||||
|
||||
}
|
||||
|
||||
//if searched through all the items and a target wasn't found, can't be completed
|
||||
if (currSearchIndex >= Item.ItemList.Count && targetItem == null) canBeCompleted = false;
|
||||
if (currSearchIndex >= Item.ItemList.Count - 1 && targetItem == null) canBeCompleted = false;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
@@ -205,16 +242,16 @@ namespace Barotrauma
|
||||
AIObjectiveGetItem getItem = otherObjective as AIObjectiveGetItem;
|
||||
if (getItem == null) return false;
|
||||
if (getItem.equip != equip) return false;
|
||||
if (getItem.itemNames != null && itemNames != null)
|
||||
if (getItem.itemIdentifiers != null && itemIdentifiers != null)
|
||||
{
|
||||
if (getItem.itemNames.Length != itemNames.Length) return false;
|
||||
for (int i = 0; i < getItem.itemNames.Length; i++)
|
||||
if (getItem.itemIdentifiers.Length != itemIdentifiers.Length) return false;
|
||||
for (int i = 0; i < getItem.itemIdentifiers.Length; i++)
|
||||
{
|
||||
if (getItem.itemNames[i] != itemNames[i]) return false;
|
||||
if (getItem.itemIdentifiers[i] != itemIdentifiers[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else if (getItem.itemNames == null && itemNames == null)
|
||||
else if (getItem.itemIdentifiers == null && itemIdentifiers == null)
|
||||
{
|
||||
return getItem.targetItem == targetItem;
|
||||
}
|
||||
@@ -224,11 +261,11 @@ namespace Barotrauma
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
if (itemNames != null)
|
||||
if (itemIdentifiers != null)
|
||||
{
|
||||
foreach (string itemName in itemNames)
|
||||
foreach (string itemName in itemIdentifiers)
|
||||
{
|
||||
var matchingItem = character.Inventory.FindItem(itemName);
|
||||
var matchingItem = character.Inventory.FindItemByTag(itemName) ?? character.Inventory.FindItemByIdentifier(itemName);
|
||||
if (matchingItem != null && (!equip || character.HasEquippedItem(matchingItem))) return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveGoTo : AIObjective
|
||||
{
|
||||
private Entity target;
|
||||
|
||||
private Vector2 targetPos;
|
||||
|
||||
private bool repeat;
|
||||
@@ -18,8 +16,17 @@ namespace Barotrauma
|
||||
|
||||
private bool getDivingGearIfNeeded;
|
||||
|
||||
public float CloseEnough = 0.5f;
|
||||
|
||||
public bool IgnoreIfTargetDead;
|
||||
|
||||
public bool AllowGoingOutside = false;
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (Target != null && Target.Removed) return 0.0f;
|
||||
if (IgnoreIfTargetDead && Target is Character character && character.IsDead) return 0.0f;
|
||||
|
||||
if (objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
@@ -32,25 +39,26 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Target != null && Target.Removed) return false;
|
||||
|
||||
if (repeat || waitUntilPathUnreachable > 0.0f) return true;
|
||||
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
|
||||
|
||||
//path doesn't exist (= hasn't been searched for yet), assume for now that the target is reachable
|
||||
if (pathSteering.CurrentPath == null) return true;
|
||||
if (pathSteering?.CurrentPath == null) return true;
|
||||
|
||||
return (!pathSteering.CurrentPath.Unreachable);
|
||||
if (!AllowGoingOutside && pathSteering.CurrentPath.HasOutdoorsNodes) return false;
|
||||
|
||||
return !pathSteering.CurrentPath.Unreachable;
|
||||
}
|
||||
}
|
||||
|
||||
public Entity Target
|
||||
{
|
||||
get { return target; }
|
||||
}
|
||||
public Entity Target { get; private set; }
|
||||
|
||||
public AIObjectiveGoTo(Entity target, Character character, bool repeat = false, bool getDivingGearIfNeeded = true)
|
||||
: base (character, "")
|
||||
{
|
||||
this.target = target;
|
||||
this.Target = target;
|
||||
this.repeat = repeat;
|
||||
|
||||
waitUntilPathUnreachable = 5.0f;
|
||||
@@ -70,58 +78,65 @@ namespace Barotrauma
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (target == character)
|
||||
if (Target == character)
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
waitUntilPathUnreachable -= deltaTime;
|
||||
|
||||
if (character.SelectedConstruction!=null && character.SelectedConstruction.GetComponent<Ladder>()==null)
|
||||
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
|
||||
{
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
|
||||
if (target != null) character.AIController.SelectTarget(target.AiTarget);
|
||||
if (Target != null) character.AIController.SelectTarget(Target.AiTarget);
|
||||
|
||||
Vector2 currTargetPos = Vector2.Zero;
|
||||
|
||||
if (target == null)
|
||||
if (Target == null)
|
||||
{
|
||||
currTargetPos = targetPos;
|
||||
}
|
||||
else
|
||||
{
|
||||
currTargetPos = target.SimPosition;
|
||||
currTargetPos = Target.SimPosition;
|
||||
|
||||
//if character is outside the sub and target isn't, transform the position
|
||||
if (character.Submarine != null && target.Submarine == null)
|
||||
//if character is inside the sub and target isn't, transform the position
|
||||
if (character.Submarine != null && Target.Submarine == null)
|
||||
{
|
||||
currTargetPos -= character.Submarine.SimPosition;
|
||||
}
|
||||
}
|
||||
|
||||
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < 0.5f * 0.5f)
|
||||
if (Vector2.DistanceSquared(currTargetPos, character.SimPosition) < CloseEnough * CloseEnough)
|
||||
{
|
||||
character.AIController.SteeringManager.Reset();
|
||||
character.AnimController.TargetDir = currTargetPos.X > character.SimPosition.X ? Direction.Right : Direction.Left;
|
||||
}
|
||||
else
|
||||
{
|
||||
character.AIController.SteeringManager.SteeringSeek(currTargetPos);
|
||||
|
||||
var indoorsSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
|
||||
|
||||
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
indoorsSteering.SteeringWander();
|
||||
}
|
||||
else if (getDivingGearIfNeeded && indoorsSteering.CurrentPath != null && indoorsSteering.CurrentPath.HasOutdoorsNodes)
|
||||
float normalSpeed = character.AnimController.GetCurrentSpeed(false);
|
||||
character.AIController.SteeringManager.SteeringSeek(currTargetPos, normalSpeed);
|
||||
if (getDivingGearIfNeeded && Target?.Submarine == null && AllowGoingOutside)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
|
||||
}
|
||||
else if (character.AIController.SteeringManager is IndoorsSteeringManager indoorsSteering)
|
||||
{
|
||||
if (indoorsSteering.CurrentPath == null || indoorsSteering.CurrentPath.Unreachable)
|
||||
{
|
||||
indoorsSteering.SteeringWander(normalSpeed);
|
||||
}
|
||||
else if (AllowGoingOutside &&
|
||||
getDivingGearIfNeeded &&
|
||||
indoorsSteering.CurrentPath != null &&
|
||||
indoorsSteering.CurrentPath.HasOutdoorsNodes)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveFindDivingGear(character, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,15 +147,18 @@ namespace Barotrauma
|
||||
bool completed = false;
|
||||
|
||||
float allowedDistance = 0.5f;
|
||||
var item = target as Item;
|
||||
|
||||
if (item != null)
|
||||
if (Target is Item item)
|
||||
{
|
||||
allowedDistance = Math.Max(ConvertUnits.ToSimUnits(item.InteractDistance), allowedDistance);
|
||||
if (item.IsInsideTrigger(character.WorldPosition)) completed = true;
|
||||
}
|
||||
else if (Target is Character targetCharacter)
|
||||
{
|
||||
if (character.CanInteractWith(targetCharacter)) completed = true;
|
||||
}
|
||||
|
||||
completed = completed || Vector2.DistanceSquared(target != null ? target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
|
||||
completed = completed || Vector2.DistanceSquared(Target != null ? Target.SimPosition : targetPos, character.SimPosition) < allowedDistance * allowedDistance;
|
||||
|
||||
if (completed) character.AIController.SteeringManager.Reset();
|
||||
|
||||
@@ -152,7 +170,7 @@ namespace Barotrauma
|
||||
AIObjectiveGoTo objective = otherObjective as AIObjectiveGoTo;
|
||||
if (objective == null) return false;
|
||||
|
||||
if (objective.target == target) return true;
|
||||
if (objective.Target == Target) return true;
|
||||
|
||||
return (objective.targetPos == targetPos);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -12,10 +14,15 @@ namespace Barotrauma
|
||||
private AITarget currentTarget;
|
||||
private float newTargetTimer;
|
||||
|
||||
private float standStillTimer;
|
||||
private float walkDuration;
|
||||
|
||||
private AIObjectiveFindSafety findSafety;
|
||||
|
||||
public AIObjectiveIdle(Character character) : base(character, "")
|
||||
{
|
||||
standStillTimer = Rand.Range(-10.0f, 10.0f);
|
||||
walkDuration = Rand.Range(0.0f, 10.0f);
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
@@ -33,6 +40,16 @@ namespace Barotrauma
|
||||
var pathSteering = character.AIController.SteeringManager as IndoorsSteeringManager;
|
||||
if (pathSteering == null) return;
|
||||
|
||||
//don't keep dragging others when idling
|
||||
if (character.SelectedCharacter != null)
|
||||
{
|
||||
character.DeselectCharacter();
|
||||
}
|
||||
if (character.SelectedConstruction != null && character.SelectedConstruction.GetComponent<Ladder>() == null)
|
||||
{
|
||||
character.SelectedConstruction = null;
|
||||
}
|
||||
|
||||
if (character.AnimController.InWater)
|
||||
{
|
||||
//attempt to find a safer place if in water
|
||||
@@ -48,10 +65,13 @@ namespace Barotrauma
|
||||
if (currentTarget != null)
|
||||
{
|
||||
Vector2 pos = character.SimPosition;
|
||||
if (character != null && character.Submarine == null) pos -= Submarine.MainSub.SimPosition;
|
||||
|
||||
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition);
|
||||
if (path.Cost > 200.0f && character.AnimController.CurrentHull!=null) return;
|
||||
if (character != null && character.Submarine == null) { pos -= Submarine.MainSub.SimPosition; }
|
||||
|
||||
string errorMsg = "(Character " + character.Name + " idling, target "
|
||||
+ ((currentTarget.Entity is Hull hull && hull.RoomName != null) ? hull.RoomName : currentTarget.Entity.ToString()) + ")";
|
||||
|
||||
var path = pathSteering.PathFinder.FindPath(pos, currentTarget.SimPosition, errorMsg);
|
||||
if (path.Cost > 1000.0f && character.AnimController.CurrentHull!=null) return;
|
||||
|
||||
pathSteering.SetPath(path);
|
||||
}
|
||||
@@ -70,6 +90,19 @@ namespace Barotrauma
|
||||
if (pathSteering == null || (pathSteering.CurrentPath != null &&
|
||||
(pathSteering.CurrentPath.NextNode == null || pathSteering.CurrentPath.Unreachable || pathSteering.CurrentPath.HasOutdoorsNodes)))
|
||||
{
|
||||
standStillTimer -= deltaTime;
|
||||
if (standStillTimer > 0.0f)
|
||||
{
|
||||
walkDuration = Rand.Range(1.0f, 5.0f);
|
||||
pathSteering.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (standStillTimer < -walkDuration)
|
||||
{
|
||||
standStillTimer = Rand.Range(1.0f, 10.0f);
|
||||
}
|
||||
|
||||
//steer away from edges of the hull
|
||||
if (character.AnimController.CurrentHull != null)
|
||||
{
|
||||
@@ -102,7 +135,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
character.AIController.SteeringManager.SteeringWander();
|
||||
character.AIController.SteeringManager.SteeringWander(character.AnimController.GetCurrentSpeed(false));
|
||||
//reset vertical steering to prevent dropping down from platforms etc
|
||||
character.AIController.SteeringManager.ResetY();
|
||||
|
||||
@@ -115,20 +148,21 @@ namespace Barotrauma
|
||||
currentTarget = null;
|
||||
return;
|
||||
}
|
||||
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, 2.0f);
|
||||
character.AIController.SteeringManager.SteeringSeek(currentTarget.SimPosition, character.AnimController.GetCurrentSpeed(true));
|
||||
}
|
||||
|
||||
private AITarget FindRandomTarget()
|
||||
{
|
||||
if (Rand.Int(5)==1)
|
||||
//random chance of navigating back to the room where the character spawned
|
||||
if (Rand.Int(5) == 1)
|
||||
{
|
||||
var idCard = character.Inventory.FindItem("ID Card");
|
||||
if (idCard==null) return null;
|
||||
var idCard = character.Inventory.FindItemByIdentifier("idcard");
|
||||
if (idCard == null) return null;
|
||||
|
||||
foreach (WayPoint wp in WayPoint.WayPointList)
|
||||
{
|
||||
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull==null) continue;
|
||||
|
||||
if (wp.SpawnType != SpawnType.Human || wp.CurrentHull == null) continue;
|
||||
|
||||
foreach (string tag in wp.IdCardTags)
|
||||
{
|
||||
if (idCard.HasTag(tag)) return wp.CurrentHull.AiTarget;
|
||||
@@ -140,9 +174,31 @@ namespace Barotrauma
|
||||
List<Hull> targetHulls = new List<Hull>(Hull.hullList);
|
||||
//ignore all hulls with fires or water in them
|
||||
targetHulls.RemoveAll(h => h.FireSources.Any() || h.WaterVolume / h.Volume > 0.1f);
|
||||
if (!targetHulls.Any()) return null;
|
||||
if (character.Submarine != null)
|
||||
{
|
||||
targetHulls.RemoveAll(h => h.Submarine != character.Submarine);
|
||||
}
|
||||
|
||||
return targetHulls[Rand.Range(0, targetHulls.Count)].AiTarget;
|
||||
//remove ballast hulls
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
if (item.HasTag("ballast") && targetHulls.Contains(item.CurrentHull))
|
||||
{
|
||||
targetHulls.Remove(item.CurrentHull);
|
||||
}
|
||||
}
|
||||
|
||||
//ignore hulls that are too low to stand inside
|
||||
if (character.AnimController is HumanoidAnimController animController)
|
||||
{
|
||||
float minHeight = ConvertUnits.ToDisplayUnits(animController.HeadPosition.Value);
|
||||
targetHulls.RemoveAll(h => h.CeilingHeight < minHeight);
|
||||
}
|
||||
if (!targetHulls.Any()) return null;
|
||||
|
||||
//prefer larger hulls
|
||||
var targetHull = ToolBox.SelectWeightedRandom(targetHulls, targetHulls.Select(h => h.Volume).ToList(), Rand.RandSync.Unsynced);
|
||||
return targetHull?.AiTarget;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -12,7 +14,12 @@ namespace Barotrauma
|
||||
private Character character;
|
||||
|
||||
private AIObjective currentOrder;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// When set above zero, the character will stand still doing nothing until the timer runs out (assuming they don't a high priority order active)
|
||||
/// </summary>
|
||||
public float WaitTimer;
|
||||
|
||||
public AIObjective CurrentOrder
|
||||
{
|
||||
get { return currentOrder; }
|
||||
@@ -38,6 +45,23 @@ namespace Barotrauma
|
||||
objectives.Add(objective);
|
||||
}
|
||||
|
||||
public Dictionary<AIObjective, CoroutineHandle> DelayedObjectives { get; private set; } = new Dictionary<AIObjective, CoroutineHandle>();
|
||||
public void AddObjective(AIObjective objective, float delay, Action callback = null)
|
||||
{
|
||||
if (DelayedObjectives.TryGetValue(objective, out CoroutineHandle coroutine))
|
||||
{
|
||||
CoroutineManager.StopCoroutines(coroutine);
|
||||
DelayedObjectives.Remove(objective);
|
||||
}
|
||||
coroutine = CoroutineManager.InvokeAfter(() =>
|
||||
{
|
||||
DelayedObjectives.Remove(objective);
|
||||
AddObjective(objective);
|
||||
callback?.Invoke();
|
||||
}, delay);
|
||||
DelayedObjectives.Add(objective, coroutine);
|
||||
}
|
||||
|
||||
public T GetObjective<T>() where T : AIObjective
|
||||
{
|
||||
foreach (AIObjective objective in objectives)
|
||||
@@ -47,15 +71,21 @@ namespace Barotrauma
|
||||
return null;
|
||||
}
|
||||
|
||||
public float GetCurrentPriority(Character character)
|
||||
private AIObjective GetCurrentObjective()
|
||||
{
|
||||
if (CurrentOrder != null &&
|
||||
(objectives.Count == 0 || currentOrder.GetPriority(this) > objectives[0].GetPriority(this)))
|
||||
{
|
||||
return CurrentOrder.GetPriority(this);
|
||||
return CurrentOrder;
|
||||
}
|
||||
|
||||
return objectives.Count == 0 ? 0.0f : objectives[0].GetPriority(this);
|
||||
return objectives.Count == 0 ? null : objectives[0];
|
||||
}
|
||||
|
||||
public float GetCurrentPriority()
|
||||
{
|
||||
var currentObjective = GetCurrentObjective();
|
||||
return currentObjective == null ? 0.0f : currentObjective.GetPriority(this);
|
||||
}
|
||||
|
||||
public void UpdateObjectives()
|
||||
@@ -67,46 +97,77 @@ namespace Barotrauma
|
||||
|
||||
//sort objectives according to priority
|
||||
objectives.Sort((x, y) => y.GetPriority(this).CompareTo(x.GetPriority(this)));
|
||||
GetCurrentObjective()?.SortSubObjectives(this);
|
||||
}
|
||||
|
||||
|
||||
public void DoCurrentObjective(float deltaTime)
|
||||
{
|
||||
if (currentOrder != null && (!objectives.Any() || objectives[0].GetPriority(this) < currentOrder.GetPriority(this)))
|
||||
CurrentObjective = GetCurrentObjective();
|
||||
|
||||
if (CurrentObjective == null || (CurrentObjective.GetPriority(this) < OrderPriority && WaitTimer > 0.0f))
|
||||
{
|
||||
CurrentObjective = currentOrder;
|
||||
currentOrder.TryComplete(deltaTime);
|
||||
WaitTimer -= deltaTime;
|
||||
character.AIController.SteeringManager.Reset();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!objectives.Any()) return;
|
||||
objectives[0].TryComplete(deltaTime);
|
||||
|
||||
CurrentObjective = objectives[0];
|
||||
CurrentObjective?.TryComplete(deltaTime);
|
||||
}
|
||||
|
||||
public void SetOrder(AIObjective objective)
|
||||
{
|
||||
currentOrder = objective;
|
||||
}
|
||||
|
||||
public void SetOrder(Order order, string option)
|
||||
public void SetOrder(Order order, string option, Character orderGiver)
|
||||
{
|
||||
currentOrder = null;
|
||||
if (order == null) return;
|
||||
|
||||
currentOrder = null;
|
||||
|
||||
switch (order.Name.ToLowerInvariant())
|
||||
switch (order.AITag.ToLowerInvariant())
|
||||
{
|
||||
case "follow":
|
||||
currentOrder = new AIObjectiveGoTo(Character.Controlled, character, true);
|
||||
currentOrder = new AIObjectiveGoTo(orderGiver, character, true)
|
||||
{
|
||||
CloseEnough = 1.5f,
|
||||
AllowGoingOutside = true,
|
||||
IgnoreIfTargetDead = true
|
||||
};
|
||||
break;
|
||||
case "wait":
|
||||
currentOrder = new AIObjectiveGoTo(character, character, true);
|
||||
currentOrder = new AIObjectiveGoTo(character, character, true)
|
||||
{
|
||||
AllowGoingOutside = true
|
||||
};
|
||||
break;
|
||||
case "fixleaks":
|
||||
case "fix leaks":
|
||||
currentOrder = new AIObjectiveFixLeaks(character);
|
||||
break;
|
||||
case "chargebatteries":
|
||||
currentOrder = new AIObjectiveChargeBatteries(character, option);
|
||||
break;
|
||||
case "rescue":
|
||||
currentOrder = new AIObjectiveRescueAll(character);
|
||||
break;
|
||||
case "repairsystems":
|
||||
currentOrder = new AIObjectiveRepairItems(character) { RequireAdequateSkills = option != "all" };
|
||||
break;
|
||||
case "pumpwater":
|
||||
currentOrder = new AIObjectivePumpWater(character, option);
|
||||
break;
|
||||
case "extinguishfires":
|
||||
currentOrder = new AIObjectiveExtinguishFires(character);
|
||||
break;
|
||||
case "steer":
|
||||
var steering = (order?.TargetEntity as Item)?.GetComponent<Steering>();
|
||||
if (steering != null) steering.PosToMaintain = steering.Item.Submarine?.WorldPosition;
|
||||
if (order.TargetItemComponent == null) return;
|
||||
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
|
||||
break;
|
||||
default:
|
||||
if (order.TargetItem == null) return;
|
||||
|
||||
currentOrder = new AIObjectiveOperateItem(order.TargetItem, character, option, false, null, order.UseController);
|
||||
|
||||
if (order.TargetItemComponent == null) return;
|
||||
currentOrder = new AIObjectiveOperateItem(order.TargetItemComponent, character, option, false, null, order.UseController);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
+30
-9
@@ -17,10 +17,18 @@ namespace Barotrauma
|
||||
|
||||
private bool requireEquip;
|
||||
|
||||
private bool useController;
|
||||
|
||||
private AIObjectiveGoTo gotoObjective;
|
||||
|
||||
public override bool CanBeCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
if (gotoObjective != null && !gotoObjective.CanBeCompleted) return false;
|
||||
|
||||
if (useController && controller == null) return false;
|
||||
|
||||
return canBeCompleted;
|
||||
}
|
||||
}
|
||||
@@ -43,23 +51,30 @@ namespace Barotrauma
|
||||
public AIObjectiveOperateItem(ItemComponent item, Character character, string option, bool requireEquip, Entity operateTarget = null, bool useController = false)
|
||||
: base (character, option)
|
||||
{
|
||||
this.component = item;
|
||||
this.component = item ?? throw new System.ArgumentNullException("item", "Attempted to create an AIObjectiveOperateItem with a null target.");
|
||||
this.requireEquip = requireEquip;
|
||||
this.operateTarget = operateTarget;
|
||||
this.useController = useController;
|
||||
|
||||
if (useController)
|
||||
{
|
||||
var controllers = item.Item.GetConnectedComponents<Controller>();
|
||||
var controllers = component.Item.GetConnectedComponents<Controller>();
|
||||
if (controllers.Any()) controller = controllers[0];
|
||||
}
|
||||
|
||||
|
||||
canBeCompleted = true;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
ItemComponent target = controller == null ? component : controller;
|
||||
ItemComponent target = useController ? controller : component;
|
||||
|
||||
if (useController && controller == null)
|
||||
{
|
||||
character.Speak(TextManager.Get("DialogCantFindController").Replace("[item]", component.Item.Name), null, 2.0f, "cantfindcontroller", 30.0f);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (target.CanBeSelected)
|
||||
{
|
||||
@@ -75,11 +90,17 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
AddSubObjective(new AIObjectiveGoTo(target.Item, character));
|
||||
AddSubObjective(gotoObjective = new AIObjectiveGoTo(target.Item, character));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!character.Inventory.Items.Contains(component.Item))
|
||||
if (component.Item.GetComponent<Pickable>() == null)
|
||||
{
|
||||
//controller/target can't be selected and the item cannot be picked -> objective can't be completed
|
||||
canBeCompleted = false;
|
||||
return;
|
||||
}
|
||||
else if (!character.Inventory.Items.Contains(component.Item))
|
||||
{
|
||||
AddSubObjective(new AIObjectiveGetItem(character, component.Item, true));
|
||||
}
|
||||
@@ -95,10 +116,10 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < CharacterInventory.limbSlots.Length; i++)
|
||||
for (int i = 0; i < character.Inventory.Capacity; i++)
|
||||
{
|
||||
if (CharacterInventory.limbSlots[i] == InvSlotType.Any ||
|
||||
!holdable.AllowedSlots.Any(s => s.HasFlag(CharacterInventory.limbSlots[i])))
|
||||
if (character.Inventory.SlotTypes[i] == InvSlotType.Any ||
|
||||
!holdable.AllowedSlots.Any(s => s.HasFlag(character.Inventory.SlotTypes[i])))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectivePumpWater : AIObjective
|
||||
{
|
||||
private const float FindPumpsInterval = 5.0f;
|
||||
|
||||
private string orderOption;
|
||||
private List<Pump> pumps;
|
||||
private float lastFindPumpsTime;
|
||||
|
||||
public AIObjectivePumpWater(Character character, string option)
|
||||
: base(character, option)
|
||||
{
|
||||
orderOption = option;
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (Timing.TotalTime >= lastFindPumpsTime + FindPumpsInterval)
|
||||
{
|
||||
FindPumps();
|
||||
}
|
||||
|
||||
if (objectiveManager.CurrentOrder == this && pumps.Count > 0)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
return otherObjective is AIObjectivePumpWater;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (Timing.TotalTime < lastFindPumpsTime + FindPumpsInterval)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
FindPumps();
|
||||
}
|
||||
|
||||
private void FindPumps()
|
||||
{
|
||||
lastFindPumpsTime = (float)Timing.TotalTime;
|
||||
|
||||
pumps = new List<Pump>();
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
//don't attempt to use pumps outside the sub
|
||||
if (item.Submarine == null) { continue; }
|
||||
|
||||
var pump = item.GetComponent<Pump>();
|
||||
if (pump == null) continue;
|
||||
|
||||
if (item.HasTag("ballast")) continue;
|
||||
|
||||
//if the pump is connected to an item with a steering component, it must be a ballast pump
|
||||
//(This may not work correctly if the signals are passed through some fancy circuit or a wifi component,
|
||||
//which is why sub creators are encouraged to tag the ballast pumps)
|
||||
bool connectedToSteering = false;
|
||||
foreach (Connection c in item.Connections)
|
||||
{
|
||||
if (c.IsPower) continue;
|
||||
if (item.GetConnectedComponentsRecursive<Steering>(c).Count > 0)
|
||||
{
|
||||
connectedToSteering = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (connectedToSteering) continue;
|
||||
|
||||
if (orderOption.ToLowerInvariant() == "stop pumping")
|
||||
{
|
||||
if (!pump.IsActive || pump.FlowPercentage == 0.0f) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!pump.Item.InWater) continue;
|
||||
if (pump.IsActive && pump.FlowPercentage <= -90.0f) continue;
|
||||
}
|
||||
|
||||
pumps.Add(pump);
|
||||
}
|
||||
|
||||
|
||||
foreach (Pump pump in pumps)
|
||||
{
|
||||
AddSubObjective(new AIObjectiveOperateItem(pump, character, orderOption, false));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItem : AIObjective
|
||||
{
|
||||
private Item item;
|
||||
|
||||
public AIObjectiveRepairItem(Character character, Item item)
|
||||
: base(character, "")
|
||||
{
|
||||
this.item = item;
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
bool insufficientSkills = true;
|
||||
bool repairablesFound = false;
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
if (item.Condition > repairable.ShowRepairUIThreshold) { continue; }
|
||||
if (repairable.DegreeOfSuccess(character) >= 0.5f) { insufficientSkills = false; }
|
||||
repairablesFound = true;
|
||||
}
|
||||
|
||||
if (!repairablesFound) { return 0.0f; }
|
||||
|
||||
float priority = 100.0f - item.Condition;
|
||||
//vertical distance matters more than horizontal (climbing up/down is harder than moving horizontally)
|
||||
float dist =
|
||||
Math.Abs(character.WorldPosition.X - item.WorldPosition.X) +
|
||||
Math.Abs(character.WorldPosition.Y - item.WorldPosition.Y) * 2.0f;
|
||||
|
||||
//heavily increase the priority if the item is already selected
|
||||
//so characters don't keep switching between nearby damaged items
|
||||
if (character.SelectedConstruction == item)
|
||||
{
|
||||
priority += 50.0f;
|
||||
}
|
||||
if (insufficientSkills)
|
||||
{
|
||||
return MathHelper.Lerp(0.0f, 50.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
|
||||
}
|
||||
else
|
||||
{
|
||||
return MathHelper.Lerp(50.0f, 100.0f, priority / 100.0f / Math.Max(dist / 100.0f, 1.0f));
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
if (item.Condition < Math.Max(repairable.ShowRepairUIThreshold, item.Prefab.Health * 0.98f)) return false;
|
||||
}
|
||||
|
||||
character?.Speak(TextManager.Get("DialogItemRepaired").Replace("[itemname]", item.Name), null, 0.0f, "itemrepaired", 10.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
return otherObjective is AIObjectiveRepairItem repairObjective && repairObjective.item == item;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
//make sure we have all the items required to fix the target item
|
||||
foreach (var kvp in repairable.requiredItems)
|
||||
{
|
||||
foreach (RelatedItem requiredItem in kvp.Value)
|
||||
{
|
||||
if (!character.Inventory.Items.Any(it => it != null && requiredItem.MatchesItem(it)))
|
||||
{
|
||||
AddSubObjective(new AIObjectiveGetItem(character, requiredItem.Identifiers, true));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (character.CanInteractWith(item))
|
||||
{
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
if (character.SelectedConstruction != item) { item.TryInteract(character, true, true); }
|
||||
repairable.CurrentFixer = character;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSubObjective(new AIObjectiveGoTo(item, character));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIObjectiveRepairItems : AIObjective
|
||||
{
|
||||
/// <summary>
|
||||
/// Should the character only attempt to fix items they have the skills to fix, or any damaged item
|
||||
/// </summary>
|
||||
public bool RequireAdequateSkills;
|
||||
|
||||
public AIObjectiveRepairItems(Character character)
|
||||
: base(character, "")
|
||||
{
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
GetBrokenItems();
|
||||
if (subObjectives.Count > 0 && objectiveManager.CurrentOrder == this)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool IsDuplicate(AIObjective otherObjective)
|
||||
{
|
||||
return otherObjective is AIObjectiveRepairItems repairItems && repairItems.RequireAdequateSkills == RequireAdequateSkills;
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
GetBrokenItems();
|
||||
}
|
||||
|
||||
private void GetBrokenItems()
|
||||
{
|
||||
foreach (Item item in Item.ItemList)
|
||||
{
|
||||
//ignore items that are in full condition
|
||||
if (item.Condition >= 100.0f) continue;
|
||||
foreach (Repairable repairable in item.Repairables)
|
||||
{
|
||||
//ignore ones that are already fixed
|
||||
if (item.Condition > repairable.ShowRepairUIThreshold) continue;
|
||||
|
||||
if (RequireAdequateSkills)
|
||||
{
|
||||
if (!repairable.HasRequiredSkills(character)) { continue; }
|
||||
}
|
||||
|
||||
AddSubObjective(new AIObjectiveRepairItem(character, item));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,37 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/*class AIObjectiveRescue : AIObjective
|
||||
class AIObjectiveRescue : AIObjective
|
||||
{
|
||||
const float TreatmentDelay = 0.5f;
|
||||
|
||||
private readonly Character targetCharacter;
|
||||
|
||||
private AIObjectiveGoTo goToObjective;
|
||||
|
||||
private float treatmentTimer;
|
||||
|
||||
public override bool CanBeCompleted
|
||||
{
|
||||
get
|
||||
{
|
||||
if (targetCharacter.Removed) return false;
|
||||
if (goToObjective != null && !goToObjective.CanBeCompleted) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public AIObjectiveRescue(Character character, Character targetCharacter)
|
||||
: base (character, "")
|
||||
: base(character, "")
|
||||
{
|
||||
Debug.Assert(character != targetCharacter);
|
||||
|
||||
this.targetCharacter = targetCharacter;
|
||||
}
|
||||
|
||||
@@ -21,14 +41,189 @@ namespace Barotrauma
|
||||
return rescueObjective != null && rescueObjective.targetCharacter == targetCharacter;
|
||||
}
|
||||
|
||||
public override float GetPriority(Character character)
|
||||
protected override void Act(float deltaTime)
|
||||
{
|
||||
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
|
||||
//target in water -> move to a dry place first
|
||||
if (targetCharacter.AnimController.InWater)
|
||||
{
|
||||
if (character.SelectedCharacter != targetCharacter)
|
||||
{
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
|
||||
}
|
||||
else
|
||||
{
|
||||
character.SelectCharacter(targetCharacter);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddSubObjective(new AIObjectiveFindSafety(character));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
|
||||
//target not in water -> we can start applying treatment
|
||||
if (!character.CanInteractWith(targetCharacter))
|
||||
{
|
||||
AddSubObjective(goToObjective = new AIObjectiveGoTo(targetCharacter, character));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.SelectedCharacter == null)
|
||||
{
|
||||
character?.Speak(TextManager.Get("DialogFoundUnconsciousTarget")
|
||||
.Replace("[targetname]", targetCharacter.Name).Replace("[roomname]", character.CurrentHull.RoomName),
|
||||
null, 1.0f,
|
||||
"foundunconscioustarget" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
|
||||
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
|
||||
character.SelectCharacter(targetCharacter);
|
||||
GiveTreatment(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
protected override bool ShouldInterruptSubObjective(AIObjective subObjective)
|
||||
{
|
||||
if (subObjective is AIObjectiveFindSafety)
|
||||
{
|
||||
if (character.SelectedCharacter != targetCharacter) return true;
|
||||
if (character.AnimController.InWater || targetCharacter.AnimController.InWater) return false;
|
||||
|
||||
foreach (Limb limb in targetCharacter.AnimController.Limbs)
|
||||
{
|
||||
if (!Submarine.RectContains(targetCharacter.CurrentHull.WorldRect, limb.WorldPosition)) return false;
|
||||
}
|
||||
|
||||
return !character.AnimController.InWater && !targetCharacter.AnimController.InWater &&
|
||||
AIObjectiveFindSafety.GetHullSafety(character.CurrentHull, character) > 50.0f;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void GiveTreatment(float deltaTime)
|
||||
{
|
||||
if (treatmentTimer > 0.0f)
|
||||
{
|
||||
treatmentTimer -= deltaTime;
|
||||
}
|
||||
treatmentTimer = TreatmentDelay;
|
||||
|
||||
var allAfflictions = targetCharacter.CharacterHealth.GetAllAfflictions()
|
||||
.Where(a => a.GetVitalityDecrease(targetCharacter.CharacterHealth) > 0)
|
||||
.ToList();
|
||||
|
||||
allAfflictions.Sort((a1, a2) =>
|
||||
{
|
||||
return Math.Sign(a2.GetVitalityDecrease(targetCharacter.CharacterHealth) - a1.GetVitalityDecrease(targetCharacter.CharacterHealth));
|
||||
});
|
||||
|
||||
//check if we already have a suitable treatment for any of the afflictions
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
|
||||
{
|
||||
if (treatmentSuitability.Value > 0.0f)
|
||||
{
|
||||
Item matchingItem = character.Inventory.FindItemByIdentifier(treatmentSuitability.Key);
|
||||
if (matchingItem == null) { continue; }
|
||||
ApplyTreatment(affliction, matchingItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//didn't have any suitable treatments available, try to find some medical items
|
||||
HashSet<string> suitableItemIdentifiers = new HashSet<string>();
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
foreach (KeyValuePair<string, float> treatmentSuitability in affliction.Prefab.TreatmentSuitability)
|
||||
{
|
||||
if (treatmentSuitability.Value > 0.0f)
|
||||
{
|
||||
suitableItemIdentifiers.Add(treatmentSuitability.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (suitableItemIdentifiers.Count > 0)
|
||||
{
|
||||
List<string> itemNameList = new List<string>();
|
||||
foreach (string itemIdentifier in suitableItemIdentifiers)
|
||||
{
|
||||
if (MapEntityPrefab.Find(null, itemIdentifier, showErrorMessages: false) is ItemPrefab itemPrefab)
|
||||
{
|
||||
itemNameList.Add(itemPrefab.Name);
|
||||
}
|
||||
//only list the first 4 items
|
||||
if (itemNameList.Count >= 4) break;
|
||||
}
|
||||
|
||||
if (itemNameList.Count > 0)
|
||||
{
|
||||
string itemListStr = "";
|
||||
if (itemNameList.Count == 1)
|
||||
{
|
||||
itemListStr = itemNameList[0];
|
||||
}
|
||||
else
|
||||
{
|
||||
itemListStr = string.Join(" or ", string.Join(", ", itemNameList.Take(itemNameList.Count - 1)), itemNameList.Last());
|
||||
}
|
||||
character?.Speak(TextManager.Get("DialogListRequiredTreatments")
|
||||
.Replace("[targetname]", targetCharacter.Name)
|
||||
.Replace("[treatmentlist]", itemListStr),
|
||||
null, 2.0f, "listrequiredtreatments" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
|
||||
character.DeselectCharacter();
|
||||
AddSubObjective(new AIObjectiveGetItem(character, suitableItemIdentifiers.ToArray(), true));
|
||||
}
|
||||
|
||||
character.AnimController.Anim = AnimController.Animation.CPR;
|
||||
}
|
||||
|
||||
private void ApplyTreatment(Affliction affliction, Item item)
|
||||
{
|
||||
var targetLimb = targetCharacter.CharacterHealth.GetAfflictionLimb(affliction);
|
||||
|
||||
bool remove = false;
|
||||
foreach (ItemComponent ic in item.components)
|
||||
{
|
||||
if (!ic.HasRequiredContainedItems(addMessage: false)) continue;
|
||||
#if CLIENT
|
||||
ic.PlaySound(ActionType.OnUse, character.WorldPosition, character);
|
||||
#endif
|
||||
ic.WasUsed = true;
|
||||
ic.ApplyStatusEffects(ActionType.OnUse, 1.0f, targetCharacter, targetLimb);
|
||||
if (ic.DeleteOnUse) remove = true;
|
||||
}
|
||||
|
||||
if (remove)
|
||||
{
|
||||
Entity.Spawner?.AddToRemoveQueue(item);
|
||||
}
|
||||
}
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
bool isCompleted = !targetCharacter.IsUnconscious || targetCharacter.IsDead;
|
||||
|
||||
if (isCompleted)
|
||||
{
|
||||
character?.Speak(TextManager.Get("DialogTargetHealed").Replace("[targetname]", targetCharacter.Name),
|
||||
null, 1.0f, "targethealed" + targetCharacter.Name, 60.0f);
|
||||
}
|
||||
|
||||
return isCompleted;
|
||||
}
|
||||
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
if (targetCharacter.AnimController.CurrentHull == null) return 0.0f;
|
||||
float distance = Vector2.DistanceSquared(character.WorldPosition, targetCharacter.WorldPosition);
|
||||
return targetCharacter.IsDead ? 1000.0f / distance : 10000.0f / distance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-7
@@ -3,7 +3,7 @@ using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
/*class AIObjectiveRescueAll : AIObjective
|
||||
class AIObjectiveRescueAll : AIObjective
|
||||
{
|
||||
private List<Character> rescueTargets;
|
||||
|
||||
@@ -18,13 +18,19 @@ namespace Barotrauma
|
||||
return true;
|
||||
}
|
||||
|
||||
public override float GetPriority(Character character)
|
||||
public override float GetPriority(AIObjectiveManager objectiveManager)
|
||||
{
|
||||
GetRescueTargets();
|
||||
if (!rescueTargets.Any()) { return 0.0f; }
|
||||
|
||||
if (objectiveManager.CurrentObjective == this)
|
||||
{
|
||||
return AIObjectiveManager.OrderPriority;
|
||||
}
|
||||
|
||||
//if there are targets to rescue, the priority is slightly less
|
||||
//than the priority of explicit orders given to the character
|
||||
return rescueTargets.Any() ? AIObjectiveManager.OrderPriority - 5.0f : 0.0f;
|
||||
return AIObjectiveManager.OrderPriority - 5.0f;
|
||||
}
|
||||
|
||||
private void GetRescueTargets()
|
||||
@@ -32,9 +38,7 @@ namespace Barotrauma
|
||||
rescueTargets = Character.CharacterList.FindAll(c =>
|
||||
c.AIController is HumanAIController &&
|
||||
c != character &&
|
||||
(c.IsDead || c.IsUnconscious) &&
|
||||
c.AnimController.CurrentHull != null &&
|
||||
AIObjectiveFindSafety.GetHullSafety(c.AnimController.CurrentHull, c) < 50.0f);
|
||||
c.IsUnconscious);
|
||||
}
|
||||
|
||||
protected override void Act(float deltaTime)
|
||||
@@ -44,5 +48,11 @@ namespace Barotrauma
|
||||
AddSubObjective(new AIObjectiveRescue(character, target));
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
public override bool IsCompleted()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ namespace Barotrauma
|
||||
private static string ConfigFile = Path.Combine("Content", "Orders.xml");
|
||||
|
||||
public static List<Order> PrefabList;
|
||||
|
||||
public Order Prefab
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public readonly string Name;
|
||||
public readonly string DoingText;
|
||||
@@ -19,15 +25,25 @@ namespace Barotrauma
|
||||
public readonly Sprite SymbolSprite;
|
||||
|
||||
public readonly Type ItemComponentType;
|
||||
public readonly string ItemName;
|
||||
public readonly string[] ItemIdentifiers;
|
||||
|
||||
public readonly string AITag;
|
||||
|
||||
public readonly Color Color;
|
||||
|
||||
//if true, the order is issued to all available characters
|
||||
public bool TargetAllCharacters;
|
||||
|
||||
public readonly float FadeOutTime;
|
||||
|
||||
public Entity TargetEntity;
|
||||
public ItemComponent TargetItemComponent;
|
||||
public readonly bool UseController;
|
||||
|
||||
public ItemComponent TargetItem;
|
||||
|
||||
public Controller ConnectedController;
|
||||
|
||||
public readonly string[] AppropriateJobs;
|
||||
public readonly string[] Options;
|
||||
public readonly string[] OptionNames;
|
||||
|
||||
static Order()
|
||||
{
|
||||
@@ -39,98 +55,125 @@ namespace Barotrauma
|
||||
foreach (XElement orderElement in doc.Root.Elements())
|
||||
{
|
||||
if (orderElement.Name.ToString().ToLowerInvariant() != "order") continue;
|
||||
|
||||
PrefabList.Add(new Order(orderElement));
|
||||
var newOrder = new Order(orderElement);
|
||||
newOrder.Prefab = newOrder;
|
||||
PrefabList.Add(newOrder);
|
||||
}
|
||||
|
||||
//PrefabList.Add(new Order("Follow", "Following"));
|
||||
|
||||
//PrefabList.Add(new Order("Dismiss", "Dismissed"));
|
||||
|
||||
//PrefabList.Add(new Order("Wait", "Wait"));
|
||||
|
||||
//PrefabList.Add(new Order("Operate Reactor", "Operating reactor", typeof(Reactor), new string[] {"Power up", "Shutdown"}));
|
||||
//PrefabList.Add(new Order("Operate Railgun", "Operating railgun", typeof(Turret), new string[] { "Fire at will", "Hold fire" }));
|
||||
|
||||
|
||||
}
|
||||
|
||||
private Order(XElement orderElement)
|
||||
{
|
||||
Name = orderElement.GetAttributeString("name", "Name not found");
|
||||
DoingText = orderElement.GetAttributeString("doingtext", "");
|
||||
AITag = orderElement.GetAttributeString("aitag", "");
|
||||
Name = TextManager.Get("OrderName." + AITag, true) ?? orderElement.GetAttributeString("name", "Name not found");
|
||||
DoingText = TextManager.Get("OrderNameDoing." + AITag, true) ?? orderElement.GetAttributeString("doingtext", "");
|
||||
|
||||
string targetItemName = orderElement.GetAttributeString("targetitemtype", "");
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(targetItemName))
|
||||
string targetItemType = orderElement.GetAttributeString("targetitemtype", "");
|
||||
if (!string.IsNullOrWhiteSpace(targetItemType))
|
||||
{
|
||||
try
|
||||
{
|
||||
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemName, true, true);
|
||||
ItemComponentType = Type.GetType("Barotrauma.Items.Components." + targetItemType, true, true);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemName + " not found", e);
|
||||
DebugConsole.ThrowError("Error in " + ConfigFile + ", item component type " + targetItemType + " not found", e);
|
||||
}
|
||||
}
|
||||
|
||||
ItemName = orderElement.GetAttributeString("targetitemname", "");
|
||||
|
||||
Color = new Color(orderElement.GetAttributeVector4("color", new Vector4(1.0f, 1.0f, 1.0f, 1.0f)));
|
||||
|
||||
ItemIdentifiers = orderElement.GetAttributeStringArray("targetitemidentifiers", new string[0], trim: true, convertToLowerInvariant: true);
|
||||
Color = orderElement.GetAttributeColor("color", Color.White);
|
||||
FadeOutTime = orderElement.GetAttributeFloat("fadeouttime", 0.0f);
|
||||
UseController = orderElement.GetAttributeBool("usecontroller", false);
|
||||
TargetAllCharacters = orderElement.GetAttributeBool("targetallcharacters", false);
|
||||
AppropriateJobs = orderElement.GetAttributeStringArray("appropriatejobs", new string[0]);
|
||||
Options = orderElement.GetAttributeStringArray("options", new string[0]);
|
||||
|
||||
string optionStr = orderElement.GetAttributeString("options", "");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(optionStr))
|
||||
string translatedOptionNames = TextManager.Get("OrderOptions." + AITag, true);
|
||||
if (translatedOptionNames == null)
|
||||
{
|
||||
Options = new string[0];
|
||||
OptionNames = orderElement.GetAttributeStringArray("optionnames", new string[0]);
|
||||
}
|
||||
else
|
||||
{
|
||||
Options = optionStr.Split(',');
|
||||
|
||||
for (int i = 0; i<Options.Length; i++)
|
||||
string[] splitOptionNames = translatedOptionNames.Split(',');
|
||||
OptionNames = new string[Options.Length];
|
||||
for (int i = 0; i < Options.Length && i < splitOptionNames.Length; i++)
|
||||
{
|
||||
Options[i] = Options[i].Trim();
|
||||
OptionNames[i] = splitOptionNames[i].Trim();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (OptionNames.Length != Options.Length)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in Order " + Name + " - the number of option names doesn't match the number of options.");
|
||||
OptionNames = Options;
|
||||
}
|
||||
|
||||
foreach (XElement subElement in orderElement.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "sprite") continue;
|
||||
SymbolSprite = new Sprite(subElement);
|
||||
break;
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "sprite":
|
||||
SymbolSprite = new Sprite(subElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Order(string name, string doingText, Type itemComponentType, string[] parameters = null)
|
||||
|
||||
public Order(Order prefab, Entity targetEntity, ItemComponent targetItem)
|
||||
{
|
||||
Name = name;
|
||||
DoingText = doingText;
|
||||
ItemComponentType = itemComponentType;
|
||||
Options = parameters == null ? new string[0] : parameters;
|
||||
}
|
||||
Prefab = prefab;
|
||||
|
||||
public Order(Order prefab, ItemComponent targetItem)
|
||||
{
|
||||
Name = prefab.Name;
|
||||
AITag = prefab.AITag;
|
||||
DoingText = prefab.DoingText;
|
||||
ItemComponentType = prefab.ItemComponentType;
|
||||
Options = prefab.Options;
|
||||
SymbolSprite = prefab.SymbolSprite;
|
||||
Color = prefab.Color;
|
||||
UseController = prefab.UseController;
|
||||
TargetAllCharacters = prefab.TargetAllCharacters;
|
||||
AppropriateJobs = prefab.AppropriateJobs;
|
||||
FadeOutTime = prefab.FadeOutTime;
|
||||
|
||||
TargetItem = targetItem;
|
||||
TargetEntity = targetEntity;
|
||||
if (targetItem != null)
|
||||
{
|
||||
if (UseController)
|
||||
{
|
||||
var controllers = targetItem.Item.GetConnectedComponents<Controller>();
|
||||
if (controllers.Count > 0) ConnectedController = controllers[0];
|
||||
}
|
||||
TargetEntity = targetItem.Item;
|
||||
TargetItemComponent = targetItem;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasAppropriateJob(Character character)
|
||||
{
|
||||
if (AppropriateJobs == null || AppropriateJobs.Length == 0) return true;
|
||||
if (character.Info == null || character.Info.Job == null) return false;
|
||||
for (int i = 0; i < AppropriateJobs.Length; i++)
|
||||
{
|
||||
if (character.Info.Job.Prefab.Identifier.ToLowerInvariant() == AppropriateJobs[i].ToLowerInvariant()) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Order(string name, string doingText, string[] parameters = null)
|
||||
:this (name,doingText, null, parameters)
|
||||
public string GetChatMessage(string targetCharacterName, string targetRoomName, string orderOption = "")
|
||||
{
|
||||
orderOption = orderOption ?? "";
|
||||
|
||||
string messageTag = "OrderDialog." + AITag;
|
||||
if (!string.IsNullOrEmpty(orderOption)) messageTag += "." + orderOption;
|
||||
|
||||
string msg = TextManager.Get(messageTag, true);
|
||||
if (msg == null) return "";
|
||||
|
||||
if (targetCharacterName == null) targetCharacterName = "";
|
||||
if (targetRoomName == null) targetRoomName = "";
|
||||
return msg.Replace("[name]", targetCharacterName).Replace("[roomname]", targetRoomName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
@@ -154,24 +155,29 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end)
|
||||
{
|
||||
System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
|
||||
public SteeringPath FindPath(Vector2 start, Vector2 end, string errorMsgStr)
|
||||
{
|
||||
float closestDist = 0.0f;
|
||||
PathNode startNode = null;
|
||||
foreach (PathNode node in nodes)
|
||||
{
|
||||
Vector2 nodePos = node.Position;
|
||||
|
||||
float dist = System.Math.Abs(start.X - nodePos.X) +
|
||||
System.Math.Abs(start.Y - nodePos.Y) * 10.0f; //higher cost for vertical movement
|
||||
|
||||
float xDiff = System.Math.Abs(start.X - nodePos.X);
|
||||
float yDiff = System.Math.Abs(start.Y - nodePos.Y);
|
||||
|
||||
if (yDiff > 1.0f && node.Waypoint.Ladders == null && node.Waypoint.Stairs == null)
|
||||
{
|
||||
yDiff += 10.0f;
|
||||
}
|
||||
|
||||
float dist = xDiff + (insideSubmarine ? yDiff * 10.0f : yDiff); //higher cost for vertical movement when inside the sub
|
||||
|
||||
//prefer nodes that are closer to the end position
|
||||
dist += Vector2.Distance(end, nodePos) / 10.0f;
|
||||
|
||||
if (dist<closestDist || startNode==null)
|
||||
dist += (Math.Abs(end.X - nodePos.X) + Math.Abs(end.Y - nodePos.Y)) / 2.0f;
|
||||
//much higher cost to waypoints that are outside
|
||||
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
|
||||
if (dist < closestDist || startNode == null)
|
||||
{
|
||||
//if searching for a path inside the sub, make sure the waypoint is visible
|
||||
if (insideSubmarine)
|
||||
@@ -182,9 +188,9 @@ namespace Barotrauma
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
if (body.UserData is Submarine) continue;
|
||||
if (body.UserData is Structure && !((Structure)body.UserData).IsPlatform) continue;
|
||||
if (body.UserData is Item && body.FixtureList[0].CollisionCategories.HasFlag(Physics.CollisionWall)) continue;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +201,7 @@ namespace Barotrauma
|
||||
|
||||
if (startNode == null)
|
||||
{
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node", Color.DarkRed);
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find a start node. "+ errorMsgStr, Color.DarkRed);
|
||||
|
||||
return new SteeringPath();
|
||||
}
|
||||
@@ -207,6 +213,10 @@ namespace Barotrauma
|
||||
Vector2 nodePos = node.Position;
|
||||
|
||||
float dist = Vector2.Distance(end, nodePos);
|
||||
//much higher cost to waypoints that are outside
|
||||
if (node.Waypoint.CurrentHull == null) dist *= 10.0f;
|
||||
//avoid stopping at a doorway
|
||||
if (node.Waypoint.ConnectedDoor != null) dist *= 10.0f;
|
||||
if (dist < closestDist || endNode == null)
|
||||
{
|
||||
//if searching for a path inside the sub, make sure the waypoint is visible
|
||||
@@ -223,16 +233,13 @@ namespace Barotrauma
|
||||
|
||||
if (endNode == null)
|
||||
{
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node", Color.DarkRed);
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find an end node. "+ errorMsgStr, Color.DarkRed);
|
||||
return new SteeringPath();
|
||||
}
|
||||
|
||||
|
||||
var path = FindPath(startNode,endNode);
|
||||
|
||||
sw.Stop();
|
||||
System.Diagnostics.Debug.WriteLine("findpath: " + sw.ElapsedMilliseconds+" ms");
|
||||
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -253,10 +260,9 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (startNode == null || endNode == null)
|
||||
{
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints", Color.DarkRed);
|
||||
DebugConsole.NewMessage("Pathfinding error, couldn't find matching pathnodes to waypoints.", Color.DarkRed);
|
||||
return new SteeringPath();
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,7 @@ namespace Barotrauma
|
||||
|
||||
private Vector2 steering;
|
||||
|
||||
//the steering amount when avoiding obstacles
|
||||
//(needs a separate variable because it's only updated when a raycast is done to detect any nearby obstacles)
|
||||
private Vector2 avoidSteering;
|
||||
private Vector2? avoidObstaclePos;
|
||||
private float rayCastTimer;
|
||||
|
||||
private float wanderAngle;
|
||||
@@ -35,19 +33,19 @@ namespace Barotrauma
|
||||
wanderAngle = Rand.Range(0.0f, MathHelper.TwoPi);
|
||||
}
|
||||
|
||||
public void SteeringSeek(Vector2 targetSimPos, float speed = 1.0f)
|
||||
public void SteeringSeek(Vector2 targetSimPos, float speed)
|
||||
{
|
||||
steering += DoSteeringSeek(targetSimPos, speed);
|
||||
}
|
||||
|
||||
public void SteeringWander(float speed = 1.0f)
|
||||
public void SteeringWander(float speed)
|
||||
{
|
||||
steering += DoSteeringWander(speed);
|
||||
}
|
||||
|
||||
public void SteeringAvoid(float deltaTime, float speed)
|
||||
public void SteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
|
||||
{
|
||||
steering += DoSteeringAvoid(deltaTime, speed);
|
||||
steering += DoSteeringAvoid(deltaTime, lookAheadDistance, speed);
|
||||
}
|
||||
|
||||
public void SteeringManual(float deltaTime, Vector2 velocity)
|
||||
@@ -70,7 +68,7 @@ namespace Barotrauma
|
||||
steering.Y = 0.0f;
|
||||
}
|
||||
|
||||
public virtual void Update(float speed = 1.0f)
|
||||
public virtual void Update(float speed)
|
||||
{
|
||||
if (steering == Vector2.Zero || !MathUtils.IsValid(steering))
|
||||
{
|
||||
@@ -88,7 +86,7 @@ namespace Barotrauma
|
||||
host.Steering = steering;
|
||||
}
|
||||
|
||||
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed = 1.0f)
|
||||
protected virtual Vector2 DoSteeringSeek(Vector2 target, float speed)
|
||||
{
|
||||
Vector2 targetVel = target - host.SimPosition;
|
||||
|
||||
@@ -108,9 +106,9 @@ namespace Barotrauma
|
||||
return newSteering;
|
||||
}
|
||||
|
||||
protected virtual Vector2 DoSteeringWander(float speed = 1.0f)
|
||||
protected virtual Vector2 DoSteeringWander(float speed)
|
||||
{
|
||||
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? new Vector2(speed, 0.0f) : host.Steering;
|
||||
Vector2 circleCenter = (host.Steering == Vector2.Zero) ? Rand.Vector(speed) : host.Steering;
|
||||
circleCenter = Vector2.Normalize(circleCenter) * CircleDistance;
|
||||
|
||||
Vector2 displacement = new Vector2(
|
||||
@@ -132,21 +130,19 @@ namespace Barotrauma
|
||||
return newSteering;
|
||||
}
|
||||
|
||||
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float speed = 1.0f)
|
||||
protected virtual Vector2 DoSteeringAvoid(float deltaTime, float lookAheadDistance, float speed)
|
||||
{
|
||||
if (steering == Vector2.Zero || host.Steering == Vector2.Zero) return Vector2.Zero;
|
||||
|
||||
float maxDistance = 2.0f;
|
||||
|
||||
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
|
||||
|
||||
float maxDistance = lookAheadDistance;
|
||||
if (rayCastTimer <= 0.0f)
|
||||
{
|
||||
Vector2 ahead = host.SimPosition + Vector2.Normalize(host.Steering) * maxDistance;
|
||||
rayCastTimer = RayCastInterval;
|
||||
Body closestBody = Submarine.CheckVisibility(host.SimPosition, ahead);
|
||||
if (closestBody == null)
|
||||
{
|
||||
avoidSteering = Vector2.Zero;
|
||||
avoidObstaclePos = null;
|
||||
return Vector2.Zero;
|
||||
}
|
||||
else
|
||||
@@ -163,19 +159,19 @@ namespace Barotrauma
|
||||
obstaclePosition.X = closestStructure.SimPosition.X;
|
||||
}
|
||||
|
||||
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
|
||||
avoidObstaclePos = obstaclePosition;
|
||||
//avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - obstaclePosition);
|
||||
}
|
||||
else if (closestBody.UserData is Item item)
|
||||
/*else if (closestBody.UserData is Item)
|
||||
{
|
||||
avoidSteering = Vector2.Normalize(Submarine.LastPickedPosition - item.SimPosition);
|
||||
}
|
||||
}*/
|
||||
else
|
||||
{
|
||||
avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
|
||||
|
||||
avoidObstaclePos = Submarine.LastPickedPosition;
|
||||
//avoidSteering = Vector2.Normalize(host.SimPosition - Submarine.LastPickedPosition);
|
||||
}
|
||||
//failed to normalize (the obstacle to avoid is at the same position as the character?)
|
||||
// -> move to a random direction
|
||||
if (!MathUtils.IsValid(avoidSteering)) avoidSteering = Rand.Vector(1.0f);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -184,7 +180,14 @@ namespace Barotrauma
|
||||
rayCastTimer -= deltaTime;
|
||||
}
|
||||
|
||||
return avoidSteering * speed;
|
||||
if (!avoidObstaclePos.HasValue) return Vector2.Zero;
|
||||
|
||||
Vector2 diff = avoidObstaclePos.Value - host.SimPosition;
|
||||
float dist = diff.Length();
|
||||
|
||||
if (dist > maxDistance) return Vector2.Zero;
|
||||
|
||||
return -diff * (1.0f - dist / maxDistance) * speed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -19,16 +20,25 @@ namespace Barotrauma
|
||||
get { return aiController; }
|
||||
}
|
||||
|
||||
public AICharacter(string file, Vector2 position, CharacterInfo characterInfo = null, bool isNetworkPlayer = false)
|
||||
: base(file, position, characterInfo, isNetworkPlayer)
|
||||
public AICharacter(string file, Vector2 position, string seed, CharacterInfo characterInfo = null, bool isNetworkPlayer = false, RagdollParams ragdoll = null)
|
||||
: base(file, position, seed, characterInfo, isNetworkPlayer, ragdoll)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public void SetAI(AIController aiController)
|
||||
{
|
||||
if (AIController != null)
|
||||
{
|
||||
OnAttacked -= AIController.OnAttacked;
|
||||
}
|
||||
|
||||
this.aiController = aiController;
|
||||
if (aiController != null)
|
||||
{
|
||||
OnAttacked += aiController.OnAttacked;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
@@ -67,7 +77,7 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (IsDead || Health <= 0.0f || IsUnconscious || Stun > 0.0f) return;
|
||||
if (IsDead || Vitality <= 0.0f || IsUnconscious || Stun > 0.0f) return;
|
||||
if (Controlled == this || !aiController.Enabled) return;
|
||||
|
||||
SoundUpdate(deltaTime);
|
||||
@@ -78,21 +88,5 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
partial void SoundUpdate(float deltaTime);
|
||||
|
||||
public override void AddDamage(CauseOfDeath causeOfDeath, float amount, Character attacker)
|
||||
{
|
||||
base.AddDamage(causeOfDeath, amount, attacker);
|
||||
|
||||
if (attacker != null) aiController.OnAttacked(attacker, amount);
|
||||
}
|
||||
|
||||
public override AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb limb = null)
|
||||
{
|
||||
AttackResult result = base.ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, limb);
|
||||
|
||||
aiController.OnAttacked(attacker, result.Damage + result.Bleeding);
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
using Barotrauma.Networking;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AIChatMessage
|
||||
{
|
||||
public readonly string Message;
|
||||
|
||||
/// <summary>
|
||||
/// An arbitrary identifier that can be used to determine what kind of a message this is
|
||||
/// and prevent characters from saying the same kind of line too often.
|
||||
/// </summary>
|
||||
public readonly string Identifier;
|
||||
|
||||
public ChatMessageType? MessageType;
|
||||
|
||||
public float SendDelay;
|
||||
public double SendTime;
|
||||
|
||||
public AIChatMessage(string message, ChatMessageType? type, string identifier = "", float delay = 0.0f)
|
||||
{
|
||||
Message = message;
|
||||
MessageType = type;
|
||||
Identifier = identifier;
|
||||
SendDelay = delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,74 +1,222 @@
|
||||
using FarseerPhysics;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AnimController : Ragdoll
|
||||
abstract class AnimController : Ragdoll
|
||||
{
|
||||
public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR };
|
||||
public Animation Anim;
|
||||
public abstract GroundedMovementParams WalkParams { get; set; }
|
||||
public abstract GroundedMovementParams RunParams { get; set; }
|
||||
public abstract SwimParams SwimSlowParams { get; set; }
|
||||
public abstract SwimParams SwimFastParams { get; set; }
|
||||
|
||||
public LimbType GrabLimb;
|
||||
|
||||
protected Character character;
|
||||
|
||||
protected float walkSpeed, swimSpeed;
|
||||
|
||||
protected float walkPos;
|
||||
|
||||
protected readonly Vector2 stepSize;
|
||||
protected readonly float legTorque;
|
||||
|
||||
public float RunSpeedMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public float SwimSpeedMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Vector2 AimSourcePos
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(AimSourceSimPos); }
|
||||
}
|
||||
|
||||
public virtual Vector2 AimSourceSimPos
|
||||
public AnimationParams CurrentAnimationParams
|
||||
{
|
||||
get
|
||||
{
|
||||
return Collider.SimPosition;
|
||||
if (ForceSelectAnimationType == AnimationType.NotDefined)
|
||||
{
|
||||
return (InWater || !CanWalk) ? (AnimationParams)CurrentSwimParams : CurrentGroundedParams;
|
||||
}
|
||||
else
|
||||
{
|
||||
return GetAnimationParamsFromType(ForceSelectAnimationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
public AnimationType ForceSelectAnimationType { get; set; }
|
||||
public GroundedMovementParams CurrentGroundedParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ForceSelectAnimationType != AnimationType.NotDefined)
|
||||
{
|
||||
return GetAnimationParamsFromType(ForceSelectAnimationType) as GroundedMovementParams;
|
||||
}
|
||||
if (!CanWalk)
|
||||
{
|
||||
DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
|
||||
return null;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IsMovingFast ? RunParams : WalkParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
public SwimParams CurrentSwimParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ForceSelectAnimationType != AnimationType.NotDefined)
|
||||
{
|
||||
return GetAnimationParamsFromType(ForceSelectAnimationType) as SwimParams;
|
||||
}
|
||||
else
|
||||
{
|
||||
return IsMovingFast? SwimFastParams : SwimSlowParams;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public AnimController(Character character, XElement element)
|
||||
: base(character, element)
|
||||
public bool CanWalk => CanEnterSubmarine;
|
||||
public bool IsMovingBackwards => !InWater && Math.Sign(targetMovement.X) == -Math.Sign(Dir);
|
||||
|
||||
// TODO: define death anim duration in XML
|
||||
protected float deathAnimTimer, deathAnimDuration = 5.0f;
|
||||
|
||||
/// <summary>
|
||||
/// Note: Presupposes that the slow speed is lower than the high speed. Otherwise will give invalid results.
|
||||
/// </summary>
|
||||
public bool IsMovingFast
|
||||
{
|
||||
this.character = character;
|
||||
|
||||
stepSize = element.GetAttributeVector2("stepsize", Vector2.One);
|
||||
stepSize = ConvertUnits.ToSimUnits(stepSize);
|
||||
|
||||
walkSpeed = element.GetAttributeFloat("walkspeed", 1.0f);
|
||||
swimSpeed = element.GetAttributeFloat("swimspeed", 1.0f);
|
||||
|
||||
RunSpeedMultiplier = element.GetAttributeFloat("runspeedmultiplier", 2f);
|
||||
SwimSpeedMultiplier = element.GetAttributeFloat("swimspeedmultiplier", 1.5f);
|
||||
|
||||
legTorque = element.GetAttributeFloat("legtorque", 0.0f);
|
||||
get
|
||||
{
|
||||
if (InWater || !CanWalk)
|
||||
{
|
||||
return TargetMovement.Length() > (SwimSlowParams.MovementSpeed + SwimFastParams.MovementSpeed) / 2.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
return Math.Abs(TargetMovement.X) > (WalkParams.MovementSpeed + RunParams.MovementSpeed) / 2.0f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: creates a new list every time, because the params might have changed. If there is a need to access the property frequently, change the implementation to an array, where the slot is updated when the param is updated(?)
|
||||
/// Currently it's not simple to implement, since the properties are not implemented here, but in the derived classes. Would require to change the params virtual and to call the base property getter/setter or something.
|
||||
/// </summary>
|
||||
public List<AnimationParams> AllAnimParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (CanWalk)
|
||||
{
|
||||
return new List<AnimationParams> { WalkParams, RunParams, SwimSlowParams, SwimFastParams };
|
||||
}
|
||||
else
|
||||
{
|
||||
return new List<AnimationParams> { SwimSlowParams, SwimFastParams };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum Animation { None, Climbing, UsingConstruction, Struggle, CPR };
|
||||
public Animation Anim;
|
||||
|
||||
public Vector2 AimSourcePos => ConvertUnits.ToDisplayUnits(AimSourceSimPos);
|
||||
public virtual Vector2 AimSourceSimPos => Collider.SimPosition;
|
||||
|
||||
protected float? GetValidOrNull(AnimationParams p, float? v)
|
||||
{
|
||||
if (p == null) { return null; }
|
||||
if (v == null) { return null; }
|
||||
if (!MathUtils.IsValid(v.Value)) { return null; }
|
||||
return v.Value;
|
||||
}
|
||||
protected Vector2? GetValidOrNull(AnimationParams p, Vector2 v)
|
||||
{
|
||||
if (p == null) { return null; }
|
||||
return v;
|
||||
}
|
||||
|
||||
public override float? HeadPosition => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams?.HeadPosition * RagdollParams.JointScale);
|
||||
public override float? TorsoPosition => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams?.TorsoPosition * RagdollParams.JointScale);
|
||||
public override float? HeadAngle => GetValidOrNull(CurrentAnimationParams, CurrentAnimationParams?.HeadAngleInRadians);
|
||||
public override float? TorsoAngle => GetValidOrNull(CurrentAnimationParams, CurrentAnimationParams?.TorsoAngleInRadians);
|
||||
public virtual Vector2? StepSize => GetValidOrNull(CurrentGroundedParams, CurrentGroundedParams.StepSize * RagdollParams.JointScale);
|
||||
|
||||
public bool AnimationTestPose { get; set; }
|
||||
|
||||
public float WalkPos { get; protected set; }
|
||||
|
||||
public AnimController(Character character, string seed, RagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
|
||||
|
||||
public virtual void UpdateAnim(float deltaTime) { }
|
||||
|
||||
public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle) { }
|
||||
public virtual void HoldItem(float deltaTime, Item item, Vector2[] handlePos, Vector2 holdPos, Vector2 aimPos, bool aim, float holdAngle, float itemAngleRelativeToHoldAngle = 0.0f) { }
|
||||
|
||||
public virtual void DragCharacter(Character target) { }
|
||||
public virtual void DragCharacter(Character target, float deltaTime) { }
|
||||
|
||||
public virtual void UpdateUseItem(bool allowMovement, Vector2 handPos) { }
|
||||
public virtual void UpdateUseItem(bool allowMovement, Vector2 handWorldPos) { }
|
||||
|
||||
}
|
||||
public float GetSpeed(AnimationType type)
|
||||
{
|
||||
GroundedMovementParams movementParams;
|
||||
switch (type)
|
||||
{
|
||||
case AnimationType.Walk:
|
||||
if (!CanWalk)
|
||||
{
|
||||
DebugConsole.ThrowError($"{character.SpeciesName} cannot walk!");
|
||||
return 0;
|
||||
}
|
||||
movementParams = WalkParams;
|
||||
break;
|
||||
case AnimationType.Run:
|
||||
if (!CanWalk)
|
||||
{
|
||||
DebugConsole.ThrowError($"{character.SpeciesName} cannot run!");
|
||||
return 0;
|
||||
}
|
||||
movementParams = RunParams;
|
||||
break;
|
||||
case AnimationType.SwimSlow:
|
||||
return SwimSlowParams.MovementSpeed;
|
||||
case AnimationType.SwimFast:
|
||||
return SwimFastParams.MovementSpeed;
|
||||
default:
|
||||
throw new NotImplementedException(type.ToString());
|
||||
}
|
||||
return IsMovingBackwards ? movementParams.MovementSpeed * movementParams.BackwardsMovementMultiplier : movementParams.MovementSpeed;
|
||||
}
|
||||
|
||||
public float GetCurrentSpeed(bool useMaxSpeed)
|
||||
{
|
||||
AnimationType animType;
|
||||
if (InWater || !CanWalk)
|
||||
{
|
||||
if (useMaxSpeed)
|
||||
{
|
||||
animType = AnimationType.SwimFast;
|
||||
}
|
||||
else
|
||||
{
|
||||
animType = AnimationType.SwimSlow;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (useMaxSpeed)
|
||||
{
|
||||
animType = AnimationType.Run;
|
||||
}
|
||||
else
|
||||
{
|
||||
animType = AnimationType.Walk;
|
||||
}
|
||||
}
|
||||
return GetSpeed(animType);
|
||||
}
|
||||
|
||||
public AnimationParams GetAnimationParamsFromType(AnimationType type)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AnimationType.Walk:
|
||||
return WalkParams;
|
||||
case AnimationType.Run:
|
||||
return RunParams;
|
||||
case AnimationType.SwimSlow:
|
||||
return SwimSlowParams;
|
||||
case AnimationType.SwimFast:
|
||||
return SwimFastParams;
|
||||
default:
|
||||
throw new NotImplementedException(type.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,52 +2,130 @@
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class FishAnimController : AnimController
|
||||
{
|
||||
//amplitude and wave length of the "sine wave" swimming animation
|
||||
//if amplitude = 0, sine wave animation isn't used
|
||||
private float waveAmplitude;
|
||||
private float waveLength;
|
||||
public override RagdollParams RagdollParams
|
||||
{
|
||||
get { return FishRagdollParams; }
|
||||
protected set { FishRagdollParams = value as FishRagdollParams; }
|
||||
}
|
||||
|
||||
private float steerTorque;
|
||||
private FishRagdollParams _ragdollParams;
|
||||
public FishRagdollParams FishRagdollParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_ragdollParams == null)
|
||||
{
|
||||
_ragdollParams = FishRagdollParams.GetDefaultRagdollParams(character.SpeciesName);
|
||||
}
|
||||
return _ragdollParams;
|
||||
}
|
||||
protected set
|
||||
{
|
||||
_ragdollParams = value;
|
||||
}
|
||||
}
|
||||
|
||||
private bool rotateTowardsMovement;
|
||||
private FishWalkParams _fishWalkParams;
|
||||
public FishWalkParams FishWalkParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fishWalkParams == null)
|
||||
{
|
||||
_fishWalkParams = FishWalkParams.GetDefaultAnimParams(character);
|
||||
}
|
||||
return _fishWalkParams;
|
||||
}
|
||||
set { _fishWalkParams = value; }
|
||||
}
|
||||
|
||||
private bool mirror, flip;
|
||||
private FishRunParams _fishRunParams;
|
||||
public FishRunParams FishRunParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fishRunParams == null)
|
||||
{
|
||||
_fishRunParams = FishRunParams.GetDefaultAnimParams(character);
|
||||
}
|
||||
return _fishRunParams;
|
||||
}
|
||||
set { _fishRunParams = value; }
|
||||
}
|
||||
|
||||
private FishSwimSlowParams _fishSwimSlowParams;
|
||||
public FishSwimSlowParams FishSwimSlowParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fishSwimSlowParams == null)
|
||||
{
|
||||
_fishSwimSlowParams = FishSwimSlowParams.GetDefaultAnimParams(character);
|
||||
}
|
||||
return _fishSwimSlowParams;
|
||||
}
|
||||
set { _fishSwimSlowParams = value; }
|
||||
}
|
||||
|
||||
private FishSwimFastParams _fishSwimFastParams;
|
||||
public FishSwimFastParams FishSwimFastParams
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_fishSwimFastParams == null)
|
||||
{
|
||||
_fishSwimFastParams = FishSwimFastParams.GetDefaultAnimParams(character);
|
||||
}
|
||||
return _fishSwimFastParams;
|
||||
}
|
||||
set { _fishSwimFastParams = value; }
|
||||
}
|
||||
|
||||
public IFishAnimation CurrentFishAnimation => CurrentAnimationParams as IFishAnimation;
|
||||
public new FishGroundedParams CurrentGroundedParams => base.CurrentGroundedParams as FishGroundedParams;
|
||||
public new FishSwimParams CurrentSwimParams => base.CurrentSwimParams as FishSwimParams;
|
||||
|
||||
public float? TailAngle => GetValidOrNull(CurrentAnimationParams, CurrentFishAnimation?.TailAngleInRadians);
|
||||
public float FootTorque => CurrentFishAnimation.FootTorque;
|
||||
public float HeadTorque => CurrentFishAnimation.HeadTorque;
|
||||
public float TorsoTorque => CurrentFishAnimation.TorsoTorque;
|
||||
public float TailTorque => CurrentFishAnimation.TailTorque;
|
||||
public float HeadMoveForce => CurrentGroundedParams.HeadMoveForce;
|
||||
public float TorsoMoveForce => CurrentGroundedParams.TorsoMoveForce;
|
||||
public float FootMoveForce => CurrentGroundedParams.FootMoveForce;
|
||||
|
||||
public override GroundedMovementParams WalkParams
|
||||
{
|
||||
get { return FishWalkParams; }
|
||||
set { FishWalkParams = value as FishWalkParams; }
|
||||
}
|
||||
|
||||
public override GroundedMovementParams RunParams
|
||||
{
|
||||
get { return FishRunParams; }
|
||||
set { FishRunParams = value as FishRunParams; }
|
||||
}
|
||||
|
||||
public override SwimParams SwimSlowParams
|
||||
{
|
||||
get { return FishSwimSlowParams; }
|
||||
set { FishSwimSlowParams = value as FishSwimSlowParams; }
|
||||
}
|
||||
|
||||
public override SwimParams SwimFastParams
|
||||
{
|
||||
get { return FishSwimFastParams; }
|
||||
set { FishSwimFastParams = value as FishSwimFastParams; }
|
||||
}
|
||||
|
||||
private float flipTimer;
|
||||
|
||||
private float? footRotation;
|
||||
|
||||
private float deathAnimTimer, deathAnimDuration = 5.0f;
|
||||
|
||||
public FishAnimController(Character character, XElement element)
|
||||
: base(character, element)
|
||||
{
|
||||
waveAmplitude = ConvertUnits.ToSimUnits(element.GetAttributeFloat("waveamplitude", 0.0f));
|
||||
waveLength = ConvertUnits.ToSimUnits(element.GetAttributeFloat("wavelength", 0.0f));
|
||||
|
||||
steerTorque = element.GetAttributeFloat("steertorque", 25.0f);
|
||||
|
||||
flip = element.GetAttributeBool("flip", true);
|
||||
mirror = element.GetAttributeBool("mirror", false);
|
||||
|
||||
float footRot = element.GetAttributeFloat("footrotation", float.NaN);
|
||||
if (float.IsNaN(footRot))
|
||||
{
|
||||
footRotation = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
footRotation = MathHelper.ToRadians(footRot);
|
||||
}
|
||||
|
||||
rotateTowardsMovement = element.GetAttributeBool("rotatetowardsmovement", true);
|
||||
}
|
||||
public FishAnimController(Character character, string seed, FishRagdollParams ragdollParams = null) : base(character, seed, ragdollParams) { }
|
||||
|
||||
public override void UpdateAnim(float deltaTime)
|
||||
{
|
||||
@@ -55,38 +133,25 @@ namespace Barotrauma
|
||||
|
||||
if (character.IsDead || character.IsUnconscious || character.Stun > 0.0f)
|
||||
{
|
||||
Collider.Enabled = false;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
|
||||
if (character.IsRemotePlayer)
|
||||
{
|
||||
if (!SimplePhysicsEnabled)
|
||||
{
|
||||
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
|
||||
MainLimb.PullJointEnabled = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 diff = (MainLimb.SimPosition - Collider.SimPosition);
|
||||
if (diff.LengthSquared() > 10.0f * 10.0f)
|
||||
{
|
||||
Collider.SetTransform(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.LinearVelocity = diff * 60.0f;
|
||||
Collider.SmoothRotate(MainLimb.Rotation);
|
||||
}
|
||||
}
|
||||
//set linear velocity even though the collider is disabled,
|
||||
//because the character won't be able to switch back from ragdoll mode until the velocity of the collider is low enough
|
||||
Collider.LinearVelocity = MainLimb.LinearVelocity;
|
||||
Collider.SetTransformIgnoreContacts(MainLimb.SimPosition, MainLimb.Rotation);
|
||||
|
||||
if (character.IsDead && deathAnimTimer < deathAnimDuration)
|
||||
{
|
||||
deathAnimTimer += deltaTime;
|
||||
UpdateDying(deltaTime);
|
||||
UpdateDying(deltaTime);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
deathAnimTimer = 0.0f;
|
||||
}
|
||||
|
||||
//re-enable collider
|
||||
if (!Collider.Enabled)
|
||||
@@ -109,18 +174,18 @@ namespace Barotrauma
|
||||
strongestImpact = 0.0f;
|
||||
}
|
||||
|
||||
|
||||
if (inWater)
|
||||
if (inWater && !forceStanding)
|
||||
{
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
UpdateSineAnim(deltaTime);
|
||||
}
|
||||
else if (currentHull != null && CanEnterSubmarine)
|
||||
else if (CanEnterSubmarine && (currentHull != null || forceStanding) && CurrentGroundedParams != null)
|
||||
{
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, 0.0f)) > 0.001f)
|
||||
//rotate collider back upright
|
||||
float standAngle = dir == Direction.Right ? CurrentGroundedParams.ColliderStandAngleInRadians : -CurrentGroundedParams.ColliderStandAngleInRadians;
|
||||
if (Math.Abs(MathUtils.GetShortestAngle(Collider.Rotation, standAngle)) > 0.001f)
|
||||
{
|
||||
//rotate collider back upright
|
||||
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, 0.0f) * 60.0f;
|
||||
Collider.AngularVelocity = MathUtils.GetShortestAngle(Collider.Rotation, standAngle) * 60.0f;
|
||||
Collider.FarseerBody.FixedRotation = false;
|
||||
}
|
||||
else
|
||||
@@ -134,26 +199,37 @@ namespace Barotrauma
|
||||
//don't flip or drag when simply physics is enabled
|
||||
if (SimplePhysicsEnabled) { return; }
|
||||
|
||||
if (!character.IsRemotePlayer)
|
||||
if (!character.IsRemotePlayer && (character.AIController == null || character.AIController.CanFlip))
|
||||
{
|
||||
if (mirror || !inWater)
|
||||
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
|
||||
{
|
||||
if (targetMovement.X > 0.1f && targetMovement.X > Math.Abs(targetMovement.Y) * 0.5f)
|
||||
if (targetMovement.X > 0.1f && targetMovement.X > Math.Abs(targetMovement.Y) * 0.2f)
|
||||
{
|
||||
TargetDir = Direction.Right;
|
||||
}
|
||||
else if (targetMovement.X < -0.1f && targetMovement.X < -Math.Abs(targetMovement.Y) * 0.5f)
|
||||
else if (targetMovement.X < -0.1f && targetMovement.X < -Math.Abs(targetMovement.Y) * 0.2f)
|
||||
{
|
||||
TargetDir = Direction.Left;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
if (head == null) head = GetLimb(LimbType.Torso);
|
||||
float refAngle = 0.0f;
|
||||
Limb refLimb = GetLimb(LimbType.Head);
|
||||
if (refLimb == null)
|
||||
{
|
||||
refAngle = CurrentAnimationParams.TorsoAngleInRadians;
|
||||
refLimb = GetLimb(LimbType.Torso);
|
||||
}
|
||||
else
|
||||
{
|
||||
refAngle = CurrentAnimationParams.HeadAngleInRadians;
|
||||
}
|
||||
|
||||
float rotation = MathUtils.WrapAngleTwoPi(head.Rotation);
|
||||
rotation = MathHelper.ToDegrees(rotation);
|
||||
float rotation = refLimb.Rotation;
|
||||
if (!float.IsNaN(refAngle)) { rotation -= refAngle * Dir; }
|
||||
|
||||
rotation = MathHelper.ToDegrees(MathUtils.WrapAngleTwoPi(rotation));
|
||||
|
||||
if (rotation < 0.0f) rotation += 360;
|
||||
|
||||
@@ -168,9 +244,10 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter);
|
||||
if (character.SelectedCharacter != null) DragCharacter(character.SelectedCharacter, deltaTime);
|
||||
|
||||
if (!flip) return;
|
||||
if (!CurrentFishAnimation.Flip || IsStuck) return;
|
||||
if (character.AIController != null && !character.AIController.CanFlip) return;
|
||||
|
||||
flipTimer += deltaTime;
|
||||
|
||||
@@ -179,7 +256,10 @@ namespace Barotrauma
|
||||
if (flipTimer > 1.0f || character.IsRemotePlayer)
|
||||
{
|
||||
Flip();
|
||||
if (mirror || !inWater) Mirror();
|
||||
if (!inWater || (CurrentSwimParams != null && CurrentSwimParams.Mirror))
|
||||
{
|
||||
Mirror();
|
||||
}
|
||||
flipTimer = 0.0f;
|
||||
}
|
||||
}
|
||||
@@ -187,7 +267,7 @@ namespace Barotrauma
|
||||
|
||||
private float eatTimer = 0.0f;
|
||||
|
||||
public override void DragCharacter(Character target)
|
||||
public override void DragCharacter(Character target, float deltaTime)
|
||||
{
|
||||
if (target == null) return;
|
||||
|
||||
@@ -202,20 +282,9 @@ namespace Barotrauma
|
||||
|
||||
Character targetCharacter = target;
|
||||
float eatSpeed = character.Mass / targetCharacter.Mass * 0.1f;
|
||||
eatTimer += deltaTime * eatSpeed;
|
||||
|
||||
eatTimer += (float)Timing.Step * eatSpeed;
|
||||
|
||||
Vector2 mouthPos = mouthLimb.SimPosition;
|
||||
if (mouthLimb.MouthPos.HasValue)
|
||||
{
|
||||
float cos = (float)Math.Cos(mouthLimb.Rotation);
|
||||
float sin = (float)Math.Sin(mouthLimb.Rotation);
|
||||
|
||||
mouthPos += new Vector2(
|
||||
mouthLimb.MouthPos.Value.X * cos - mouthLimb.MouthPos.Value.Y * sin,
|
||||
mouthLimb.MouthPos.Value.X * sin + mouthLimb.MouthPos.Value.Y * cos);
|
||||
}
|
||||
|
||||
Vector2 mouthPos = GetMouthPosition().Value;
|
||||
Vector2 attackSimPosition = character.Submarine == null ? ConvertUnits.ToSimUnits(target.WorldPosition) : target.SimPosition;
|
||||
|
||||
Vector2 limbDiff = attackSimPosition - mouthPos;
|
||||
@@ -232,19 +301,21 @@ namespace Barotrauma
|
||||
float pullStrength = (float)(Math.Sin(eatTimer) * Math.Max(Math.Sin(eatTimer * 0.5f), 0.0f));
|
||||
mouthLimb.body.ApplyForce(limbDiff * mouthLimb.Mass * 50.0f * pullStrength);
|
||||
|
||||
if (eatTimer % 1.0f < 0.5f && (eatTimer - (float)Timing.Step * eatSpeed) % 1.0f > 0.5f)
|
||||
character.ApplyStatusEffects(ActionType.OnEating, deltaTime);
|
||||
|
||||
if (eatTimer % 1.0f < 0.5f && (eatTimer - deltaTime * eatSpeed) % 1.0f > 0.5f)
|
||||
{
|
||||
//apply damage to the target character to get some blood particles flying
|
||||
targetCharacter.AnimController.MainLimb.AddDamage(targetCharacter.SimPosition, DamageType.None, Rand.Range(10.0f, 25.0f), 10.0f, false);
|
||||
targetCharacter.AnimController.MainLimb.AddDamage(targetCharacter.SimPosition, 0.0f, 20.0f, 0.0f, false);
|
||||
|
||||
//keep severing joints until there is only one limb left
|
||||
LimbJoint[] nonSeveredJoints = Array.FindAll(targetCharacter.AnimController.LimbJoints, l => !l.IsSevered && l.CanBeSevered);
|
||||
LimbJoint[] nonSeveredJoints = Array.FindAll(targetCharacter.AnimController.LimbJoints,
|
||||
l => !l.IsSevered && l.CanBeSevered && l.LimbA != null && !l.LimbA.IsSevered && l.LimbB != null && !l.LimbB.IsSevered);
|
||||
if (nonSeveredJoints.Length == 0)
|
||||
{
|
||||
//only one limb left, the character is now full eaten
|
||||
Entity.Spawner.AddToRemoveQueue(targetCharacter);
|
||||
character.SelectedCharacter = null;
|
||||
character.Health += 10.0f;
|
||||
}
|
||||
else //sever a random joint
|
||||
{
|
||||
@@ -260,42 +331,124 @@ namespace Barotrauma
|
||||
|
||||
void UpdateSineAnim(float deltaTime)
|
||||
{
|
||||
movement = TargetMovement * swimSpeed;
|
||||
if (CurrentSwimParams == null) { return; }
|
||||
movement = TargetMovement;
|
||||
|
||||
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, 0.5f);
|
||||
if (movement.LengthSquared() > 0.00001f)
|
||||
{
|
||||
Collider.LinearVelocity = Vector2.Lerp(Collider.LinearVelocity, movement, 0.5f);
|
||||
}
|
||||
|
||||
//limbs are disabled when simple physics is enabled, no need to move them
|
||||
if (SimplePhysicsEnabled) { return; }
|
||||
|
||||
MainLimb.PullJointEnabled = true;
|
||||
MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
|
||||
|
||||
if (movement.LengthSquared() < 0.00001f) return;
|
||||
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
|
||||
|
||||
if (movement.LengthSquared() < 0.00001f)
|
||||
{
|
||||
WalkPos = MathHelper.SmoothStep(WalkPos, MathHelper.PiOver2, deltaTime * 5);
|
||||
MainLimb.PullJointWorldAnchorB = Vector2.Lerp(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, 0.5f);
|
||||
return;
|
||||
}
|
||||
|
||||
float movementAngle = MathUtils.VectorToAngle(movement) - MathHelper.PiOver2;
|
||||
|
||||
if (rotateTowardsMovement)
|
||||
|
||||
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle.Value : HeadAngle.Value) * Dir;
|
||||
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
|
||||
{
|
||||
Collider.SmoothRotate(movementAngle, 25.0f);
|
||||
MainLimb.body.SmoothRotate(movementAngle, steerTorque);
|
||||
movementAngle += MathHelper.TwoPi;
|
||||
}
|
||||
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
|
||||
{
|
||||
movementAngle -= MathHelper.TwoPi;
|
||||
}
|
||||
|
||||
if (CurrentSwimParams.RotateTowardsMovement)
|
||||
{
|
||||
Collider.SmoothRotate(movementAngle, CurrentSwimParams.SteerTorque);
|
||||
if (TorsoAngle.HasValue)
|
||||
{
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
if (torso != null)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque);
|
||||
}
|
||||
}
|
||||
if (HeadAngle.HasValue)
|
||||
{
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
if (head != null)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque);
|
||||
}
|
||||
}
|
||||
if (TailAngle.HasValue)
|
||||
{
|
||||
Limb tail = GetLimb(LimbType.Tail);
|
||||
//tail?.body.SmoothRotate(movementAngle + TailAngle.Value * Dir, TailTorque);
|
||||
if (tail != null)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.SmoothRotate(HeadAngle * Dir, 25.0f);
|
||||
MainLimb.body.SmoothRotate(HeadAngle * Dir, steerTorque);
|
||||
movementAngle = Dir > 0 ? -MathHelper.PiOver2 : MathHelper.PiOver2;
|
||||
if (MainLimb.type == LimbType.Head && HeadAngle.HasValue)
|
||||
{
|
||||
Collider.SmoothRotate(HeadAngle.Value * Dir, CurrentSwimParams.SteerTorque);
|
||||
}
|
||||
else if (MainLimb.type == LimbType.Torso && TorsoAngle.HasValue)
|
||||
{
|
||||
Collider.SmoothRotate(TorsoAngle.Value * Dir, CurrentSwimParams.SteerTorque);
|
||||
}
|
||||
if (TorsoAngle.HasValue)
|
||||
{
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
torso?.body.SmoothRotate(TorsoAngle.Value * Dir, TorsoTorque);
|
||||
}
|
||||
if (HeadAngle.HasValue)
|
||||
{
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
head?.body.SmoothRotate(HeadAngle.Value * Dir, HeadTorque);
|
||||
}
|
||||
if (TailAngle.HasValue)
|
||||
{
|
||||
Limb tail = GetLimb(LimbType.Tail);
|
||||
tail?.body.SmoothRotate(TailAngle.Value * Dir, TailTorque);
|
||||
}
|
||||
}
|
||||
|
||||
Limb tail = GetLimb(LimbType.Tail);
|
||||
if (tail != null && waveAmplitude > 0.0f)
|
||||
var waveLength = Math.Abs(CurrentSwimParams.WaveLength * RagdollParams.JointScale);
|
||||
var waveAmplitude = Math.Abs(CurrentSwimParams.WaveAmplitude);
|
||||
if (waveLength > 0 && waveAmplitude > 0)
|
||||
{
|
||||
walkPos -= movement.Length();
|
||||
|
||||
float waveRotation = (float)Math.Sin(walkPos / waveLength);
|
||||
|
||||
tail.body.ApplyTorque(waveRotation * tail.Mass * 100.0f * waveAmplitude);
|
||||
WalkPos -= movement.Length() / Math.Abs(waveLength);
|
||||
WalkPos = MathUtils.WrapAngleTwoPi(WalkPos);
|
||||
}
|
||||
|
||||
foreach (var limb in Limbs)
|
||||
{
|
||||
switch (limb.type)
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.RightFoot:
|
||||
if (CurrentSwimParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID))
|
||||
{
|
||||
SmoothRotateWithoutWrapping(limb, movementAngle + CurrentSwimParams.FootAnglesInRadians[limb.limbParams.ID] * Dir, MainLimb, FootTorque);
|
||||
}
|
||||
break;
|
||||
case LimbType.Tail:
|
||||
if (waveLength > 0 && waveAmplitude > 0)
|
||||
{
|
||||
float waveRotation = (float)Math.Sin(WalkPos);
|
||||
limb.body.ApplyTorque(waveRotation * limb.Mass * CurrentSwimParams.TailTorque * waveAmplitude);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < Limbs.Length; i++)
|
||||
{
|
||||
@@ -304,13 +457,24 @@ namespace Barotrauma
|
||||
Vector2 pullPos = Limbs[i].PullJointWorldAnchorA;
|
||||
Limbs[i].body.ApplyForce(movement * Limbs[i].SteerForce * Limbs[i].Mass, pullPos);
|
||||
}
|
||||
|
||||
|
||||
if (CurrentSwimParams.UseSineMovement)
|
||||
{
|
||||
MainLimb.PullJointWorldAnchorB = Vector2.SmoothStep(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, (float)Math.Abs(Math.Sin(WalkPos)));
|
||||
}
|
||||
else
|
||||
{
|
||||
//MainLimb.PullJointWorldAnchorB = Collider.SimPosition;
|
||||
MainLimb.PullJointWorldAnchorB = Vector2.Lerp(MainLimb.PullJointWorldAnchorB, Collider.SimPosition, 0.5f);
|
||||
}
|
||||
|
||||
floorY = Limbs[0].SimPosition.Y;
|
||||
}
|
||||
|
||||
void UpdateWalkAnim(float deltaTime)
|
||||
{
|
||||
movement = MathUtils.SmoothStep(movement, TargetMovement * walkSpeed, 0.2f);
|
||||
if (CurrentGroundedParams == null) { return; }
|
||||
movement = MathUtils.SmoothStep(movement, TargetMovement, 0.2f);
|
||||
|
||||
Collider.LinearVelocity = new Vector2(
|
||||
movement.X,
|
||||
@@ -319,30 +483,83 @@ namespace Barotrauma
|
||||
//limbs are disabled when simple physics is enabled, no need to move them
|
||||
if (SimplePhysicsEnabled) { return; }
|
||||
|
||||
float mainLimbHeight, mainLimbAngle;
|
||||
if (MainLimb.type == LimbType.Torso)
|
||||
float mainLimbHeight = ColliderHeightFromFloor;
|
||||
|
||||
Vector2 colliderBottom = GetColliderBottom();
|
||||
|
||||
float movementAngle = 0.0f;
|
||||
float mainLimbAngle = (MainLimb.type == LimbType.Torso ? TorsoAngle.Value : HeadAngle.Value) * Dir;
|
||||
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) > MathHelper.Pi)
|
||||
{
|
||||
mainLimbHeight = TorsoPosition;
|
||||
mainLimbAngle = torsoAngle;
|
||||
movementAngle += MathHelper.TwoPi;
|
||||
}
|
||||
else
|
||||
while (MainLimb.Rotation - (movementAngle + mainLimbAngle) < -MathHelper.Pi)
|
||||
{
|
||||
mainLimbHeight = HeadPosition;
|
||||
mainLimbAngle = headAngle;
|
||||
movementAngle -= MathHelper.TwoPi;
|
||||
}
|
||||
|
||||
MainLimb.body.SmoothRotate(mainLimbAngle * Dir, 50.0f);
|
||||
Limb torso = GetLimb(LimbType.Torso);
|
||||
if (torso != null)
|
||||
{
|
||||
if (TorsoAngle.HasValue)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(torso, movementAngle + TorsoAngle.Value * Dir, MainLimb, TorsoTorque);
|
||||
}
|
||||
if (TorsoPosition.HasValue)
|
||||
{
|
||||
Vector2 pos = colliderBottom + Vector2.UnitY * TorsoPosition.Value;
|
||||
|
||||
MainLimb.MoveToPos(GetColliderBottom() + Vector2.UnitY * mainLimbHeight, 10.0f);
|
||||
|
||||
MainLimb.PullJointEnabled = true;
|
||||
MainLimb.PullJointWorldAnchorB = GetColliderBottom() + Vector2.UnitY * mainLimbHeight;
|
||||
if (torso != MainLimb)
|
||||
pos.X = torso.SimPosition.X;
|
||||
else
|
||||
mainLimbHeight = TorsoPosition.Value;
|
||||
|
||||
walkPos -= MainLimb.LinearVelocity.X * 0.05f;
|
||||
torso.MoveToPos(pos, TorsoMoveForce);
|
||||
torso.PullJointEnabled = true;
|
||||
torso.PullJointWorldAnchorB = pos;
|
||||
}
|
||||
}
|
||||
|
||||
Vector2 transformedStepSize = new Vector2(
|
||||
(float)Math.Cos(walkPos) * stepSize.X * 3.0f,
|
||||
(float)Math.Sin(walkPos) * stepSize.Y * 2.0f);
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
if (head != null)
|
||||
{
|
||||
if (HeadAngle.HasValue)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(head, movementAngle + HeadAngle.Value * Dir, MainLimb, HeadTorque);
|
||||
}
|
||||
if (HeadPosition.HasValue)
|
||||
{
|
||||
Vector2 pos = colliderBottom + Vector2.UnitY * HeadPosition.Value;
|
||||
|
||||
if (head != MainLimb)
|
||||
pos.X = head.SimPosition.X;
|
||||
else
|
||||
mainLimbHeight = HeadPosition.Value;
|
||||
|
||||
head.MoveToPos(pos, HeadMoveForce);
|
||||
head.PullJointEnabled = true;
|
||||
head.PullJointWorldAnchorB = pos;
|
||||
}
|
||||
}
|
||||
|
||||
if (TailAngle.HasValue)
|
||||
{
|
||||
var tail = GetLimb(LimbType.Tail);
|
||||
if (tail != null)
|
||||
{
|
||||
SmoothRotateWithoutWrapping(tail, movementAngle + TailAngle.Value * Dir, MainLimb, TailTorque);
|
||||
}
|
||||
}
|
||||
|
||||
WalkPos -= MainLimb.LinearVelocity.X * (CurrentAnimationParams.CycleSpeed / RagdollParams.JointScale / 100.0f);
|
||||
|
||||
Vector2 transformedStepSize = Vector2.Zero;
|
||||
if (Math.Abs(TargetMovement.X) > 0.01f)
|
||||
{
|
||||
transformedStepSize = new Vector2(
|
||||
(float)Math.Cos(WalkPos) * StepSize.Value.X * 3.0f,
|
||||
(float)Math.Sin(WalkPos) * StepSize.Value.Y * 2.0f);
|
||||
}
|
||||
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
@@ -350,56 +567,78 @@ namespace Barotrauma
|
||||
{
|
||||
case LimbType.LeftFoot:
|
||||
case LimbType.RightFoot:
|
||||
Vector2 footPos = new Vector2(limb.SimPosition.X, MainLimb.SimPosition.Y - mainLimbHeight);
|
||||
Vector2 footPos = new Vector2(limb.SimPosition.X, colliderBottom.Y);
|
||||
|
||||
if (limb.RefJointIndex>-1)
|
||||
if (limb.RefJointIndex > -1)
|
||||
{
|
||||
RevoluteJoint refJoint = LimbJoints[limb.RefJointIndex];
|
||||
footPos.X = refJoint.WorldAnchorA.X;
|
||||
if (LimbJoints.Length <= limb.RefJointIndex)
|
||||
{
|
||||
DebugConsole.ThrowError($"Reference joint index {limb.RefJointIndex} is out of array. This is probably due to a missing joint. If you just deleted a joint, don't do that without first removing the reference joint indices from the limbs. If this is not the case, please ensure that you have defined the index to the right joint.");
|
||||
}
|
||||
else
|
||||
{
|
||||
footPos.X = LimbJoints[limb.RefJointIndex].WorldAnchorA.X;
|
||||
}
|
||||
}
|
||||
footPos.X += limb.StepOffset.X * Dir;
|
||||
footPos.Y += limb.StepOffset.Y;
|
||||
|
||||
if (limb.type == LimbType.LeftFoot)
|
||||
{
|
||||
limb.MoveToPos(footPos +new Vector2(
|
||||
limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f;
|
||||
limb.DebugTargetPos = footPos + new Vector2(
|
||||
transformedStepSize.X + movement.X * 0.1f,
|
||||
(transformedStepSize.Y > 0.0f) ? transformedStepSize.Y : 0.0f),
|
||||
8.0f);
|
||||
(transformedStepSize.Y > 0.0f) ? transformedStepSize.Y : 0.0f);
|
||||
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
|
||||
}
|
||||
else if (limb.type == LimbType.RightFoot)
|
||||
{
|
||||
limb.MoveToPos(footPos + new Vector2(
|
||||
limb.DebugRefPos = footPos + Vector2.UnitX * movement.X * 0.1f;
|
||||
limb.DebugTargetPos = footPos + new Vector2(
|
||||
-transformedStepSize.X + movement.X * 0.1f,
|
||||
(-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f),
|
||||
8.0f);
|
||||
(-transformedStepSize.Y > 0.0f) ? -transformedStepSize.Y : 0.0f);
|
||||
limb.MoveToPos(limb.DebugTargetPos, FootMoveForce);
|
||||
}
|
||||
|
||||
if (footRotation != null) limb.body.SmoothRotate((float)footRotation * Dir, 50.0f);
|
||||
|
||||
if (CurrentGroundedParams.FootAnglesInRadians.ContainsKey(limb.limbParams.ID))
|
||||
{
|
||||
SmoothRotateWithoutWrapping(limb,
|
||||
movementAngle + CurrentGroundedParams.FootAnglesInRadians[limb.limbParams.ID] * Dir,
|
||||
MainLimb, FootTorque);
|
||||
}
|
||||
break;
|
||||
case LimbType.LeftLeg:
|
||||
case LimbType.RightLeg:
|
||||
if (legTorque != 0.0f) limb.body.ApplyTorque(limb.Mass * legTorque * Dir);
|
||||
if (Math.Abs(CurrentGroundedParams.LegTorque) > 0.001f) limb.body.ApplyTorque(limb.Mass * CurrentGroundedParams.LegTorque * Dir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void UpdateDying(float deltaTime)
|
||||
{
|
||||
if (deathAnimDuration <= 0.0f) return;
|
||||
|
||||
float animStrength = (1.0f - deathAnimTimer / deathAnimDuration);
|
||||
|
||||
Limb head = GetLimb(LimbType.Head);
|
||||
Limb tail = GetLimb(LimbType.Tail);
|
||||
|
||||
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(walkPos)) * 10.0f);
|
||||
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * (float)Math.Sin(walkPos)) * 10.0f);
|
||||
if (head != null && !head.IsSevered) head.body.ApplyTorque((float)(Math.Sqrt(head.Mass) * Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
|
||||
if (tail != null && !tail.IsSevered) tail.body.ApplyTorque((float)(Math.Sqrt(tail.Mass) * -Dir * Math.Sin(WalkPos)) * 30.0f * animStrength);
|
||||
|
||||
walkPos += deltaTime * 5.0f;
|
||||
WalkPos += deltaTime * 10.0f * animStrength;
|
||||
|
||||
Vector2 centerOfMass = GetCenterOfMass();
|
||||
|
||||
foreach (Limb limb in Limbs)
|
||||
{
|
||||
#if CLIENT
|
||||
if (limb.LightSource != null)
|
||||
{
|
||||
limb.LightSource.Color = Color.Lerp(limb.InitialLightSourceColor, Color.TransparentBlack, deathAnimTimer / deathAnimDuration);
|
||||
}
|
||||
#endif
|
||||
if (limb.type == LimbType.Head || limb.type == LimbType.Tail || limb.IsSevered || !limb.body.Enabled) continue;
|
||||
if (limb.Mass <= 0.0f)
|
||||
{
|
||||
@@ -420,20 +659,33 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
limb.body.ApplyForce(diff * (float)(Math.Sin(walkPos) * Math.Sqrt(limb.Mass)) * 10.0f);
|
||||
limb.body.ApplyForce(diff * (float)(Math.Sin(WalkPos) * Math.Sqrt(limb.Mass)) * 30.0f * animStrength);
|
||||
}
|
||||
}
|
||||
|
||||
private void SmoothRotateWithoutWrapping(Limb limb, float angle, Limb referenceLimb, float torque)
|
||||
{
|
||||
//make sure the angle "has the same number of revolutions" as the reference limb
|
||||
//(e.g. we don't want to rotate the legs to 0 if the torso is at 360, because that'd blow up the hip joints)
|
||||
while (referenceLimb.Rotation - angle > MathHelper.TwoPi)
|
||||
{
|
||||
angle += MathHelper.TwoPi;
|
||||
}
|
||||
while (referenceLimb.Rotation - angle < -MathHelper.TwoPi)
|
||||
{
|
||||
angle -= MathHelper.TwoPi;
|
||||
}
|
||||
|
||||
limb?.body.SmoothRotate(angle, torque, wrapAngle: false);
|
||||
}
|
||||
|
||||
public override void Flip()
|
||||
{
|
||||
base.Flip();
|
||||
|
||||
foreach (Limb l in Limbs)
|
||||
{
|
||||
if (!l.DoesFlip) continue;
|
||||
|
||||
l.body.SetTransform(l.SimPosition,
|
||||
-l.body.Rotation);
|
||||
l.body.SetTransform(l.SimPosition, -l.body.Rotation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+803
-368
File diff suppressed because it is too large
Load Diff
+398
@@ -0,0 +1,398 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum AnimationType
|
||||
{
|
||||
NotDefined,
|
||||
Walk,
|
||||
Run,
|
||||
SwimSlow,
|
||||
SwimFast
|
||||
}
|
||||
|
||||
abstract class GroundedMovementParams : AnimationParams
|
||||
{
|
||||
[Serialize("1.0, 1.0", true), Editable(DecimalCount = 2, ToolTip = "How big steps the character takes.")]
|
||||
public Vector2 StepSize
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's head is positioned.")]
|
||||
public float HeadPosition { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(DecimalCount = 2, ToolTip = "How high above the ground the character's torso is positioned.")]
|
||||
public float TorsoPosition { get; set; }
|
||||
|
||||
[Serialize(0.75f, true), Editable(MinValueFloat = 0.1f, MaxValueFloat = 0.99f, DecimalCount = 2, ToolTip = "The character's movement speed is multiplied with this value when moving backwards.")]
|
||||
public float BackwardsMovementMultiplier { get; set; }
|
||||
}
|
||||
|
||||
abstract class SwimParams : AnimationParams
|
||||
{
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
public float SteerTorque { get; set; }
|
||||
}
|
||||
|
||||
abstract class AnimationParams : EditableParams
|
||||
{
|
||||
public string SpeciesName { get; private set; }
|
||||
public bool IsGroundedAnimation => AnimationType == AnimationType.Walk || AnimationType == AnimationType.Run;
|
||||
public bool IsSwimAnimation => AnimationType == AnimationType.SwimSlow || AnimationType == AnimationType.SwimFast;
|
||||
|
||||
protected static Dictionary<string, Dictionary<string, AnimationParams>> allAnimations = new Dictionary<string, Dictionary<string, AnimationParams>>();
|
||||
|
||||
[Serialize(1.0f, true), Editable(DecimalCount = 2)]
|
||||
public float MovementSpeed { get; set; }
|
||||
|
||||
[Serialize(1.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2,
|
||||
ToolTip = "The speed of the \"animation cycle\", i.e. how fast the character takes steps or moves the tail/legs/arms (the outcome depends what the clip is about)")]
|
||||
public float CycleSpeed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(float.NaN, true), Editable(-360f, 360f)]
|
||||
public float HeadAngle
|
||||
{
|
||||
get => float.IsNaN(HeadAngleInRadians) ? float.NaN : MathHelper.ToDegrees(HeadAngleInRadians);
|
||||
set
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
HeadAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public float HeadAngleInRadians { get; private set; } = float.NaN;
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(float.NaN, true), Editable(-360f, 360f)]
|
||||
public float TorsoAngle
|
||||
{
|
||||
get => float.IsNaN(TorsoAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TorsoAngleInRadians);
|
||||
set
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
TorsoAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public float TorsoAngleInRadians { get; private set; } = float.NaN;
|
||||
|
||||
[Serialize(AnimationType.NotDefined, true), Editable]
|
||||
public virtual AnimationType AnimationType { get; protected set; }
|
||||
|
||||
public static string GetDefaultFileName(string speciesName, AnimationType animType) => $"{speciesName.CapitaliseFirstInvariant()}{animType.ToString()}";
|
||||
public static string GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Animations/";
|
||||
public static string GetDefaultFile(string speciesName, AnimationType animType) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName, animType)}.xml";
|
||||
|
||||
protected static string GetFolder(string speciesName)
|
||||
{
|
||||
var folder = XMLExtensions.TryLoadXml(Character.GetConfigFile(speciesName))?.Root?.Element("animations")?.GetAttributeString("folder", string.Empty);
|
||||
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
|
||||
{
|
||||
folder = GetDefaultFolder(speciesName);
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects a random filepath from multiple paths, matching the specified animation type.
|
||||
/// </summary>
|
||||
public static string GetRandomFilePath(IEnumerable<string> filePaths, AnimationType type)
|
||||
{
|
||||
return filePaths.GetRandom(f => AnimationPredicate(f, type), Rand.RandSync.Server);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects all file paths that match the specified animation type.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> FilterFilesByType(IEnumerable<string> filePaths, AnimationType type)
|
||||
{
|
||||
return filePaths.Where(f => AnimationPredicate(f, type));
|
||||
}
|
||||
|
||||
private static bool AnimationPredicate(string filePath, AnimationType type)
|
||||
{
|
||||
var doc = XMLExtensions.TryLoadXml(filePath);
|
||||
if (doc == null) { return false; }
|
||||
var typeString = doc.Root.GetAttributeString("animationtype", null);
|
||||
if (string.IsNullOrWhiteSpace(typeString))
|
||||
{
|
||||
typeString = doc.Root.GetAttributeString("AnimationType", "NotDefined");
|
||||
}
|
||||
return Enum.TryParse(typeString, out AnimationType fileType) && fileType == type;
|
||||
}
|
||||
|
||||
public static T GetDefaultAnimParams<T>(string speciesName, AnimationType animType) where T : AnimationParams, new() => GetAnimParams<T>(speciesName, animType, GetDefaultFileName(speciesName, animType));
|
||||
|
||||
/// <summary>
|
||||
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
|
||||
/// If a custom folder is used, it's defined in the character info file.
|
||||
/// </summary>
|
||||
public static T GetAnimParams<T>(string speciesName, AnimationType animType, string fileName = null) where T : AnimationParams, new()
|
||||
{
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
anims = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, anims);
|
||||
}
|
||||
if (fileName == null || !anims.TryGetValue(fileName, out AnimationParams anim))
|
||||
{
|
||||
string selectedFile = null;
|
||||
string folder = GetFolder(speciesName);
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
var files = Directory.GetFiles(folder);
|
||||
if (files.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files from the folder: {folder}. Using the default animation.");
|
||||
selectedFile = GetDefaultFile(speciesName, animType);
|
||||
}
|
||||
var filteredFiles = FilterFilesByType(files, animType);
|
||||
if (filteredFiles.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Could not find any animation files that match the animation type {animType} from the folder: {folder}. Using the default animation.");
|
||||
selectedFile = GetDefaultFile(speciesName, animType);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
// Files found, but none specified.
|
||||
selectedFile = GetDefaultFile(speciesName, animType);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFile = filteredFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
|
||||
if (selectedFile == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Could not find an animation file that matches the name {fileName} and the animation type {animType}. Using the default animations.");
|
||||
selectedFile = GetDefaultFile(speciesName, animType);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[Animationparams] Invalid directory: {folder}. Using the default animation.");
|
||||
selectedFile = GetDefaultFile(speciesName, animType);
|
||||
}
|
||||
if (selectedFile == null)
|
||||
{
|
||||
throw new Exception("[AnimationParams] Selected file null!");
|
||||
}
|
||||
DebugConsole.Log($"[AnimationParams] Loading animations from {selectedFile}.");
|
||||
T a = new T();
|
||||
if (a.Load(selectedFile, speciesName))
|
||||
{
|
||||
if (!anims.ContainsKey(a.Name))
|
||||
{
|
||||
anims.Add(a.Name, a);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[AnimationParams] Failed to load an animation {a} at {selectedFile} of type {animType} for the character {speciesName}");
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return (T)anim;
|
||||
}
|
||||
|
||||
public static AnimationParams Create(string fullPath, string speciesName, AnimationType animationType, Type type)
|
||||
{
|
||||
if (type == typeof(HumanWalkParams))
|
||||
{
|
||||
return Create<HumanWalkParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(HumanRunParams))
|
||||
{
|
||||
return Create<HumanRunParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(HumanSwimSlowParams))
|
||||
{
|
||||
return Create<HumanSwimSlowParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(HumanSwimFastParams))
|
||||
{
|
||||
return Create<HumanSwimFastParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(FishWalkParams))
|
||||
{
|
||||
return Create<FishWalkParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(FishRunParams))
|
||||
{
|
||||
return Create<FishRunParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(FishSwimSlowParams))
|
||||
{
|
||||
return Create<FishSwimSlowParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
if (type == typeof(FishSwimFastParams))
|
||||
{
|
||||
return Create<FishSwimFastParams>(fullPath, speciesName, animationType);
|
||||
}
|
||||
throw new NotImplementedException(type.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Overrides old animations, if found!
|
||||
/// </summary>
|
||||
public static T Create<T>(string fullPath, string speciesName, AnimationType animationType) where T : AnimationParams, new()
|
||||
{
|
||||
if (animationType == AnimationType.NotDefined)
|
||||
{
|
||||
throw new Exception("Cannot create an animation file of type " + animationType.ToString());
|
||||
}
|
||||
if (!allAnimations.TryGetValue(speciesName, out Dictionary<string, AnimationParams> anims))
|
||||
{
|
||||
anims = new Dictionary<string, AnimationParams>();
|
||||
allAnimations.Add(speciesName, anims);
|
||||
}
|
||||
var fileName = Path.GetFileNameWithoutExtension(fullPath);
|
||||
if (anims.ContainsKey(fileName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[AnimationParams] Removing the old animation of type {animationType}.", Color.Red);
|
||||
anims.Remove(fileName);
|
||||
}
|
||||
var instance = new T();
|
||||
XElement animationElement = new XElement(GetDefaultFileName(speciesName, animationType), new XAttribute("animationtype", animationType.ToString()));
|
||||
instance.doc = new XDocument(animationElement);
|
||||
instance.UpdatePath(fullPath);
|
||||
instance.IsLoaded = instance.Deserialize(animationElement);
|
||||
instance.Save();
|
||||
instance.Load(fullPath, speciesName);
|
||||
anims.Add(instance.Name, instance);
|
||||
DebugConsole.NewMessage($"[AnimationParams] New animation file of type {animationType} created.", Color.GhostWhite);
|
||||
return instance as T;
|
||||
}
|
||||
|
||||
protected bool Load(string file, string speciesName)
|
||||
{
|
||||
if (Load(file))
|
||||
{
|
||||
SpeciesName = speciesName;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected override void UpdatePath(string newPath)
|
||||
{
|
||||
if (SpeciesName == null)
|
||||
{
|
||||
base.UpdatePath(newPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the key by removing and re-adding the animation.
|
||||
if (allAnimations.TryGetValue(SpeciesName, out Dictionary<string, AnimationParams> animations))
|
||||
{
|
||||
animations.Remove(Name);
|
||||
}
|
||||
base.UpdatePath(newPath);
|
||||
if (animations != null)
|
||||
{
|
||||
if (!animations.ContainsKey(Name))
|
||||
{
|
||||
animations.Add(Name, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected static string ParseFootAngles(Dictionary<int, float> footAngles)
|
||||
{
|
||||
//convert to the format "id1:angle,id2:angle,id3:angle"
|
||||
return string.Join(",", footAngles.Select(kv => kv.Key + ": " + kv.Value.ToString("G", CultureInfo.InvariantCulture)).ToArray());
|
||||
}
|
||||
|
||||
protected static void SetFootAngles(Dictionary<int, float> footAngles, string value)
|
||||
{
|
||||
footAngles.Clear();
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string[] keyValuePairs = value.Split(',');
|
||||
foreach (string joinedKvp in keyValuePairs)
|
||||
{
|
||||
string[] keyValuePair = joinedKvp.Split(':');
|
||||
if (keyValuePair.Length != 2 ||
|
||||
!int.TryParse(keyValuePair[0].Trim(), out int limbIndex) ||
|
||||
!float.TryParse(keyValuePair[1].Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out float angle))
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to parse foot angles (" + value + ")");
|
||||
continue;
|
||||
}
|
||||
footAngles[limbIndex] = angle;
|
||||
}
|
||||
}
|
||||
|
||||
public static Type GetParamTypeFromAnimType(AnimationType type, bool isHumanoid)
|
||||
{
|
||||
if (isHumanoid)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AnimationType.Walk:
|
||||
return typeof(HumanWalkParams);
|
||||
case AnimationType.Run:
|
||||
return typeof(HumanRunParams);
|
||||
case AnimationType.SwimSlow:
|
||||
return typeof(HumanSwimSlowParams);
|
||||
case AnimationType.SwimFast:
|
||||
return typeof(HumanSwimFastParams);
|
||||
default:
|
||||
throw new NotImplementedException(type.ToString());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AnimationType.Walk:
|
||||
return typeof(FishWalkParams);
|
||||
case AnimationType.Run:
|
||||
return typeof(FishRunParams);
|
||||
case AnimationType.SwimSlow:
|
||||
return typeof(FishSwimSlowParams);
|
||||
case AnimationType.SwimFast:
|
||||
return typeof(FishSwimFastParams);
|
||||
default:
|
||||
throw new NotImplementedException(type.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Memento
|
||||
protected void CreateSnapshot<T>() where T : AnimationParams, new()
|
||||
{
|
||||
Serialize();
|
||||
var copy = new T
|
||||
{
|
||||
IsLoaded = true,
|
||||
doc = new XDocument(doc)
|
||||
};
|
||||
copy.Deserialize();
|
||||
copy.Serialize();
|
||||
memento.Store(copy);
|
||||
}
|
||||
public override void Undo() => Deserialize(memento.Undo().MainElement);
|
||||
public override void Redo() => Deserialize(memento.Redo().MainElement);
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class FishWalkParams : FishGroundedParams
|
||||
{
|
||||
public static FishWalkParams GetDefaultAnimParams(Character character)
|
||||
{
|
||||
return Check(character) ? GetDefaultAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk) : Empty;
|
||||
}
|
||||
public static FishWalkParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return Check(character) ? GetAnimParams<FishWalkParams>(character.SpeciesName, AnimationType.Walk, fileName) : Empty;
|
||||
}
|
||||
|
||||
protected static FishWalkParams Empty = new FishWalkParams();
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<FishWalkParams>();
|
||||
}
|
||||
|
||||
class FishRunParams : FishGroundedParams
|
||||
{
|
||||
public static FishRunParams GetDefaultAnimParams(Character character)
|
||||
{
|
||||
return Check(character) ? GetDefaultAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run) : Empty;
|
||||
}
|
||||
public static FishRunParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return Check(character) ? GetAnimParams<FishRunParams>(character.SpeciesName, AnimationType.Run, fileName) : Empty;
|
||||
}
|
||||
|
||||
protected static FishRunParams Empty = new FishRunParams();
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<FishRunParams>();
|
||||
}
|
||||
|
||||
class FishSwimFastParams : FishSwimParams
|
||||
{
|
||||
public static FishSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
|
||||
public static FishSwimFastParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<FishSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
|
||||
}
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<FishSwimFastParams>();
|
||||
}
|
||||
|
||||
class FishSwimSlowParams : FishSwimParams
|
||||
{
|
||||
public static FishSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
|
||||
public static FishSwimSlowParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<FishSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
|
||||
}
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<FishSwimSlowParams>();
|
||||
}
|
||||
|
||||
abstract class FishGroundedParams : GroundedMovementParams, IFishAnimation
|
||||
{
|
||||
protected static bool Check(Character character)
|
||||
{
|
||||
if (!character.AnimController.CanWalk)
|
||||
{
|
||||
DebugConsole.ThrowError($"{character.SpeciesName} cannot use run animations!");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[Serialize(true, true), Editable(ToolTip = "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
|
||||
public bool Flip { get; set; }
|
||||
|
||||
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the head to the correct position.")]
|
||||
public float HeadMoveForce { get; set; }
|
||||
|
||||
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the torso to the correct position.")]
|
||||
public float TorsoMoveForce { get; set; }
|
||||
|
||||
[Serialize(8.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
|
||||
public float FootMoveForce { get; set; }
|
||||
|
||||
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
|
||||
public float HeadTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")]
|
||||
public float TorsoTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
|
||||
public float TailTorque { get; set; }
|
||||
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
|
||||
public float FootTorque { get; set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "Optional torque that's constantly applied to legs.")]
|
||||
public float LegTorque { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The angle of the collider when standing (i.e. out of water).
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(0f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "The angle of the character's collider when standing.")]
|
||||
public float ColliderStandAngle
|
||||
{
|
||||
get => MathHelper.ToDegrees(ColliderStandAngleInRadians);
|
||||
set => ColliderStandAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
public float ColliderStandAngleInRadians { get; private set; }
|
||||
|
||||
[Serialize(null, true), Editable]
|
||||
public string FootAngles
|
||||
{
|
||||
get => ParseFootAngles(FootAnglesInRadians);
|
||||
set => SetFootAngles(FootAnglesInRadians, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key = limb id, value = angle in radians
|
||||
/// </summary>
|
||||
public Dictionary<int, float> FootAnglesInRadians { get; set; } = new Dictionary<int, float>();
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(float.NaN, true), Editable(-360f, 360f)]
|
||||
public float TailAngle
|
||||
{
|
||||
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
|
||||
set
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
TailAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public float TailAngleInRadians { get; private set; } = float.NaN;
|
||||
}
|
||||
|
||||
abstract class FishSwimParams : SwimParams, IFishAnimation
|
||||
{
|
||||
[Serialize(false, true), Editable(ToolTip = "TODO")]
|
||||
public bool UseSineMovement { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable(ToolTip = "Should the character be flipped depending on which direction it faces. Should usually be enabled on all characters that have distinctive upper and lower sides.")]
|
||||
public bool Flip { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable(ToolTip = "If enabled, the character will simply be mirrored horizontally when it wants to turn around. If disabled, it will rotate itself to face the other direction.")]
|
||||
public bool Mirror { get; set; }
|
||||
|
||||
[Serialize(1f, true), Editable]
|
||||
public float WaveAmplitude { get; set; }
|
||||
|
||||
[Serialize(10.0f, true), Editable]
|
||||
public float WaveLength { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable(ToolTip = "Should the character face towards the direction it's heading.")]
|
||||
public bool RotateTowardsMovement { get; set; }
|
||||
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the torso to the correct orientation.")]
|
||||
public float TorsoTorque { get; set; }
|
||||
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the head to the correct orientation.")]
|
||||
public float HeadTorque { get; set; }
|
||||
|
||||
[Serialize(50.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the tail to the correct orientation.")]
|
||||
public float TailTorque { get; set; }
|
||||
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
|
||||
public float FootTorque { get; set; }
|
||||
|
||||
[Serialize(null, true), Editable]
|
||||
public string FootAngles
|
||||
{
|
||||
get => ParseFootAngles(FootAnglesInRadians);
|
||||
set => SetFootAngles(FootAnglesInRadians, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Key = limb id, value = angle in radians
|
||||
/// </summary>
|
||||
public Dictionary<int, float> FootAnglesInRadians { get; set; } = new Dictionary<int, float>();
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(float.NaN, true), Editable(-360f, 360f)]
|
||||
public float TailAngle
|
||||
{
|
||||
get => float.IsNaN(TailAngleInRadians) ? float.NaN : MathHelper.ToDegrees(TailAngleInRadians);
|
||||
set
|
||||
{
|
||||
if (!float.IsNaN(value))
|
||||
{
|
||||
TailAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
public float TailAngleInRadians { get; private set; } = float.NaN;
|
||||
}
|
||||
|
||||
interface IFishAnimation
|
||||
{
|
||||
bool Flip { get; set; }
|
||||
string FootAngles { get; set; }
|
||||
Dictionary<int, float> FootAnglesInRadians { get; set; }
|
||||
float TailAngle { get; set; }
|
||||
float TailAngleInRadians { get; }
|
||||
float HeadTorque { get; set; }
|
||||
float TorsoTorque { get; set; }
|
||||
float TailTorque { get; set; }
|
||||
float FootTorque { get; set; }
|
||||
}
|
||||
}
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class HumanWalkParams : HumanGroundedParams
|
||||
{
|
||||
public static HumanWalkParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk);
|
||||
public static HumanWalkParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<HumanWalkParams>(character.SpeciesName, AnimationType.Walk, fileName);
|
||||
}
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<HumanWalkParams>();
|
||||
}
|
||||
|
||||
class HumanRunParams : HumanGroundedParams
|
||||
{
|
||||
public static HumanRunParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run);
|
||||
public static HumanRunParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<HumanRunParams>(character.SpeciesName, AnimationType.Run, fileName);
|
||||
}
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<HumanRunParams>();
|
||||
}
|
||||
|
||||
class HumanSwimFastParams: HumanSwimParams
|
||||
{
|
||||
public static HumanSwimFastParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast);
|
||||
public static HumanSwimFastParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<HumanSwimFastParams>(character.SpeciesName, AnimationType.SwimFast, fileName);
|
||||
}
|
||||
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<HumanSwimFastParams>();
|
||||
}
|
||||
|
||||
class HumanSwimSlowParams : HumanSwimParams
|
||||
{
|
||||
public static HumanSwimSlowParams GetDefaultAnimParams(Character character) => GetDefaultAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow);
|
||||
public static HumanSwimSlowParams GetAnimParams(Character character, string fileName = null)
|
||||
{
|
||||
return GetAnimParams<HumanSwimSlowParams>(character.SpeciesName, AnimationType.SwimSlow, fileName);
|
||||
}
|
||||
|
||||
public override void CreateSnapshot() => CreateSnapshot<HumanSwimSlowParams>();
|
||||
}
|
||||
|
||||
abstract class HumanSwimParams : SwimParams, IHumanAnimation
|
||||
{
|
||||
[Serialize(0.5f, true), Editable(DecimalCount = 2)]
|
||||
public float LegMoveAmount { get; set; }
|
||||
|
||||
[Serialize(5.0f, true), Editable]
|
||||
public float LegCycleLength { get; set; }
|
||||
|
||||
[Serialize("0.5, 0.1", true), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveAmount { get; set; }
|
||||
|
||||
[Serialize(0.5f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10, DecimalCount = 2)]
|
||||
public float HandMoveStrength { get; set; }
|
||||
|
||||
[Serialize(5.0f, true), Editable]
|
||||
public float HandCycleSpeed { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2)]
|
||||
public Vector2 HandMoveOffset { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(0.0f, true), Editable(-360f, 360f)]
|
||||
public float FootAngle
|
||||
{
|
||||
get => MathHelper.ToDegrees(FootAngleInRadians);
|
||||
set
|
||||
{
|
||||
FootAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
public float FootAngleInRadians { get; private set; }
|
||||
|
||||
[Serialize(25.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
|
||||
public float FootRotateStrength { get; set; }
|
||||
}
|
||||
|
||||
abstract class HumanGroundedParams : GroundedMovementParams, IHumanAnimation
|
||||
{
|
||||
[Serialize(0.3f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1, DecimalCount = 2, ToolTip = "How much force is used to force the character upright.")]
|
||||
public float GetUpForce { get; set; }
|
||||
|
||||
// -- TODO: use a separate clip for crawling -> replace these when implemented.
|
||||
|
||||
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the torso when crouching.")]
|
||||
public float CrouchingTorsoPos { get; set; }
|
||||
|
||||
[Serialize(0.65f, true), Editable(MinValueFloat = 0, MaxValueFloat = 5, DecimalCount = 2, ToolTip = "Height of the head when crouching.")]
|
||||
public float CrouchingHeadPos { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees
|
||||
/// </summary>
|
||||
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the torso when crouching.")]
|
||||
public float CrouchingTorsoAngle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees
|
||||
/// </summary>
|
||||
[Serialize(-10f, true), Editable(MinValueFloat = -360, MaxValueFloat = 360, ToolTip = "Angle of the head when crouching.")]
|
||||
public float CrouchingHeadAngle { get; set; }
|
||||
|
||||
// --
|
||||
|
||||
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's head leans forwards when moving.")]
|
||||
public float HeadLeanAmount { get; set; }
|
||||
|
||||
[Serialize(0.25f, true), Editable(DecimalCount = 2, ToolTip = "How much the character's torso leans forwards when moving.")]
|
||||
public float TorsoLeanAmount { get; set; }
|
||||
|
||||
[Serialize(15.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much force is used to move the feet to the correct position.")]
|
||||
public float FootMoveStrength { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(0.0f, true), Editable(-360f, 360f)]
|
||||
public float FootAngle
|
||||
{
|
||||
get => MathHelper.ToDegrees(FootAngleInRadians);
|
||||
set
|
||||
{
|
||||
FootAngleInRadians = MathHelper.ToRadians(value);
|
||||
}
|
||||
}
|
||||
public float FootAngleInRadians { get; private set; }
|
||||
|
||||
[Serialize(20.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to rotate the feet to the correct orientation.")]
|
||||
public float FootRotateStrength { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them.")]
|
||||
public Vector2 FootMoveOffset { get; set; }
|
||||
|
||||
[Serialize("0.0, 0.0", true), Editable(DecimalCount = 2, ToolTip = "Added to the calculated foot positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their feet one unit behind them.")]
|
||||
public Vector2 CrouchingFootMoveOffset { get; set; }
|
||||
|
||||
[Serialize(10.0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100, ToolTip = "How much torque is used to bend the characters legs when taking a step.")]
|
||||
public float LegBendTorque { get; set; }
|
||||
|
||||
[Serialize("0.4, 0.15", true), Editable(DecimalCount = 2, ToolTip = "How much the hands move along each axis.")]
|
||||
public Vector2 HandMoveAmount { get; set; }
|
||||
|
||||
[Serialize("-0.15, 0.0", true), Editable(DecimalCount = 2, ToolTip = "Added to the calculated hand positions, e.g. a value of {-1.0, 0.0f} would make the character \"drag\" their hands one unit behind them.")]
|
||||
public Vector2 HandMoveOffset { get; set; }
|
||||
|
||||
[Serialize(0.7f, true), Editable(MinValueFloat = 0, MaxValueFloat = 2, DecimalCount = 2, ToolTip = "How much force is used to move the hands.")]
|
||||
public float HandMoveStrength { get; set; }
|
||||
|
||||
[Serialize(-1.0f, true), Editable(DecimalCount = 2, ToolTip = "The position of the hands is clamped below this (relative to the position of the character's torso).")]
|
||||
public float HandClampY { get; set; }
|
||||
}
|
||||
|
||||
public interface IHumanAnimation
|
||||
{
|
||||
float FootAngle { get; set; }
|
||||
float FootAngleInRadians { get; }
|
||||
float FootRotateStrength { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
abstract class EditableParams : ISerializableEntity
|
||||
{
|
||||
public bool IsLoaded { get; protected set; }
|
||||
public string Name { get; private set; }
|
||||
public string FileName { get; private set; }
|
||||
public string Folder { get; private set; }
|
||||
public string FullPath { get; private set; }
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; protected set; }
|
||||
|
||||
protected XDocument doc;
|
||||
public XDocument Doc
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
DebugConsole.ThrowError("[Params] Not loaded!");
|
||||
return new XDocument();
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
protected set
|
||||
{
|
||||
doc = value;
|
||||
}
|
||||
}
|
||||
|
||||
public XElement MainElement => doc.Root;
|
||||
public XElement OriginalElement { get; protected set; }
|
||||
|
||||
protected virtual bool Deserialize(XElement element = null)
|
||||
{
|
||||
element = element ?? MainElement;
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
return SerializableProperties != null;
|
||||
}
|
||||
|
||||
protected virtual bool Serialize(XElement element = null)
|
||||
{
|
||||
element = element ?? MainElement;
|
||||
SerializableProperty.SerializeProperties(this, element, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected virtual bool Load(string file)
|
||||
{
|
||||
UpdatePath(file);
|
||||
doc = XMLExtensions.TryLoadXml(FullPath);
|
||||
if (doc == null) { return false; }
|
||||
IsLoaded = Deserialize(MainElement);
|
||||
OriginalElement = new XElement(MainElement);
|
||||
return IsLoaded;
|
||||
}
|
||||
|
||||
protected virtual void UpdatePath(string fullPath)
|
||||
{
|
||||
FullPath = fullPath;
|
||||
Name = Path.GetFileNameWithoutExtension(FullPath);
|
||||
FileName = Path.GetFileName(FullPath);
|
||||
Folder = Path.GetDirectoryName(FullPath);
|
||||
}
|
||||
|
||||
public virtual bool Save(string fileNameWithoutExtension = null, XmlWriterSettings settings = null)
|
||||
{
|
||||
if (!Directory.Exists(Folder))
|
||||
{
|
||||
Directory.CreateDirectory(Folder);
|
||||
}
|
||||
OriginalElement = MainElement;
|
||||
Serialize();
|
||||
if (settings == null)
|
||||
{
|
||||
settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
OmitXmlDeclaration = true,
|
||||
NewLineOnAttributes = true
|
||||
};
|
||||
}
|
||||
if (fileNameWithoutExtension != null)
|
||||
{
|
||||
UpdatePath(Path.Combine(Folder, $"{fileNameWithoutExtension}.xml"));
|
||||
}
|
||||
using (var writer = XmlWriter.Create(FullPath, settings))
|
||||
{
|
||||
Doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual bool Reset(bool forceReload = false)
|
||||
{
|
||||
if (forceReload)
|
||||
{
|
||||
return Load(FullPath);
|
||||
}
|
||||
return Deserialize(OriginalElement);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
|
||||
public virtual void AddToEditor(ParamsEditor editor)
|
||||
{
|
||||
if (!IsLoaded)
|
||||
{
|
||||
DebugConsole.ThrowError("[Params] Not loaded!");
|
||||
return;
|
||||
}
|
||||
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
|
||||
}
|
||||
#endif
|
||||
|
||||
#region Memento
|
||||
public readonly Memento<EditableParams> memento = new Memento<EditableParams>();
|
||||
public abstract void CreateSnapshot();
|
||||
public abstract void Undo();
|
||||
public abstract void Redo();
|
||||
public void ClearHistory() => memento.Clear();
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
+649
@@ -0,0 +1,649 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Xml;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class HumanRagdollParams : RagdollParams
|
||||
{
|
||||
public static HumanRagdollParams GetRagdollParams(string speciesName, string fileName = null) => GetRagdollParams<HumanRagdollParams>(speciesName, fileName);
|
||||
public static HumanRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<HumanRagdollParams>(speciesName);
|
||||
}
|
||||
|
||||
class FishRagdollParams : RagdollParams
|
||||
{
|
||||
public static FishRagdollParams GetDefaultRagdollParams(string speciesName) => GetDefaultRagdollParams<FishRagdollParams>(speciesName);
|
||||
}
|
||||
|
||||
class RagdollParams : EditableParams
|
||||
{
|
||||
public const float MIN_SCALE = 0.1f;
|
||||
public const float MAX_SCALE = 2;
|
||||
|
||||
public string SpeciesName { get; private set; }
|
||||
|
||||
[Serialize(0f, true), Editable(-360, 360, ToolTip = "Rotation offset (in degrees) used for animations and widgets. If the sprites in the sheet are in different orientations, use the orientation of the torso for the final version of your character (while editing the character in the editor, you can change the orientation freely).")]
|
||||
public float SpritesheetOrientation { get; set; }
|
||||
|
||||
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
|
||||
public float LimbScale { get; set; }
|
||||
|
||||
[Serialize(1.0f, true), Editable(MIN_SCALE, MAX_SCALE, DecimalCount = 3)]
|
||||
public float JointScale { get; set; }
|
||||
|
||||
[Serialize(1f, true), Editable(DecimalCount = 2)]
|
||||
public float TextureScale { get; set; }
|
||||
|
||||
[Serialize(45f, true), Editable(0f, 1000f)]
|
||||
public float ColliderHeightFromFloor { get; set; }
|
||||
|
||||
[Serialize(50f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float ImpactTolerance { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool CanEnterSubmarine { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool Draggable { get; set; }
|
||||
|
||||
private static Dictionary<string, Dictionary<string, RagdollParams>> allRagdolls = new Dictionary<string, Dictionary<string, RagdollParams>>();
|
||||
|
||||
public List<ColliderParams> ColliderParams { get; private set; } = new List<ColliderParams>();
|
||||
public List<LimbParams> Limbs { get; private set; } = new List<LimbParams>();
|
||||
public List<JointParams> Joints { get; private set; } = new List<JointParams>();
|
||||
|
||||
protected IEnumerable<RagdollSubParams> GetAllSubParams() =>
|
||||
ColliderParams.Select(c => c as RagdollSubParams)
|
||||
.Concat(Limbs.Select(j => j as RagdollSubParams)
|
||||
.Concat(Joints.Select(j => j as RagdollSubParams)));
|
||||
|
||||
public static string GetDefaultFileName(string speciesName) => $"{speciesName.CapitaliseFirstInvariant()}DefaultRagdoll";
|
||||
public static string GetDefaultFolder(string speciesName) => $"Content/Characters/{speciesName.CapitaliseFirstInvariant()}/Ragdolls/";
|
||||
public static string GetDefaultFile(string speciesName) => $"{GetDefaultFolder(speciesName)}{GetDefaultFileName(speciesName)}.xml";
|
||||
|
||||
private static readonly object[] dummyParams = new object[]
|
||||
{
|
||||
new XAttribute("type", "Dummy"),
|
||||
new XElement("collider", new XAttribute("radius", 1)),
|
||||
new XElement("limb",
|
||||
new XAttribute("id", 0),
|
||||
new XAttribute("type", LimbType.Head.ToString()),
|
||||
new XAttribute("width", 1),
|
||||
new XAttribute("height", 1),
|
||||
new XElement("sprite",
|
||||
new XAttribute("sourcerect", $"0, 0, 1, 1")))
|
||||
};
|
||||
|
||||
protected static string GetFolder(string speciesName)
|
||||
{
|
||||
var folder = XMLExtensions.TryLoadXml(Character.GetConfigFile(speciesName))?.Root?.Element("ragdolls")?.GetAttributeString("folder", string.Empty);
|
||||
if (string.IsNullOrEmpty(folder) || folder.ToLowerInvariant() == "default")
|
||||
{
|
||||
folder = GetDefaultFolder(speciesName);
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
public static T GetDefaultRagdollParams<T>(string speciesName) where T : RagdollParams, new() => GetRagdollParams<T>(speciesName, GetDefaultFileName(speciesName));
|
||||
|
||||
/// <summary>
|
||||
/// If the file name is left null, default file is selected. If fails, will select the default file. Note: Use the filename without the extensions, don't use the full path!
|
||||
/// If a custom folder is used, it's defined in the character info file.
|
||||
/// </summary>
|
||||
public static T GetRagdollParams<T>(string speciesName, string fileName = null) where T : RagdollParams, new()
|
||||
{
|
||||
if (!allRagdolls.TryGetValue(speciesName, out Dictionary<string, RagdollParams> ragdolls))
|
||||
{
|
||||
ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
}
|
||||
if (string.IsNullOrEmpty(fileName) || !ragdolls.TryGetValue(fileName, out RagdollParams ragdoll))
|
||||
{
|
||||
string selectedFile = null;
|
||||
string folder = GetFolder(speciesName);
|
||||
if (Directory.Exists(folder))
|
||||
{
|
||||
var files = Directory.GetFiles(folder);
|
||||
if (files.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"[RagdollParams] Could not find any ragdoll files from the folder: {folder}. Using the default ragdoll.");
|
||||
selectedFile = GetDefaultFile(speciesName);
|
||||
}
|
||||
else if (string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
// Files found, but none specified
|
||||
selectedFile = GetDefaultFile(speciesName);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectedFile = files.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant() == fileName.ToLowerInvariant());
|
||||
if (selectedFile == null)
|
||||
{
|
||||
DebugConsole.ThrowError($"[RagdollParams] Could not find a ragdoll file that matches the name {fileName}. Using the default ragdoll.");
|
||||
selectedFile = GetDefaultFile(speciesName);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[RagdollParams] Invalid directory: {folder}. Using the default ragdoll.");
|
||||
selectedFile = GetDefaultFile(speciesName);
|
||||
}
|
||||
if (selectedFile == null)
|
||||
{
|
||||
throw new Exception("[RagdollParams] Selected file null!");
|
||||
}
|
||||
DebugConsole.Log($"[RagdollParams] Loading ragdoll from {selectedFile}.");
|
||||
T r = new T();
|
||||
if (r.Load(selectedFile, speciesName))
|
||||
{
|
||||
if (!ragdolls.ContainsKey(r.Name))
|
||||
{
|
||||
ragdolls.Add(r.Name, r);
|
||||
}
|
||||
return r;
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugConsole.ThrowError($"[RagdollParams] Failed to load ragdoll {r} at {selectedFile} for the character {speciesName}. Creating a dummy file.");
|
||||
var defaultFile = GetDefaultFile(speciesName);
|
||||
if (File.Exists(defaultFile))
|
||||
{
|
||||
DebugConsole.ThrowError($"[RagdollParams] Renaming the invalid file as {selectedFile}.invalid");
|
||||
// Rename the old file so that it's not lost.
|
||||
File.Move(defaultFile, defaultFile + ".invalid");
|
||||
}
|
||||
return CreateDefault<T>(defaultFile, speciesName, dummyParams);
|
||||
}
|
||||
}
|
||||
return (T)ragdoll;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a default ragdoll for the species using a predefined configuration.
|
||||
/// Note: Use only to create ragdolls for new characters, because this overrides the old ragdoll!
|
||||
/// </summary>
|
||||
public static T CreateDefault<T>(string fullPath, string speciesName, params object[] ragdollConfig) where T : RagdollParams, new()
|
||||
{
|
||||
// Remove the old ragdolls, if found.
|
||||
if (allRagdolls.ContainsKey(speciesName))
|
||||
{
|
||||
DebugConsole.NewMessage($"[RagdollParams] Removing the old ragdolls from {speciesName}.", Color.Red);
|
||||
allRagdolls.Remove(speciesName);
|
||||
}
|
||||
var ragdolls = new Dictionary<string, RagdollParams>();
|
||||
allRagdolls.Add(speciesName, ragdolls);
|
||||
var instance = new T();
|
||||
XElement ragdollElement = new XElement("Ragdoll", ragdollConfig);
|
||||
instance.doc = new XDocument(ragdollElement);
|
||||
instance.UpdatePath(fullPath);
|
||||
instance.IsLoaded = instance.Deserialize(ragdollElement);
|
||||
instance.Save();
|
||||
instance.Load(fullPath, speciesName);
|
||||
ragdolls.Add(instance.Name, instance);
|
||||
DebugConsole.NewMessage("[RagdollParams] New default ragdoll params successfully created at " + fullPath, Color.NavajoWhite);
|
||||
return instance as T;
|
||||
}
|
||||
|
||||
protected override void UpdatePath(string fullPath)
|
||||
{
|
||||
if (SpeciesName == null)
|
||||
{
|
||||
base.UpdatePath(fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update the key by removing and re-adding the ragdoll.
|
||||
if (allRagdolls.TryGetValue(SpeciesName, out Dictionary<string, RagdollParams> ragdolls))
|
||||
{
|
||||
ragdolls.Remove(Name);
|
||||
}
|
||||
base.UpdatePath(fullPath);
|
||||
if (ragdolls != null)
|
||||
{
|
||||
if (!ragdolls.ContainsKey(Name))
|
||||
{
|
||||
ragdolls.Add(Name, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool Save(string fileNameWithoutExtension = null)
|
||||
{
|
||||
OriginalElement = MainElement;
|
||||
GetAllSubParams().ForEach(p => p.SetCurrentElementAsOriginalElement());
|
||||
Serialize();
|
||||
return base.Save(fileNameWithoutExtension, new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
OmitXmlDeclaration = true,
|
||||
NewLineOnAttributes = false
|
||||
});
|
||||
}
|
||||
|
||||
protected bool Load(string file, string speciesName)
|
||||
{
|
||||
if (Load(file))
|
||||
{
|
||||
SpeciesName = speciesName;
|
||||
CreateColliders();
|
||||
CreateLimbs();
|
||||
CreateJoints();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool Reset(bool forceReload = false)
|
||||
{
|
||||
if (forceReload)
|
||||
{
|
||||
return Load(FullPath, SpeciesName);
|
||||
}
|
||||
Deserialize(OriginalElement, recursive: true);
|
||||
GetAllSubParams().ForEach(sp => sp.Reset());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected void CreateColliders()
|
||||
{
|
||||
ColliderParams.Clear();
|
||||
for (int i = 0; i < MainElement.Elements("collider").Count(); i++)
|
||||
{
|
||||
var element = MainElement.Elements("collider").ElementAt(i);
|
||||
string name = i > 0 ? "Secondary Collider" : "Main Collider";
|
||||
ColliderParams.Add(new ColliderParams(element, this, name));
|
||||
}
|
||||
}
|
||||
|
||||
protected void CreateLimbs()
|
||||
{
|
||||
Limbs.Clear();
|
||||
foreach (var element in MainElement.Elements("limb"))
|
||||
{
|
||||
Limbs.Add(new LimbParams(element, this));
|
||||
}
|
||||
Limbs = Limbs.OrderBy(l => l.ID).ToList();
|
||||
}
|
||||
|
||||
protected void CreateJoints()
|
||||
{
|
||||
Joints.Clear();
|
||||
foreach (var element in MainElement.Elements("joint"))
|
||||
{
|
||||
Joints.Add(new JointParams(element, this));
|
||||
}
|
||||
}
|
||||
|
||||
protected bool Deserialize(XElement element = null, bool recursive = true)
|
||||
{
|
||||
if (base.Deserialize(element))
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
GetAllSubParams().ForEach(p => p.Deserialize());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected bool Serialize(XElement element = null, bool recursive = true)
|
||||
{
|
||||
if (base.Serialize(element))
|
||||
{
|
||||
if (recursive)
|
||||
{
|
||||
GetAllSubParams().ForEach(p => p.Serialize());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#region Memento
|
||||
public override void CreateSnapshot()
|
||||
{
|
||||
Serialize();
|
||||
var copy = new RagdollParams
|
||||
{
|
||||
IsLoaded = true,
|
||||
doc = new XDocument(doc)
|
||||
};
|
||||
copy.CreateColliders();
|
||||
copy.CreateLimbs();
|
||||
copy.CreateJoints();
|
||||
copy.Deserialize();
|
||||
copy.Serialize();
|
||||
memento.Store(copy);
|
||||
}
|
||||
public override void Undo() => RevertTo(memento.Undo() as RagdollParams);
|
||||
public override void Redo() => RevertTo(memento.Redo() as RagdollParams);
|
||||
|
||||
private void RevertTo(RagdollParams source)
|
||||
{
|
||||
Deserialize(source.MainElement, recursive: false);
|
||||
var sourceSubParams = source.GetAllSubParams().ToList();
|
||||
var subParams = GetAllSubParams().ToList();
|
||||
for (int i = 0; i < subParams.Count; i++)
|
||||
{
|
||||
subParams[i].Deserialize(sourceSubParams[i].Element, recursive: false);
|
||||
var subSubParams = subParams[i].SubParams;
|
||||
for (int j = 0; j < subSubParams.Count; j++)
|
||||
{
|
||||
subSubParams[j].Deserialize(sourceSubParams[i].SubParams[j].Element, recursive: false);
|
||||
// Since we cannot use recursion here, we have to go deeper manually, if necessary.
|
||||
}
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#if CLIENT
|
||||
public override void AddToEditor(ParamsEditor editor)
|
||||
{
|
||||
base.AddToEditor(editor);
|
||||
var subParams = GetAllSubParams();
|
||||
foreach (var subParam in subParams)
|
||||
{
|
||||
subParam.AddToEditor(editor);
|
||||
//TODO: divider sprite
|
||||
new GUIFrame(new RectTransform(new Point(editor.EditorBox.Rect.Width, 10), editor.EditorBox.Content.RectTransform),
|
||||
style: "ConnectionPanelWire");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
class JointParams : RagdollSubParams
|
||||
{
|
||||
public JointParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
|
||||
|
||||
private string name;
|
||||
[Serialize("", true), Editable]
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = GenerateName();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
set
|
||||
{
|
||||
name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GenerateName() => $"Joint {Limb1} - {Limb2}";
|
||||
|
||||
[Serialize(-1, true), Editable]
|
||||
public int Limb1 { get; set; }
|
||||
|
||||
[Serialize(-1, true), Editable]
|
||||
public int Limb2 { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should be converted to sim units.
|
||||
/// </summary>
|
||||
[Serialize("1.0, 1.0", true), Editable]
|
||||
public Vector2 Limb1Anchor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Should be converted to sim units.
|
||||
/// </summary>
|
||||
[Serialize("1.0, 1.0", true), Editable]
|
||||
public Vector2 Limb2Anchor { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool CanBeSevered { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool LimitEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(0f, true), Editable]
|
||||
public float UpperLimit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// In degrees.
|
||||
/// </summary>
|
||||
[Serialize(0f, true), Editable]
|
||||
public float LowerLimit { get; set; }
|
||||
|
||||
[Serialize(0.25f, true), Editable]
|
||||
public float Stiffness { get; set; }
|
||||
}
|
||||
|
||||
class LimbParams : RagdollSubParams
|
||||
{
|
||||
public LimbParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll)
|
||||
{
|
||||
var spriteElement = element.Element("sprite");
|
||||
if (spriteElement != null)
|
||||
{
|
||||
normalSpriteParams = new SpriteParams(spriteElement, ragdoll);
|
||||
SubParams.Add(normalSpriteParams);
|
||||
}
|
||||
var damagedElement = element.Element("damagedsprite");
|
||||
if (damagedElement != null)
|
||||
{
|
||||
damagedSpriteParams = new SpriteParams(damagedElement, ragdoll);
|
||||
// Hide the damaged sprite params in the editor for now.
|
||||
//SubParams.Add(damagedSpriteParams);
|
||||
}
|
||||
var deformElement = element.Element("deformablesprite");
|
||||
if (deformElement != null)
|
||||
{
|
||||
deformSpriteParams = new SpriteParams(deformElement, ragdoll);
|
||||
SubParams.Add(deformSpriteParams);
|
||||
}
|
||||
}
|
||||
|
||||
public readonly SpriteParams normalSpriteParams;
|
||||
public readonly SpriteParams damagedSpriteParams;
|
||||
public readonly SpriteParams deformSpriteParams;
|
||||
|
||||
private string name;
|
||||
[Serialize("", true), Editable]
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = GenerateName();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
set
|
||||
{
|
||||
name = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override string GenerateName() => $"Limb {ID}";
|
||||
|
||||
/// <summary>
|
||||
/// Note that editing this in-game doesn't currently have any effect (unless the ragdoll is recreated). It should be visible, but readonly in the editor.
|
||||
/// </summary>
|
||||
[Serialize(-1, true), Editable]
|
||||
public int ID { get; set; }
|
||||
|
||||
[Serialize(LimbType.None, true), Editable]
|
||||
public LimbType Type { get; set; }
|
||||
|
||||
[Serialize(true, true), Editable]
|
||||
public bool Flip { get; set; }
|
||||
|
||||
[Serialize(0, true), Editable]
|
||||
public int HealthIndex { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(ToolTip = "Higher values make AI characters prefer attacking this limb.")]
|
||||
public float AttackPriority { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 500)]
|
||||
public float SteerForce { get; set; }
|
||||
|
||||
[Serialize("0, 0", true), Editable(ToolTip = "Only applicable if this limb is a foot. Determines the \"neutral position\" of the foot relative to a joint determined by the \"RefJoint\" parameter. For example, a value of {-100, 0} would mean that the foot is positioned on the floor, 100 units behind the reference joint.")]
|
||||
public Vector2 StepOffset { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Radius { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Height { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Width { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 10000)]
|
||||
public float Mass { get; set; }
|
||||
|
||||
[Serialize(10f, true), Editable(MinValueFloat = 0, MaxValueFloat = 100)]
|
||||
public float Density { get; set; }
|
||||
|
||||
[Serialize("0, 0", true), Editable(ToolTip = "The position which is used to lead the IK chain to the IK goal. Only applicable if the limb is hand or foot.")]
|
||||
public Vector2 PullPos { get; set; }
|
||||
|
||||
[Serialize(-1, true), Editable(ToolTip = "Only applicable if this limb is a foot. Determines which joint is used as the \"neutral x-position\" for the foot movement. For example in the case of a humanoid-shaped characters this would usually be the waist. The position can be offset using the StepOffset parameter.")]
|
||||
public int RefJoint { get; set; }
|
||||
|
||||
[Serialize(false, true), Editable]
|
||||
public bool IgnoreCollisions { get; set; }
|
||||
|
||||
[Serialize("", true), Editable]
|
||||
public string Notes { get; set; }
|
||||
|
||||
// Non-editable ->
|
||||
[Serialize(0.3f, true)]
|
||||
public float Friction { get; set; }
|
||||
|
||||
[Serialize(0.05f, true)]
|
||||
public float Restitution { get; set; }
|
||||
}
|
||||
|
||||
class SpriteParams : RagdollSubParams
|
||||
{
|
||||
public SpriteParams(XElement element, RagdollParams ragdoll) : base(element, ragdoll) { }
|
||||
|
||||
[Serialize("0, 0, 0, 0", true), Editable]
|
||||
public Rectangle SourceRect { get; set; }
|
||||
|
||||
[Serialize("0.5, 0.5", true), Editable(DecimalCount = 2, ToolTip = "Relative to the collider.")]
|
||||
public Vector2 Origin { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(DecimalCount = 3)]
|
||||
public float Depth { get; set; }
|
||||
|
||||
[Serialize("", true)]
|
||||
public string Texture { get; set; }
|
||||
}
|
||||
|
||||
class ColliderParams : RagdollSubParams
|
||||
{
|
||||
public ColliderParams(XElement element, RagdollParams ragdoll, string name = null) : base(element, ragdoll)
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
|
||||
private string name;
|
||||
[Serialize("", true), Editable]
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
name = GenerateName();
|
||||
}
|
||||
return name;
|
||||
}
|
||||
set
|
||||
{
|
||||
name = value;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Radius { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Height { get; set; }
|
||||
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0, MaxValueFloat = 1000)]
|
||||
public float Width { get; set; }
|
||||
}
|
||||
|
||||
abstract class RagdollSubParams : ISerializableEntity
|
||||
{
|
||||
public virtual string Name { get; set; }
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties { get; private set; }
|
||||
public XElement Element { get; set; }
|
||||
public XElement OriginalElement { get; protected set; }
|
||||
public List<RagdollSubParams> SubParams { get; set; } = new List<RagdollSubParams>();
|
||||
public RagdollParams Ragdoll { get; private set; }
|
||||
|
||||
public virtual string GenerateName() => Element.Name.ToString();
|
||||
|
||||
public RagdollSubParams(XElement element, RagdollParams ragdoll)
|
||||
{
|
||||
Element = element;
|
||||
OriginalElement = new XElement(element);
|
||||
Ragdoll = ragdoll;
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
}
|
||||
|
||||
public virtual bool Deserialize(XElement element = null, bool recursive = true)
|
||||
{
|
||||
element = element ?? Element;
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, element);
|
||||
if (recursive)
|
||||
{
|
||||
SubParams.ForEach(sp => sp.Deserialize());
|
||||
}
|
||||
return SerializableProperties != null;
|
||||
}
|
||||
|
||||
public virtual bool Serialize(XElement element = null, bool recursive = true)
|
||||
{
|
||||
element = element ?? Element;
|
||||
SerializableProperty.SerializeProperties(this, element, true);
|
||||
if (recursive)
|
||||
{
|
||||
SubParams.ForEach(sp => sp.Serialize());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void SetCurrentElementAsOriginalElement()
|
||||
{
|
||||
OriginalElement = Element;
|
||||
SubParams.ForEach(sp => sp.SetCurrentElementAsOriginalElement());
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Deserialize(OriginalElement, false);
|
||||
SubParams.ForEach(sp => sp.Reset());
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
public SerializableEntityEditor SerializableEntityEditor { get; protected set; }
|
||||
public virtual void AddToEditor(ParamsEditor editor)
|
||||
{
|
||||
SerializableEntityEditor = new SerializableEntityEditor(editor.EditorBox.Content.RectTransform, this, false, true);
|
||||
SubParams.ForEach(sp => sp.AddToEditor(editor));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,109 +1,210 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
enum CauseOfDeath
|
||||
{
|
||||
Damage, Bloodloss, Pressure, Suffocation, Drowning, Burn, Husk, Disconnected
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum DamageType
|
||||
{
|
||||
None = 0,
|
||||
Blunt = 1,
|
||||
Slash = 2,
|
||||
Burn = 4,
|
||||
Any = Blunt | Slash | Burn
|
||||
}
|
||||
|
||||
{
|
||||
public enum HitDetection
|
||||
{
|
||||
Distance,
|
||||
Contact
|
||||
}
|
||||
|
||||
public enum AttackContext
|
||||
{
|
||||
NotDefined,
|
||||
Water,
|
||||
Ground
|
||||
}
|
||||
|
||||
public enum AttackTarget
|
||||
{
|
||||
Any,
|
||||
Character,
|
||||
Structure // Including hulls etc. Evaluated as anything but a character.
|
||||
}
|
||||
|
||||
public enum AIBehaviorAfterAttack
|
||||
{
|
||||
FallBack,
|
||||
PursueIfCanAttack,
|
||||
Pursue
|
||||
}
|
||||
|
||||
struct AttackResult
|
||||
{
|
||||
public readonly float Damage;
|
||||
public readonly float Bleeding;
|
||||
public readonly List<Affliction> Afflictions;
|
||||
|
||||
public readonly Limb HitLimb;
|
||||
|
||||
public readonly List<DamageModifier> AppliedDamageModifiers;
|
||||
|
||||
public AttackResult(float damage, float bleeding, List<DamageModifier> appliedDamageModifiers = null)
|
||||
|
||||
public AttackResult(List<Affliction> afflictions, Limb hitLimb, List<DamageModifier> appliedDamageModifiers = null)
|
||||
{
|
||||
this.Damage = damage;
|
||||
this.Bleeding = bleeding;
|
||||
HitLimb = hitLimb;
|
||||
Afflictions = new List<Affliction>();
|
||||
|
||||
this.AppliedDamageModifiers = appliedDamageModifiers;
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
Afflictions.Add(affliction.Prefab.Instantiate(affliction.Strength, affliction.Source));
|
||||
}
|
||||
AppliedDamageModifiers = appliedDamageModifiers;
|
||||
Damage = Afflictions.Sum(a => a.GetVitalityDecrease(null));
|
||||
}
|
||||
|
||||
public AttackResult(float damage, List<DamageModifier> appliedDamageModifiers = null)
|
||||
{
|
||||
Damage = damage;
|
||||
HitLimb = null;
|
||||
|
||||
Afflictions = null;
|
||||
|
||||
AppliedDamageModifiers = appliedDamageModifiers;
|
||||
}
|
||||
}
|
||||
|
||||
partial class Attack
|
||||
partial class Attack : ISerializableEntity
|
||||
{
|
||||
[Serialize(HitDetection.Distance, false)]
|
||||
public readonly XElement SourceElement;
|
||||
|
||||
[Serialize(AttackContext.NotDefined, true), Editable]
|
||||
public AttackContext Context { get; private set; }
|
||||
|
||||
[Serialize(AttackTarget.Any, true), Editable]
|
||||
public AttackTarget TargetType { get; private set; }
|
||||
|
||||
[Serialize(HitDetection.Distance, true), Editable]
|
||||
public HitDetection HitDetectionType { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(AIBehaviorAfterAttack.FallBack, true), Editable(ToolTip = "The preferred AI behavior after the attack.")]
|
||||
public AIBehaviorAfterAttack AfterAttack { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "Min distance from the attack limb to the target before the AI tries to attack.")]
|
||||
public float Range { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 2000.0f, ToolTip = "Min distance from the attack limb to the target to do damage. In distance based hit detection, the hit will be registered as soon as the target is within the damage range, unless the attack duration has expired.")]
|
||||
public float DamageRange { get; set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0.25f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10.0f, DecimalCount = 2, ToolTip = "An approximation of the attack duration. Effectively defines the time window in which the hit can be registered. If set to too low value, it's possible that the attack won't hit the target in time.")]
|
||||
public float Duration { get; private set; }
|
||||
|
||||
[Serialize(DamageType.None, false)]
|
||||
public DamageType DamageType { get; private set; }
|
||||
[Serialize(5f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "How long the AI waits between the attacks.")]
|
||||
public float CoolDown { get; private set; } = 5;
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 100.0f, DecimalCount = 2, ToolTip = "Used as the attack cooldown between different kind of attacks. Does not have effect, if set to 0.")]
|
||||
public float SecondaryCoolDown { get; private set; } = 0;
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 10000.0f)]
|
||||
public float StructureDamage { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
public float Damage { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
public float BleedingDamage { get; private set; }
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f)]
|
||||
public float ItemDamage { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Legacy support. Use Afflictions.
|
||||
/// </summary>
|
||||
[Serialize(0.0f, false)]
|
||||
public float Stun { get; private set; }
|
||||
|
||||
[Serialize(false, false)]
|
||||
[Serialize(false, true), Editable]
|
||||
public bool OnlyHumans { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize("", true), Editable]
|
||||
public string ApplyForceOnLimbs
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Join(", ", ForceOnLimbIndices);
|
||||
}
|
||||
set
|
||||
{
|
||||
ForceOnLimbIndices.Clear();
|
||||
if (string.IsNullOrEmpty(value)) { return; }
|
||||
foreach (string limbIndexStr in value.Split(','))
|
||||
{
|
||||
if (int.TryParse(limbIndexStr.Trim(), out int limbIndex))
|
||||
{
|
||||
ForceOnLimbIndices.Add(limbIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb (or limbs defined using ApplyForceOnLimbs). The direction of the force is towards the target that's being attacked.")]
|
||||
public float Force { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the attacking limb.")]
|
||||
public float Torque { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(false, true), Editable]
|
||||
public bool ApplyForcesOnlyOnce { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = -1000.0f, MaxValueFloat = 1000.0f, ToolTip = "Applied to the target the attack hits. The direction of the impulse is from this limb towards the target (use negative values to pull the target closer).")]
|
||||
public float TargetImpulse { get; private set; }
|
||||
|
||||
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
|
||||
public Vector2 TargetImpulseWorld { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(-1000.0f, 1000.0f, ToolTip = "Applied to the target the attack hits. The direction of the force is from this limb towards the target (use negative values to pull the target closer).")]
|
||||
public float TargetForce { get; private set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize("0.0, 0.0", true), Editable(ToolTip = "Applied to the target, in world space coordinates(i.e. 0, -1 pushes the target downwards).")]
|
||||
public Vector2 TargetForceWorld { get; private set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float SeverLimbsProbability { get; set; }
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float StickChance { get; set; }
|
||||
|
||||
[Serialize(0.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float Priority { get; private set; }
|
||||
|
||||
public IEnumerable<StatusEffect> StatusEffects
|
||||
{
|
||||
get { return statusEffects; }
|
||||
}
|
||||
|
||||
public string Name => "Attack";
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = new Dictionary<string, SerializableProperty>();
|
||||
|
||||
//the indices of the limbs Force is applied on
|
||||
//(if none, force is applied only to the limb the attack is attached to)
|
||||
public readonly List<int> ApplyForceOnLimbs;
|
||||
|
||||
public readonly List<int> ForceOnLimbIndices = new List<int>();
|
||||
|
||||
public readonly List<Affliction> Afflictions = new List<Affliction>();
|
||||
|
||||
/// <summary>
|
||||
/// Only affects ai decision making.
|
||||
/// </summary>
|
||||
public List<PropertyConditional> Conditionals { get; private set; } = new List<PropertyConditional>();
|
||||
|
||||
private readonly List<StatusEffect> statusEffects;
|
||||
|
||||
public float GetDamage(float deltaTime)
|
||||
public void SetUser(Character user)
|
||||
{
|
||||
return (Duration == 0.0f) ? Damage : Damage * deltaTime;
|
||||
if (statusEffects == null) { return; }
|
||||
foreach (StatusEffect statusEffect in statusEffects)
|
||||
{
|
||||
statusEffect.SetUser(user);
|
||||
}
|
||||
}
|
||||
|
||||
public float GetBleedingDamage(float deltaTime)
|
||||
|
||||
public List<Affliction> GetMultipliedAfflictions(float multiplier)
|
||||
{
|
||||
return (Duration == 0.0f) ? BleedingDamage : BleedingDamage * deltaTime;
|
||||
List<Affliction> multipliedAfflictions = new List<Affliction>();
|
||||
foreach (Affliction affliction in Afflictions)
|
||||
{
|
||||
multipliedAfflictions.Add(affliction.Prefab.Instantiate(affliction.Strength * multiplier, affliction.Source));
|
||||
}
|
||||
return multipliedAfflictions;
|
||||
}
|
||||
|
||||
public float GetStructureDamage(float deltaTime)
|
||||
@@ -111,36 +212,48 @@ namespace Barotrauma
|
||||
return (Duration == 0.0f) ? StructureDamage : StructureDamage * deltaTime;
|
||||
}
|
||||
|
||||
public Attack(float damage, float structureDamage, float bleedingDamage, float range = 0.0f)
|
||||
public float GetItemDamage(float deltaTime)
|
||||
{
|
||||
Range = range;
|
||||
DamageRange = range;
|
||||
this.Damage = damage;
|
||||
this.StructureDamage = structureDamage;
|
||||
this.BleedingDamage = bleedingDamage;
|
||||
return (Duration == 0.0f) ? ItemDamage : ItemDamage * deltaTime;
|
||||
}
|
||||
|
||||
public Attack(XElement element)
|
||||
public float GetTotalDamage(bool includeStructureDamage = false)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
DamageRange = element.GetAttributeFloat("damagerange", Range);
|
||||
|
||||
InitProjSpecific(element);
|
||||
|
||||
string limbIndicesStr = element.GetAttributeString("applyforceonlimbs", "");
|
||||
if (!string.IsNullOrWhiteSpace(limbIndicesStr))
|
||||
float totalDamage = includeStructureDamage ? StructureDamage : 0.0f;
|
||||
foreach (Affliction affliction in Afflictions)
|
||||
{
|
||||
ApplyForceOnLimbs = new List<int>();
|
||||
foreach (string limbIndexStr in limbIndicesStr.Split(','))
|
||||
{
|
||||
int limbIndex;
|
||||
if (int.TryParse(limbIndexStr, out limbIndex))
|
||||
{
|
||||
ApplyForceOnLimbs.Add(limbIndex);
|
||||
}
|
||||
}
|
||||
totalDamage += affliction.GetVitalityDecrease(null);
|
||||
}
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
public Attack(float damage, float bleedingDamage, float burnDamage, float structureDamage, float range = 0.0f)
|
||||
{
|
||||
if (damage > 0.0f) Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage));
|
||||
if (bleedingDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
|
||||
if (burnDamage > 0.0f) Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
|
||||
|
||||
Range = range;
|
||||
DamageRange = range;
|
||||
StructureDamage = structureDamage;
|
||||
}
|
||||
|
||||
public Attack(XElement element, string parentDebugName)
|
||||
{
|
||||
SourceElement = element;
|
||||
Deserialize();
|
||||
|
||||
if (element.Attribute("damage") != null ||
|
||||
element.Attribute("bluntdamage") != null ||
|
||||
element.Attribute("burndamage") != null ||
|
||||
element.Attribute("bleedingdamage") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Define damage as afflictions instead of using the damage attribute (e.g. <Affliction identifier=\"internaldamage\" strength=\"10\" />).");
|
||||
}
|
||||
|
||||
DamageRange = element.GetAttributeFloat("damagerange", 0f);
|
||||
|
||||
InitProjSpecific(element);
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
@@ -151,13 +264,56 @@ namespace Barotrauma
|
||||
{
|
||||
statusEffects = new List<StatusEffect>();
|
||||
}
|
||||
statusEffects.Add(StatusEffect.Load(subElement));
|
||||
statusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
|
||||
break;
|
||||
case "affliction":
|
||||
AfflictionPrefab afflictionPrefab;
|
||||
if (subElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - define afflictions using identifiers instead of names.");
|
||||
string afflictionName = subElement.GetAttributeString("name", "").ToLowerInvariant();
|
||||
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Name.ToLowerInvariant() == afflictionName);
|
||||
if (afflictionPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionName + "\" not found.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string afflictionIdentifier = subElement.GetAttributeString("identifier", "").ToLowerInvariant();
|
||||
afflictionPrefab = AfflictionPrefab.List.Find(ap => ap.Identifier.ToLowerInvariant() == afflictionIdentifier);
|
||||
if (afflictionPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in Attack (" + parentDebugName + ") - Affliction prefab \"" + afflictionIdentifier + "\" not found.");
|
||||
}
|
||||
}
|
||||
|
||||
float afflictionStrength = subElement.GetAttributeFloat(1.0f, "amount", "strength");
|
||||
Afflictions.Add(afflictionPrefab.Instantiate(afflictionStrength));
|
||||
break;
|
||||
case "conditional":
|
||||
foreach (XAttribute attribute in subElement.Attributes())
|
||||
{
|
||||
Conditionals.Add(new PropertyConditional(attribute));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public void Serialize()
|
||||
{
|
||||
if (SourceElement == null) { return; }
|
||||
SerializableProperty.SerializeProperties(this, SourceElement, true);
|
||||
}
|
||||
|
||||
public void Deserialize()
|
||||
{
|
||||
if (SourceElement == null) { return; }
|
||||
SerializableProperties = SerializableProperty.DeserializeProperties(this, SourceElement);
|
||||
}
|
||||
|
||||
public AttackResult DoDamage(Character attacker, IDamageable target, Vector2 worldPosition, float deltaTime, bool playSound = true)
|
||||
{
|
||||
@@ -167,6 +323,8 @@ namespace Barotrauma
|
||||
if (targetCharacter != null && targetCharacter.ConfigPath != Character.HumanConfigFile) return new AttackResult();
|
||||
}
|
||||
|
||||
SetUser(attacker);
|
||||
|
||||
DamageParticles(deltaTime, worldPosition);
|
||||
|
||||
var attackResult = target.AddDamage(attacker, worldPosition, this, deltaTime, playSound);
|
||||
@@ -179,13 +337,24 @@ namespace Barotrauma
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
if (effect.Targets.HasFlag(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (effect.Targets.HasFlag(StatusEffect.TargetType.Character) && target is Character)
|
||||
if (target is Character)
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, (Character)target, (Character)target);
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, (Character)target, (Character)target);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, (Character)target, attackResult.HitLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, (Character)target, ((Character)target).AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +370,8 @@ namespace Barotrauma
|
||||
if (targetLimb.character != null && targetLimb.character.ConfigPath != Character.HumanConfigFile) return new AttackResult();
|
||||
}
|
||||
|
||||
SetUser(attacker);
|
||||
|
||||
DamageParticles(deltaTime, worldPosition);
|
||||
|
||||
var attackResult = targetLimb.character.ApplyAttack(attacker, worldPosition, this, deltaTime, playSound, targetLimb);
|
||||
@@ -209,19 +380,88 @@ namespace Barotrauma
|
||||
|
||||
foreach (StatusEffect effect in statusEffects)
|
||||
{
|
||||
if (effect.Targets.HasFlag(StatusEffect.TargetType.This))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.This))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, attacker, attacker);
|
||||
}
|
||||
if (effect.Targets.HasFlag(StatusEffect.TargetType.Character))
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb);
|
||||
}
|
||||
if (effect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
effect.Apply(effectType, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
public float AttackTimer { get; private set; }
|
||||
public float CoolDownTimer { get; set; }
|
||||
public float SecondaryCoolDownTimer { get; set; }
|
||||
public bool IsRunning { get; private set; }
|
||||
|
||||
public void UpdateCoolDown(float deltaTime)
|
||||
{
|
||||
CoolDownTimer -= deltaTime;
|
||||
SecondaryCoolDownTimer -= deltaTime;
|
||||
if (CoolDownTimer < 0) { CoolDownTimer = 0; }
|
||||
if (SecondaryCoolDownTimer < 0) { SecondaryCoolDownTimer = 0; }
|
||||
}
|
||||
|
||||
public void UpdateAttackTimer(float deltaTime)
|
||||
{
|
||||
IsRunning = true;
|
||||
AttackTimer += deltaTime;
|
||||
if (AttackTimer >= Duration)
|
||||
{
|
||||
ResetAttackTimer();
|
||||
SetCoolDown();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResetAttackTimer()
|
||||
{
|
||||
AttackTimer = 0;
|
||||
IsRunning = false;
|
||||
}
|
||||
|
||||
public void SetCoolDown()
|
||||
{
|
||||
CoolDownTimer = CoolDown;
|
||||
SecondaryCoolDownTimer = SecondaryCoolDown;
|
||||
}
|
||||
|
||||
public void ResetCoolDown()
|
||||
{
|
||||
CoolDownTimer = 0;
|
||||
SecondaryCoolDownTimer = 0;
|
||||
}
|
||||
|
||||
partial void DamageParticles(float deltaTime, Vector2 worldPosition);
|
||||
|
||||
public bool IsValidContext(AttackContext context) => Context == context || Context == AttackContext.NotDefined;
|
||||
|
||||
public bool IsValidTarget(AttackTarget targetType) => TargetType == AttackTarget.Any || TargetType == targetType;
|
||||
|
||||
public bool IsValidTarget(Entity target)
|
||||
{
|
||||
switch (TargetType)
|
||||
{
|
||||
case AttackTarget.Character:
|
||||
return target is Character;
|
||||
case AttackTarget.Structure:
|
||||
return !(target is Character);
|
||||
case AttackTarget.Any:
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
enum CauseOfDeathType
|
||||
{
|
||||
Unknown, Pressure, Suffocation, Drowning, Affliction, Disconnected
|
||||
}
|
||||
|
||||
class CauseOfDeath
|
||||
{
|
||||
public readonly CauseOfDeathType Type;
|
||||
public readonly AfflictionPrefab Affliction;
|
||||
public readonly Character Killer;
|
||||
public readonly Entity DamageSource;
|
||||
|
||||
public CauseOfDeath(CauseOfDeathType type, AfflictionPrefab affliction, Character killer, Entity damageSource)
|
||||
{
|
||||
if (type == CauseOfDeathType.Affliction && affliction == null)
|
||||
{
|
||||
string errorMsg = "Invalid cause of death (the type of the cause of death was Affliction, but affliction was not specified).\n" + Environment.StackTrace;
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("InvalidCauseOfDeath", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
type = CauseOfDeathType.Unknown;
|
||||
}
|
||||
|
||||
Type = type;
|
||||
Affliction = affliction;
|
||||
Killer = killer;
|
||||
DamageSource = damageSource;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,8 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Lidgren.Network;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
@@ -7,10 +11,15 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public enum Gender { None, Male, Female };
|
||||
|
||||
public enum Gender { None, Male, Female };
|
||||
public enum Race { None, White, Black, Asian };
|
||||
|
||||
partial class CharacterInfo
|
||||
{
|
||||
private static Dictionary<string, XDocument> cachedConfigs = new Dictionary<string, XDocument>();
|
||||
|
||||
private static ushort idCounter;
|
||||
|
||||
public string Name;
|
||||
public string DisplayName
|
||||
{
|
||||
@@ -49,24 +58,107 @@ namespace Barotrauma
|
||||
return disguiseName;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Note: Can be null.
|
||||
/// </summary>
|
||||
public Character Character;
|
||||
|
||||
public readonly string File;
|
||||
|
||||
public Job Job;
|
||||
|
||||
private List<ushort> pickedItems;
|
||||
|
||||
public ushort? HullID = null;
|
||||
|
||||
private Vector2[] headSpriteRange;
|
||||
|
||||
private Gender gender;
|
||||
|
||||
|
||||
public int Salary;
|
||||
|
||||
private int headSpriteId;
|
||||
private Vector2 headSpriteRange;
|
||||
|
||||
private Sprite headSprite;
|
||||
public Sprite HeadSprite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (headSprite == null)
|
||||
{
|
||||
LoadHeadSprite();
|
||||
}
|
||||
return headSprite;
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite portrait;
|
||||
public Sprite Portrait
|
||||
{
|
||||
get
|
||||
{
|
||||
if (portrait == null)
|
||||
{
|
||||
LoadHeadSprite();
|
||||
}
|
||||
return portrait;
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite clothingSprite;
|
||||
public Sprite ClothingSprite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (clothingSprite == null)
|
||||
{
|
||||
if (Job != null && Job.Prefab.ClothingElement != null)
|
||||
{
|
||||
clothingSprite = new Sprite(Job.Prefab.ClothingElement.Element("sprite"));
|
||||
}
|
||||
}
|
||||
return clothingSprite;
|
||||
}
|
||||
}
|
||||
|
||||
private Sprite portraitBackground;
|
||||
public Sprite PortraitBackground
|
||||
{
|
||||
get
|
||||
{
|
||||
if (portraitBackground == null)
|
||||
{
|
||||
var portraitBackgroundElement = SourceElement.Element("portraitbackground");
|
||||
if (portraitBackgroundElement != null)
|
||||
{
|
||||
portraitBackground = new Sprite(portraitBackgroundElement.Element("sprite"));
|
||||
}
|
||||
}
|
||||
return portraitBackground;
|
||||
}
|
||||
}
|
||||
|
||||
private List<WearableSprite> attachmentSprites;
|
||||
public List<WearableSprite> AttachmentsSprites
|
||||
{
|
||||
get
|
||||
{
|
||||
if (attachmentSprites == null)
|
||||
{
|
||||
LoadAttachmentSprites();
|
||||
}
|
||||
return attachmentSprites;
|
||||
}
|
||||
}
|
||||
|
||||
public XElement SourceElement { get; set; }
|
||||
|
||||
public XElement HairElement { get; private set; }
|
||||
public XElement BeardElement { get; private set; }
|
||||
public XElement MoustacheElement { get; private set; }
|
||||
public XElement FaceAttachment { get; private set; }
|
||||
|
||||
public int HairIndex { get; set; } = -1;
|
||||
public int BeardIndex { get; set; } = -1;
|
||||
public int MoustacheIndex { get; set; } = -1;
|
||||
public int FaceAttachmentIndex { get; set; } = -1;
|
||||
|
||||
public bool IsAttachmentsLoaded => HairIndex > -1 && BeardIndex > -1 && MoustacheIndex > -1 && FaceAttachmentIndex > -1;
|
||||
|
||||
public readonly string ragdollFileName = string.Empty;
|
||||
|
||||
public bool StartItemsGiven;
|
||||
|
||||
@@ -74,19 +166,14 @@ namespace Barotrauma
|
||||
|
||||
public byte TeamID;
|
||||
|
||||
public List<ushort> PickedItemIDs
|
||||
{
|
||||
get { return pickedItems; }
|
||||
}
|
||||
|
||||
public Sprite HeadSprite
|
||||
{
|
||||
get
|
||||
{
|
||||
if (headSprite == null) LoadHeadSprite();
|
||||
return headSprite;
|
||||
}
|
||||
}
|
||||
private NPCPersonalityTrait personalityTrait;
|
||||
|
||||
//unique ID given to character infos in MP
|
||||
//used by clients to identify which infos are the same to prevent duplicate characters in round summary
|
||||
public ushort ID;
|
||||
|
||||
public XElement InventoryData;
|
||||
|
||||
|
||||
public List<string> SpriteTags
|
||||
{
|
||||
@@ -94,6 +181,12 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
public NPCPersonalityTrait PersonalityTrait
|
||||
{
|
||||
get { return personalityTrait; }
|
||||
}
|
||||
|
||||
private int headSpriteId;
|
||||
public int HeadSpriteId
|
||||
{
|
||||
get { return headSpriteId; }
|
||||
@@ -102,15 +195,21 @@ namespace Barotrauma
|
||||
int oldId = headSpriteId;
|
||||
|
||||
headSpriteId = value;
|
||||
Vector2 spriteRange = headSpriteRange[gender == Gender.Male ? 0 : 1];
|
||||
Vector2 spriteRange = headSpriteRange;
|
||||
|
||||
if (headSpriteId < (int)spriteRange.X) headSpriteId = (int)(spriteRange.Y);
|
||||
if (headSpriteId > (int)spriteRange.Y) headSpriteId = (int)(spriteRange.X);
|
||||
|
||||
if (headSpriteId != oldId) headSprite = null;
|
||||
if (headSpriteId != oldId)
|
||||
{
|
||||
headSprite = null;
|
||||
attachmentSprites = null;
|
||||
ResetHeadAttachments();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Gender gender;
|
||||
public Gender Gender
|
||||
{
|
||||
get { return gender; }
|
||||
@@ -118,101 +217,151 @@ namespace Barotrauma
|
||||
{
|
||||
if (gender == value) return;
|
||||
gender = value;
|
||||
|
||||
int genderIndex = (this.gender == Gender.Female) ? 1 : 0;
|
||||
if (headSpriteRange[genderIndex] != Vector2.Zero)
|
||||
{
|
||||
HeadSpriteId = Rand.Range((int)headSpriteRange[genderIndex].X, (int)headSpriteRange[genderIndex].Y + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
HeadSpriteId = 0;
|
||||
}
|
||||
|
||||
LoadHeadSprite();
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null)
|
||||
{
|
||||
this.File = file;
|
||||
|
||||
headSpriteRange = new Vector2[2];
|
||||
|
||||
pickedItems = new List<ushort>();
|
||||
|
||||
SpriteTags = new List<string>();
|
||||
|
||||
//ID = -1;
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file);
|
||||
if (doc == null) return;
|
||||
|
||||
if (doc.Root.GetAttributeBool("genders", false))
|
||||
{
|
||||
if (gender == Gender.None)
|
||||
{
|
||||
float femaleRatio = doc.Root.GetAttributeFloat("femaleratio", 0.5f);
|
||||
this.gender = (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < femaleRatio) ? Gender.Female : Gender.Male;
|
||||
}
|
||||
else
|
||||
{
|
||||
this.gender = gender;
|
||||
Gender = Gender.Male;
|
||||
//SetRandomGender();
|
||||
}
|
||||
CalculateHeadSpriteRange();
|
||||
ResetHeadAttachments();
|
||||
headSprite = null;
|
||||
attachmentSprites = null;
|
||||
//SetRandomHead();
|
||||
//LoadHeadSprite();
|
||||
}
|
||||
|
||||
headSpriteRange[0] = doc.Root.GetAttributeVector2("headid", Vector2.Zero);
|
||||
headSpriteRange[1] = headSpriteRange[0];
|
||||
if (headSpriteRange[0] == Vector2.Zero)
|
||||
{
|
||||
headSpriteRange[0] = doc.Root.GetAttributeVector2("maleheadid", Vector2.Zero);
|
||||
headSpriteRange[1] = doc.Root.GetAttributeVector2("femaleheadid", Vector2.Zero);
|
||||
}
|
||||
|
||||
int genderIndex = (this.gender == Gender.Female) ? 1 : 0;
|
||||
if (headSpriteRange[genderIndex] != Vector2.Zero)
|
||||
{
|
||||
HeadSpriteId = Rand.Range((int)headSpriteRange[genderIndex].X, (int)headSpriteRange[genderIndex].Y + 1);
|
||||
}
|
||||
|
||||
this.Job = (jobPrefab == null) ? Job.Random() : new Job(jobPrefab);
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
this.Name = name;
|
||||
return;
|
||||
}
|
||||
|
||||
name = "";
|
||||
|
||||
if (doc.Root.Element("name") != null)
|
||||
{
|
||||
string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", "");
|
||||
if (firstNamePath != "")
|
||||
{
|
||||
firstNamePath = firstNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
|
||||
this.Name = ToolBox.GetRandomLine(firstNamePath);
|
||||
}
|
||||
|
||||
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
|
||||
if (lastNamePath != "")
|
||||
{
|
||||
lastNamePath = lastNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
|
||||
if (this.Name != "") this.Name += " ";
|
||||
this.Name += ToolBox.GetRandomLine(lastNamePath);
|
||||
}
|
||||
}
|
||||
|
||||
Salary = CalculateSalary();
|
||||
}
|
||||
|
||||
private void LoadHeadSprite()
|
||||
private Race race;
|
||||
public Race Race
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(File);
|
||||
if (doc == null) return;
|
||||
get { return race; }
|
||||
set
|
||||
{
|
||||
if (race == value) { return; }
|
||||
race = value;
|
||||
if (race == Race.None)
|
||||
{
|
||||
race = Race.White;
|
||||
//SetRandomRace();
|
||||
}
|
||||
CalculateHeadSpriteRange();
|
||||
ResetHeadAttachments();
|
||||
headSprite = null;
|
||||
attachmentSprites = null;
|
||||
//SetRandomHead();
|
||||
//LoadHeadSprite();
|
||||
}
|
||||
}
|
||||
|
||||
XElement ragdollElement = doc.Root.Element("ragdoll");
|
||||
foreach (XElement limbElement in ragdollElement.Elements())
|
||||
private RagdollParams ragdoll;
|
||||
public RagdollParams Ragdoll
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ragdoll == null)
|
||||
{
|
||||
string speciesName = SourceElement.GetAttributeString("name", string.Empty);
|
||||
bool isHumanoid = SourceElement.GetAttributeBool("humanoid", false);
|
||||
ragdoll = isHumanoid
|
||||
? HumanRagdollParams.GetRagdollParams(speciesName, ragdollFileName)
|
||||
: RagdollParams.GetRagdollParams<FishRagdollParams>(speciesName, ragdollFileName) as RagdollParams;
|
||||
}
|
||||
return ragdoll;
|
||||
}
|
||||
set { ragdoll = value; }
|
||||
}
|
||||
|
||||
// Used for creating the data
|
||||
public CharacterInfo(string file, string name = "", Gender gender = Gender.None, JobPrefab jobPrefab = null, string ragdollFileName = null)
|
||||
{
|
||||
ID = idCounter;
|
||||
idCounter++;
|
||||
File = file;
|
||||
SpriteTags = new List<string>();
|
||||
XDocument doc = GetConfig(file);
|
||||
SourceElement = doc.Root;
|
||||
if (doc.Root.GetAttributeBool("genders", false))
|
||||
{
|
||||
this.gender = gender == Gender.None ? SetRandomGender() : gender;
|
||||
}
|
||||
Enum.TryParse(doc.Root.GetAttributeString("race", "None"), true, out race);
|
||||
if (race == Race.None)
|
||||
{
|
||||
SetRandomRace();
|
||||
}
|
||||
CalculateHeadSpriteRange();
|
||||
SetRandomHeadID();
|
||||
Job = (jobPrefab == null) ? Job.Random(Rand.RandSync.Server) : new Job(jobPrefab);
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
Name = name;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = "";
|
||||
if (doc.Root.Element("name") != null)
|
||||
{
|
||||
string firstNamePath = doc.Root.Element("name").GetAttributeString("firstname", "");
|
||||
if (firstNamePath != "")
|
||||
{
|
||||
firstNamePath = firstNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male");
|
||||
Name = ToolBox.GetRandomLine(firstNamePath);
|
||||
}
|
||||
|
||||
string lastNamePath = doc.Root.Element("name").GetAttributeString("lastname", "");
|
||||
if (lastNamePath != "")
|
||||
{
|
||||
lastNamePath = lastNamePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "female" : "male");
|
||||
if (Name != "") Name += " ";
|
||||
Name += ToolBox.GetRandomLine(lastNamePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
personalityTrait = NPCPersonalityTrait.GetRandom(name + HeadSpriteId);
|
||||
Salary = CalculateSalary();
|
||||
if (ragdollFileName != null)
|
||||
{
|
||||
this.ragdollFileName = ragdollFileName;
|
||||
}
|
||||
LoadHeadAttachments();
|
||||
}
|
||||
|
||||
// Used for loading the data
|
||||
public CharacterInfo(XElement element)
|
||||
{
|
||||
ID = idCounter;
|
||||
idCounter++;
|
||||
Name = element.GetAttributeString("name", "unnamed");
|
||||
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
|
||||
gender = (genderStr == "male") ? Gender.Male : Gender.Female;
|
||||
Enum.TryParse(element.GetAttributeString("race", "white"), true, out race);
|
||||
File = element.GetAttributeString("file", "");
|
||||
SourceElement = GetConfig(File).Root;
|
||||
Salary = element.GetAttributeInt("salary", 1000);
|
||||
headSpriteId = element.GetAttributeInt("headspriteid", 1);
|
||||
HairIndex = element.GetAttributeInt("hairindex", -1);
|
||||
BeardIndex = element.GetAttributeInt("beardindex", -1);
|
||||
MoustacheIndex = element.GetAttributeInt("moustacheindex", -1);
|
||||
FaceAttachmentIndex = element.GetAttributeInt("faceattachmentindex", -1);
|
||||
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
|
||||
string personalityName = element.GetAttributeString("personality", "");
|
||||
ragdollFileName = element.GetAttributeString("ragdoll", string.Empty);
|
||||
if (!string.IsNullOrEmpty(personalityName))
|
||||
{
|
||||
personalityTrait = NPCPersonalityTrait.List.Find(p => p.Name == personalityName);
|
||||
}
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
|
||||
Job = new Job(subElement);
|
||||
break;
|
||||
}
|
||||
LoadHeadAttachments();
|
||||
}
|
||||
|
||||
public void LoadHeadSprite()
|
||||
{
|
||||
foreach (XElement limbElement in Ragdoll.MainElement.Elements())
|
||||
{
|
||||
if (limbElement.GetAttributeString("type", "").ToLowerInvariant() != "head") continue;
|
||||
|
||||
@@ -220,21 +369,25 @@ namespace Barotrauma
|
||||
|
||||
string spritePath = spriteElement.Attribute("texture").Value;
|
||||
|
||||
spritePath = spritePath.Replace("[GENDER]", (this.gender == Gender.Female) ? "f" : "");
|
||||
spritePath = spritePath.Replace("[GENDER]", (gender == Gender.Female) ? "female" : "male");
|
||||
spritePath = spritePath.Replace("[RACE]", race.ToString().ToLowerInvariant());
|
||||
spritePath = spritePath.Replace("[HEADID]", HeadSpriteId.ToString());
|
||||
|
||||
string fileName = Path.GetFileNameWithoutExtension(spritePath);
|
||||
|
||||
//go through the files in the directory to find a matching sprite
|
||||
var files = Directory.GetFiles(Path.GetDirectoryName(spritePath)).ToList();
|
||||
foreach (string file in files)
|
||||
foreach (string file in Directory.GetFiles(Path.GetDirectoryName(spritePath)))
|
||||
{
|
||||
if (!file.EndsWith(".png", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
string fileWithoutTags = Path.GetFileNameWithoutExtension(file);
|
||||
fileWithoutTags = fileWithoutTags.Split('[', ']').First();
|
||||
|
||||
if (fileWithoutTags != fileName) continue;
|
||||
|
||||
|
||||
headSprite = new Sprite(spriteElement, "", file);
|
||||
portrait = new Sprite(spriteElement, "", file) { RelativeOrigin = Vector2.Zero };
|
||||
|
||||
//extract the tags out of the filename
|
||||
SpriteTags = file.Split('[', ']').Skip(1).ToList();
|
||||
@@ -249,52 +402,215 @@ namespace Barotrauma
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateCharacterItems()
|
||||
|
||||
public Gender SetRandomGender() => gender = (Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < SourceElement.GetAttributeFloat("femaleratio", 0.5f)) ? Gender.Female : Gender.Male;
|
||||
public Race SetRandomRace() => race = new Race[] { Race.White, Race.Black, Race.Asian }.GetRandom(Rand.RandSync.Server);
|
||||
public int SetRandomHead() => HeadSpriteId = SetRandomHeadID();
|
||||
|
||||
private XDocument GetConfig(string file)
|
||||
{
|
||||
pickedItems.Clear();
|
||||
foreach (Item item in Character.Inventory.Items)
|
||||
if (!cachedConfigs.TryGetValue(file, out XDocument doc))
|
||||
{
|
||||
pickedItems.Add(item == null ? (ushort)0 : item.ID);
|
||||
doc = XMLExtensions.TryLoadXml(file);
|
||||
if (doc == null) { return null; }
|
||||
cachedConfigs.Add(file, doc);
|
||||
}
|
||||
return doc;
|
||||
}
|
||||
|
||||
private int SetRandomHeadID()
|
||||
{
|
||||
if (headSpriteRange != Vector2.Zero)
|
||||
{
|
||||
headSpriteId = Rand.Range((int)headSpriteRange.X, (int)headSpriteRange.Y + 1, Rand.RandSync.Server);
|
||||
}
|
||||
else
|
||||
{
|
||||
headSpriteId = 0;
|
||||
}
|
||||
return headSpriteId;
|
||||
}
|
||||
|
||||
private List<XElement> hairs;
|
||||
private List<XElement> beards;
|
||||
private List<XElement> moustaches;
|
||||
private List<XElement> faceAttachments;
|
||||
|
||||
private IEnumerable<XElement> wearables;
|
||||
private IEnumerable<XElement> Wearables
|
||||
{
|
||||
get
|
||||
{
|
||||
if (wearables == null)
|
||||
{
|
||||
var attachments = SourceElement.Element("HeadAttachments");
|
||||
if (attachments != null)
|
||||
{
|
||||
wearables = attachments.Elements("Wearable");
|
||||
}
|
||||
}
|
||||
return wearables;
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterInfo(XElement element)
|
||||
private IEnumerable<XElement> FilterElementsByGenderAndRace(IEnumerable<XElement> elements)
|
||||
{
|
||||
Name = element.GetAttributeString("name", "unnamed");
|
||||
if (elements == null) { return elements; }
|
||||
return elements.Where(w =>
|
||||
Enum.TryParse(w.GetAttributeString("gender", "male"), true, out Gender g) && g == gender &&
|
||||
Enum.TryParse(w.GetAttributeString("race", "None"), true, out Race r) && r == race);
|
||||
}
|
||||
|
||||
string genderStr = element.GetAttributeString("gender", "male").ToLowerInvariant();
|
||||
gender = (genderStr == "m") ? Gender.Male : Gender.Female;
|
||||
|
||||
File = element.GetAttributeString("file", "");
|
||||
Salary = element.GetAttributeInt("salary", 1000);
|
||||
headSpriteId = element.GetAttributeInt("headspriteid", 1);
|
||||
StartItemsGiven = element.GetAttributeBool("startitemsgiven", false);
|
||||
|
||||
int hullId = element.GetAttributeInt("hull", -1);
|
||||
if (hullId > 0 && hullId <= ushort.MaxValue) this.HullID = (ushort)hullId;
|
||||
|
||||
pickedItems = new List<ushort>();
|
||||
|
||||
string pickedItemString = element.GetAttributeString("items", "");
|
||||
if (!string.IsNullOrEmpty(pickedItemString))
|
||||
private void CalculateHeadSpriteRange()
|
||||
{
|
||||
if (SourceElement == null) { return; }
|
||||
headSpriteRange = SourceElement.GetAttributeVector2("headidrange", Vector2.Zero);
|
||||
if (headSpriteRange == Vector2.Zero)
|
||||
{
|
||||
string[] itemIds = pickedItemString.Split(',');
|
||||
foreach (string s in itemIds)
|
||||
// If range is defined, we use it as it is
|
||||
// Else we calculate the range from the wearables.
|
||||
var wearables = FilterElementsByGenderAndRace(Wearables);
|
||||
if (wearables == null)
|
||||
{
|
||||
pickedItems.Add((ushort)int.Parse(s));
|
||||
headSpriteRange = Vector2.Zero;
|
||||
return;
|
||||
}
|
||||
if (wearables.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"[CharacterInfo] No headidrange defined and no wearables matching the gender {gender} and the race {race} could be found. Total wearables found: {Wearables.Count()}.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ignore head ids that are less than 1, because they are not supported.
|
||||
var ids = wearables.Select(w => w.GetAttributeInt("headid", -1)).Where(id => id > 0);
|
||||
if (ids.None())
|
||||
{
|
||||
DebugConsole.ThrowError($"[CharacterInfo] Wearables with matching gender and race were found but none with a valid headid! Total wearables found: {Wearables.Count()}.");
|
||||
return;
|
||||
}
|
||||
ids = ids.OrderBy(id => id);
|
||||
headSpriteRange = new Vector2(ids.First(), ids.Last());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
/// <summary>
|
||||
/// Loads only the elements according to the indices, not the sprites.
|
||||
/// </summary>
|
||||
public void LoadHeadAttachments()
|
||||
{
|
||||
if (Wearables != null)
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "job") continue;
|
||||
if (hairs == null)
|
||||
{
|
||||
hairs = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Hair), WearableType.Hair);
|
||||
}
|
||||
if (beards == null)
|
||||
{
|
||||
beards = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Beard), WearableType.Beard);
|
||||
}
|
||||
if (moustaches == null)
|
||||
{
|
||||
moustaches = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.Moustache), WearableType.Moustache);
|
||||
}
|
||||
if (faceAttachments == null)
|
||||
{
|
||||
faceAttachments = AddEmpty(FilterByTypeAndHeadID(FilterElementsByGenderAndRace(wearables), WearableType.FaceAttachment), WearableType.FaceAttachment);
|
||||
}
|
||||
|
||||
Job = new Job(subElement);
|
||||
break;
|
||||
if (IsValidIndex(HairIndex, hairs))
|
||||
{
|
||||
HairElement = hairs[HairIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
HairElement = GetRandomElement(hairs);
|
||||
HairIndex = hairs.IndexOf(HairElement);
|
||||
}
|
||||
if (IsValidIndex(BeardIndex, beards))
|
||||
{
|
||||
BeardElement = beards[BeardIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
BeardElement = GetRandomElement(beards);
|
||||
BeardIndex = beards.IndexOf(BeardElement);
|
||||
}
|
||||
if (IsValidIndex(MoustacheIndex, moustaches))
|
||||
{
|
||||
MoustacheElement = moustaches[MoustacheIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
MoustacheElement = GetRandomElement(moustaches);
|
||||
MoustacheIndex = moustaches.IndexOf(MoustacheElement);
|
||||
}
|
||||
if (IsValidIndex(FaceAttachmentIndex, faceAttachments))
|
||||
{
|
||||
FaceAttachment = faceAttachments[FaceAttachmentIndex];
|
||||
}
|
||||
else
|
||||
{
|
||||
FaceAttachment = GetRandomElement(faceAttachments);
|
||||
FaceAttachmentIndex = faceAttachments.IndexOf(FaceAttachment);
|
||||
}
|
||||
|
||||
List<XElement> AddEmpty(IEnumerable<XElement> elements, WearableType type)
|
||||
{
|
||||
// Let's add an empty element so that there's a chance that we don't get any actual element -> allows bald and beardless guys, for example.
|
||||
var emptyElement = new XElement("EmptyWearable", type.ToString());
|
||||
var list = new List<XElement>() { emptyElement };
|
||||
list.AddRange(elements);
|
||||
return list;
|
||||
}
|
||||
|
||||
XElement GetRandomElement(IEnumerable<XElement> elements)
|
||||
{
|
||||
var filtered = elements.Where(e => IsWearableAllowed(e)).ToList();
|
||||
if (filtered.Count == 0) { return null; }
|
||||
var weights = GetWeights(filtered).ToList();
|
||||
var element = ToolBox.SelectWeightedRandom(filtered, weights, Rand.RandSync.Server);
|
||||
return element == null || element.Name == "Empty" ? null : element;
|
||||
}
|
||||
|
||||
IEnumerable<XElement> FilterByTypeAndHeadID(IEnumerable<XElement> elements, WearableType targetType)
|
||||
{
|
||||
return elements.Where(e =>
|
||||
{
|
||||
if (Enum.TryParse(e.GetAttributeString("type", ""), true, out WearableType type) && type != targetType) { return false; }
|
||||
int headId = e.GetAttributeInt("headid", -1);
|
||||
// if the head id is less than 1, the id is not valid and the condition is ignored.
|
||||
return headId < 1 || headId == headSpriteId;
|
||||
});
|
||||
}
|
||||
|
||||
bool IsWearableAllowed(XElement element)
|
||||
{
|
||||
string spriteName = element.Element("sprite").GetAttributeString("name", string.Empty);
|
||||
return IsAllowed(HairElement, spriteName) && IsAllowed(BeardElement, spriteName) && IsAllowed(MoustacheElement, spriteName) && IsAllowed(FaceAttachment, spriteName);
|
||||
}
|
||||
|
||||
bool IsAllowed(XElement element, string spriteName)
|
||||
{
|
||||
if (element != null)
|
||||
{
|
||||
var disallowed = element.GetAttributeStringArray("disallow", new string[0]);
|
||||
if (disallowed.Any(s => spriteName.Contains(s)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IsValidIndex(int index, List<XElement> list) => index >= 0 && index < list.Count;
|
||||
IEnumerable<float> GetWeights(IEnumerable<XElement> elements) => elements.Select(h => h.GetAttributeFloat("commonness", 1f));
|
||||
}
|
||||
}
|
||||
|
||||
partial void LoadAttachmentSprites();
|
||||
|
||||
private int CalculateSalary()
|
||||
{
|
||||
if (Name == null || Job == null) return 0;
|
||||
@@ -303,12 +619,49 @@ namespace Barotrauma
|
||||
|
||||
foreach (Skill skill in Job.Skills)
|
||||
{
|
||||
salary += skill.Level * 10;
|
||||
salary += (int)skill.Level * 50;
|
||||
}
|
||||
|
||||
return salary;
|
||||
}
|
||||
|
||||
public void IncreaseSkillLevel(string skillIdentifier, float increase, Vector2 worldPos)
|
||||
{
|
||||
if (Job == null || GameMain.Client != null) return;
|
||||
|
||||
float prevLevel = Job.GetSkillLevel(skillIdentifier);
|
||||
Job.IncreaseSkillLevel(skillIdentifier, increase);
|
||||
|
||||
float newLevel = Job.GetSkillLevel(skillIdentifier);
|
||||
|
||||
OnSkillChanged(skillIdentifier, prevLevel, newLevel, worldPos);
|
||||
|
||||
if (GameMain.Server != null && (int)newLevel != (int)prevLevel)
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(Character, new object[] { NetEntityEvent.Type.UpdateSkills });
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSkillLevel(string skillIdentifier, float level, Vector2 worldPos)
|
||||
{
|
||||
if (Job == null) return;
|
||||
|
||||
var skill = Job.Skills.Find(s => s.Identifier == skillIdentifier);
|
||||
if (skill == null)
|
||||
{
|
||||
Job.Skills.Add(new Skill(skillIdentifier, level));
|
||||
OnSkillChanged(skillIdentifier, 0.0f, skill.Level, worldPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
float prevLevel = skill.Level;
|
||||
skill.Level = level;
|
||||
OnSkillChanged(skillIdentifier, prevLevel, skill.Level, worldPos);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSkillChanged(string skillIdentifier, float prevLevel, float newLevel, Vector2 textPopupPos);
|
||||
|
||||
public virtual XElement Save(XElement parentElement)
|
||||
{
|
||||
XElement charElement = new XElement("Character");
|
||||
@@ -316,44 +669,204 @@ namespace Barotrauma
|
||||
charElement.Add(
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("file", File),
|
||||
new XAttribute("gender", gender == Gender.Male ? "m" : "f"),
|
||||
new XAttribute("gender", gender == Gender.Male ? "male" : "female"),
|
||||
new XAttribute("race", race.ToString()),
|
||||
new XAttribute("salary", Salary),
|
||||
new XAttribute("headspriteid", HeadSpriteId),
|
||||
new XAttribute("startitemsgiven", StartItemsGiven));
|
||||
new XAttribute("hairindex", HairIndex),
|
||||
new XAttribute("beardindex", BeardIndex),
|
||||
new XAttribute("moustacheindex", MoustacheIndex),
|
||||
new XAttribute("faceattachmentindex", FaceAttachmentIndex),
|
||||
new XAttribute("startitemsgiven", StartItemsGiven),
|
||||
new XAttribute("ragdoll", ragdollFileName),
|
||||
new XAttribute("personality", personalityTrait == null ? "" : personalityTrait.Name));
|
||||
|
||||
// TODO: animations?
|
||||
|
||||
if (Character != null)
|
||||
{
|
||||
if (Character.Inventory != null)
|
||||
{
|
||||
UpdateCharacterItems();
|
||||
}
|
||||
|
||||
if (Character.AnimController.CurrentHull != null)
|
||||
{
|
||||
HullID = Character.AnimController.CurrentHull.ID;
|
||||
charElement.Add(new XAttribute("hull", Character.AnimController.CurrentHull.ID));
|
||||
}
|
||||
}
|
||||
|
||||
if (pickedItems.Count > 0)
|
||||
{
|
||||
charElement.Add(new XAttribute("items", string.Join(",", pickedItems)));
|
||||
}
|
||||
|
||||
Job.Save(charElement);
|
||||
|
||||
parentElement.Add(charElement);
|
||||
return charElement;
|
||||
}
|
||||
|
||||
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
|
||||
{
|
||||
SpawnInventoryItemsRecursive(inventory, itemData);
|
||||
}
|
||||
|
||||
private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element)
|
||||
{
|
||||
foreach (XElement itemElement in element.Elements())
|
||||
{
|
||||
var newItem = Item.Load(itemElement, inventory.Owner.Submarine);
|
||||
int slotIndex = itemElement.GetAttributeInt("i", 0);
|
||||
if (newItem == null) continue;
|
||||
|
||||
Entity.Spawner.CreateNetworkEvent(newItem, false);
|
||||
|
||||
inventory.TryPutItem(newItem, slotIndex, false, false, null);
|
||||
|
||||
int itemContainerIndex = 0;
|
||||
var itemContainers = newItem.GetComponents<ItemContainer>().ToList();
|
||||
foreach (XElement childInvElement in itemElement.Elements())
|
||||
{
|
||||
if (itemContainerIndex >= itemContainers.Count) break;
|
||||
if (childInvElement.Name.ToString().ToLowerInvariant() != "inventory") continue;
|
||||
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement);
|
||||
itemContainerIndex++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerWrite(NetBuffer msg)
|
||||
{
|
||||
msg.Write(ID);
|
||||
msg.Write(Name);
|
||||
msg.Write(Gender == Gender.Female);
|
||||
msg.Write((byte)Race);
|
||||
msg.Write((byte)HeadSpriteId);
|
||||
msg.Write((byte)HairIndex);
|
||||
msg.Write((byte)BeardIndex);
|
||||
msg.Write((byte)MoustacheIndex);
|
||||
msg.Write((byte)FaceAttachmentIndex);
|
||||
msg.Write(ragdollFileName);
|
||||
|
||||
if (Job != null)
|
||||
{
|
||||
msg.Write(Job.Prefab.Identifier);
|
||||
msg.Write((byte)Job.Skills.Count);
|
||||
foreach (Skill skill in Job.Skills)
|
||||
{
|
||||
msg.Write(skill.Identifier);
|
||||
msg.Write(skill.Level);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Write("");
|
||||
}
|
||||
// TODO: animations
|
||||
}
|
||||
|
||||
public static CharacterInfo ClientRead(string configPath, NetBuffer inc)
|
||||
{
|
||||
ushort infoID = inc.ReadUInt16();
|
||||
string newName = inc.ReadString();
|
||||
bool isFemale = inc.ReadBoolean();
|
||||
int race = inc.ReadByte();
|
||||
int headSpriteID = inc.ReadByte();
|
||||
int hairIndex = inc.ReadByte();
|
||||
int beardIndex = inc.ReadByte();
|
||||
int moustacheIndex = inc.ReadByte();
|
||||
int faceAttachmentIndex = inc.ReadByte();
|
||||
string ragdollFile = inc.ReadString();
|
||||
|
||||
string jobIdentifier = inc.ReadString();
|
||||
JobPrefab jobPrefab = null;
|
||||
Dictionary<string, float> skillLevels = new Dictionary<string, float>();
|
||||
if (!string.IsNullOrEmpty(jobIdentifier))
|
||||
{
|
||||
jobPrefab = JobPrefab.List.Find(jp => jp.Identifier == jobIdentifier);
|
||||
int skillCount = inc.ReadByte();
|
||||
for (int i = 0; i < skillCount; i++)
|
||||
{
|
||||
string skillIdentifier = inc.ReadString();
|
||||
float skillLevel = inc.ReadSingle();
|
||||
skillLevels.Add(skillIdentifier, skillLevel);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: animations
|
||||
|
||||
CharacterInfo ch = new CharacterInfo(configPath, newName, isFemale ? Gender.Female : Gender.Male, jobPrefab, ragdollFile)
|
||||
{
|
||||
ID = infoID,
|
||||
race = (Race)race,
|
||||
headSpriteId = headSpriteID,
|
||||
HairIndex = hairIndex,
|
||||
BeardIndex = beardIndex,
|
||||
MoustacheIndex = moustacheIndex,
|
||||
FaceAttachmentIndex = faceAttachmentIndex
|
||||
};
|
||||
ch.CalculateHeadSpriteRange();
|
||||
ch.ReloadHeadAttachments();
|
||||
|
||||
System.Diagnostics.Debug.Assert(skillLevels.Count == ch.Job.Skills.Count);
|
||||
if (ch.Job != null)
|
||||
{
|
||||
foreach (KeyValuePair<string, float> skill in skillLevels)
|
||||
{
|
||||
Skill matchingSkill = ch.Job.Skills.Find(s => s.Identifier == skill.Key);
|
||||
if (matchingSkill == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Skill \"" + skill.Key + "\" not found in character \"" + newName + "\"");
|
||||
continue;
|
||||
}
|
||||
matchingSkill.Level = skill.Value;
|
||||
}
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
public void ReloadHeadAttachments()
|
||||
{
|
||||
ResetLoadedAttachments();
|
||||
LoadHeadAttachments();
|
||||
}
|
||||
|
||||
public void ResetHeadAttachments()
|
||||
{
|
||||
ResetAttachmentIndices();
|
||||
ResetLoadedAttachments();
|
||||
}
|
||||
|
||||
private void ResetAttachmentIndices()
|
||||
{
|
||||
HairIndex = -1;
|
||||
BeardIndex = -1;
|
||||
MoustacheIndex = -1;
|
||||
FaceAttachmentIndex = -1;
|
||||
}
|
||||
|
||||
private void ResetLoadedAttachments()
|
||||
{
|
||||
hairs = null;
|
||||
beards = null;
|
||||
moustaches = null;
|
||||
faceAttachments = null;
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
Character = null;
|
||||
//if (headSprite != null)
|
||||
//{
|
||||
// headSprite.Remove();
|
||||
// headSprite = null;
|
||||
//}
|
||||
if (headSprite != null)
|
||||
{
|
||||
headSprite.Remove();
|
||||
headSprite = null;
|
||||
}
|
||||
if (portrait != null)
|
||||
{
|
||||
portrait.Remove();
|
||||
portrait = null;
|
||||
}
|
||||
if (portraitBackground != null)
|
||||
{
|
||||
portraitBackground.Remove();
|
||||
portraitBackground = null;
|
||||
}
|
||||
if (attachmentSprites != null)
|
||||
{
|
||||
attachmentSprites.ForEach(a => a.Sprite.Remove());
|
||||
attachmentSprites = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,8 +53,10 @@ namespace Barotrauma
|
||||
Aim = 0x200,
|
||||
Attack = 0x400,
|
||||
Ragdoll = 0x800,
|
||||
Health = 0x1000,
|
||||
Grab = 0x2000,
|
||||
|
||||
MaxVal = 0xFFF
|
||||
MaxVal = 0x3FFF
|
||||
}
|
||||
private InputNetFlags dequeuedInput = 0;
|
||||
private InputNetFlags prevDequeuedInput = 0;
|
||||
@@ -108,13 +110,14 @@ namespace Barotrauma
|
||||
LastNetworkUpdateID = 0;
|
||||
LastProcessedID = 0;
|
||||
}
|
||||
|
||||
|
||||
private void UpdateNetInput()
|
||||
{
|
||||
if (this != Character.Controlled)
|
||||
if (this != Controlled)
|
||||
{
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
#if CLIENT
|
||||
//freeze AI characters if more than 1 seconds have passed since last update from the server
|
||||
if (lastRecvPositionUpdateTime < NetTime.Now - 1.0f)
|
||||
{
|
||||
@@ -127,6 +130,7 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else if (GameMain.Server != null && (!(this is AICharacter) || IsRemotePlayer))
|
||||
{
|
||||
@@ -153,12 +157,13 @@ namespace Barotrauma
|
||||
dequeuedInput = memInput[memInput.Count - 1].states;
|
||||
|
||||
double aimAngle = ((double)memInput[memInput.Count - 1].intAim / 65535.0) * 2.0 * Math.PI;
|
||||
cursorPosition = (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position)
|
||||
+ new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
|
||||
cursorPosition = AimRefPosition + new Vector2((float)Math.Cos(aimAngle), (float)Math.Sin(aimAngle)) * 60.0f;
|
||||
|
||||
//reset focus when attempting to use/select something
|
||||
if (memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Use) ||
|
||||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select))
|
||||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Select) ||
|
||||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Health) ||
|
||||
memInput[memInput.Count - 1].states.HasFlag(InputNetFlags.Grab))
|
||||
{
|
||||
focusedItem = null;
|
||||
focusedCharacter = null;
|
||||
@@ -205,7 +210,7 @@ namespace Barotrauma
|
||||
AnimController.Collider.Rotation,
|
||||
LastNetworkUpdateID,
|
||||
AnimController.TargetDir,
|
||||
SelectedCharacter == null ? (Entity)selectedConstruction : (Entity)SelectedCharacter,
|
||||
SelectedCharacter == null ? (Entity)SelectedConstruction : (Entity)SelectedCharacter,
|
||||
AnimController.Anim);
|
||||
|
||||
memLocalState.Add(posInfo);
|
||||
@@ -218,6 +223,8 @@ namespace Barotrauma
|
||||
if (IsKeyDown(InputType.Run)) newInput |= InputNetFlags.Run;
|
||||
if (IsKeyDown(InputType.Crouch)) newInput |= InputNetFlags.Crouch;
|
||||
if (IsKeyHit(InputType.Select)) newInput |= InputNetFlags.Select; //TODO: clean up the way this input is registered
|
||||
if (IsKeyHit(InputType.Health)) newInput |= InputNetFlags.Health;
|
||||
if (IsKeyHit(InputType.Grab)) newInput |= InputNetFlags.Grab;
|
||||
if (IsKeyDown(InputType.Use)) newInput |= InputNetFlags.Use;
|
||||
if (IsKeyDown(InputType.Aim)) newInput |= InputNetFlags.Aim;
|
||||
if (IsKeyDown(InputType.Attack)) newInput |= InputNetFlags.Attack;
|
||||
@@ -225,14 +232,16 @@ namespace Barotrauma
|
||||
|
||||
if (AnimController.TargetDir == Direction.Left) newInput |= InputNetFlags.FacingLeft;
|
||||
|
||||
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position);
|
||||
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
|
||||
relativeCursorPos.Normalize();
|
||||
UInt16 intAngle = (UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI));
|
||||
|
||||
NetInputMem newMem = new NetInputMem();
|
||||
newMem.states = newInput;
|
||||
newMem.intAim = intAngle;
|
||||
if (focusedItem != null)
|
||||
NetInputMem newMem = new NetInputMem
|
||||
{
|
||||
states = newInput,
|
||||
intAim = intAngle
|
||||
};
|
||||
if (focusedItem != null && (!newMem.states.HasFlag(InputNetFlags.Grab) && !newMem.states.HasFlag(InputNetFlags.Health)))
|
||||
{
|
||||
newMem.interact = focusedItem.ID;
|
||||
}
|
||||
@@ -292,13 +301,26 @@ namespace Barotrauma
|
||||
UInt16 newAim = 0;
|
||||
UInt16 newInteract = 0;
|
||||
|
||||
if (newInput != InputNetFlags.None && newInput != InputNetFlags.FacingLeft)
|
||||
{
|
||||
c.KickAFKTimer = 0.0f;
|
||||
}
|
||||
else if (AnimController.Dir < 0.0f != newInput.HasFlag(InputNetFlags.FacingLeft))
|
||||
{
|
||||
//character changed the direction they're facing
|
||||
c.KickAFKTimer = 0.0f;
|
||||
}
|
||||
|
||||
if (newInput.HasFlag(InputNetFlags.Aim))
|
||||
{
|
||||
newAim = msg.ReadUInt16();
|
||||
}
|
||||
if (newInput.HasFlag(InputNetFlags.Select) || newInput.HasFlag(InputNetFlags.Use))
|
||||
if (newInput.HasFlag(InputNetFlags.Select) ||
|
||||
newInput.HasFlag(InputNetFlags.Use) ||
|
||||
newInput.HasFlag(InputNetFlags.Health) ||
|
||||
newInput.HasFlag(InputNetFlags.Grab))
|
||||
{
|
||||
newInteract = msg.ReadUInt16();
|
||||
newInteract = msg.ReadUInt16();
|
||||
}
|
||||
|
||||
//if (AllowInput)
|
||||
@@ -334,7 +356,7 @@ namespace Barotrauma
|
||||
switch (eventType)
|
||||
{
|
||||
case 0:
|
||||
inventory.ServerRead(type, msg, c);
|
||||
Inventory.ServerRead(type, msg, c);
|
||||
break;
|
||||
case 1:
|
||||
bool doingCPR = msg.ReadBoolean();
|
||||
@@ -359,20 +381,10 @@ namespace Barotrauma
|
||||
|
||||
if (IsUnconscious)
|
||||
{
|
||||
Kill(lastAttackCauseOfDeath);
|
||||
var causeOfDeath = CharacterHealth.GetCauseOfDeath();
|
||||
Kill(causeOfDeath.First, causeOfDeath.Second);
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
LimbType grabLimb = (LimbType)msg.ReadByte();
|
||||
if (c.Character != this)
|
||||
{
|
||||
#if DEBUG
|
||||
DebugConsole.Log("Received a character update message from a client who's not controlling the character");
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
AnimController.GrabLimb = grabLimb;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -388,18 +400,34 @@ namespace Barotrauma
|
||||
switch ((NetEntityEvent.Type)extraData[0])
|
||||
{
|
||||
case NetEntityEvent.Type.InventoryState:
|
||||
msg.WriteRangedInteger(0, 2, 0);
|
||||
inventory.ClientWrite(msg, extraData);
|
||||
msg.WriteRangedInteger(0, 3, 0);
|
||||
Inventory.ClientWrite(msg, extraData);
|
||||
break;
|
||||
case NetEntityEvent.Type.Control:
|
||||
msg.WriteRangedInteger(0, 2, 1);
|
||||
msg.WriteRangedInteger(0, 3, 1);
|
||||
Client owner = ((Client)extraData[1]);
|
||||
msg.Write(owner == null ? (byte)0 : owner.ID);
|
||||
break;
|
||||
case NetEntityEvent.Type.Status:
|
||||
msg.WriteRangedInteger(0, 2, 2);
|
||||
msg.WriteRangedInteger(0, 3, 2);
|
||||
WriteStatus(msg);
|
||||
break;
|
||||
case NetEntityEvent.Type.UpdateSkills:
|
||||
msg.WriteRangedInteger(0, 3, 3);
|
||||
if (Info?.Job == null)
|
||||
{
|
||||
msg.Write((byte)0);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Write((byte)Info.Job.Skills.Count);
|
||||
foreach (Skill skill in Info.Job.Skills)
|
||||
{
|
||||
msg.Write(skill.Identifier);
|
||||
msg.Write(skill.Level);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
DebugConsole.ThrowError("Invalid NetworkEvent type for entity " + ToString() + " (" + (NetEntityEvent.Type)extraData[0] + ")");
|
||||
break;
|
||||
@@ -454,16 +482,17 @@ namespace Barotrauma
|
||||
if (AnimController is HumanoidAnimController)
|
||||
{
|
||||
tempBuffer.Write(((HumanoidAnimController)AnimController).Crouching);
|
||||
tempBuffer.Write((byte)AnimController.GrabLimb);
|
||||
}
|
||||
|
||||
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null);
|
||||
AttackContext currentContext = GetAttackContext();
|
||||
// TODO: do we need to filter the attack target here?
|
||||
bool hasAttackLimb = AnimController.Limbs.Any(l => l != null && l.attack != null && l.attack.IsValidContext(currentContext));
|
||||
tempBuffer.Write(hasAttackLimb);
|
||||
if (hasAttackLimb) tempBuffer.Write(attack);
|
||||
|
||||
if (aiming)
|
||||
{
|
||||
Vector2 relativeCursorPos = cursorPosition - (ViewTarget == null ? AnimController.AimSourcePos : ViewTarget.Position);
|
||||
Vector2 relativeCursorPos = cursorPosition - AimRefPosition;
|
||||
tempBuffer.Write((UInt16)(65535.0 * Math.Atan2(relativeCursorPos.Y, relativeCursorPos.X) / (2.0 * Math.PI)));
|
||||
}
|
||||
tempBuffer.Write(IsRagdolled);
|
||||
@@ -471,10 +500,10 @@ namespace Barotrauma
|
||||
tempBuffer.Write(AnimController.TargetDir == Direction.Right);
|
||||
}
|
||||
|
||||
if (SelectedCharacter != null || selectedConstruction != null)
|
||||
if (SelectedCharacter != null || SelectedConstruction != null)
|
||||
{
|
||||
tempBuffer.Write(true);
|
||||
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : selectedConstruction.ID);
|
||||
tempBuffer.Write(SelectedCharacter != null ? SelectedCharacter.ID : SelectedConstruction.ID);
|
||||
if (SelectedCharacter != null)
|
||||
{
|
||||
tempBuffer.Write(AnimController.Anim == AnimController.Animation.CPR);
|
||||
@@ -505,11 +534,15 @@ namespace Barotrauma
|
||||
DebugConsole.ThrowError("Client attempted to write character status to a networked message");
|
||||
return;
|
||||
}
|
||||
|
||||
msg.Write(isDead);
|
||||
if (isDead)
|
||||
|
||||
msg.Write(IsDead);
|
||||
if (IsDead)
|
||||
{
|
||||
msg.Write((byte)causeOfDeath);
|
||||
msg.WriteRangedInteger(0, Enum.GetValues(typeof(CauseOfDeathType)).Length - 1, (int)CauseOfDeath.Type);
|
||||
if (CauseOfDeath.Type == CauseOfDeathType.Affliction)
|
||||
{
|
||||
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(CauseOfDeath.Affliction));
|
||||
}
|
||||
|
||||
if (AnimController?.LimbJoints == null)
|
||||
{
|
||||
@@ -535,29 +568,8 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.WriteRangedSingle(health, minHealth, maxHealth, 8);
|
||||
|
||||
msg.Write(oxygen < 100.0f);
|
||||
if (oxygen < 100.0f)
|
||||
{
|
||||
msg.WriteRangedSingle(oxygen, -100.0f, 100.0f, 8);
|
||||
}
|
||||
|
||||
msg.Write(bleeding > 0.0f);
|
||||
if (bleeding > 0.0f)
|
||||
{
|
||||
msg.WriteRangedSingle(bleeding, 0.0f, 5.0f, 8);
|
||||
}
|
||||
|
||||
msg.Write(Stun > 0.0f);
|
||||
if (Stun > 0.0f)
|
||||
{
|
||||
msg.WriteRangedSingle(MathHelper.Clamp(Stun, 0.0f, MaxStun), 0.0f, MaxStun, 8);
|
||||
}
|
||||
|
||||
CharacterHealth.ServerWrite(msg);
|
||||
msg.Write(IsRagdolled);
|
||||
|
||||
msg.Write(HuskInfectionState > 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,6 +580,7 @@ namespace Barotrauma
|
||||
msg.Write(Info == null);
|
||||
msg.Write(ID);
|
||||
msg.Write(ConfigPath);
|
||||
msg.Write(seed);
|
||||
|
||||
msg.Write(WorldPosition.X);
|
||||
msg.Write(WorldPosition.Y);
|
||||
@@ -576,7 +589,7 @@ namespace Barotrauma
|
||||
|
||||
//character with no characterinfo (e.g. some monster)
|
||||
if (Info == null) return;
|
||||
|
||||
|
||||
Client ownerClient = GameMain.Server.ConnectedClients.Find(c => c.Character == this);
|
||||
if (ownerClient != null)
|
||||
{
|
||||
@@ -593,27 +606,9 @@ namespace Barotrauma
|
||||
msg.Write(false);
|
||||
}
|
||||
|
||||
msg.Write(Info.Name);
|
||||
msg.Write(TeamID);
|
||||
|
||||
msg.Write(this is AICharacter);
|
||||
msg.Write(Info.Gender == Gender.Female);
|
||||
msg.Write((byte)Info.HeadSpriteId);
|
||||
if (info.Job != null)
|
||||
{
|
||||
msg.Write(Info.Job.Name);
|
||||
msg.Write((byte)info.Job.Skills.Count);
|
||||
foreach (Skill skill in info.Job.Skills)
|
||||
{
|
||||
msg.Write(skill.Name);
|
||||
msg.WriteRangedInteger(0, 100, MathHelper.Clamp(skill.Level, 0, 100));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Write("");
|
||||
}
|
||||
}
|
||||
|
||||
info.ServerWrite(msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class DamageModifier
|
||||
{
|
||||
[Serialize(DamageType.None, false)]
|
||||
public DamageType DamageType
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false)]
|
||||
public float DamageMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(1.0f, false)]
|
||||
public float BleedingMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("0.0,360", false)]
|
||||
public Vector2 ArmorSector
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool IsArmor
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool DeflectProjectiles
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
#if CLIENT
|
||||
[Serialize("", false)]
|
||||
public string DamageSound
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
#endif
|
||||
|
||||
public DamageModifier(XElement element)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class Affliction
|
||||
{
|
||||
public readonly AfflictionPrefab Prefab;
|
||||
|
||||
public float Strength;
|
||||
|
||||
public float DamagePerSecond;
|
||||
public float DamagePerSecondTimer;
|
||||
public float PreviousVitalityDecrease;
|
||||
|
||||
/// <summary>
|
||||
/// Which character gave this affliction
|
||||
/// </summary>
|
||||
public Character Source;
|
||||
|
||||
public Affliction(AfflictionPrefab prefab, float strength)
|
||||
{
|
||||
Prefab = prefab;
|
||||
Strength = strength;
|
||||
}
|
||||
|
||||
public Affliction CreateMultiplied(float multiplier)
|
||||
{
|
||||
return Prefab.Instantiate(Strength * multiplier, Source);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "Affliction (" + Prefab.Name + ")";
|
||||
}
|
||||
|
||||
public float GetVitalityDecrease(CharacterHealth characterHealth)
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxStrength - currentEffect.MinStrength <= 0.0f) return 0.0f;
|
||||
|
||||
float currVitalityDecrease = MathHelper.Lerp(
|
||||
currentEffect.MinVitalityDecrease,
|
||||
currentEffect.MaxVitalityDecrease,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
|
||||
if (currentEffect.MultiplyByMaxVitality) currVitalityDecrease *= characterHealth == null ? 100.0f : characterHealth.MaxVitality;
|
||||
|
||||
return currVitalityDecrease;
|
||||
}
|
||||
|
||||
public float GetScreenDistortStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxScreenDistortStrength - currentEffect.MinScreenDistortStrength <= 0.0f) return 0.0f;
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinScreenDistortStrength,
|
||||
currentEffect.MaxScreenDistortStrength,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
}
|
||||
|
||||
public float GetRadialDistortStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxRadialDistortStrength - currentEffect.MinRadialDistortStrength <= 0.0f) return 0.0f;
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinRadialDistortStrength,
|
||||
currentEffect.MaxRadialDistortStrength,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
}
|
||||
|
||||
public float GetChromaticAberrationStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxChromaticAberrationStrength - currentEffect.MinChromaticAberrationStrength <= 0.0f) return 0.0f;
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinChromaticAberrationStrength,
|
||||
currentEffect.MaxChromaticAberrationStrength,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
}
|
||||
|
||||
public float GetScreenBlurStrength()
|
||||
{
|
||||
if (Strength < Prefab.ActivationThreshold) return 0.0f;
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return 0.0f;
|
||||
if (currentEffect.MaxScreenBlurStrength - currentEffect.MinScreenBlurStrength <= 0.0f) return 0.0f;
|
||||
|
||||
return MathHelper.Lerp(
|
||||
currentEffect.MinScreenBlurStrength,
|
||||
currentEffect.MaxScreenBlurStrength,
|
||||
(Strength - currentEffect.MinStrength) / (currentEffect.MaxStrength - currentEffect.MinStrength));
|
||||
}
|
||||
|
||||
public void CalculateDamagePerSecond(float currentVitalityDecrease)
|
||||
{
|
||||
DamagePerSecond = Math.Max(DamagePerSecond, currentVitalityDecrease - PreviousVitalityDecrease);
|
||||
if (DamagePerSecondTimer >= 1.0f)
|
||||
{
|
||||
DamagePerSecond = currentVitalityDecrease - PreviousVitalityDecrease;
|
||||
PreviousVitalityDecrease = currentVitalityDecrease;
|
||||
DamagePerSecondTimer = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
AfflictionPrefab.Effect currentEffect = Prefab.GetActiveEffect(Strength);
|
||||
if (currentEffect == null) return;
|
||||
|
||||
Strength += currentEffect.StrengthChange * deltaTime;
|
||||
foreach (StatusEffect statusEffect in currentEffect.StatusEffects)
|
||||
{
|
||||
if (statusEffect.HasTargetType(StatusEffect.TargetType.Character))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, characterHealth.Character);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, characterHealth.Character, targetLimb);
|
||||
}
|
||||
if (targetLimb != null && statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
||||
{
|
||||
statusEffect.Apply(ActionType.OnActive, deltaTime, targetLimb.character, targetLimb.character.AnimController.Limbs.Cast<ISerializableEntity>().ToList());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class AfflictionBleeding : Affliction
|
||||
{
|
||||
public AfflictionBleeding(AfflictionPrefab prefab, float strength) :
|
||||
base(prefab, strength)
|
||||
{
|
||||
}
|
||||
|
||||
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
base.Update(characterHealth, targetLimb, deltaTime);
|
||||
characterHealth.BloodlossAmount += Strength * (1.0f / 60.0f) * deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
-45
@@ -1,67 +1,88 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework;
|
||||
#endif
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class HuskInfection
|
||||
partial class AfflictionHusk : Affliction
|
||||
{
|
||||
public enum InfectionState
|
||||
{
|
||||
Dormant, Transition, Active
|
||||
}
|
||||
|
||||
const float IncubationDuration = 300.0f;
|
||||
private bool subscribedToDeathEvent;
|
||||
|
||||
private InfectionState state;
|
||||
|
||||
private Limb huskAppendage;
|
||||
|
||||
private float incubationTimer;
|
||||
public float IncubationTimer
|
||||
{
|
||||
get { return incubationTimer; }
|
||||
set
|
||||
{
|
||||
incubationTimer = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public InfectionState State
|
||||
{
|
||||
get { return state; }
|
||||
}
|
||||
|
||||
public bool CanSpeak
|
||||
public AfflictionHusk(AfflictionPrefab prefab, float strength) :
|
||||
base(prefab, strength)
|
||||
{
|
||||
get { return IncubationTimer < 0.5f; }
|
||||
}
|
||||
|
||||
public HuskInfection(Character character)
|
||||
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
character.OnDeath += CharacterDead;
|
||||
}
|
||||
float prevStrength = Strength;
|
||||
base.Update(characterHealth, targetLimb, deltaTime);
|
||||
|
||||
public void Update(float deltaTime, Character character)
|
||||
{
|
||||
float prevTimer = IncubationTimer;
|
||||
|
||||
UpdateProjSpecific(prevTimer,character);
|
||||
if (IncubationTimer < 0.5f)
|
||||
if (!subscribedToDeathEvent)
|
||||
{
|
||||
UpdateDormantState(deltaTime, character);
|
||||
characterHealth.Character.OnDeath += CharacterDead;
|
||||
subscribedToDeathEvent = true;
|
||||
}
|
||||
else if (IncubationTimer < 1.0f)
|
||||
|
||||
if (characterHealth.Character == Character.Controlled) UpdateMessages(prevStrength, characterHealth.Character);
|
||||
if (Strength < Prefab.MaxStrength * 0.5f)
|
||||
{
|
||||
UpdateTransitionState(deltaTime, character);
|
||||
UpdateDormantState(deltaTime, characterHealth.Character);
|
||||
}
|
||||
else if (Strength < Prefab.MaxStrength)
|
||||
{
|
||||
characterHealth.Character.SpeechImpediment = 100.0f;
|
||||
UpdateTransitionState(deltaTime, characterHealth.Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
UpdateActiveState(deltaTime, character);
|
||||
characterHealth.Character.SpeechImpediment = 100.0f;
|
||||
UpdateActiveState(deltaTime, characterHealth.Character);
|
||||
}
|
||||
}
|
||||
partial void UpdateProjSpecific(float prevTimer, Character character);
|
||||
|
||||
private void UpdateMessages(float prevStrength, Character character)
|
||||
{
|
||||
#if CLIENT
|
||||
if (Strength < Prefab.MaxStrength * 0.5f)
|
||||
{
|
||||
if (prevStrength % 10.0f > 0.05f && Strength % 10.0f < 0.05f)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("HuskDormant"), Color.Red);
|
||||
}
|
||||
}
|
||||
else if (Strength < Prefab.MaxStrength)
|
||||
{
|
||||
if (state == InfectionState.Dormant && Character.Controlled == character)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("HuskCantSpeak"), Color.Red);
|
||||
}
|
||||
}
|
||||
else if (state != InfectionState.Active && Character.Controlled == character)
|
||||
{
|
||||
GUI.AddMessage(TextManager.Get("HuskActivate").Replace("[Attack]", GameMain.Config.KeyBind(InputType.Attack).ToString()),
|
||||
Color.Red);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private void UpdateDormantState(float deltaTime, Character character)
|
||||
{
|
||||
@@ -69,10 +90,8 @@ namespace Barotrauma
|
||||
{
|
||||
DeactivateHusk(character);
|
||||
}
|
||||
|
||||
float prevTimer = IncubationTimer;
|
||||
|
||||
state = InfectionState.Dormant;
|
||||
IncubationTimer += deltaTime / IncubationDuration;
|
||||
}
|
||||
|
||||
private void UpdateTransitionState(float deltaTime, Character character)
|
||||
@@ -82,7 +101,6 @@ namespace Barotrauma
|
||||
DeactivateHusk(character);
|
||||
}
|
||||
|
||||
IncubationTimer += deltaTime / IncubationDuration;
|
||||
state = InfectionState.Transition;
|
||||
}
|
||||
|
||||
@@ -94,7 +112,14 @@ namespace Barotrauma
|
||||
state = InfectionState.Active;
|
||||
}
|
||||
|
||||
character.AddDamage(CauseOfDeath.Husk, 0.5f * deltaTime, null);
|
||||
foreach (Limb limb in character.AnimController.Limbs)
|
||||
{
|
||||
character.LastDamageSource = null;
|
||||
character.DamageLimb(
|
||||
limb.WorldPosition, limb,
|
||||
new List<Affliction>() { AfflictionPrefab.InternalDamage.Instantiate(0.5f * deltaTime / character.AnimController.Limbs.Length) },
|
||||
0.0f, false, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateHusk(Character character)
|
||||
@@ -108,7 +133,7 @@ namespace Barotrauma
|
||||
//husk appendage already created, don't do anything
|
||||
if (huskAppendage != null) return;
|
||||
|
||||
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine("Content", "Characters", "Human", "huskappendage.xml"));
|
||||
XDocument doc = XMLExtensions.TryLoadXml(Path.Combine("Content", "Characters", "Human", "Huskappendage.xml"));
|
||||
if (doc == null || doc.Root == null) return;
|
||||
|
||||
var limbElement = doc.Root.Element("limb");
|
||||
@@ -132,11 +157,11 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
var torso = character.AnimController.GetLimb(LimbType.Torso);
|
||||
|
||||
huskAppendage = new Limb(character, limbElement);
|
||||
|
||||
huskAppendage = new Limb(character.AnimController, character, new LimbParams(limbElement, character.AnimController.RagdollParams));
|
||||
huskAppendage.body.Submarine = character.Submarine;
|
||||
huskAppendage.body.SetTransform(torso.SimPosition, torso.Rotation);
|
||||
|
||||
|
||||
character.AnimController.AddLimb(huskAppendage);
|
||||
character.AnimController.AddJoint(jointElement);
|
||||
}
|
||||
@@ -158,13 +183,14 @@ namespace Barotrauma
|
||||
public void Remove(Character character)
|
||||
{
|
||||
DeactivateHusk(character);
|
||||
|
||||
if (character != null) character.OnDeath -= CharacterDead;
|
||||
subscribedToDeathEvent = false;
|
||||
}
|
||||
|
||||
private void CharacterDead(Character character, CauseOfDeath causeOfDeath)
|
||||
{
|
||||
if (GameMain.Client != null) return;
|
||||
if (GameMain.Client != null) { return; }
|
||||
if (Strength < Prefab.MaxStrength * 0.5f || character.Removed) { return; }
|
||||
|
||||
//don't turn the character into a husk if any of its limbs are severed
|
||||
if (character.AnimController?.LimbJoints != null)
|
||||
@@ -174,7 +200,7 @@ namespace Barotrauma
|
||||
if (limbJoint.IsSevered) return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//create the AI husk in a coroutine to ensure that we don't modify the character list while enumerating it
|
||||
CoroutineManager.StartCoroutine(CreateAIHusk(character));
|
||||
}
|
||||
@@ -184,8 +210,8 @@ namespace Barotrauma
|
||||
character.Enabled = false;
|
||||
Entity.Spawner.AddToRemoveQueue(character);
|
||||
|
||||
var characterFiles = GameMain.SelectedPackage.GetFilesOfType(ContentType.Character);
|
||||
var configFile = characterFiles.Find(f => Path.GetFileNameWithoutExtension(f) == "humanhusk");
|
||||
var characterFiles = GameMain.Instance.GetFilesOfType(ContentType.Character);
|
||||
var configFile = characterFiles.FirstOrDefault(f => Path.GetFileNameWithoutExtension(f) == "humanhusk");
|
||||
|
||||
if (string.IsNullOrEmpty(configFile))
|
||||
{
|
||||
@@ -193,7 +219,16 @@ namespace Barotrauma
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
var husk = Character.Create(configFile, character.WorldPosition, character.Info, false, true);
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile);
|
||||
if (doc?.Root == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to turn character \"" + character.Name + "\" into a husk - humanhusk config file ("+configFile+") could not be read.");
|
||||
yield return CoroutineStatus.Success;
|
||||
}
|
||||
|
||||
character.Info.Ragdoll = null;
|
||||
character.Info.SourceElement = doc.Root;
|
||||
var husk = Character.Create(configFile, character.WorldPosition, character.Info.Name, character.Info, false, true);
|
||||
|
||||
foreach (Limb limb in husk.AnimController.Limbs)
|
||||
{
|
||||
@@ -0,0 +1,336 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public static class CPRSettings
|
||||
{
|
||||
public static float ReviveChancePerSkill { get; private set; }
|
||||
public static float ReviveChanceExponent { get; private set; }
|
||||
public static float ReviveChanceMin { get; private set; }
|
||||
public static float ReviveChanceMax { get; private set; }
|
||||
public static float StabilizationPerSkill { get; private set; }
|
||||
public static float StabilizationMin { get; private set; }
|
||||
public static float StabilizationMax { get; private set; }
|
||||
public static float DamageSkillThreshold { get; private set; }
|
||||
public static float DamageSkillMultiplier { get; private set; }
|
||||
|
||||
public static void Load(XElement element)
|
||||
{
|
||||
ReviveChancePerSkill = Math.Max(element.GetAttributeFloat("revivechanceperskill", 0.01f), 0.0f);
|
||||
ReviveChanceExponent = Math.Max(element.GetAttributeFloat("revivechanceexponent", 2.0f), 0.0f);
|
||||
ReviveChanceMin = MathHelper.Clamp(element.GetAttributeFloat("revivechancemin", 0.05f), 0.0f, 1.0f);
|
||||
ReviveChanceMax = MathHelper.Clamp(element.GetAttributeFloat("revivechancemax", 0.9f), ReviveChanceMin, 1.0f);
|
||||
|
||||
StabilizationPerSkill = Math.Max(element.GetAttributeFloat("stabilizationperskill", 0.01f), 0.0f);
|
||||
StabilizationMin = MathHelper.Max(element.GetAttributeFloat("stabilizationmin", 0.05f), 0.0f);
|
||||
StabilizationMax = MathHelper.Max(element.GetAttributeFloat("stabilizationmax", 2.0f), StabilizationMin);
|
||||
|
||||
DamageSkillThreshold = MathHelper.Clamp(element.GetAttributeFloat("damageskillthreshold", 40.0f), 0.0f, 100.0f);
|
||||
DamageSkillMultiplier = MathHelper.Clamp(element.GetAttributeFloat("damageskillmultiplier", 0.1f), 0.0f, 100.0f);
|
||||
}
|
||||
}
|
||||
|
||||
class AfflictionPrefab
|
||||
{
|
||||
public class Effect
|
||||
{
|
||||
//this effect is applied when the strength is within this range
|
||||
public float MinStrength, MaxStrength;
|
||||
|
||||
public readonly float MinVitalityDecrease = 0.0f;
|
||||
public readonly float MaxVitalityDecrease = 0.0f;
|
||||
|
||||
//how much the strength of the affliction changes per second
|
||||
public readonly float StrengthChange = 0.0f;
|
||||
|
||||
public readonly bool MultiplyByMaxVitality;
|
||||
|
||||
public float MinScreenBlurStrength, MaxScreenBlurStrength;
|
||||
public float MinScreenDistortStrength, MaxScreenDistortStrength;
|
||||
public float MinRadialDistortStrength, MaxRadialDistortStrength;
|
||||
public float MinChromaticAberrationStrength, MaxChromaticAberrationStrength;
|
||||
|
||||
public string DialogFlag;
|
||||
|
||||
//statuseffects applied on the character when the affliction is active
|
||||
public readonly List<StatusEffect> StatusEffects = new List<StatusEffect>();
|
||||
|
||||
public Effect(XElement element, string parentDebugName)
|
||||
{
|
||||
MinStrength = element.GetAttributeFloat("minstrength", 0);
|
||||
MaxStrength = element.GetAttributeFloat("maxstrength", 0);
|
||||
|
||||
MultiplyByMaxVitality = element.GetAttributeBool("multiplybymaxvitality", false);
|
||||
|
||||
MinVitalityDecrease = element.GetAttributeFloat("minvitalitydecrease", 0.0f);
|
||||
MaxVitalityDecrease = element.GetAttributeFloat("maxvitalitydecrease", 0.0f);
|
||||
MaxVitalityDecrease = Math.Max(MinVitalityDecrease, MaxVitalityDecrease);
|
||||
|
||||
MinScreenDistortStrength = element.GetAttributeFloat("minscreendistort", 0.0f);
|
||||
MaxScreenDistortStrength = element.GetAttributeFloat("maxscreendistort", 0.0f);
|
||||
MaxScreenDistortStrength = Math.Max(MinScreenDistortStrength, MaxScreenDistortStrength);
|
||||
|
||||
MinRadialDistortStrength = element.GetAttributeFloat("minradialdistort", 0.0f);
|
||||
MaxRadialDistortStrength = element.GetAttributeFloat("maxradialdistort", 0.0f);
|
||||
MaxRadialDistortStrength = Math.Max(MinRadialDistortStrength, MaxRadialDistortStrength);
|
||||
|
||||
MinChromaticAberrationStrength = element.GetAttributeFloat("minchromaticaberration", 0.0f);
|
||||
MaxChromaticAberrationStrength = element.GetAttributeFloat("maxchromaticaberration", 0.0f);
|
||||
MaxChromaticAberrationStrength = Math.Max(MinChromaticAberrationStrength, MaxChromaticAberrationStrength);
|
||||
|
||||
MinScreenBlurStrength = element.GetAttributeFloat("minscreenblur", 0.0f);
|
||||
MaxScreenBlurStrength = element.GetAttributeFloat("maxscreenblur", 0.0f);
|
||||
MaxScreenBlurStrength = Math.Max(MinScreenBlurStrength, MaxScreenBlurStrength);
|
||||
|
||||
DialogFlag = element.GetAttributeString("dialogflag", "");
|
||||
|
||||
StrengthChange = element.GetAttributeFloat("strengthchange", 0.0f);
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "statuseffect":
|
||||
StatusEffects.Add(StatusEffect.Load(subElement, parentDebugName));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static AfflictionPrefab InternalDamage;
|
||||
public static AfflictionPrefab Bleeding;
|
||||
public static AfflictionPrefab Burn;
|
||||
public static AfflictionPrefab OxygenLow;
|
||||
public static AfflictionPrefab Bloodloss;
|
||||
public static AfflictionPrefab Pressure;
|
||||
public static AfflictionPrefab Stun;
|
||||
public static AfflictionPrefab Husk;
|
||||
|
||||
public static List<AfflictionPrefab> List = new List<AfflictionPrefab>();
|
||||
|
||||
//Arbitrary string that is used to identify the type of the affliction.
|
||||
//Afflictions with the same type stack up, and items may be configured to cure specific types of afflictions.
|
||||
public readonly string AfflictionType;
|
||||
|
||||
//Does the affliction affect a specific limb or the whole character
|
||||
public readonly bool LimbSpecific;
|
||||
|
||||
//If not a limb-specific affliction, which limb is the indicator shown on in the health menu
|
||||
//(e.g. mental health problems on head, lack of oxygen on torso...)
|
||||
public readonly LimbType IndicatorLimb;
|
||||
|
||||
public readonly string Identifier;
|
||||
|
||||
public readonly string Name, Description;
|
||||
|
||||
public readonly string CauseOfDeathDescription, SelfCauseOfDeathDescription;
|
||||
|
||||
//how high the strength has to be for the affliction to take affect
|
||||
public readonly float ActivationThreshold = 0.0f;
|
||||
//how high the strength has to be for the affliction icon to be shown in the UI
|
||||
public readonly float ShowIconThreshold = 0.0f;
|
||||
public readonly float MaxStrength = 100.0f;
|
||||
|
||||
public float BurnOverlayAlpha;
|
||||
public float DamageOverlayAlpha;
|
||||
|
||||
//steam achievement given when the affliction is removed from the controlled character
|
||||
public readonly string AchievementOnRemoved;
|
||||
|
||||
public readonly Sprite Icon;
|
||||
public readonly Color IconColor;
|
||||
|
||||
private List<Effect> effects = new List<Effect>();
|
||||
|
||||
private Dictionary<string, float> treatmentSuitability = new Dictionary<string, float>();
|
||||
|
||||
private readonly string typeName;
|
||||
|
||||
private readonly ConstructorInfo constructor;
|
||||
|
||||
public Dictionary<string, float> TreatmentSuitability
|
||||
{
|
||||
get { return treatmentSuitability; }
|
||||
}
|
||||
|
||||
public static void LoadAll(IEnumerable<string> filePaths)
|
||||
{
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
if (doc == null || doc.Root == null) continue;
|
||||
|
||||
foreach (XElement element in doc.Root.Elements())
|
||||
{
|
||||
switch (element.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "internaldamage":
|
||||
List.Add(InternalDamage = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "bleeding":
|
||||
List.Add(Bleeding = new AfflictionPrefab(element, typeof(AfflictionBleeding)));
|
||||
break;
|
||||
case "burn":
|
||||
List.Add(Burn = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "oxygenlow":
|
||||
List.Add(OxygenLow = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "bloodloss":
|
||||
List.Add(Bloodloss = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "pressure":
|
||||
List.Add(Pressure = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "stun":
|
||||
List.Add(Stun = new AfflictionPrefab(element, typeof(Affliction)));
|
||||
break;
|
||||
case "husk":
|
||||
case "afflictionhusk":
|
||||
List.Add(Husk = new AfflictionPrefab(element, typeof(AfflictionHusk)));
|
||||
break;
|
||||
case "cprsettings":
|
||||
CPRSettings.Load(element);
|
||||
break;
|
||||
default:
|
||||
List.Add(new AfflictionPrefab(element));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (InternalDamage == null) DebugConsole.ThrowError("Affliction \"Internal Damage\" not defined in the affliction prefabs.");
|
||||
if (Bleeding == null) DebugConsole.ThrowError("Affliction \"Bleeding\" not defined in the affliction prefabs.");
|
||||
if (Burn == null) DebugConsole.ThrowError("Affliction \"Burn\" not defined in the affliction prefabs.");
|
||||
if (OxygenLow == null) DebugConsole.ThrowError("Affliction \"OxygenLow\" not defined in the affliction prefabs.");
|
||||
if (Bloodloss == null) DebugConsole.ThrowError("Affliction \"Bloodloss\" not defined in the affliction prefabs.");
|
||||
if (Pressure == null) DebugConsole.ThrowError("Affliction \"Pressure\" not defined in the affliction prefabs.");
|
||||
if (Stun == null) DebugConsole.ThrowError("Affliction \"Stun\" not defined in the affliction prefabs.");
|
||||
if (Husk == null) DebugConsole.ThrowError("Affliction \"Husk\" not defined in the affliction prefabs.");
|
||||
}
|
||||
|
||||
public AfflictionPrefab(XElement element, Type type = null)
|
||||
{
|
||||
typeName = type == null ? element.Name.ToString() : type.Name;
|
||||
|
||||
Identifier = element.GetAttributeString("identifier", "");
|
||||
|
||||
AfflictionType = element.GetAttributeString("type", "");
|
||||
Name = TextManager.Get("AfflictionName." + Identifier, true) ?? element.GetAttributeString("name", "");
|
||||
Description = TextManager.Get("AfflictionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
|
||||
|
||||
LimbSpecific = element.GetAttributeBool("limbspecific", false);
|
||||
if (!LimbSpecific)
|
||||
{
|
||||
string indicatorLimbName = element.GetAttributeString("indicatorlimb", "Torso");
|
||||
if (!Enum.TryParse(indicatorLimbName, out IndicatorLimb))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in affliction prefab " + Name + " - limb type \"" + indicatorLimbName + "\" not found.");
|
||||
}
|
||||
}
|
||||
|
||||
ActivationThreshold = element.GetAttributeFloat("activationthreshold", 0.0f);
|
||||
ShowIconThreshold = element.GetAttributeFloat("showiconthreshold", ActivationThreshold);
|
||||
MaxStrength = element.GetAttributeFloat("maxstrength", 100.0f);
|
||||
|
||||
DamageOverlayAlpha = element.GetAttributeFloat("damageoverlayalpha", 0.0f);
|
||||
BurnOverlayAlpha = element.GetAttributeFloat("burnoverlayalpha", 0.0f);
|
||||
|
||||
CauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeath." + Identifier, true) ?? element.GetAttributeString("causeofdeathdescription", "");
|
||||
SelfCauseOfDeathDescription = TextManager.Get("AfflictionCauseOfDeathSelf." + Identifier, true) ?? element.GetAttributeString("selfcauseofdeathdescription", "");
|
||||
|
||||
AchievementOnRemoved = element.GetAttributeString("achievementonremoved", "");
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "icon":
|
||||
Icon = new Sprite(subElement);
|
||||
IconColor = subElement.GetAttributeColor("color", Color.White);
|
||||
break;
|
||||
case "effect":
|
||||
effects.Add(new Effect(subElement, Name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (type == null)
|
||||
{
|
||||
type = Type.GetType("Barotrauma." + typeName, true, true);
|
||||
if (type == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an affliction class of the type \"" + typeName + "\".");
|
||||
return;
|
||||
}
|
||||
|
||||
constructor = type.GetConstructor(new[] { typeof(AfflictionPrefab), typeof(float) });
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "AfflictionPrefab (" + Name + ")";
|
||||
}
|
||||
|
||||
public Affliction Instantiate(float strength, Character source = null)
|
||||
{
|
||||
object instance = null;
|
||||
try
|
||||
{
|
||||
instance = constructor.Invoke(new object[] { this, strength });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
|
||||
}
|
||||
Affliction affliction = instance as Affliction;
|
||||
affliction.Source = source;
|
||||
return affliction;
|
||||
}
|
||||
|
||||
public Effect GetActiveEffect(float currentStrength)
|
||||
{
|
||||
foreach (Effect effect in effects)
|
||||
{
|
||||
if (currentStrength > effect.MinStrength && currentStrength <= effect.MaxStrength) return effect;
|
||||
}
|
||||
|
||||
//if above the strength range of all effects, use the highest strength effect
|
||||
Effect strongestEffect = null;
|
||||
float largestStrength = currentStrength;
|
||||
foreach (Effect effect in effects)
|
||||
{
|
||||
if (currentStrength > effect.MaxStrength &&
|
||||
(strongestEffect == null || effect.MaxStrength > largestStrength))
|
||||
{
|
||||
strongestEffect = effect;
|
||||
largestStrength = effect.MaxStrength;
|
||||
}
|
||||
}
|
||||
return strongestEffect;
|
||||
}
|
||||
|
||||
public float GetTreatmentSuitability(Item item)
|
||||
{
|
||||
if (item == null || !treatmentSuitability.ContainsKey(item.Prefab.Identifier.ToLowerInvariant()))
|
||||
{
|
||||
return 0.0f;
|
||||
}
|
||||
return treatmentSuitability[item.Prefab.Identifier.ToLowerInvariant()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Barotrauma.Extensions;
|
||||
using System.Xml.Linq;
|
||||
using System;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class AfflictionPsychosis : Affliction
|
||||
{
|
||||
|
||||
public AfflictionPsychosis(AfflictionPrefab prefab, float strength) : base(prefab, strength)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public override void Update(CharacterHealth characterHealth, Limb targetLimb, float deltaTime)
|
||||
{
|
||||
base.Update(characterHealth, targetLimb, deltaTime);
|
||||
UpdateProjSpecific(characterHealth, targetLimb, deltaTime);
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(CharacterHealth characterHealth, Limb targetLimb, float deltaTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
using Lidgren.Network;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class CharacterHealth
|
||||
{
|
||||
class LimbHealth
|
||||
{
|
||||
public Sprite IndicatorSprite;
|
||||
|
||||
public Rectangle HighlightArea;
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
public readonly List<Affliction> Afflictions = new List<Affliction>();
|
||||
|
||||
public readonly Dictionary<string, float> VitalityMultipliers = new Dictionary<string, float>();
|
||||
public readonly Dictionary<string, float> VitalityTypeMultipliers = new Dictionary<string, float>();
|
||||
|
||||
private readonly CharacterHealth characterHealth;
|
||||
|
||||
public float TotalDamage
|
||||
{
|
||||
get { return Afflictions.Sum(a => a.GetVitalityDecrease(characterHealth)); }
|
||||
}
|
||||
|
||||
public LimbHealth() { }
|
||||
|
||||
public LimbHealth(XElement element, CharacterHealth characterHealth)
|
||||
{
|
||||
Name = TextManager.Get("HealthLimbName." + element.GetAttributeString("name", ""));
|
||||
this.characterHealth = characterHealth;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "sprite":
|
||||
IndicatorSprite = new Sprite(subElement);
|
||||
HighlightArea = subElement.GetAttributeRect("highlightarea", new Rectangle(0, 0, (int)IndicatorSprite.size.X, (int)IndicatorSprite.size.Y));
|
||||
break;
|
||||
case "vitalitymultiplier":
|
||||
if (subElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in character health config (" + characterHealth.Character.Name + ") - define vitality multipliers using affliction identifiers or types instead of names.");
|
||||
continue;
|
||||
}
|
||||
|
||||
string afflictionIdentifier = subElement.GetAttributeString("identifier", "");
|
||||
string afflictionType = subElement.GetAttributeString("type", "");
|
||||
float multiplier = subElement.GetAttributeFloat("multiplier", 1.0f);
|
||||
if (!string.IsNullOrEmpty(afflictionIdentifier))
|
||||
{
|
||||
VitalityMultipliers.Add(afflictionIdentifier.ToLowerInvariant(), multiplier);
|
||||
}
|
||||
else
|
||||
{
|
||||
VitalityTypeMultipliers.Add(afflictionType.ToLowerInvariant(), multiplier);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Affliction> GetActiveAfflictions(AfflictionPrefab prefab)
|
||||
{
|
||||
return Afflictions.FindAll(a => a.Prefab == prefab);
|
||||
}
|
||||
public List<Affliction> GetActiveAfflictions(string afflictionType)
|
||||
{
|
||||
return Afflictions.FindAll(a => a.Prefab.AfflictionType == afflictionType);
|
||||
}
|
||||
}
|
||||
|
||||
const float InsufficientOxygenThreshold = 30.0f;
|
||||
const float LowOxygenThreshold = 50.0f;
|
||||
protected float minVitality, maxVitality;
|
||||
|
||||
public bool Unkillable;
|
||||
|
||||
//bleeding settings
|
||||
public bool DoesBleed { get; private set; }
|
||||
|
||||
public bool UseHealthWindow { get; set; }
|
||||
|
||||
private List<LimbHealth> limbHealths = new List<LimbHealth>();
|
||||
//non-limb-specific afflictions
|
||||
private List<Affliction> afflictions = new List<Affliction>();
|
||||
|
||||
private HashSet<Affliction> irremovableAfflictions = new HashSet<Affliction>();
|
||||
private Affliction bloodlossAffliction;
|
||||
private Affliction oxygenLowAffliction;
|
||||
private Affliction pressureAffliction;
|
||||
private Affliction stunAffliction;
|
||||
|
||||
public bool IsUnconscious
|
||||
{
|
||||
get { return Vitality <= 0.0f; }
|
||||
}
|
||||
|
||||
public float CrushDepth { get; private set; }
|
||||
|
||||
public float Vitality { get; private set; }
|
||||
|
||||
public float MaxVitality
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Character?.Info?.Job?.Prefab != null)
|
||||
{
|
||||
return maxVitality + Character.Info.Job.Prefab.VitalityModifier;
|
||||
}
|
||||
return maxVitality;
|
||||
}
|
||||
}
|
||||
|
||||
public float MinVitality
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Character?.Info?.Job?.Prefab != null)
|
||||
{
|
||||
return -MaxVitality;
|
||||
}
|
||||
return minVitality;
|
||||
}
|
||||
}
|
||||
|
||||
public float OxygenAmount
|
||||
{
|
||||
get
|
||||
{
|
||||
if (!Character.NeedsAir || Unkillable) return 100.0f;
|
||||
return -oxygenLowAffliction.Strength + 100;
|
||||
}
|
||||
set
|
||||
{
|
||||
if (!Character.NeedsAir || Unkillable) return;
|
||||
oxygenLowAffliction.Strength = MathHelper.Clamp(-value + 100, 0.0f, 200.0f);
|
||||
}
|
||||
}
|
||||
|
||||
public float BloodlossAmount
|
||||
{
|
||||
get { return bloodlossAffliction.Strength; }
|
||||
set { bloodlossAffliction.Strength = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public float StunTimer
|
||||
{
|
||||
get { return stunAffliction.Strength; }
|
||||
set { stunAffliction.Strength = MathHelper.Clamp(value, 0.0f, stunAffliction.Prefab.MaxStrength); }
|
||||
}
|
||||
|
||||
public Affliction PressureAffliction
|
||||
{
|
||||
get { return pressureAffliction; }
|
||||
}
|
||||
|
||||
public Character Character { get; private set; }
|
||||
|
||||
public CharacterHealth(Character character)
|
||||
{
|
||||
this.Character = character;
|
||||
Vitality = 100.0f;
|
||||
maxVitality = 100.0f;
|
||||
|
||||
DoesBleed = true;
|
||||
UseHealthWindow = false;
|
||||
|
||||
InitIrremovableAfflictions();
|
||||
|
||||
limbHealths.Add(new LimbHealth());
|
||||
|
||||
InitProjSpecific(null, character);
|
||||
}
|
||||
|
||||
public CharacterHealth(XElement element, Character character)
|
||||
{
|
||||
this.Character = character;
|
||||
InitIrremovableAfflictions();
|
||||
|
||||
CrushDepth = element.GetAttributeFloat("crushdepth", float.NegativeInfinity);
|
||||
|
||||
maxVitality = element.GetAttributeFloat("vitality", 100.0f);
|
||||
Vitality = maxVitality;
|
||||
|
||||
DoesBleed = element.GetAttributeBool("doesbleed", true);
|
||||
UseHealthWindow = element.GetAttributeBool("usehealthwindow", false);
|
||||
|
||||
minVitality = (character.ConfigPath == Character.HumanConfigFile) ? -100.0f : 0.0f;
|
||||
|
||||
limbHealths.Clear();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "limb") continue;
|
||||
limbHealths.Add(new LimbHealth(subElement, this));
|
||||
}
|
||||
if (limbHealths.Count == 0)
|
||||
{
|
||||
limbHealths.Add(new LimbHealth());
|
||||
}
|
||||
|
||||
InitProjSpecific(element, character);
|
||||
}
|
||||
|
||||
private void InitIrremovableAfflictions()
|
||||
{
|
||||
irremovableAfflictions.Add(bloodlossAffliction = new Affliction(AfflictionPrefab.Bloodloss, 0.0f));
|
||||
irremovableAfflictions.Add(stunAffliction = new Affliction(AfflictionPrefab.Stun, 0.0f));
|
||||
irremovableAfflictions.Add(pressureAffliction = new Affliction(AfflictionPrefab.Pressure, 0.0f));
|
||||
irremovableAfflictions.Add(oxygenLowAffliction = new Affliction(AfflictionPrefab.OxygenLow, 0.0f));
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
{
|
||||
afflictions.Add(affliction);
|
||||
}
|
||||
}
|
||||
|
||||
partial void InitProjSpecific(XElement element, Character character);
|
||||
|
||||
public IEnumerable<Affliction> GetAllAfflictions()
|
||||
{
|
||||
return afflictions.Concat(limbHealths.SelectMany(lh => lh.Afflictions).ToList());
|
||||
}
|
||||
|
||||
public Affliction GetAffliction(string afflictionType, bool allowLimbAfflictions = true)
|
||||
{
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
|
||||
}
|
||||
if (!allowLimbAfflictions) return null;
|
||||
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
foreach (Affliction affliction in limbHealth.Afflictions)
|
||||
{
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Affliction GetAffliction(string afflictionType, Limb limb)
|
||||
{
|
||||
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
|
||||
{
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) return affliction;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Limb GetAfflictionLimb(Affliction affliction)
|
||||
{
|
||||
for (int i = 0; i < limbHealths.Count; i++)
|
||||
{
|
||||
if (!limbHealths[i].Afflictions.Contains(affliction)) continue;
|
||||
return Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the total strength of the afflictions of a specific type attached to a specific limb
|
||||
/// </summary>
|
||||
/// <param name="afflictionType">Type of the affliction</param>
|
||||
/// <param name="limb">The limb the affliction is attached to</param>
|
||||
/// <param name="requireLimbSpecific">Does the affliction have to be attached to only the specific limb.
|
||||
/// Most monsters for example don't have separate healths for different limbs, essentially meaning that every affliction is applied to every limb.</param>
|
||||
public float GetAfflictionStrength(string afflictionType, Limb limb, bool requireLimbSpecific)
|
||||
{
|
||||
if (requireLimbSpecific && limbHealths.Count == 1) return 0.0f;
|
||||
|
||||
float strength = 0.0f;
|
||||
foreach (Affliction affliction in limbHealths[limb.HealthIndex].Afflictions)
|
||||
{
|
||||
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
|
||||
}
|
||||
return strength;
|
||||
}
|
||||
|
||||
public float GetAfflictionStrength(string afflictionType, bool allowLimbAfflictions = true)
|
||||
{
|
||||
float strength = 0.0f;
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
|
||||
}
|
||||
if (!allowLimbAfflictions) return strength;
|
||||
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
foreach (Affliction affliction in limbHealth.Afflictions)
|
||||
{
|
||||
if (affliction.Strength < affliction.Prefab.ActivationThreshold) continue;
|
||||
if (affliction.Prefab.AfflictionType == afflictionType) strength += affliction.Strength;
|
||||
}
|
||||
}
|
||||
|
||||
return strength;
|
||||
}
|
||||
|
||||
public void ApplyAffliction(Limb targetLimb, Affliction affliction)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (affliction.Prefab.LimbSpecific)
|
||||
{
|
||||
if (targetLimb == null)
|
||||
{
|
||||
//if a limb-specific affliction is applied to no specific limb, apply to all limbs
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
AddLimbAffliction(limbHealth, affliction);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddLimbAffliction(targetLimb, affliction);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAffliction(affliction);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReduceAffliction(Limb targetLimb, string affliction, float amount)
|
||||
{
|
||||
affliction = affliction.ToLowerInvariant();
|
||||
|
||||
List<Affliction> matchingAfflictions = new List<Affliction>(afflictions);
|
||||
|
||||
if (targetLimb != null)
|
||||
{
|
||||
matchingAfflictions.AddRange(limbHealths[targetLimb.HealthIndex].Afflictions);
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
matchingAfflictions.AddRange(limbHealth.Afflictions);
|
||||
}
|
||||
}
|
||||
matchingAfflictions.RemoveAll(a =>
|
||||
a.Prefab.Identifier.ToLowerInvariant() != affliction &&
|
||||
a.Prefab.AfflictionType.ToLowerInvariant() != affliction);
|
||||
|
||||
if (matchingAfflictions.Count == 0) return;
|
||||
|
||||
do
|
||||
{
|
||||
float reduceAmount = amount / matchingAfflictions.Count;
|
||||
for (int i = matchingAfflictions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var matchingAffliction = matchingAfflictions[i];
|
||||
if (matchingAffliction.Strength < reduceAmount)
|
||||
{
|
||||
amount -= matchingAffliction.Strength;
|
||||
matchingAffliction.Strength = 0.0f;
|
||||
matchingAfflictions.RemoveAt(i);
|
||||
SteamAchievementManager.OnAfflictionRemoved(matchingAffliction, Character);
|
||||
}
|
||||
else
|
||||
{
|
||||
matchingAffliction.Strength -= reduceAmount;
|
||||
amount -= reduceAmount;
|
||||
}
|
||||
}
|
||||
} while (matchingAfflictions.Count > 0 && amount > 0.0f);
|
||||
|
||||
}
|
||||
|
||||
public void ApplyDamage(Limb hitLimb, AttackResult attackResult)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
if (hitLimb.HealthIndex < 0 || hitLimb.HealthIndex >= limbHealths.Count)
|
||||
{
|
||||
DebugConsole.ThrowError("Limb health index out of bounds. Character\"" + Character.Name +
|
||||
"\" only has health configured for" + limbHealths.Count + " limbs but the limb " + hitLimb.type + " is targeting index " + hitLimb.HealthIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (Affliction newAffliction in attackResult.Afflictions)
|
||||
{
|
||||
if (newAffliction.Prefab.LimbSpecific)
|
||||
{
|
||||
AddLimbAffliction(hitLimb, newAffliction);
|
||||
}
|
||||
else
|
||||
{
|
||||
AddAffliction(newAffliction);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
limbHealth.Afflictions.RemoveAll(a =>
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.InternalDamage.AfflictionType ||
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.Burn.AfflictionType ||
|
||||
a.Prefab.AfflictionType == AfflictionPrefab.Bleeding.AfflictionType);
|
||||
|
||||
if (damageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damageAmount));
|
||||
if (bleedingDamageAmount > 0.0f && DoesBleed) limbHealth.Afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamageAmount));
|
||||
if (burnDamageAmount > 0.0f) limbHealth.Afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamageAmount));
|
||||
}
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) { Kill(); }
|
||||
}
|
||||
|
||||
public void RemoveAllAfflictions()
|
||||
{
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
limbHealth.Afflictions.Clear();
|
||||
}
|
||||
|
||||
afflictions.RemoveAll(a => !irremovableAfflictions.Contains(a));
|
||||
foreach (Affliction affliction in irremovableAfflictions)
|
||||
{
|
||||
affliction.Strength = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
private void AddLimbAffliction(Limb limb, Affliction newAffliction)
|
||||
{
|
||||
if (!newAffliction.Prefab.LimbSpecific) return;
|
||||
AddLimbAffliction(limbHealths[limb.HealthIndex], newAffliction);
|
||||
}
|
||||
|
||||
private void AddLimbAffliction(LimbHealth limbHealth, Affliction newAffliction)
|
||||
{
|
||||
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
|
||||
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
|
||||
|
||||
foreach (Affliction affliction in limbHealth.Afflictions)
|
||||
{
|
||||
if (newAffliction.Prefab == affliction.Prefab)
|
||||
{
|
||||
affliction.Strength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + newAffliction.Strength * (100.0f / MaxVitality));
|
||||
affliction.Source = newAffliction.Source;
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) Kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
|
||||
//or modify the affliction instance of an Attack or a StatusEffect
|
||||
|
||||
var copyAffliction = newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality)),
|
||||
newAffliction.Source);
|
||||
limbHealth.Afflictions.Add(copyAffliction);
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) Kill();
|
||||
}
|
||||
|
||||
|
||||
private void AddAffliction(Affliction newAffliction)
|
||||
{
|
||||
if (!DoesBleed && newAffliction is AfflictionBleeding) return;
|
||||
if (!Character.NeedsAir && newAffliction.Prefab == AfflictionPrefab.OxygenLow) return;
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
if (newAffliction.Prefab == affliction.Prefab)
|
||||
{
|
||||
float newStrength = Math.Min(affliction.Prefab.MaxStrength, affliction.Strength + newAffliction.Strength * (100.0f / MaxVitality));
|
||||
if (affliction == stunAffliction) { Character.SetStun(newStrength, true, true); }
|
||||
affliction.Strength = newStrength;
|
||||
affliction.Source = newAffliction.Source;
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) Kill();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//create a new instance of the affliction to make sure we don't use the same instance for multiple characters
|
||||
//or modify the affliction instance of an Attack or a StatusEffect
|
||||
afflictions.Add(newAffliction.Prefab.Instantiate(
|
||||
Math.Min(newAffliction.Prefab.MaxStrength, newAffliction.Strength * (100.0f / MaxVitality)),
|
||||
source: newAffliction.Source));
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) Kill();
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
UpdateOxygen(deltaTime);
|
||||
|
||||
for (int i = 0; i < limbHealths.Count; i++)
|
||||
{
|
||||
for (int j = limbHealths[i].Afflictions.Count - 1; j >= 0; j--)
|
||||
{
|
||||
if (limbHealths[i].Afflictions[j].Strength <= 0.0f)
|
||||
{
|
||||
SteamAchievementManager.OnAfflictionRemoved(limbHealths[i].Afflictions[j], Character);
|
||||
limbHealths[i].Afflictions.RemoveAt(j);
|
||||
}
|
||||
}
|
||||
foreach (Affliction affliction in limbHealths[i].Afflictions)
|
||||
{
|
||||
Limb targetLimb = Character.AnimController.Limbs.FirstOrDefault(l => l.HealthIndex == i);
|
||||
affliction.Update(this, targetLimb, deltaTime);
|
||||
affliction.DamagePerSecondTimer += deltaTime;
|
||||
if (affliction is AfflictionBleeding)
|
||||
{
|
||||
UpdateBleedingProjSpecific((AfflictionBleeding)affliction, targetLimb, deltaTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = afflictions.Count - 1; i >= 0; i--)
|
||||
{
|
||||
if (irremovableAfflictions.Contains(afflictions[i])) continue;
|
||||
if (afflictions[i].Strength <= 0.0f)
|
||||
{
|
||||
SteamAchievementManager.OnAfflictionRemoved(afflictions[i], Character);
|
||||
afflictions.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < afflictions.Count; i++)
|
||||
{
|
||||
afflictions[i].Update(this, null, deltaTime);
|
||||
afflictions[i].DamagePerSecondTimer += deltaTime;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
foreach (Limb limb in Character.AnimController.Limbs)
|
||||
{
|
||||
limb.BurnOverlayStrength = 0.0f;
|
||||
limb.DamageOverlayStrength = 0.0f;
|
||||
if (limbHealths[limb.HealthIndex].Afflictions.Count == 0) continue;
|
||||
foreach (Affliction a in limbHealths[limb.HealthIndex].Afflictions)
|
||||
{
|
||||
limb.BurnOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.BurnOverlayAlpha;
|
||||
limb.DamageOverlayStrength += a.Strength / a.Prefab.MaxStrength * a.Prefab.DamageOverlayAlpha;
|
||||
}
|
||||
limb.BurnOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
|
||||
limb.DamageOverlayStrength /= limbHealths[limb.HealthIndex].Afflictions.Count;
|
||||
}
|
||||
#endif
|
||||
|
||||
CalculateVitality();
|
||||
if (Vitality <= MinVitality) Kill();
|
||||
}
|
||||
|
||||
private void UpdateOxygen(float deltaTime)
|
||||
{
|
||||
if (!Character.NeedsAir) return;
|
||||
|
||||
float prevOxygen = OxygenAmount;
|
||||
if (IsUnconscious)
|
||||
{
|
||||
//the character dies of oxygen deprivation in 100 seconds after losing consciousness
|
||||
OxygenAmount = MathHelper.Clamp(OxygenAmount - 1.0f * deltaTime, -100.0f, 100.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
OxygenAmount = MathHelper.Clamp(OxygenAmount + deltaTime * (Character.OxygenAvailable < InsufficientOxygenThreshold ? -5.0f : 10.0f), -100.0f, 100.0f);
|
||||
}
|
||||
|
||||
UpdateOxygenProjSpecific(prevOxygen);
|
||||
}
|
||||
|
||||
partial void UpdateOxygenProjSpecific(float prevOxygen);
|
||||
|
||||
partial void UpdateBleedingProjSpecific(AfflictionBleeding affliction, Limb targetLimb, float deltaTime);
|
||||
|
||||
public void CalculateVitality()
|
||||
{
|
||||
Vitality = MaxVitality;
|
||||
if (Unkillable) { return; }
|
||||
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
foreach (Affliction affliction in limbHealth.Afflictions)
|
||||
{
|
||||
float vitalityDecrease = affliction.GetVitalityDecrease(this);
|
||||
if (limbHealth.VitalityMultipliers.ContainsKey(affliction.Prefab.Identifier.ToLowerInvariant()))
|
||||
{
|
||||
vitalityDecrease *= limbHealth.VitalityMultipliers[affliction.Prefab.Identifier.ToLowerInvariant()];
|
||||
}
|
||||
if (limbHealth.VitalityTypeMultipliers.ContainsKey(affliction.Prefab.AfflictionType.ToLowerInvariant()))
|
||||
{
|
||||
vitalityDecrease *= limbHealth.VitalityTypeMultipliers[affliction.Prefab.AfflictionType.ToLowerInvariant()];
|
||||
}
|
||||
Vitality -= vitalityDecrease;
|
||||
affliction.CalculateDamagePerSecond(vitalityDecrease);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Affliction affliction in afflictions)
|
||||
{
|
||||
float vitalityDecrease = affliction.GetVitalityDecrease(this);
|
||||
Vitality -= vitalityDecrease;
|
||||
affliction.CalculateDamagePerSecond(vitalityDecrease);
|
||||
}
|
||||
}
|
||||
|
||||
private void Kill()
|
||||
{
|
||||
if (Unkillable) { return; }
|
||||
var causeOfDeath = GetCauseOfDeath();
|
||||
Character.Kill(causeOfDeath.First, causeOfDeath.Second);
|
||||
}
|
||||
|
||||
public Pair<CauseOfDeathType, Affliction> GetCauseOfDeath()
|
||||
{
|
||||
List<Affliction> currentAfflictions = GetAllAfflictions(true);
|
||||
|
||||
Affliction strongestAffliction = null;
|
||||
float largestStrength = 0.0f;
|
||||
foreach (Affliction affliction in currentAfflictions)
|
||||
{
|
||||
if (strongestAffliction == null || affliction.GetVitalityDecrease(this) > largestStrength)
|
||||
{
|
||||
strongestAffliction = affliction;
|
||||
largestStrength = affliction.GetVitalityDecrease(this);
|
||||
}
|
||||
}
|
||||
|
||||
CauseOfDeathType causeOfDeath = strongestAffliction == null ? CauseOfDeathType.Unknown : CauseOfDeathType.Affliction;
|
||||
if (strongestAffliction == oxygenLowAffliction)
|
||||
{
|
||||
causeOfDeath = Character.AnimController.InWater ? CauseOfDeathType.Drowning : CauseOfDeathType.Suffocation;
|
||||
}
|
||||
|
||||
return new Pair<CauseOfDeathType, Affliction>(causeOfDeath, strongestAffliction);
|
||||
}
|
||||
|
||||
private List<Affliction> GetAllAfflictions(bool mergeSameAfflictions)
|
||||
{
|
||||
List<Affliction> allAfflictions = new List<Affliction>(afflictions);
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
allAfflictions.AddRange(limbHealth.Afflictions);
|
||||
}
|
||||
|
||||
if (mergeSameAfflictions)
|
||||
{
|
||||
List<Affliction> mergedAfflictions = new List<Affliction>();
|
||||
foreach (Affliction affliction in allAfflictions)
|
||||
{
|
||||
var existingAffliction = mergedAfflictions.Find(a => a.Prefab == affliction.Prefab);
|
||||
if (existingAffliction == null)
|
||||
{
|
||||
var newAffliction = affliction.Prefab.Instantiate(affliction.Strength);
|
||||
newAffliction.DamagePerSecond = affliction.DamagePerSecond;
|
||||
newAffliction.DamagePerSecondTimer = affliction.DamagePerSecondTimer;
|
||||
mergedAfflictions.Add(newAffliction);
|
||||
}
|
||||
else
|
||||
{
|
||||
existingAffliction.DamagePerSecond += affliction.DamagePerSecond;
|
||||
existingAffliction.Strength += affliction.Strength;
|
||||
}
|
||||
}
|
||||
|
||||
return mergedAfflictions;
|
||||
}
|
||||
|
||||
return allAfflictions;
|
||||
}
|
||||
|
||||
public void ServerWrite(NetBuffer msg)
|
||||
{
|
||||
List<Affliction> activeAfflictions = afflictions.FindAll(a => a.Strength > 0.0f && a.Strength >= a.Prefab.ActivationThreshold);
|
||||
|
||||
msg.Write((byte)activeAfflictions.Count);
|
||||
foreach (Affliction affliction in activeAfflictions)
|
||||
{
|
||||
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(affliction.Prefab));
|
||||
msg.Write(affliction.Strength);
|
||||
}
|
||||
|
||||
List<Pair<LimbHealth, Affliction>> limbAfflictions = new List<Pair<LimbHealth, Affliction>>();
|
||||
foreach (LimbHealth limbHealth in limbHealths)
|
||||
{
|
||||
foreach (Affliction limbAffliction in limbHealth.Afflictions)
|
||||
{
|
||||
if (limbAffliction.Strength <= 0.0f || limbAffliction.Strength < limbAffliction.Prefab.ActivationThreshold) continue;
|
||||
limbAfflictions.Add(new Pair<LimbHealth, Affliction>(limbHealth, limbAffliction));
|
||||
}
|
||||
}
|
||||
|
||||
msg.Write((byte)limbAfflictions.Count);
|
||||
foreach (var limbAffliction in limbAfflictions)
|
||||
{
|
||||
msg.WriteRangedInteger(0, limbHealths.Count - 1, limbHealths.IndexOf(limbAffliction.First));
|
||||
msg.WriteRangedInteger(0, AfflictionPrefab.List.Count - 1, AfflictionPrefab.List.IndexOf(limbAffliction.Second.Prefab));
|
||||
msg.Write(limbAffliction.Second.Strength);
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
RemoveProjSpecific();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class DamageModifier
|
||||
{
|
||||
[Serialize(1.0f, false)]
|
||||
public float DamageMultiplier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("0.0,360", false)]
|
||||
public Vector2 ArmorSector
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool IsArmor
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool DeflectProjectiles
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string[] AfflictionIdentifiers
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string[] AfflictionTypes
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if CLIENT
|
||||
[Serialize("", false)]
|
||||
public string DamageSound
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
#endif
|
||||
|
||||
public DamageModifier(XElement element, string parentDebugName)
|
||||
{
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
ArmorSector = new Vector2(MathHelper.ToRadians(ArmorSector.X), MathHelper.ToRadians(ArmorSector.Y));
|
||||
|
||||
if (element.Attribute("afflictionnames") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in DamageModifier config (" + parentDebugName + ") - define afflictions using identifiers or types instead of names.");
|
||||
}
|
||||
|
||||
AfflictionIdentifiers = element.GetAttributeStringArray("afflictionidentifiers", new string[0]);
|
||||
for (int i = 0; i < AfflictionIdentifiers.Length; i++)
|
||||
{
|
||||
AfflictionIdentifiers[i] = AfflictionIdentifiers[i].ToLowerInvariant();
|
||||
}
|
||||
AfflictionTypes = element.GetAttributeStringArray("afflictiontypes", new string[0]);
|
||||
for (int i = 0; i < AfflictionTypes.Length; i++)
|
||||
{
|
||||
AfflictionTypes[i] = AfflictionTypes[i].ToLowerInvariant();
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesAffliction(Affliction affliction)
|
||||
{
|
||||
foreach (string afflictionName in AfflictionIdentifiers)
|
||||
{
|
||||
if (affliction.Prefab.Identifier.ToLowerInvariant() == afflictionName) return true;
|
||||
}
|
||||
foreach (string afflictionType in AfflictionTypes)
|
||||
{
|
||||
if (affliction.Prefab.AfflictionType.ToLowerInvariant() == afflictionType) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ namespace Barotrauma
|
||||
{
|
||||
class Job
|
||||
{
|
||||
|
||||
private readonly JobPrefab prefab;
|
||||
|
||||
private Dictionary<string, Skill> skills;
|
||||
@@ -31,12 +30,7 @@ namespace Barotrauma
|
||||
{
|
||||
get { return prefab.Items; }
|
||||
}
|
||||
|
||||
//public List<bool> EquipSpawnItem
|
||||
//{
|
||||
// get { return prefab.EquipItem; }
|
||||
//}
|
||||
|
||||
|
||||
public List<Skill> Skills
|
||||
{
|
||||
get { return skills.Values.ToList(); }
|
||||
@@ -49,61 +43,94 @@ namespace Barotrauma
|
||||
skills = new Dictionary<string, Skill>();
|
||||
foreach (SkillPrefab skillPrefab in prefab.Skills)
|
||||
{
|
||||
skills.Add(skillPrefab.Name, new Skill(skillPrefab));
|
||||
skills.Add(skillPrefab.Identifier, new Skill(skillPrefab));
|
||||
}
|
||||
}
|
||||
|
||||
public Job(XElement element)
|
||||
{
|
||||
string name = element.GetAttributeString("name", "").ToLowerInvariant();
|
||||
prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name);
|
||||
string identifier = element.GetAttributeString("identifier", "").ToLowerInvariant();
|
||||
prefab = JobPrefab.List.Find(jp => jp.Identifier.ToLowerInvariant() == identifier);
|
||||
|
||||
string name = "";
|
||||
if (prefab == null)
|
||||
{
|
||||
name = element.GetAttributeString("name", "").ToLowerInvariant();
|
||||
prefab = JobPrefab.List.Find(jp => jp.Name.ToLowerInvariant() == name);
|
||||
}
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find the job \"" + name + "\" (identifier " + identifier + "). Giving the character a random job.");
|
||||
prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count)];
|
||||
}
|
||||
|
||||
skills = new Dictionary<string, Skill>();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "skill") continue;
|
||||
string skillName = subElement.GetAttributeString("name", "");
|
||||
if (string.IsNullOrEmpty(name)) continue;
|
||||
string skillIdentifier = subElement.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrEmpty(skillIdentifier)) continue;
|
||||
skills.Add(
|
||||
skillName,
|
||||
new Skill(skillName, subElement.GetAttributeInt("level", 0)));
|
||||
skillIdentifier,
|
||||
new Skill(skillIdentifier, subElement.GetAttributeFloat("level", 0)));
|
||||
}
|
||||
}
|
||||
|
||||
public static Job Random()
|
||||
public static Job Random(Rand.RandSync randSync)
|
||||
{
|
||||
JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, Rand.RandSync.Server)];
|
||||
JobPrefab prefab = JobPrefab.List[Rand.Int(JobPrefab.List.Count - 1, randSync)];
|
||||
|
||||
return new Job(prefab);
|
||||
}
|
||||
|
||||
public int GetSkillLevel(string skillName)
|
||||
public float GetSkillLevel(string skillIdentifier)
|
||||
{
|
||||
Skill skill = null;
|
||||
skills.TryGetValue(skillName, out skill);
|
||||
skills.TryGetValue(skillIdentifier, out Skill skill);
|
||||
|
||||
return (skill==null) ? 0 : skill.Level;
|
||||
return (skill == null) ? 0.0f : skill.Level;
|
||||
}
|
||||
|
||||
public void GiveJobItems(Character character, WayPoint spawnPoint)
|
||||
public void IncreaseSkillLevel(string skillIdentifier, float increase)
|
||||
{
|
||||
if (skills.TryGetValue(skillIdentifier, out Skill skill))
|
||||
{
|
||||
skill.Level += increase;
|
||||
}
|
||||
}
|
||||
|
||||
public void GiveJobItems(Character character, WayPoint spawnPoint = null)
|
||||
{
|
||||
if (SpawnItems == null) return;
|
||||
|
||||
foreach (XElement itemElement in SpawnItems.Elements())
|
||||
{
|
||||
InitializeJobItem(character, spawnPoint, itemElement);
|
||||
InitializeJobItem(character, itemElement, spawnPoint);
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeJobItem(Character character, WayPoint spawnPoint, XElement itemElement, Item parentItem = null)
|
||||
private void InitializeJobItem(Character character, XElement itemElement, WayPoint spawnPoint = null, Item parentItem = null)
|
||||
{
|
||||
string itemName = itemElement.GetAttributeString("name", "");
|
||||
|
||||
ItemPrefab itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
ItemPrefab itemPrefab;
|
||||
if (itemElement.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
|
||||
return;
|
||||
string itemName = itemElement.Attribute("name").Value;
|
||||
DebugConsole.ThrowError("Error in Job config (" + Name + ") - use item identifiers instead of names to configure the items.");
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemName + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Tried to spawn \"" + Name + "\" with the item \"" + itemIdentifier + "\". Matching item prefab not found.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Item item = new Item(itemPrefab, character.Position, null);
|
||||
@@ -125,7 +152,7 @@ namespace Barotrauma
|
||||
character.Inventory.TryPutItem(item, null, item.AllowedSlots);
|
||||
}
|
||||
|
||||
if (item.Prefab.NameMatches("ID Card") && spawnPoint != null)
|
||||
if (item.Prefab.Identifier == "idcard" && spawnPoint != null)
|
||||
{
|
||||
foreach (string s in spawnPoint.IdCardTags)
|
||||
{
|
||||
@@ -146,7 +173,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (XElement childItemElement in itemElement.Elements())
|
||||
{
|
||||
InitializeJobItem(character, spawnPoint, childItemElement, item);
|
||||
InitializeJobItem(character, childItemElement, spawnPoint, item);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +185,7 @@ namespace Barotrauma
|
||||
|
||||
foreach (KeyValuePair<string, Skill> skill in skills)
|
||||
{
|
||||
jobElement.Add(new XElement("skill", new XAttribute("name", skill.Value.Name), new XAttribute("level", skill.Value.Level)));
|
||||
jobElement.Add(new XElement("skill", new XAttribute("identifier", skill.Value.Identifier), new XAttribute("level", skill.Value.Level)));
|
||||
}
|
||||
|
||||
parentElement.Add(jobElement);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -11,31 +12,53 @@ namespace Barotrauma
|
||||
public readonly List<string> ItemNames;
|
||||
|
||||
public List<SkillPrefab> Skills;
|
||||
|
||||
[Serialize("1,1,1,1", false)]
|
||||
public Color UIColor
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//the number of these characters in the crew the player starts with
|
||||
public readonly int InitialCount;
|
||||
[Serialize("notfound", false)]
|
||||
public string Identifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("notfound", false)]
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize("", false)]
|
||||
public string Description
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//the number of these characters in the crew the player starts with in the single player campaign
|
||||
[Serialize(0, false)]
|
||||
public int InitialCount
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//if set to true, a client that has chosen this as their preferred job will get it no matter what
|
||||
[Serialize(false, false)]
|
||||
public bool AllowAlways
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//how many crew members can have the job (only one captain etc)
|
||||
//how many crew members can have the job (only one captain etc)
|
||||
[Serialize(100, false)]
|
||||
public int MaxNumber
|
||||
{
|
||||
get;
|
||||
@@ -44,39 +67,46 @@ namespace Barotrauma
|
||||
|
||||
//how many crew members are REQUIRED to have the job
|
||||
//(i.e. if one captain is required, one captain is chosen even if all the players have set captain to lowest preference)
|
||||
[Serialize(0, false)]
|
||||
public int MinNumber
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
public float MinKarma
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
[Serialize(10.0f, false)]
|
||||
public float Commonness
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//how much the vitality of the character is increased/reduced from the default value
|
||||
[Serialize(0.0f, false)]
|
||||
public float VitalityModifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public XElement ClothingElement { get; private set; }
|
||||
|
||||
public JobPrefab(XElement element)
|
||||
{
|
||||
Name = element.GetAttributeString("name", "name not found");
|
||||
SerializableProperty.DeserializeProperties(this, element);
|
||||
|
||||
Description = element.GetAttributeString("description", "");
|
||||
string translatedName = TextManager.Get("JobName." + Identifier, true);
|
||||
if (!string.IsNullOrEmpty(translatedName)) Name = translatedName;
|
||||
|
||||
MinNumber = element.GetAttributeInt("minnumber", 0);
|
||||
MaxNumber = element.GetAttributeInt("maxnumber", 10);
|
||||
MinKarma = element.GetAttributeFloat("minkarma", 0.0f);
|
||||
|
||||
InitialCount = element.GetAttributeInt("initialcount", 0);
|
||||
|
||||
Commonness = element.GetAttributeInt("commonness", 10);
|
||||
|
||||
AllowAlways = element.GetAttributeBool("allowalways", false);
|
||||
string translatedDescription = TextManager.Get("JobDescription." + Identifier, true);
|
||||
if (!string.IsNullOrEmpty(translatedDescription)) Description = translatedDescription;
|
||||
|
||||
ItemNames = new List<string>();
|
||||
|
||||
@@ -90,8 +120,32 @@ namespace Barotrauma
|
||||
Items = subElement;
|
||||
foreach (XElement itemElement in subElement.Elements())
|
||||
{
|
||||
string itemName = itemElement.GetAttributeString("name", "");
|
||||
if (!string.IsNullOrWhiteSpace(itemName)) ItemNames.Add(itemName);
|
||||
if (itemElement.Element("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - use identifiers instead of names to configure the items.");
|
||||
ItemNames.Add(itemElement.GetAttributeString("name", ""));
|
||||
continue;
|
||||
}
|
||||
|
||||
string itemIdentifier = itemElement.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrWhiteSpace(itemIdentifier))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item with no identifier.");
|
||||
ItemNames.Add("");
|
||||
}
|
||||
else
|
||||
{
|
||||
var prefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (prefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in job config \"" + Name + "\" - item prefab \""+itemIdentifier+"\" not found.");
|
||||
ItemNames.Add("");
|
||||
}
|
||||
else
|
||||
{
|
||||
ItemNames.Add(prefab.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "skills":
|
||||
@@ -104,6 +158,12 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
Skills.Sort((x,y) => y.LevelRange.X.CompareTo(x.LevelRange.X));
|
||||
|
||||
ClothingElement = element.Element("PortraitClothing");
|
||||
if (ClothingElement == null)
|
||||
{
|
||||
ClothingElement = element.Element("portraitclothing");
|
||||
}
|
||||
}
|
||||
|
||||
public static JobPrefab Random()
|
||||
@@ -111,7 +171,7 @@ namespace Barotrauma
|
||||
return List[Rand.Int(List.Count)];
|
||||
}
|
||||
|
||||
public static void LoadAll(List<string> filePaths)
|
||||
public static void LoadAll(IEnumerable<string> filePaths)
|
||||
{
|
||||
List = new List<JobPrefab>();
|
||||
|
||||
|
||||
@@ -5,48 +5,47 @@ namespace Barotrauma
|
||||
{
|
||||
class Skill
|
||||
{
|
||||
SkillPrefab prefab;
|
||||
|
||||
string name;
|
||||
int level;
|
||||
private SkillPrefab prefab;
|
||||
|
||||
private float level;
|
||||
|
||||
static string[] levelNames = new string[] {
|
||||
"Untrained", "Incompetent", "Novice",
|
||||
"Adequate", "Competent", "Proficient",
|
||||
"Professional", "Master", "Legendary" };
|
||||
|
||||
public string Name
|
||||
string identifier;
|
||||
public string Identifier
|
||||
{
|
||||
get { return name; }
|
||||
get { return identifier; }
|
||||
}
|
||||
|
||||
public int Level
|
||||
|
||||
public float Level
|
||||
{
|
||||
get { return level; }
|
||||
set { level = MathHelper.Clamp(value, 0, 100); }
|
||||
set { level = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
||||
}
|
||||
|
||||
public Skill(SkillPrefab prefab)
|
||||
{
|
||||
this.prefab = prefab;
|
||||
this.name = prefab.Name;
|
||||
this.identifier = prefab.Identifier;
|
||||
|
||||
this.level = (int)Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y);
|
||||
this.level = Rand.Range(prefab.LevelRange.X, prefab.LevelRange.Y, Rand.RandSync.Server);
|
||||
}
|
||||
|
||||
public Skill(string name, int level)
|
||||
public Skill(string identifier, float level)
|
||||
{
|
||||
this.name = name;
|
||||
|
||||
this.identifier = identifier;
|
||||
this.level = level;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// returns the "name" of some skill level (0-10 -> untrained, etc)
|
||||
/// </summary>
|
||||
public static string GetLevelName(int level)
|
||||
public static string GetLevelName(float level)
|
||||
{
|
||||
level = MathHelper.Clamp(level, 0, 100);
|
||||
level = MathHelper.Clamp(level, 0.0f, 100.0f);
|
||||
int scaledLevel = (int)Math.Floor((level / 100.0f) * levelNames.Length);
|
||||
|
||||
return levelNames[Math.Min(scaledLevel, levelNames.Length - 1)];
|
||||
|
||||
@@ -5,17 +5,12 @@ namespace Barotrauma
|
||||
{
|
||||
class SkillPrefab
|
||||
{
|
||||
private string name;
|
||||
|
||||
private string description;
|
||||
|
||||
private Vector2 levelRange;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return name; }
|
||||
}
|
||||
|
||||
public readonly string Identifier;
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return description; }
|
||||
@@ -28,7 +23,7 @@ namespace Barotrauma
|
||||
|
||||
public SkillPrefab(XElement element)
|
||||
{
|
||||
name = element.GetAttributeString("name", "");
|
||||
Identifier = element.GetAttributeString("identifier", "");
|
||||
|
||||
var levelString = element.GetAttributeString("level", "");
|
||||
if (levelString.Contains(","))
|
||||
@@ -41,7 +36,5 @@ namespace Barotrauma
|
||||
levelRange = new Vector2(skillLevel, skillLevel);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
//using Microsoft.Xna.Framework.Graphics;
|
||||
using Barotrauma.Items.Components;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics;
|
||||
using FarseerPhysics.Dynamics;
|
||||
using FarseerPhysics.Dynamics.Contacts;
|
||||
using FarseerPhysics.Dynamics.Joints;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Extensions;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -15,31 +13,89 @@ namespace Barotrauma
|
||||
{
|
||||
public enum LimbType
|
||||
{
|
||||
None, LeftHand, RightHand, LeftArm, RightArm,
|
||||
None, LeftHand, RightHand, LeftArm, RightArm, LeftForearm, RightForearm,
|
||||
LeftLeg, RightLeg, LeftFoot, RightFoot, Head, Torso, Tail, Legs, RightThigh, LeftThigh, Waist
|
||||
};
|
||||
|
||||
class LimbJoint : RevoluteJoint
|
||||
partial class LimbJoint : RevoluteJoint
|
||||
{
|
||||
public bool IsSevered;
|
||||
public bool CanBeSevered;
|
||||
|
||||
public bool CanBeSevered => jointParams.CanBeSevered;
|
||||
public readonly JointParams jointParams;
|
||||
public readonly Ragdoll ragdoll;
|
||||
public readonly Limb LimbA, LimbB;
|
||||
|
||||
public LimbJoint(Limb limbA, Limb limbB, JointParams jointParams, Ragdoll ragdoll) : this(limbA, limbB, Vector2.One, Vector2.One)
|
||||
{
|
||||
this.jointParams = jointParams;
|
||||
this.ragdoll = ragdoll;
|
||||
LoadParams();
|
||||
}
|
||||
|
||||
public LimbJoint(Limb limbA, Limb limbB, Vector2 anchor1, Vector2 anchor2)
|
||||
: base(limbA.body.FarseerBody, limbB.body.FarseerBody, anchor1, anchor2)
|
||||
{
|
||||
CollideConnected = false;
|
||||
MotorEnabled = true;
|
||||
MaxMotorTorque = 0.25f;
|
||||
|
||||
LimbA = limbA;
|
||||
LimbB = limbB;
|
||||
}
|
||||
|
||||
public void SaveParams()
|
||||
{
|
||||
// Saving to the params is handled only in the params level.
|
||||
return;
|
||||
|
||||
jointParams.Stiffness = MaxMotorTorque;
|
||||
if (ragdoll.IsFlipped)
|
||||
{
|
||||
jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorA.X, LocalAnchorA.Y) / jointParams.Ragdoll.JointScale);
|
||||
jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(new Vector2(-LocalAnchorB.X, LocalAnchorB.Y) / jointParams.Ragdoll.JointScale);
|
||||
jointParams.UpperLimit = MathHelper.ToDegrees(-LowerLimit);
|
||||
jointParams.LowerLimit = MathHelper.ToDegrees(-UpperLimit);
|
||||
}
|
||||
else
|
||||
{
|
||||
jointParams.Limb1Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorA / jointParams.Ragdoll.JointScale);
|
||||
jointParams.Limb2Anchor = ConvertUnits.ToDisplayUnits(LocalAnchorB / jointParams.Ragdoll.JointScale);
|
||||
jointParams.UpperLimit = MathHelper.ToDegrees(UpperLimit);
|
||||
jointParams.LowerLimit = MathHelper.ToDegrees(LowerLimit);
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadParams()
|
||||
{
|
||||
MaxMotorTorque = jointParams.Stiffness;
|
||||
LimitEnabled = jointParams.LimitEnabled;
|
||||
if (float.IsNaN(jointParams.LowerLimit))
|
||||
{
|
||||
jointParams.LowerLimit = 0;
|
||||
}
|
||||
if (float.IsNaN(jointParams.UpperLimit))
|
||||
{
|
||||
jointParams.UpperLimit = 0;
|
||||
}
|
||||
if (ragdoll.IsFlipped)
|
||||
{
|
||||
LocalAnchorA = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb1Anchor.X, jointParams.Limb1Anchor.Y) * jointParams.Ragdoll.JointScale);
|
||||
LocalAnchorB = ConvertUnits.ToSimUnits(new Vector2(-jointParams.Limb2Anchor.X, jointParams.Limb2Anchor.Y) * jointParams.Ragdoll.JointScale);
|
||||
UpperLimit = MathHelper.ToRadians(-jointParams.LowerLimit);
|
||||
LowerLimit = MathHelper.ToRadians(-jointParams.UpperLimit);
|
||||
}
|
||||
else
|
||||
{
|
||||
LocalAnchorA = ConvertUnits.ToSimUnits(jointParams.Limb1Anchor * jointParams.Ragdoll.JointScale);
|
||||
LocalAnchorB = ConvertUnits.ToSimUnits(jointParams.Limb2Anchor * jointParams.Ragdoll.JointScale);
|
||||
UpperLimit = MathHelper.ToRadians(jointParams.UpperLimit);
|
||||
LowerLimit = MathHelper.ToRadians(jointParams.LowerLimit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
partial class Limb
|
||||
partial class Limb : ISerializableEntity
|
||||
{
|
||||
// Note: not used
|
||||
private const float LimbDensity = 15;
|
||||
private const float LimbAngularDamping = 7;
|
||||
|
||||
@@ -47,13 +103,16 @@ namespace Barotrauma
|
||||
private const float SeveredFadeOutTime = 10.0f;
|
||||
|
||||
public readonly Character character;
|
||||
|
||||
/// <summary>
|
||||
/// Note that during the limb initialization, character.AnimController returns null, whereas this field is already assigned.
|
||||
/// </summary>
|
||||
public readonly Ragdoll ragdoll;
|
||||
public readonly LimbParams limbParams;
|
||||
|
||||
//the physics body of the limb
|
||||
public PhysicsBody body;
|
||||
|
||||
protected readonly Vector2 stepOffset;
|
||||
|
||||
public Sprite sprite, damagedSprite;
|
||||
|
||||
public Vector2 StepOffset => ConvertUnits.ToSimUnits(limbParams.StepOffset) * ragdoll.RagdollParams.JointScale;
|
||||
|
||||
public bool inWater;
|
||||
|
||||
@@ -68,16 +127,19 @@ namespace Barotrauma
|
||||
|
||||
public Vector2? MouthPos;
|
||||
|
||||
//a timer for delaying when a hitsound/attacksound can be played again
|
||||
public float SoundTimer;
|
||||
public const float SoundInterval = 0.4f;
|
||||
|
||||
public readonly Attack attack;
|
||||
private List<DamageModifier> damageModifiers;
|
||||
|
||||
private Direction dir;
|
||||
|
||||
public int HealthIndex => limbParams.HealthIndex;
|
||||
public float Scale => limbParams.Ragdoll.LimbScale;
|
||||
public float AttackPriority => limbParams.AttackPriority;
|
||||
public bool DoesFlip => limbParams.Flip;
|
||||
public float SteerForce => limbParams.SteerForce;
|
||||
|
||||
public float AttackTimer;
|
||||
public Vector2 DebugTargetPos;
|
||||
public Vector2 DebugRefPos;
|
||||
|
||||
public bool IsSevered
|
||||
{
|
||||
@@ -87,13 +149,11 @@ namespace Barotrauma
|
||||
isSevered = value;
|
||||
if (!isSevered) severedFadeOutTimer = 0.0f;
|
||||
#if CLIENT
|
||||
if (isSevered) damage = 100.0f;
|
||||
if (isSevered) damageOverlayStrength = 100.0f;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public bool DoesFlip { get; private set; }
|
||||
|
||||
public Vector2 WorldPosition
|
||||
{
|
||||
get { return character.Submarine == null ? Position : Position + character.Submarine.Position; }
|
||||
@@ -114,13 +174,9 @@ namespace Barotrauma
|
||||
get { return body.Rotation; }
|
||||
}
|
||||
|
||||
public float Scale { get; private set; }
|
||||
|
||||
//where an animcontroller is trying to pull the limb, only used for debug visualization
|
||||
public Vector2 AnimTargetPos { get; private set; }
|
||||
|
||||
public float SteerForce { get; private set; }
|
||||
|
||||
public float Mass
|
||||
{
|
||||
get { return body.Mass; }
|
||||
@@ -136,16 +192,19 @@ namespace Barotrauma
|
||||
public float Dir
|
||||
{
|
||||
get { return ((dir == Direction.Left) ? -1.0f : 1.0f); }
|
||||
set { dir = (value==-1.0f) ? Direction.Left : Direction.Right; }
|
||||
set { dir = (value == -1.0f) ? Direction.Left : Direction.Right; }
|
||||
}
|
||||
|
||||
public int RefJointIndex { get; private set; }
|
||||
public int RefJointIndex => limbParams.RefJoint;
|
||||
|
||||
public Vector2 StepOffset
|
||||
private List<WearableSprite> wearingItems;
|
||||
public List<WearableSprite> WearingItems
|
||||
{
|
||||
get { return stepOffset; }
|
||||
get { return wearingItems; }
|
||||
}
|
||||
|
||||
public List<WearableSprite> OtherWearables { get; private set; } = new List<WearableSprite>();
|
||||
|
||||
public bool PullJointEnabled
|
||||
{
|
||||
get { return pullJoint.Enabled; }
|
||||
@@ -161,100 +220,105 @@ namespace Barotrauma
|
||||
public Vector2 PullJointWorldAnchorA
|
||||
{
|
||||
get { return pullJoint.WorldAnchorA; }
|
||||
set
|
||||
{
|
||||
if (!MathUtils.IsValid(value))
|
||||
{
|
||||
string errorMsg = "Attempted to set the anchor A of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
if (Vector2.DistanceSquared(SimPosition, value) > 50.0f * 50.0f)
|
||||
{
|
||||
Vector2 diff = value - SimPosition;
|
||||
string errorMsg = "Attempted to move the anchor A of a limb's pull joint extremely far from the limb (diff: " + diff +
|
||||
", limb enabled: " + body.Enabled +
|
||||
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
|
||||
+ Environment.StackTrace;
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorA:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
pullJoint.WorldAnchorA = value;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 PullJointWorldAnchorB
|
||||
{
|
||||
get { return pullJoint.WorldAnchorB; }
|
||||
set
|
||||
{
|
||||
{
|
||||
if (!MathUtils.IsValid(value))
|
||||
{
|
||||
string errorMsg = "Attempted to set the anchor of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
|
||||
string errorMsg = "Attempted to set the anchor B of a limb's pull joint to an invalid value (" + value + ")\n" + Environment.StackTrace;
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchor:InvalidValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (Vector2.DistanceSquared(pullJoint.WorldAnchorA, value) > 50.0f * 50.0f)
|
||||
{
|
||||
Vector2 diff = value - pullJoint.WorldAnchorA;
|
||||
string errorMsg = "Attempted to move the anchor of a limb's pull joint extremely far from the limb (diff: " + diff +
|
||||
string errorMsg = "Attempted to move the anchor B of a limb's pull joint extremely far from the limb (diff: " + diff +
|
||||
", limb enabled: " + body.Enabled +
|
||||
", simple physics enabled: " + character.AnimController.SimplePhysicsEnabled + ")\n"
|
||||
+ Environment.StackTrace;
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchorB:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#if DEBUG
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
GameAnalyticsManager.AddErrorEventOnce("Limb.SetPullJointAnchor:ExcessiveValue", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
pullJoint.WorldAnchorB = value;
|
||||
}
|
||||
}
|
||||
public List<WearableSprite> WearingItems { get; private set; }
|
||||
|
||||
public Limb (Character character, XElement element, float scale = 1.0f)
|
||||
public Vector2 PullJointLocalAnchorA
|
||||
{
|
||||
get { return pullJoint.LocalAnchorA; }
|
||||
}
|
||||
|
||||
public string Name => limbParams.Name;
|
||||
|
||||
public Dictionary<string, SerializableProperty> SerializableProperties
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Limb(Ragdoll ragdoll, Character character, LimbParams limbParams)
|
||||
{
|
||||
this.ragdoll = ragdoll;
|
||||
this.character = character;
|
||||
|
||||
WearingItems = new List<WearableSprite>();
|
||||
|
||||
this.limbParams = limbParams;
|
||||
wearingItems = new List<WearableSprite>();
|
||||
dir = Direction.Right;
|
||||
DoesFlip = element.GetAttributeBool("flip", false);
|
||||
|
||||
Scale = scale;
|
||||
|
||||
body = new PhysicsBody(element, scale);
|
||||
|
||||
if (element.GetAttributeBool("ignorecollisions", false))
|
||||
body = new PhysicsBody(limbParams);
|
||||
type = limbParams.Type;
|
||||
if (limbParams.IgnoreCollisions)
|
||||
{
|
||||
body.CollisionCategories = Category.None;
|
||||
body.CollidesWith = Category.None;
|
||||
|
||||
ignoreCollisions = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
//limbs don't collide with each other
|
||||
body.CollisionCategories = Physics.CollisionCharacter;
|
||||
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem;
|
||||
body.CollidesWith = Physics.CollisionAll & ~Physics.CollisionCharacter & ~Physics.CollisionItem & ~Physics.CollisionItemBlocking;
|
||||
}
|
||||
|
||||
body.UserData = this;
|
||||
|
||||
RefJointIndex = -1;
|
||||
|
||||
Vector2 pullJointPos = Vector2.Zero;
|
||||
|
||||
if (element.Attribute("type") != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
type = (LimbType)Enum.Parse(typeof(LimbType), element.Attribute("type").Value, true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
type = LimbType.None;
|
||||
DebugConsole.ThrowError("Error in "+element+"! \""+element.Attribute("type").Value+"\" is not a valid limb type");
|
||||
}
|
||||
|
||||
|
||||
pullJointPos = element.GetAttributeVector2("pullpos", Vector2.Zero) * scale;
|
||||
pullJointPos = ConvertUnits.ToSimUnits(pullJointPos);
|
||||
|
||||
stepOffset = element.GetAttributeVector2("stepoffset", Vector2.Zero) * scale;
|
||||
stepOffset = ConvertUnits.ToSimUnits(stepOffset);
|
||||
|
||||
RefJointIndex = element.GetAttributeInt("refjoint", -1);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
type = LimbType.None;
|
||||
}
|
||||
|
||||
pullJoint = new FixedMouseJoint(body.FarseerBody, pullJointPos)
|
||||
pullJoint = new FixedMouseJoint(body.FarseerBody, ConvertUnits.ToSimUnits(limbParams.PullPos * Scale))
|
||||
{
|
||||
Enabled = false,
|
||||
MaxForce = ((type == LimbType.LeftHand || type == LimbType.RightHand) ? 400.0f : 150.0f) * body.Mass
|
||||
@@ -262,8 +326,7 @@ namespace Barotrauma
|
||||
|
||||
GameMain.World.AddJoint(pullJoint);
|
||||
|
||||
SteerForce = element.GetAttributeFloat("steerforce", 0.0f);
|
||||
|
||||
var element = limbParams.Element;
|
||||
if (element.Attribute("mouthpos") != null)
|
||||
{
|
||||
MouthPos = ConvertUnits.ToSimUnits(element.GetAttributeVector2("mouthpos", Vector2.Zero));
|
||||
@@ -278,58 +341,33 @@ namespace Barotrauma
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "sprite":
|
||||
string spritePath = subElement.Attribute("texture").Value;
|
||||
|
||||
string spritePathWithTags = spritePath;
|
||||
|
||||
if (character.Info != null)
|
||||
{
|
||||
spritePath = spritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : "");
|
||||
spritePath = spritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString());
|
||||
|
||||
if (character.Info.HeadSprite != null && character.Info.SpriteTags.Any())
|
||||
{
|
||||
string tags = "";
|
||||
character.Info.SpriteTags.ForEach(tag => tags += "[" + tag + "]");
|
||||
|
||||
spritePathWithTags = Path.Combine(
|
||||
Path.GetDirectoryName(spritePath),
|
||||
Path.GetFileNameWithoutExtension(spritePath) + tags + Path.GetExtension(spritePath));
|
||||
}
|
||||
}
|
||||
|
||||
if (File.Exists(spritePathWithTags))
|
||||
{
|
||||
sprite = new Sprite(subElement, "", spritePathWithTags);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
sprite = new Sprite(subElement, "", spritePath);
|
||||
}
|
||||
|
||||
break;
|
||||
case "damagedsprite":
|
||||
string damagedSpritePath = subElement.Attribute("texture").Value;
|
||||
|
||||
if (character.Info != null)
|
||||
{
|
||||
damagedSpritePath = damagedSpritePath.Replace("[GENDER]", (character.Info.Gender == Gender.Female) ? "f" : "");
|
||||
damagedSpritePath = damagedSpritePath.Replace("[HEADID]", character.Info.HeadSpriteId.ToString());
|
||||
}
|
||||
|
||||
damagedSprite = new Sprite(subElement, "", damagedSpritePath);
|
||||
break;
|
||||
case "attack":
|
||||
attack = new Attack(subElement);
|
||||
attack = new Attack(subElement, (character == null ? "null" : character.Name) + ", limb " + type);
|
||||
if (attack.DamageRange <= 0)
|
||||
{
|
||||
switch (body.BodyShape)
|
||||
{
|
||||
case PhysicsBody.Shape.Circle:
|
||||
attack.DamageRange = body.radius;
|
||||
break;
|
||||
case PhysicsBody.Shape.Capsule:
|
||||
attack.DamageRange = body.height / 2 + body.radius;
|
||||
break;
|
||||
case PhysicsBody.Shape.Rectangle:
|
||||
attack.DamageRange = new Vector2(body.width / 2.0f, body.height / 2.0f).Length();
|
||||
break;
|
||||
}
|
||||
attack.DamageRange = ConvertUnits.ToDisplayUnits(attack.DamageRange);
|
||||
}
|
||||
break;
|
||||
case "damagemodifier":
|
||||
damageModifiers.Add(new DamageModifier(subElement));
|
||||
damageModifiers.Add(new DamageModifier(subElement, character.Name));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SerializableProperties = SerializableProperty.GetProperties(this);
|
||||
|
||||
InitProjSpecific(element);
|
||||
}
|
||||
partial void InitProjSpecific(XElement element);
|
||||
@@ -352,43 +390,53 @@ namespace Barotrauma
|
||||
pullJoint.LocalAnchorA = new Vector2(-pullJoint.LocalAnchorA.X, pullJoint.LocalAnchorA.Y);
|
||||
}
|
||||
|
||||
public AttackResult AddDamage(Vector2 position, DamageType damageType, float amount, float bleedingAmount, bool playSound)
|
||||
public AttackResult AddDamage(Vector2 position, float damage, float bleedingDamage, float burnDamage, bool playSound)
|
||||
{
|
||||
List<Affliction> afflictions = new List<Affliction>();
|
||||
if (damage > 0.0f) afflictions.Add(AfflictionPrefab.InternalDamage.Instantiate(damage));
|
||||
if (bleedingDamage > 0.0f) afflictions.Add(AfflictionPrefab.Bleeding.Instantiate(bleedingDamage));
|
||||
if (burnDamage > 0.0f) afflictions.Add(AfflictionPrefab.Burn.Instantiate(burnDamage));
|
||||
|
||||
return AddDamage(position, afflictions, playSound);
|
||||
}
|
||||
|
||||
public AttackResult AddDamage(Vector2 position, List<Affliction> afflictions, bool playSound)
|
||||
{
|
||||
List<DamageModifier> appliedDamageModifiers = new List<DamageModifier>();
|
||||
|
||||
foreach (DamageModifier damageModifier in damageModifiers)
|
||||
//create a copy of the original affliction list to prevent modifying the afflictions of an Attack/StatusEffect etc
|
||||
afflictions = new List<Affliction>(afflictions);
|
||||
for (int i = 0; i < afflictions.Count; i++)
|
||||
{
|
||||
if (damageModifier.DamageType == DamageType.None) continue;
|
||||
if (damageModifier.DamageType.HasFlag(damageType) && SectorHit(damageModifier.ArmorSector, position))
|
||||
foreach (DamageModifier damageModifier in damageModifiers)
|
||||
{
|
||||
appliedDamageModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (WearableSprite wearable in WearingItems)
|
||||
{
|
||||
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
|
||||
{
|
||||
if (damageModifier.DamageType == DamageType.None) continue;
|
||||
if (damageModifier.DamageType.HasFlag(damageType) && SectorHit(damageModifier.ArmorSector, position))
|
||||
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
|
||||
if (SectorHit(damageModifier.ArmorSector, position))
|
||||
{
|
||||
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
|
||||
appliedDamageModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (WearableSprite wearable in wearingItems)
|
||||
{
|
||||
foreach (DamageModifier damageModifier in wearable.WearableComponent.DamageModifiers)
|
||||
{
|
||||
if (!damageModifier.MatchesAffliction(afflictions[i])) continue;
|
||||
if (SectorHit(damageModifier.ArmorSector, position))
|
||||
{
|
||||
afflictions[i] = afflictions[i].CreateMultiplied(damageModifier.DamageMultiplier);
|
||||
appliedDamageModifiers.Add(damageModifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (DamageModifier damageModifier in appliedDamageModifiers)
|
||||
{
|
||||
amount *= damageModifier.DamageMultiplier;
|
||||
bleedingAmount *= damageModifier.BleedingMultiplier;
|
||||
}
|
||||
AddDamageProjSpecific(position, afflictions, playSound, appliedDamageModifiers);
|
||||
|
||||
AddDamageProjSpecific(position, damageType, amount, bleedingAmount, playSound, appliedDamageModifiers);
|
||||
|
||||
return new AttackResult(amount, bleedingAmount, appliedDamageModifiers);
|
||||
return new AttackResult(afflictions, this, appliedDamageModifiers);
|
||||
}
|
||||
|
||||
partial void AddDamageProjSpecific(Vector2 position, DamageType damageType, float amount, float bleedingAmount, bool playSound, List<DamageModifier> appliedDamageModifiers);
|
||||
partial void AddDamageProjSpecific(Vector2 position, List<Affliction> afflictions, bool playSound, List<DamageModifier> appliedDamageModifiers);
|
||||
|
||||
public bool SectorHit(Vector2 armorSector, Vector2 simPosition)
|
||||
{
|
||||
@@ -408,7 +456,7 @@ namespace Barotrauma
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
UpdateProjSpecific(deltaTime);
|
||||
|
||||
|
||||
if (LinearVelocity.X > 500.0f)
|
||||
{
|
||||
//DebugConsole.ThrowError("CHARACTER EXPLODED");
|
||||
@@ -430,22 +478,26 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (character.IsDead) return;
|
||||
|
||||
SoundTimer -= deltaTime;
|
||||
if (attack != null)
|
||||
{
|
||||
attack.UpdateCoolDown(deltaTime);
|
||||
}
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public void UpdateAttack(float deltaTime, Vector2 attackPosition, IDamageable damageTarget)
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if the attack successfully hit something. If the distance is not given, it will be calculated.
|
||||
/// </summary>
|
||||
public bool UpdateAttack(float deltaTime, Vector2 attackPosition, IDamageable damageTarget, out AttackResult attackResult, float distance = -1)
|
||||
{
|
||||
float dist = ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition));
|
||||
|
||||
AttackTimer += deltaTime;
|
||||
|
||||
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
|
||||
attackResult = default(AttackResult);
|
||||
float dist = distance > -1 ? distance : ConvertUnits.ToDisplayUnits(Vector2.Distance(SimPosition, attackPosition));
|
||||
bool wasRunning = attack.IsRunning;
|
||||
attack.UpdateAttackTimer(deltaTime);
|
||||
|
||||
bool wasHit = false;
|
||||
Body structureBody = null;
|
||||
if (damageTarget != null)
|
||||
{
|
||||
switch (attack.HitDetectionType)
|
||||
@@ -456,15 +508,29 @@ namespace Barotrauma
|
||||
List<Body> ignoredBodies = character.AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
||||
ignoredBodies.Add(character.AnimController.Collider.FarseerBody);
|
||||
|
||||
var body = Submarine.PickBody(
|
||||
structureBody = Submarine.PickBody(
|
||||
SimPosition, attackPosition,
|
||||
ignoredBodies, Physics.CollisionWall);
|
||||
|
||||
wasHit = body == null;
|
||||
|
||||
if (damageTarget is Item && structureBody?.UserData is Item)
|
||||
{
|
||||
// If the attack is aimed to an item and hits an item, it's successful
|
||||
wasHit = true;
|
||||
}
|
||||
else if (damageTarget is Structure && structureBody?.UserData is Structure)
|
||||
{
|
||||
// If the attack is aimed to a structure and hits a structure, it's successful
|
||||
wasHit = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// If the attack is aimed to a character but hits a structure, the hit is blocked.
|
||||
wasHit = structureBody == null;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case HitDetection.Contact:
|
||||
List<Body> targetBodies = new List<Body>();
|
||||
var targetBodies = new List<Body>();
|
||||
if (damageTarget is Character targetCharacter)
|
||||
{
|
||||
foreach (Limb limb in targetCharacter.AnimController.Limbs)
|
||||
@@ -498,10 +564,10 @@ namespace Barotrauma
|
||||
contactEdge.Contact.IsTouching &&
|
||||
targetBodies.Any(b => b == contactEdge.Contact.FixtureA?.Body || b == contactEdge.Contact.FixtureB?.Body))
|
||||
{
|
||||
structureBody = targetBodies.LastOrDefault();
|
||||
wasHit = true;
|
||||
break;
|
||||
}
|
||||
|
||||
contactEdge = contactEdge.Next;
|
||||
}
|
||||
}
|
||||
@@ -511,63 +577,134 @@ namespace Barotrauma
|
||||
|
||||
if (wasHit)
|
||||
{
|
||||
if (AttackTimer >= attack.Duration && damageTarget != null)
|
||||
wasHit = damageTarget != null;
|
||||
}
|
||||
|
||||
if (wasHit)
|
||||
{
|
||||
bool playSound = false;
|
||||
#if CLIENT
|
||||
playSound = LastAttackSoundTime < Timing.TotalTime - SoundInterval;
|
||||
if (playSound)
|
||||
{
|
||||
attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, (SoundTimer <= 0.0f));
|
||||
SoundTimer = SoundInterval;
|
||||
LastAttackSoundTime = SoundInterval;
|
||||
}
|
||||
#endif
|
||||
attackResult = attack.DoDamage(character, damageTarget, WorldPosition, 1.0f, playSound);
|
||||
if (structureBody != null && attack.StickChance > Rand.Range(0.0f, 1.0f, Rand.RandSync.Server))
|
||||
{
|
||||
// TODO: use the hit pos?
|
||||
var localFront = body.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
|
||||
var from = body.FarseerBody.GetWorldPoint(localFront);
|
||||
var to = from;
|
||||
var drawPos = body.DrawPosition;
|
||||
StickTo(structureBody, from, to);
|
||||
}
|
||||
attack.ResetAttackTimer();
|
||||
attack.SetCoolDown();
|
||||
}
|
||||
|
||||
Vector2 diff = attackPosition - SimPosition;
|
||||
if (diff.LengthSquared() < 0.00001f) return;
|
||||
|
||||
if (attack.ApplyForceOnLimbs != null)
|
||||
bool applyForces = (!attack.ApplyForcesOnlyOnce || !wasRunning) && diff.LengthSquared() > 0.00001f;
|
||||
if (applyForces)
|
||||
{
|
||||
foreach (int limbIndex in attack.ApplyForceOnLimbs)
|
||||
body.ApplyTorque(Mass * character.AnimController.Dir * attack.Torque);
|
||||
if (attack.ForceOnLimbIndices != null && attack.ForceOnLimbIndices.Count > 0)
|
||||
{
|
||||
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
|
||||
foreach (int limbIndex in attack.ForceOnLimbIndices)
|
||||
{
|
||||
if (limbIndex < 0 || limbIndex >= character.AnimController.Limbs.Length) continue;
|
||||
|
||||
Limb limb = character.AnimController.Limbs[limbIndex];
|
||||
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
|
||||
limb.body.ApplyLinearImpulse(
|
||||
limb.Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
|
||||
Limb limb = character.AnimController.Limbs[limbIndex];
|
||||
Vector2 forcePos = limb.pullJoint == null ? limb.body.SimPosition : limb.pullJoint.WorldAnchorA;
|
||||
limb.body.ApplyLinearImpulse(limb.Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
|
||||
body.ApplyLinearImpulse(Mass * attack.Force * Vector2.Normalize(attackPosition - SimPosition), forcePos);
|
||||
}
|
||||
}
|
||||
else
|
||||
return wasHit;
|
||||
}
|
||||
|
||||
private WeldJoint attachJoint;
|
||||
private WeldJoint colliderJoint;
|
||||
public bool IsStuck => attachJoint != null;
|
||||
|
||||
/// <summary>
|
||||
/// Attach the limb to a target with WeldJoints.
|
||||
/// Uses sim units.
|
||||
/// </summary>
|
||||
private void StickTo(Body target, Vector2 from, Vector2 to)
|
||||
{
|
||||
if (attachJoint != null)
|
||||
{
|
||||
Vector2 forcePos = pullJoint == null ? body.SimPosition : pullJoint.WorldAnchorA;
|
||||
body.ApplyLinearImpulse(Mass * attack.Force *
|
||||
Vector2.Normalize(attackPosition - SimPosition), forcePos);
|
||||
// Already attached to the target body, no need to do anything
|
||||
if (attachJoint.BodyB == target) { return; }
|
||||
Release();
|
||||
}
|
||||
|
||||
if (!ragdoll.IsStuck)
|
||||
{
|
||||
PhysicsBody mainLimbBody = ragdoll.MainLimb.body;
|
||||
Body colliderBody = ragdoll.Collider.FarseerBody;
|
||||
Vector2 mainLimbLocalFront = mainLimbBody.GetLocalFront(MathHelper.ToRadians(ragdoll.RagdollParams.SpritesheetOrientation));
|
||||
if (Dir < 0)
|
||||
{
|
||||
mainLimbLocalFront.X = -mainLimbLocalFront.X;
|
||||
}
|
||||
Vector2 mainLimbFront = mainLimbBody.FarseerBody.GetWorldPoint(mainLimbLocalFront);
|
||||
colliderBody.SetTransform(mainLimbBody.SimPosition, mainLimbBody.Rotation);
|
||||
// Attach the collider to the main body so that they don't go out of sync (TODO: why is the collider still rotated 90d off?)
|
||||
colliderJoint = new WeldJoint(colliderBody, mainLimbBody.FarseerBody, mainLimbFront, mainLimbFront, true)
|
||||
{
|
||||
KinematicBodyB = true,
|
||||
CollideConnected = false
|
||||
};
|
||||
GameMain.World.AddJoint(colliderJoint);
|
||||
}
|
||||
|
||||
attachJoint = new WeldJoint(body.FarseerBody, target, from, to, true)
|
||||
{
|
||||
FrequencyHz = 1,
|
||||
DampingRatio = 0.5f,
|
||||
KinematicBodyB = true,
|
||||
CollideConnected = false
|
||||
};
|
||||
GameMain.World.AddJoint(attachJoint);
|
||||
}
|
||||
|
||||
|
||||
public void Release()
|
||||
{
|
||||
if (!IsStuck) { return; }
|
||||
GameMain.World.RemoveJoint(attachJoint);
|
||||
attachJoint = null;
|
||||
if (colliderJoint != null)
|
||||
{
|
||||
GameMain.World.RemoveJoint(colliderJoint);
|
||||
colliderJoint = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Remove()
|
||||
{
|
||||
if (sprite != null)
|
||||
{
|
||||
sprite.Remove();
|
||||
sprite = null;
|
||||
}
|
||||
|
||||
if (damagedSprite != null)
|
||||
{
|
||||
damagedSprite.Remove();
|
||||
damagedSprite = null;
|
||||
}
|
||||
|
||||
if (body != null)
|
||||
{
|
||||
body.Remove();
|
||||
body = null;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (LightSource != null)
|
||||
{
|
||||
LightSource.Remove();
|
||||
}
|
||||
#endif
|
||||
body?.Remove();
|
||||
body = null;
|
||||
Release();
|
||||
RemoveProjSpecific();
|
||||
}
|
||||
|
||||
partial void RemoveProjSpecific();
|
||||
|
||||
public void LoadParams()
|
||||
{
|
||||
attack?.Deserialize();
|
||||
pullJoint.LocalAnchorA = ConvertUnits.ToSimUnits(limbParams.PullPos * Scale);
|
||||
LoadParamsProjSpecific();
|
||||
}
|
||||
|
||||
partial void LoadParamsProjSpecific();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class NPCPersonalityTrait
|
||||
{
|
||||
private static List<NPCPersonalityTrait> list = new List<NPCPersonalityTrait>();
|
||||
public static List<NPCPersonalityTrait> List
|
||||
{
|
||||
get { return list; }
|
||||
}
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
public readonly List<string> AllowedDialogTags;
|
||||
|
||||
private float commonness;
|
||||
|
||||
public NPCPersonalityTrait(XElement element)
|
||||
{
|
||||
Name = element.GetAttributeString("name", "");
|
||||
AllowedDialogTags = new List<string>(element.GetAttributeStringArray("alloweddialogtags", new string[0]));
|
||||
commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
|
||||
list.Add(this);
|
||||
}
|
||||
|
||||
public static NPCPersonalityTrait GetRandom(string seed)
|
||||
{
|
||||
var rand = new MTRandom(ToolBox.StringToInt(seed));
|
||||
return ToolBox.SelectWeightedRandom(list, list.Select(t => t.commonness).ToList(), rand);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using Facepunch.Steamworks;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Xml.Linq;
|
||||
|
||||
@@ -9,46 +11,102 @@ namespace Barotrauma
|
||||
public enum ContentType
|
||||
{
|
||||
None,
|
||||
Submarine,
|
||||
Jobs,
|
||||
Item,
|
||||
Character,
|
||||
Structure,
|
||||
ItemAssembly,
|
||||
Character,
|
||||
Structure,
|
||||
Outpost,
|
||||
Text,
|
||||
Executable,
|
||||
ServerExecutable,
|
||||
LocationTypes,
|
||||
LocationTypes,
|
||||
MapGenerationParameters,
|
||||
LevelGenerationParameters,
|
||||
RandomEvents,
|
||||
LevelObjectPrefabs,
|
||||
RandomEvents,
|
||||
Missions,
|
||||
BackgroundCreaturePrefabs, BackgroundSpritePrefabs,
|
||||
BackgroundCreaturePrefabs,
|
||||
Sounds,
|
||||
RuinConfig,
|
||||
Particles,
|
||||
Decals
|
||||
Decals,
|
||||
NPCConversations,
|
||||
Afflictions,
|
||||
Tutorials,
|
||||
UIStyle
|
||||
}
|
||||
|
||||
public class ContentPackage
|
||||
{
|
||||
|
||||
public static string Folder = "Data/ContentPackages/";
|
||||
|
||||
public static List<ContentPackage> list = new List<ContentPackage>();
|
||||
|
||||
|
||||
string name;
|
||||
|
||||
public string Name
|
||||
public static List<ContentPackage> List = new List<ContentPackage>();
|
||||
|
||||
//these types of files are included in the MD5 hash calculation,
|
||||
//meaning that the players must have the exact same files to play together
|
||||
private static HashSet<ContentType> multiplayerIncompatibleContent = new HashSet<ContentType>
|
||||
{
|
||||
get { return name; }
|
||||
ContentType.Jobs,
|
||||
ContentType.Item,
|
||||
ContentType.Character,
|
||||
ContentType.Structure,
|
||||
ContentType.LocationTypes,
|
||||
ContentType.MapGenerationParameters,
|
||||
ContentType.LevelGenerationParameters,
|
||||
ContentType.Missions,
|
||||
ContentType.LevelObjectPrefabs,
|
||||
ContentType.RuinConfig,
|
||||
ContentType.Outpost,
|
||||
ContentType.Afflictions
|
||||
};
|
||||
|
||||
//at least one file of each these types is required in core content packages
|
||||
private static HashSet<ContentType> corePackageRequiredFiles = new HashSet<ContentType>
|
||||
{
|
||||
ContentType.Jobs,
|
||||
ContentType.Item,
|
||||
ContentType.Character,
|
||||
ContentType.Structure,
|
||||
ContentType.Outpost,
|
||||
ContentType.Text,
|
||||
ContentType.Executable,
|
||||
ContentType.ServerExecutable,
|
||||
ContentType.LocationTypes,
|
||||
ContentType.MapGenerationParameters,
|
||||
ContentType.LevelGenerationParameters,
|
||||
ContentType.RandomEvents,
|
||||
ContentType.Missions,
|
||||
ContentType.BackgroundCreaturePrefabs,
|
||||
ContentType.RuinConfig,
|
||||
ContentType.NPCConversations,
|
||||
ContentType.Afflictions,
|
||||
ContentType.UIStyle
|
||||
};
|
||||
|
||||
public static IEnumerable<ContentType> CorePackageRequiredFiles
|
||||
{
|
||||
get { return corePackageRequiredFiles; }
|
||||
}
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public string Path
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private Md5Hash md5Hash;
|
||||
public string SteamWorkshopUrl;
|
||||
|
||||
public bool HideInWorkshopMenu
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
private Md5Hash md5Hash;
|
||||
public Md5Hash MD5hash
|
||||
{
|
||||
get
|
||||
@@ -58,108 +116,194 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public List<ContentFile> files;
|
||||
//core packages are content packages that are required for the game to work
|
||||
//e.g. they include the executable, some location types, level generation params and other files the game won't work without
|
||||
//one (and only one) core package must always be selected
|
||||
public bool CorePackage
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Version GameVersion
|
||||
{
|
||||
get; set;
|
||||
}
|
||||
|
||||
public List<ContentFile> Files;
|
||||
|
||||
public bool HasMultiplayerIncompatibleContent
|
||||
{
|
||||
get { return Files.Any(f => multiplayerIncompatibleContent.Contains(f.Type)); }
|
||||
}
|
||||
|
||||
private ContentPackage()
|
||||
{
|
||||
files = new List<ContentFile>();
|
||||
Files = new List<ContentFile>();
|
||||
}
|
||||
|
||||
public ContentPackage(string filePath)
|
||||
public ContentPackage(string filePath, string setPath = "")
|
||||
: this()
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
|
||||
Path = filePath;
|
||||
Path = setPath == string.Empty ? filePath : setPath;
|
||||
|
||||
if (doc==null)
|
||||
if (doc?.Root == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't load content package \""+filePath+"\"!");
|
||||
DebugConsole.ThrowError("Couldn't load content package \"" + filePath + "\"!");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
name = doc.Root.GetAttributeString("name", "");
|
||||
Name = doc.Root.GetAttributeString("name", "");
|
||||
HideInWorkshopMenu = doc.Root.GetAttributeBool("hideinworkshopmenu", false);
|
||||
CorePackage = doc.Root.GetAttributeBool("corepackage", false);
|
||||
SteamWorkshopUrl = doc.Root.GetAttributeString("steamworkshopurl", "");
|
||||
GameVersion = new Version(doc.Root.GetAttributeString("gameversion", "0.0.0.0"));
|
||||
|
||||
List<string> errorMsgs = new List<string>();
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
ContentType type;
|
||||
if (!Enum.TryParse(subElement.Name.ToString(), true, out type))
|
||||
if (!Enum.TryParse(subElement.Name.ToString(), true, out ContentType type))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in content package \""+name+"\" - \""+subElement.Name.ToString()+"\" is not a valid content type.");
|
||||
continue;
|
||||
errorMsgs.Add("Error in content package \"" + Name + "\" - \"" + subElement.Name.ToString() + "\" is not a valid content type.");
|
||||
type = ContentType.None;
|
||||
}
|
||||
Files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type));
|
||||
}
|
||||
|
||||
bool compatible = IsCompatible();
|
||||
//If we know that the package is not compatible, don't display error messages.
|
||||
if (compatible)
|
||||
{
|
||||
foreach (string errorMsg in errorMsgs)
|
||||
{
|
||||
DebugConsole.ThrowError(errorMsg);
|
||||
}
|
||||
files.Add(new ContentFile(subElement.GetAttributeString("file", ""), type));
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return name;
|
||||
return Name;
|
||||
}
|
||||
|
||||
public static ContentPackage CreatePackage(string name)
|
||||
public bool IsCompatible()
|
||||
{
|
||||
ContentPackage newPackage = new ContentPackage("Content/Data/"+name);
|
||||
newPackage.name = name;
|
||||
newPackage.Path = Folder + name;
|
||||
list.Add(newPackage);
|
||||
if (Files.All(f => f.Type == ContentType.Submarine))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
//content package compatibility checks were added in 0.9
|
||||
//0.9 is not compatible with older content packages
|
||||
if (GameVersion < new Version(0, 9))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
//do additional checks here if later versions add changes that break compatibility
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ContainsRequiredCorePackageFiles()
|
||||
{
|
||||
return corePackageRequiredFiles.All(fileType => Files.Any(file => file.Type == fileType));
|
||||
}
|
||||
public bool ContainsRequiredCorePackageFiles(out List<ContentType> missingContentTypes)
|
||||
{
|
||||
missingContentTypes = new List<ContentType>();
|
||||
foreach (ContentType contentType in corePackageRequiredFiles)
|
||||
{
|
||||
if (!Files.Any(file => file.Type == contentType))
|
||||
{
|
||||
missingContentTypes.Add(contentType);
|
||||
}
|
||||
}
|
||||
return missingContentTypes.Count == 0;
|
||||
}
|
||||
|
||||
public static ContentPackage CreatePackage(string name, string path, bool corePackage)
|
||||
{
|
||||
ContentPackage newPackage = new ContentPackage()
|
||||
{
|
||||
Name = name,
|
||||
Path = path,
|
||||
CorePackage = corePackage,
|
||||
GameVersion = GameMain.Version
|
||||
};
|
||||
|
||||
return newPackage;
|
||||
}
|
||||
|
||||
public ContentFile AddFile(string path, ContentType type)
|
||||
{
|
||||
if (files.Find(file => file.path == path && file.type == type) != null) return null;
|
||||
if (Files.Find(file => file.Path == path && file.Type == type) != null) return null;
|
||||
|
||||
ContentFile cf = new ContentFile(path, type);
|
||||
files.Add(cf);
|
||||
Files.Add(cf);
|
||||
|
||||
return cf;
|
||||
}
|
||||
|
||||
public void RemoveFile(ContentFile file)
|
||||
{
|
||||
files.Remove(file);
|
||||
Files.Remove(file);
|
||||
}
|
||||
|
||||
public void Save(string filePath)
|
||||
{
|
||||
XDocument doc = new XDocument();
|
||||
doc.Add(new XElement("contentpackage",
|
||||
new XAttribute("name", name),
|
||||
new XAttribute("path", Path)));
|
||||
|
||||
foreach (ContentFile file in files)
|
||||
doc.Add(new XElement("contentpackage",
|
||||
new XAttribute("name", Name),
|
||||
new XAttribute("path", Path),
|
||||
new XAttribute("corepackage", CorePackage)));
|
||||
|
||||
|
||||
doc.Root.Add(new XAttribute("gameversion", GameVersion.ToString()));
|
||||
|
||||
if (!string.IsNullOrEmpty(SteamWorkshopUrl))
|
||||
{
|
||||
doc.Root.Add(new XElement(file.type.ToString(), new XAttribute("file", file.path)));
|
||||
doc.Root.Add(new XAttribute("steamworkshopurl", SteamWorkshopUrl));
|
||||
}
|
||||
|
||||
doc.Save(System.IO.Path.Combine(filePath, name+".xml"));
|
||||
|
||||
foreach (ContentFile file in Files)
|
||||
{
|
||||
doc.Root.Add(new XElement(file.Type.ToString(), new XAttribute("file", file.Path)));
|
||||
}
|
||||
|
||||
doc.Save(filePath);
|
||||
}
|
||||
|
||||
private void CalculateHash()
|
||||
public void CalculateHash(bool logging = false)
|
||||
{
|
||||
List<byte[]> hashes = new List<byte[]>();
|
||||
|
||||
var md5 = MD5.Create();
|
||||
foreach (ContentFile file in files)
|
||||
{
|
||||
if (file.type == ContentType.Executable || file.type == ContentType.ServerExecutable) continue;
|
||||
|
||||
try
|
||||
if (logging)
|
||||
{
|
||||
DebugConsole.NewMessage("****************************** Calculating cp hash " + Name);
|
||||
}
|
||||
|
||||
foreach (ContentFile file in Files)
|
||||
{
|
||||
if (!multiplayerIncompatibleContent.Contains(file.Type)) continue;
|
||||
|
||||
try
|
||||
{
|
||||
using (var stream = File.OpenRead(file.path))
|
||||
var hash = CalculateFileHash(file);
|
||||
if (logging)
|
||||
{
|
||||
hashes.Add(md5.ComputeHash(stream));
|
||||
}
|
||||
var fileMd5 = new Md5Hash(hash);
|
||||
DebugConsole.NewMessage(" " + file.Path + ": " + fileMd5.ShortHash);
|
||||
}
|
||||
hashes.Add(hash);
|
||||
}
|
||||
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Error while calculating content package hash: ", e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
byte[] bytes = new byte[hashes.Count * 16];
|
||||
@@ -169,18 +313,107 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
md5Hash = new Md5Hash(bytes);
|
||||
if (logging)
|
||||
{
|
||||
DebugConsole.NewMessage("****************************** Package hash: " + md5Hash.ShortHash);
|
||||
}
|
||||
}
|
||||
|
||||
public List<string> GetFilesOfType(ContentType type)
|
||||
private byte[] CalculateFileHash(ContentFile file)
|
||||
{
|
||||
List<ContentFile> contentFiles = files.FindAll(f => f.type == type);
|
||||
var md5 = MD5.Create();
|
||||
|
||||
List<string> filePaths = new List<string>();
|
||||
foreach (ContentFile contentFile in contentFiles)
|
||||
List<string> filePaths = new List<string> { file.Path };
|
||||
List<byte> data = new List<byte>();
|
||||
|
||||
switch (file.Type)
|
||||
{
|
||||
filePaths.Add(contentFile.path);
|
||||
case ContentType.Character:
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file.Path);
|
||||
string speciesName = doc.Root.GetAttributeString("name", "");
|
||||
filePaths.Add(RagdollParams.GetDefaultFile(speciesName));
|
||||
foreach (AnimationType animationType in Enum.GetValues(typeof(AnimationType)))
|
||||
{
|
||||
filePaths.Add(AnimationParams.GetDefaultFile(speciesName, animationType));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return filePaths;
|
||||
|
||||
foreach (string filePath in filePaths)
|
||||
{
|
||||
if (!File.Exists(filePath)) continue;
|
||||
using (var stream = File.OpenRead(filePath))
|
||||
{
|
||||
byte[] fileData = new byte[stream.Length];
|
||||
stream.Read(fileData, 0, (int)stream.Length);
|
||||
if (filePath.EndsWith(".xml", true, System.Globalization.CultureInfo.InvariantCulture))
|
||||
{
|
||||
string text = System.Text.Encoding.UTF8.GetString(fileData);
|
||||
text = text.Replace("\n", "").Replace("\r", "");
|
||||
fileData = System.Text.Encoding.UTF8.GetBytes(text);
|
||||
}
|
||||
data.AddRange(fileData);
|
||||
}
|
||||
}
|
||||
return md5.ComputeHash(data.ToArray());
|
||||
}
|
||||
|
||||
public static string GetFileExtension(ContentType contentType)
|
||||
{
|
||||
switch (contentType)
|
||||
{
|
||||
case ContentType.Executable:
|
||||
case ContentType.ServerExecutable:
|
||||
return ".exe";
|
||||
default:
|
||||
return ".xml";
|
||||
}
|
||||
}
|
||||
|
||||
public static bool IsModFilePathAllowed(ContentFile contentFile)
|
||||
{
|
||||
string path = contentFile.Path;
|
||||
while (true)
|
||||
{
|
||||
string temp = System.IO.Path.GetDirectoryName(path);
|
||||
if (string.IsNullOrEmpty(temp)) { break; }
|
||||
path = temp;
|
||||
}
|
||||
switch (contentFile.Type)
|
||||
{
|
||||
case ContentType.Submarine:
|
||||
return path == "Submarines";
|
||||
default:
|
||||
return path == "Mods";
|
||||
}
|
||||
}
|
||||
public static bool IsModFilePathAllowed(string path)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
string temp = System.IO.Path.GetDirectoryName(path);
|
||||
if (string.IsNullOrEmpty(temp)) { break; }
|
||||
path = temp;
|
||||
}
|
||||
return path == "Mods";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns all xml files.
|
||||
/// </summary>
|
||||
public static IEnumerable<string> GetAllContentFiles(IEnumerable<ContentPackage> contentPackages)
|
||||
{
|
||||
return contentPackages.SelectMany(f => f.Files).Select(f => f.Path).Where(p => p.EndsWith(".xml"));
|
||||
}
|
||||
|
||||
public static IEnumerable<string> GetFilesOfType(IEnumerable<ContentPackage> contentPackages, ContentType type)
|
||||
{
|
||||
return contentPackages.SelectMany(f => f.Files).Where(f => f.Type == type).Select(f => f.Path);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetFilesOfType(ContentType type)
|
||||
{
|
||||
return Files.Where(f => f.Type == type).Select(f => f.Path);
|
||||
}
|
||||
|
||||
public static void LoadAll(string folder)
|
||||
@@ -191,40 +424,42 @@ namespace Barotrauma
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to create directory \"" + folder + "\"", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
string[] files = Directory.GetFiles(folder, "*.xml");
|
||||
|
||||
list.Clear();
|
||||
List.Clear();
|
||||
|
||||
foreach (string filePath in files)
|
||||
{
|
||||
ContentPackage package = new ContentPackage(filePath);
|
||||
list.Add(package);
|
||||
List.Add(package);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ContentFile
|
||||
{
|
||||
public string path;
|
||||
public ContentType type;
|
||||
public readonly string Path;
|
||||
public ContentType Type;
|
||||
|
||||
public ContentFile(string path, ContentType type)
|
||||
public Workshop.Item WorkShopItem;
|
||||
|
||||
public ContentFile(string path, ContentType type, Workshop.Item workShopItem = null)
|
||||
{
|
||||
Directory.GetCurrentDirectory();
|
||||
//Path.get
|
||||
this.path = path;
|
||||
this.type = type;
|
||||
Path = path;
|
||||
Type = type;
|
||||
WorkShopItem = workShopItem;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return path;
|
||||
return Path;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,10 +14,13 @@ namespace Barotrauma
|
||||
public readonly IEnumerator<object> Coroutine;
|
||||
public readonly string Name;
|
||||
|
||||
public Exception Exception;
|
||||
|
||||
public CoroutineHandle(IEnumerator<object> coroutine, string name = "")
|
||||
{
|
||||
Coroutine = coroutine;
|
||||
Name = string.IsNullOrWhiteSpace(name) ? coroutine.ToString() : name;
|
||||
Exception = null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -37,9 +40,9 @@ namespace Barotrauma
|
||||
return handle;
|
||||
}
|
||||
|
||||
public static void InvokeAfter(Action action, float delay)
|
||||
public static CoroutineHandle InvokeAfter(Action action, float delay)
|
||||
{
|
||||
StartCoroutine(DoInvokeAfter(action, delay));
|
||||
return StartCoroutine(DoInvokeAfter(action, delay));
|
||||
}
|
||||
|
||||
private static IEnumerable<object> DoInvokeAfter(Action action, float delay)
|
||||
@@ -107,6 +110,7 @@ namespace Barotrauma
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Coroutine " + handle.Name + " threw an exception: " + e.Message + "\n" + e.StackTrace.ToString());
|
||||
handle.Exception = e;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,31 +12,56 @@ namespace Barotrauma
|
||||
|
||||
private int state;
|
||||
|
||||
private Vector2 spawnPos;
|
||||
|
||||
private bool spawnPending;
|
||||
|
||||
public override Vector2 DebugDrawPos
|
||||
{
|
||||
get { return spawnPos; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "ScriptedEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")";
|
||||
return "ArtifactEvent (" + (itemPrefab == null ? "null" : itemPrefab.Name) + ")";
|
||||
}
|
||||
|
||||
public ArtifactEvent(XElement element)
|
||||
: base(element)
|
||||
public ArtifactEvent(ScriptedEventPrefab prefab)
|
||||
: base(prefab)
|
||||
{
|
||||
string itemName = element.GetAttributeString("itemname", "");
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
|
||||
if (itemPrefab == null)
|
||||
if (prefab.ConfigElement.Attribute("itemname") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
|
||||
DebugConsole.ThrowError("Error in ArtifactEvent - use item identifier instead of the name of the item.");
|
||||
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in ArtifactEvent - couldn't find an item prefab with the identifier " + itemIdentifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
public override void Init(bool affectSubImmediately)
|
||||
{
|
||||
base.Init();
|
||||
spawnPos = Level.Loaded.GetRandomItemPos(
|
||||
(Rand.Range(0.0f, 1.0f, Rand.RandSync.Server) < 0.5f) ? Level.PositionType.MainPath : Level.PositionType.Cave | Level.PositionType.Ruin,
|
||||
500.0f, 10000.0f, 30.0f);
|
||||
|
||||
Vector2 position = Level.Loaded.GetRandomItemPos(
|
||||
Level.PositionType.Cave | Level.PositionType.MainPath | Level.PositionType.Ruin, 500.0f, 10000.0f, 30.0f);
|
||||
|
||||
item = new Item(itemPrefab, position, null);
|
||||
spawnPending = true;
|
||||
}
|
||||
|
||||
private void SpawnItem()
|
||||
{
|
||||
item = new Item(itemPrefab, spawnPos, null);
|
||||
item.body.FarseerBody.IsKinematic = true;
|
||||
|
||||
//try to find a nearby artifact holder (or any alien itemcontainer) and place the artifact inside it
|
||||
@@ -58,10 +83,21 @@ namespace Barotrauma
|
||||
{
|
||||
DebugConsole.NewMessage("Initialized ArtifactEvent (" + item.Name + ")", Color.White);
|
||||
}
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
Entity.Spawner.CreateNetworkEvent(item, false);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (spawnPending)
|
||||
{
|
||||
SpawnItem();
|
||||
spawnPending = false;
|
||||
}
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
|
||||
@@ -1,14 +1,47 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class EventManager
|
||||
partial class EventManager
|
||||
{
|
||||
const float CriticalPriority = 50.0f;
|
||||
const float IntensityUpdateInterval = 5.0f;
|
||||
|
||||
private List<ScriptedEvent> events;
|
||||
|
||||
private Level level;
|
||||
|
||||
//The "intensity" of the current situation (a value between 0.0 - 1.0).
|
||||
//High when a disaster has struck, low when nothing special is going on.
|
||||
private float currentIntensity;
|
||||
//The exact intensity of the current situation, current intensity is lerped towards this value
|
||||
private float targetIntensity;
|
||||
|
||||
//How low the intensity has to be for an event to be triggered.
|
||||
//Gradually increases with time, so additional problems can still appear eventually even if
|
||||
//the sub is laying broken on the ocean floor or if the players are trying to abuse the system
|
||||
//by intentionally keeping the intensity high by causing breaches, damaging themselves or such
|
||||
private float eventThreshold = 0.2f;
|
||||
|
||||
//New events can't be triggered when the cooldown is active.
|
||||
private float eventCoolDown;
|
||||
|
||||
private float intensityUpdateTimer;
|
||||
|
||||
private float avgCrewHealth, avgHullIntegrity, floodingAmount, fireAmount, enemyDanger;
|
||||
|
||||
private float roundDuration;
|
||||
|
||||
private List<ScriptedEventSet> selectedEventSets;
|
||||
|
||||
private EventManagerSettings settings;
|
||||
|
||||
public float CurrentIntensity
|
||||
{
|
||||
get { return currentIntensity; }
|
||||
}
|
||||
|
||||
public List<ScriptedEvent> Events
|
||||
{
|
||||
get { return events; }
|
||||
@@ -16,40 +49,160 @@ namespace Barotrauma
|
||||
|
||||
public EventManager(GameSession session)
|
||||
{
|
||||
events = new List<ScriptedEvent>();
|
||||
events = new List<ScriptedEvent>();
|
||||
selectedEventSets = new List<ScriptedEventSet>();
|
||||
}
|
||||
|
||||
|
||||
public bool Enabled = true;
|
||||
|
||||
public void StartRound(Level level)
|
||||
{
|
||||
CreateScriptedEvents(level);
|
||||
if (GameMain.Client != null) return;
|
||||
|
||||
var suitableSettings = EventManagerSettings.List.FindAll(s =>
|
||||
level.Difficulty >= s.MinLevelDifficulty &&
|
||||
level.Difficulty <= s.MaxLevelDifficulty);
|
||||
|
||||
if (suitableSettings.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("No suitable event manager settings found for the selected level (difficulty " + level.Difficulty + ")");
|
||||
settings = EventManagerSettings.List[Rand.Int(EventManagerSettings.List.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
else
|
||||
{
|
||||
settings = suitableSettings[Rand.Int(suitableSettings.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
|
||||
this.level = level;
|
||||
var initialEventSet = SelectRandomEvents(ScriptedEventSet.List);
|
||||
if (initialEventSet != null) selectedEventSets.Add(initialEventSet);
|
||||
/*CreateInitialEvents();
|
||||
foreach (ScriptedEvent ev in events)
|
||||
{
|
||||
ev.Init();
|
||||
}
|
||||
ev.Init(false);
|
||||
}*/
|
||||
|
||||
roundDuration = 0.0f;
|
||||
intensityUpdateTimer = 0.0f;
|
||||
CalculateCurrentIntensity(0.0f);
|
||||
currentIntensity = targetIntensity;
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
eventCoolDown = 0.0f;
|
||||
}
|
||||
|
||||
public void EndRound()
|
||||
{
|
||||
selectedEventSets.Clear();
|
||||
events.Clear();
|
||||
}
|
||||
|
||||
private void CreateScriptedEvents(Level level)
|
||||
private ScriptedEventSet SelectRandomEvents(List<ScriptedEventSet> eventSets)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(events.Count == 0);
|
||||
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
|
||||
if (GameSettings.VerboseLogging)
|
||||
var allowedEventSets =
|
||||
eventSets.Where(es => level.Difficulty >= es.MinLevelDifficulty && level.Difficulty <= es.MaxLevelDifficulty);
|
||||
|
||||
float totalCommonness = allowedEventSets.Sum(e => e.GetCommonness(level));
|
||||
float randomNumber = (float)rand.NextDouble() * totalCommonness;
|
||||
foreach (ScriptedEventSet eventSet in allowedEventSets)
|
||||
{
|
||||
DebugConsole.NewMessage("Generating events (seed: " + level.Seed + ")", Color.White);
|
||||
float commonness = eventSet.GetCommonness(level);
|
||||
if (randomNumber <= commonness)
|
||||
{
|
||||
return eventSet;
|
||||
}
|
||||
randomNumber -= commonness;
|
||||
}
|
||||
|
||||
events.AddRange(ScriptedEvent.GenerateLevelEvents(rand, level));
|
||||
return null;
|
||||
}
|
||||
|
||||
private void CreateEvents()
|
||||
{
|
||||
for (int i = selectedEventSets.Count - 1; i >= 0; i--)
|
||||
{
|
||||
ScriptedEventSet eventSet = selectedEventSets[i];
|
||||
|
||||
float distanceTraveled = MathHelper.Clamp(
|
||||
(Submarine.MainSub.WorldPosition.X - level.StartPosition.X) / (level.EndPosition.X - level.StartPosition.X),
|
||||
0.0f, 1.0f);
|
||||
|
||||
if (Level.Loaded?.StartOutpost != null &&
|
||||
Submarine.MainSub.DockedTo.Contains(Level.Loaded.StartOutpost))
|
||||
{
|
||||
distanceTraveled = 0.0f;
|
||||
}
|
||||
|
||||
if ((Submarine.MainSub == null || distanceTraveled < eventSet.MinDistanceTraveled) &&
|
||||
roundDuration < eventSet.MinMissionTime)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CurrentIntensity < eventSet.MinIntensity || CurrentIntensity > eventSet.MaxIntensity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
selectedEventSets.RemoveAt(i);
|
||||
|
||||
if (eventSet.ChooseRandom)
|
||||
{
|
||||
if (eventSet.EventPrefabs.Count > 0)
|
||||
{
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
var newEvent = eventSet.EventPrefabs[rand.NextInt32() % eventSet.EventPrefabs.Count].CreateInstance();
|
||||
newEvent.Init(true);
|
||||
DebugConsole.Log("Initialized event " + newEvent.ToString());
|
||||
events.Add(newEvent);
|
||||
}
|
||||
if (eventSet.ChildSets.Count > 0)
|
||||
{
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(level.Seed));
|
||||
var newEventSet = SelectRandomEvents(eventSet.ChildSets);
|
||||
if (newEventSet != null) selectedEventSets.Add(newEventSet);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (ScriptedEventPrefab eventPrefab in eventSet.EventPrefabs)
|
||||
{
|
||||
var newEvent = eventPrefab.CreateInstance();
|
||||
newEvent.Init(true);
|
||||
DebugConsole.Log("Initialized event " + newEvent.ToString());
|
||||
events.Add(newEvent);
|
||||
}
|
||||
|
||||
selectedEventSets.AddRange(eventSet.ChildSets);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
events.RemoveAll(t => t.IsFinished);
|
||||
if (!Enabled) return;
|
||||
|
||||
//clients only calculate the intensity but don't create any events
|
||||
//(the intensity is used for controlling the background music)
|
||||
CalculateCurrentIntensity(deltaTime);
|
||||
|
||||
if (GameMain.Client != null) { return; }
|
||||
|
||||
roundDuration += deltaTime;
|
||||
|
||||
eventThreshold += settings.EventThresholdIncrease * deltaTime;
|
||||
if (eventCoolDown > 0.0f)
|
||||
{
|
||||
eventCoolDown -= deltaTime;
|
||||
}
|
||||
else if (currentIntensity < eventThreshold)
|
||||
{
|
||||
CreateEvents();
|
||||
eventThreshold = settings.DefaultEventThreshold;
|
||||
eventCoolDown = settings.EventCooldown;
|
||||
}
|
||||
|
||||
foreach (ScriptedEvent ev in events)
|
||||
{
|
||||
if (!ev.IsFinished)
|
||||
@@ -58,5 +211,100 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void CalculateCurrentIntensity(float deltaTime)
|
||||
{
|
||||
intensityUpdateTimer -= deltaTime;
|
||||
if (intensityUpdateTimer > 0.0f) return;
|
||||
intensityUpdateTimer = IntensityUpdateInterval;
|
||||
|
||||
// crew health --------------------------------------------------------
|
||||
|
||||
avgCrewHealth = 0.0f;
|
||||
int characterCount = 0;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsDead) continue;
|
||||
if ((character.AIController is HumanAIController || character.IsRemotePlayer || character == Character.Controlled) &&
|
||||
(GameMain.NetworkMember?.Character == null || GameMain.NetworkMember.Character.TeamID == character.TeamID))
|
||||
{
|
||||
avgCrewHealth += character.Vitality / character.MaxVitality * (character.IsUnconscious ? 0.5f : 1.0f);
|
||||
characterCount++;
|
||||
}
|
||||
}
|
||||
if (characterCount > 0)
|
||||
{
|
||||
avgCrewHealth = avgCrewHealth / characterCount;
|
||||
}
|
||||
|
||||
// enemy amount --------------------------------------------------------
|
||||
|
||||
enemyDanger = 0.0f;
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (character.IsDead || character.IsUnconscious || !character.Enabled) continue;
|
||||
|
||||
EnemyAIController enemyAI = character.AIController as EnemyAIController;
|
||||
if (enemyAI == null) continue;
|
||||
|
||||
if (character.CurrentHull?.Submarine != null &&
|
||||
(character.CurrentHull.Submarine == Submarine.MainSub || Submarine.MainSub.DockedTo.Contains(character.CurrentHull.Submarine)))
|
||||
{
|
||||
//crawler inside the sub adds 0.1f to enemy danger, mantis 0.25f
|
||||
enemyDanger += enemyAI.CombatStrength / 1000.0f;
|
||||
}
|
||||
else if (enemyAI.SelectedAiTarget?.Entity?.Submarine != null)
|
||||
{
|
||||
//enemy outside and targeting the sub or something in it
|
||||
//moloch adds 0.24 to enemy danger, a crawler 0.02
|
||||
enemyDanger += enemyAI.CombatStrength / 5000.0f;
|
||||
}
|
||||
}
|
||||
enemyDanger = MathHelper.Clamp(enemyDanger, 0.0f, 1.0f);
|
||||
|
||||
// hull status (gaps, flooding, fire) --------------------------------------------------------
|
||||
|
||||
float holeCount = 0.0f;
|
||||
floodingAmount = 0.0f;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
{
|
||||
if (hull.Submarine == null || hull.Submarine.IsOutpost) { continue; }
|
||||
foreach (Gap gap in hull.ConnectedGaps)
|
||||
{
|
||||
if (!gap.IsRoomToRoom) holeCount += gap.Open;
|
||||
}
|
||||
floodingAmount += hull.WaterVolume / hull.Volume / Hull.hullList.Count;
|
||||
fireAmount += hull.FireSources.Sum(fs => fs.Size.X);
|
||||
}
|
||||
//hull integrity at 0.0 if there are 10 or more wide-open holes
|
||||
avgHullIntegrity = MathHelper.Clamp(1.0f - holeCount / 10.0f, 0.0f, 1.0f);
|
||||
|
||||
//a fire of any size bumps up the fire amount to 20%
|
||||
//if the total width of the fires is 1000 or more, the fire amount is considered to be at 100%
|
||||
fireAmount = MathHelper.Clamp(fireAmount / 1000.0f, fireAmount > 0.0f ? 0.2f : 0.0f, 1.0f);
|
||||
|
||||
//flooding less than 10% of the sub is ignored
|
||||
//to prevent ballast tanks from affecting the intensity
|
||||
if (floodingAmount < 0.1f) floodingAmount = 0.0f;
|
||||
|
||||
// calculate final intensity --------------------------------------------------------
|
||||
|
||||
targetIntensity =
|
||||
((1.0f - avgCrewHealth) + (1.0f - avgHullIntegrity) + floodingAmount) / 3.0f;
|
||||
targetIntensity += fireAmount * 0.5f;
|
||||
targetIntensity += enemyDanger;
|
||||
targetIntensity = MathHelper.Clamp(targetIntensity, 0.0f, 1.0f);
|
||||
|
||||
if (targetIntensity > currentIntensity)
|
||||
{
|
||||
//50 seconds for intensity to go from 0.0 to 1.0
|
||||
currentIntensity = MathHelper.Min(currentIntensity + 0.02f * IntensityUpdateInterval, targetIntensity);
|
||||
}
|
||||
else
|
||||
{
|
||||
//400 seconds for intensity to go from 1.0 to 0.0
|
||||
currentIntensity = MathHelper.Max(0.0025f * IntensityUpdateInterval, targetIntensity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class EventManagerSettings
|
||||
{
|
||||
public static readonly List<EventManagerSettings> List = new List<EventManagerSettings>();
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
//How much the event threshold increases per second. 0.0005f = 0.03f per minute
|
||||
public readonly float EventThresholdIncrease = 0.0005f;
|
||||
|
||||
//The threshold is reset to this value after an event has been triggered.
|
||||
public readonly float DefaultEventThreshold = 0.2f;
|
||||
|
||||
public readonly float EventCooldown = 360.0f;
|
||||
|
||||
public readonly float MinEventDifficulty = 0.0f;
|
||||
public readonly float MaxEventDifficulty = 100.0f;
|
||||
|
||||
public readonly float MinLevelDifficulty = 0.0f;
|
||||
public readonly float MaxLevelDifficulty = 100.0f;
|
||||
|
||||
static EventManagerSettings()
|
||||
{
|
||||
Load(Path.Combine("Content", "EventManagerSettings.xml"));
|
||||
}
|
||||
|
||||
private static void Load(string file)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file);
|
||||
if (doc == null || doc.Root == null) return;
|
||||
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
List.Add(new EventManagerSettings(subElement));
|
||||
}
|
||||
}
|
||||
|
||||
public EventManagerSettings(XElement element)
|
||||
{
|
||||
Name = element.Name.ToString();
|
||||
EventThresholdIncrease = element.GetAttributeFloat("EventThresholdIncrease", 0.0005f);
|
||||
DefaultEventThreshold = element.GetAttributeFloat("DefaultEventThreshold", 0.2f);
|
||||
EventCooldown = element.GetAttributeFloat("EventCooldown", 360.0f);
|
||||
|
||||
MinEventDifficulty = element.GetAttributeFloat("MinEventDifficulty", 0.0f);
|
||||
MaxEventDifficulty = element.GetAttributeFloat("MaxEventDifficulty", 100.0f);
|
||||
|
||||
MinLevelDifficulty = element.GetAttributeFloat("MinLevelDifficulty", 0.0f);
|
||||
MaxLevelDifficulty = element.GetAttributeFloat("MaxLevelDifficulty", 100.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class MalfunctionEvent : ScriptedEvent
|
||||
{
|
||||
private string[] targetItemIdentifiers;
|
||||
|
||||
private List<Item> targetItems;
|
||||
|
||||
private int minItemAmount, maxItemAmount;
|
||||
|
||||
private float decreaseConditionAmount;
|
||||
|
||||
private float duration;
|
||||
|
||||
private float timer;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "MalfunctionEvent (" + string.Join(", ", targetItemIdentifiers) + ")";
|
||||
}
|
||||
|
||||
public MalfunctionEvent(ScriptedEventPrefab prefab)
|
||||
: base(prefab)
|
||||
{
|
||||
targetItems = new List<Item>();
|
||||
|
||||
minItemAmount = prefab.ConfigElement.GetAttributeInt("minitemamount", 1);
|
||||
maxItemAmount = prefab.ConfigElement.GetAttributeInt("maxitemamount", minItemAmount);
|
||||
|
||||
decreaseConditionAmount = prefab.ConfigElement.GetAttributeFloat("decreaseconditionamount", 0.0f);
|
||||
duration = prefab.ConfigElement.GetAttributeFloat("duration", 0.0f);
|
||||
|
||||
targetItemIdentifiers = prefab.ConfigElement.GetAttributeStringArray("itemidentifiers", new string[0]);
|
||||
}
|
||||
|
||||
public override bool CanAffectSubImmediately(Level level)
|
||||
{
|
||||
return Item.ItemList.Count(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier)) >= maxItemAmount;
|
||||
}
|
||||
|
||||
public override void Init(bool affectSubImmediately)
|
||||
{
|
||||
var matchingItems = Item.ItemList.FindAll(i => i.Condition > 0.0f && targetItemIdentifiers.Contains(i.Prefab.Identifier));
|
||||
int itemAmount = Rand.Range(minItemAmount, maxItemAmount, Rand.RandSync.Server);
|
||||
for (int i = 0; i < itemAmount; i++)
|
||||
{
|
||||
if (matchingItems.Count == 0) break;
|
||||
targetItems.Add(matchingItems[Rand.Int(matchingItems.Count, Rand.RandSync.Server)]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
if (isFinished) return;
|
||||
if (targetItems.Count == 0 || timer >= duration)
|
||||
{
|
||||
Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
targetItems.RemoveAll(i => i.Removed || i.Condition <= 0.0f);
|
||||
foreach (Item item in targetItems)
|
||||
{
|
||||
if (duration <= 0.0f)
|
||||
{
|
||||
item.Condition = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
item.Condition -= decreaseConditionAmount / duration * deltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
timer += deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace Barotrauma
|
||||
: base(prefab, locations)
|
||||
{
|
||||
itemConfig = prefab.ConfigElement.Element("Items");
|
||||
|
||||
requiredDeliveryAmount = prefab.ConfigElement.GetAttributeInt("requireddeliveryamount", 0);
|
||||
}
|
||||
|
||||
@@ -25,7 +24,7 @@ namespace Barotrauma
|
||||
{
|
||||
items = new List<Item>();
|
||||
|
||||
if (itemConfig==null)
|
||||
if (itemConfig == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to initialize items for cargo mission (itemConfig == null)");
|
||||
return;
|
||||
@@ -41,9 +40,29 @@ namespace Barotrauma
|
||||
|
||||
private void LoadItemAsChild(XElement element, Item parent)
|
||||
{
|
||||
string itemName = element.GetAttributeString("name", "");
|
||||
ItemPrefab itemPrefab;
|
||||
if (element.Attribute("name") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in cargo mission \"" + Name + "\" - use item identifiers instead of names to configure the items.");
|
||||
string itemName = element.GetAttributeString("name", "");
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemName + "\" not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemIdentifier = element.GetAttributeString("identifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + itemIdentifier + "\" not found");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ItemPrefab itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Couldn't spawn item for cargo mission: item prefab \"" + element.Name.ToString() + "\" not found");
|
||||
|
||||
@@ -26,13 +26,18 @@ namespace Barotrauma
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public int Winner
|
||||
{
|
||||
get { return winner; }
|
||||
}
|
||||
|
||||
public override string Description
|
||||
{
|
||||
get
|
||||
{
|
||||
if (descriptions == null) return "";
|
||||
|
||||
if (GameMain.NetworkMember==null || GameMain.NetworkMember.Character==null)
|
||||
|
||||
if (GameMain.NetworkMember == null || GameMain.NetworkMember.Character == null)
|
||||
{
|
||||
//non-team-specific description
|
||||
return descriptions[0];
|
||||
@@ -61,9 +66,9 @@ namespace Barotrauma
|
||||
{
|
||||
descriptions = new string[]
|
||||
{
|
||||
prefab.ConfigElement.GetAttributeString("descriptionneutral", ""),
|
||||
prefab.ConfigElement.GetAttributeString("description1", ""),
|
||||
prefab.ConfigElement.GetAttributeString("description2", "")
|
||||
TextManager.Get("MissionDescriptionNeutral." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("descriptionneutral", ""),
|
||||
TextManager.Get("MissionDescription1." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("description1", ""),
|
||||
TextManager.Get("MissionDescription2." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("description2", "")
|
||||
};
|
||||
|
||||
for (int i = 0; i < descriptions.Length; i++)
|
||||
@@ -73,11 +78,11 @@ namespace Barotrauma
|
||||
descriptions[i] = descriptions[i].Replace("[location" + (n + 1) + "]", locations[n].Name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
teamNames = new string[]
|
||||
{
|
||||
prefab.ConfigElement.GetAttributeString("teamname1", "Team A"),
|
||||
prefab.ConfigElement.GetAttributeString("teamname2", "Team B")
|
||||
TextManager.Get("MissionTeam1." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname1", "Team A"),
|
||||
TextManager.Get("MissionTeam2." + prefab.Identifier, true) ?? prefab.ConfigElement.GetAttributeString("teamname2", "Team B")
|
||||
};
|
||||
}
|
||||
|
||||
@@ -94,6 +99,11 @@ namespace Barotrauma
|
||||
return teamNames[teamID];
|
||||
}
|
||||
|
||||
public bool IsInWinningTeam(Character character)
|
||||
{
|
||||
return character != null && winner > -1 && character.TeamID - 1 == winner;
|
||||
}
|
||||
|
||||
public override bool AssignTeamIDs(List<Client> clients, out byte hostTeam)
|
||||
{
|
||||
List<Client> randList = new List<Client>(clients);
|
||||
@@ -143,7 +153,7 @@ namespace Barotrauma
|
||||
|
||||
subs = new Submarine[] { Submarine.MainSubs[0], Submarine.MainSubs[1] };
|
||||
subs[0].TeamID = 1; subs[1].TeamID = 2;
|
||||
subs[1].SetPosition(Level.Loaded.EndPosition - new Vector2(0.0f, 2000.0f));
|
||||
subs[1].SetPosition(subs[1].FindSpawnPos(Level.Loaded.EndPosition));
|
||||
subs[1].FlipX();
|
||||
|
||||
//prevent wifi components from communicating between subs
|
||||
@@ -167,8 +177,8 @@ namespace Barotrauma
|
||||
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
//hide all subs from radar to make sneak attacks possible
|
||||
submarine.OnRadar = false;
|
||||
//hide all subs from sonar to make sneak attacks possible
|
||||
submarine.OnSonar = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,10 +219,10 @@ namespace Barotrauma
|
||||
{
|
||||
for (int i = 0; i < teamDead.Length; i++)
|
||||
{
|
||||
if (!teamDead[i] && teamDead[1-i])
|
||||
if (!teamDead[i] && teamDead[1 - i])
|
||||
{
|
||||
//make sure nobody in the other team can be revived because that would be pretty weird
|
||||
crews[1-i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeath.Damage); });
|
||||
crews[1 - i].ForEach(c => { if (!c.IsDead) c.Kill(CauseOfDeathType.Unknown, null); });
|
||||
|
||||
winner = i;
|
||||
|
||||
@@ -226,12 +236,10 @@ namespace Barotrauma
|
||||
}
|
||||
else
|
||||
{
|
||||
if (winner >= 0 && subs[winner] != null &&
|
||||
(winner == 0 && subs[winner].AtStartPosition) || (winner == 1 && subs[winner].AtEndPosition) &&
|
||||
crews[winner].Any(c => !c.IsDead && c.Submarine == subs[winner]))
|
||||
if (winner >= 0)
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.GameSession.CrewManager.WinningTeam = winner+1;
|
||||
GameMain.GameSession.CrewManager.WinningTeam = winner + 1;
|
||||
#endif
|
||||
if (GameMain.Server != null) GameMain.Server.EndGame();
|
||||
}
|
||||
|
||||
@@ -1,18 +1,47 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class Mission
|
||||
{
|
||||
{
|
||||
public readonly MissionPrefab Prefab;
|
||||
protected bool completed;
|
||||
|
||||
private readonly MissionPrefab prefab;
|
||||
|
||||
|
||||
public readonly List<string> Headers;
|
||||
public readonly List<string> Messages;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return prefab.Name; }
|
||||
get { return Prefab.Name; }
|
||||
}
|
||||
|
||||
private string successMessage;
|
||||
public virtual string SuccessMessage
|
||||
{
|
||||
get { return successMessage; }
|
||||
private set { successMessage = value; }
|
||||
}
|
||||
|
||||
private string failureMessage;
|
||||
public virtual string FailureMessage
|
||||
{
|
||||
get { return failureMessage; }
|
||||
private set { failureMessage = value; }
|
||||
}
|
||||
|
||||
private string description;
|
||||
public virtual string Description
|
||||
{
|
||||
get { return description; }
|
||||
private set { description = value; }
|
||||
}
|
||||
|
||||
public int Reward
|
||||
{
|
||||
get { return Prefab.Reward; }
|
||||
}
|
||||
|
||||
public bool Completed
|
||||
@@ -20,72 +49,38 @@ namespace Barotrauma
|
||||
get { return completed; }
|
||||
set { completed = value; }
|
||||
}
|
||||
|
||||
public int Reward
|
||||
{
|
||||
get { return prefab.Reward; }
|
||||
}
|
||||
|
||||
|
||||
public virtual bool AllowRespawn
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
public virtual Vector2 RadarPosition
|
||||
public virtual Vector2 SonarPosition
|
||||
{
|
||||
get { return Vector2.Zero; }
|
||||
}
|
||||
|
||||
public string RadarLabel
|
||||
{
|
||||
get { return prefab.RadarLabel; }
|
||||
}
|
||||
|
||||
public List<string> Headers
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public List<string> Messages
|
||||
{
|
||||
get; private set;
|
||||
}
|
||||
|
||||
public virtual string SuccessMessage
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public string FailureMessage
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public virtual string Description
|
||||
{
|
||||
get;
|
||||
protected set;
|
||||
}
|
||||
|
||||
public MissionPrefab Prefab
|
||||
{
|
||||
get { return prefab; }
|
||||
}
|
||||
|
||||
public string SonarLabel
|
||||
{
|
||||
get { return Prefab.SonarLabel; }
|
||||
}
|
||||
|
||||
public readonly Location[] Locations;
|
||||
|
||||
public Mission(MissionPrefab prefab, Location[] locations)
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(locations.Length == 2);
|
||||
|
||||
this.prefab = prefab;
|
||||
Prefab = prefab;
|
||||
|
||||
Description = prefab.Description;
|
||||
SuccessMessage = prefab.SuccessMessage;
|
||||
FailureMessage = prefab.FailureMessage;
|
||||
Headers = new List<string>(prefab.Headers);
|
||||
Messages = new List<string>(prefab.Messages);
|
||||
|
||||
|
||||
Locations = locations;
|
||||
|
||||
for (int n = 0; n < 2; n++)
|
||||
{
|
||||
if (Description != null) Description = Description.Replace("[location" + (n + 1) + "]", locations[n].Name);
|
||||
@@ -97,46 +92,35 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, string seed, string missionType = "", bool isSinglePlayer = false)
|
||||
public static Mission LoadRandom(Location[] locations, string seed, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
{
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), missionType, isSinglePlayer);
|
||||
return LoadRandom(locations, new MTRandom(ToolBox.StringToInt(seed)), requireCorrectLocationType, missionType, isSinglePlayer);
|
||||
}
|
||||
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, string missionType = "", bool isSinglePlayer = false)
|
||||
public static Mission LoadRandom(Location[] locations, MTRandom rand, bool requireCorrectLocationType, MissionType missionType, bool isSinglePlayer = false)
|
||||
{
|
||||
//todo: use something else than strings to define the mission type
|
||||
missionType = missionType.ToLowerInvariant();
|
||||
|
||||
List<MissionPrefab> allowedMissions = new List<MissionPrefab>();
|
||||
if (missionType == "random")
|
||||
if (missionType == MissionType.Random)
|
||||
{
|
||||
allowedMissions.AddRange(MissionPrefab.List);
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
allowedMissions.RemoveAll(mission => !GameMain.Server.AllowedRandomMissionTypes.Any(a => mission.TypeMatches(a)));
|
||||
allowedMissions.RemoveAll(mission => !GameMain.Server.AllowedRandomMissionTypes.Contains(mission.type));
|
||||
}
|
||||
}
|
||||
else if (missionType == "none")
|
||||
else if (missionType == MissionType.None)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(missionType))
|
||||
{
|
||||
allowedMissions.AddRange(MissionPrefab.List);
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedMissions = MissionPrefab.List.FindAll(m => m.TypeMatches(missionType));
|
||||
allowedMissions = MissionPrefab.List.FindAll(m => m.type == missionType);
|
||||
}
|
||||
|
||||
if (isSinglePlayer)
|
||||
allowedMissions.RemoveAll(m => isSinglePlayer ? m.MultiplayerOnly : m.SingleplayerOnly);
|
||||
if (requireCorrectLocationType)
|
||||
{
|
||||
allowedMissions.RemoveAll(m => m.MultiplayerOnly);
|
||||
}
|
||||
else
|
||||
{
|
||||
allowedMissions.RemoveAll(m => m.SingleplayerOnly);
|
||||
allowedMissions.RemoveAll(m => !m.IsAllowed(locations[0], locations[1]));
|
||||
}
|
||||
|
||||
float probabilitySum = allowedMissions.Sum(m => m.Commonness);
|
||||
@@ -178,7 +162,7 @@ namespace Barotrauma
|
||||
{
|
||||
var mode = GameMain.GameSession.GameMode as CampaignMode;
|
||||
if (mode == null) return;
|
||||
|
||||
|
||||
mode.Money += Reward;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,43 +5,59 @@ using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
enum MissionType
|
||||
{
|
||||
Random,
|
||||
None,
|
||||
Salvage,
|
||||
Monster,
|
||||
Cargo,
|
||||
Combat
|
||||
}
|
||||
|
||||
class MissionPrefab
|
||||
{
|
||||
public static List<MissionPrefab> List = new List<MissionPrefab>();
|
||||
public static List<string> MissionTypes = new List<string>() { "Random" };
|
||||
public static readonly List<MissionPrefab> List = new List<MissionPrefab>();
|
||||
|
||||
private string name;
|
||||
|
||||
public string Name
|
||||
private static readonly Dictionary<MissionType, Type> missionClasses = new Dictionary<MissionType, Type>()
|
||||
{
|
||||
get { return name; }
|
||||
}
|
||||
|
||||
private Type missionType;
|
||||
{ MissionType.Salvage, typeof(SalvageMission) },
|
||||
{ MissionType.Monster, typeof(MonsterMission) },
|
||||
{ MissionType.Cargo, typeof(CargoMission) },
|
||||
{ MissionType.Combat, typeof(CombatMission) },
|
||||
};
|
||||
|
||||
private ConstructorInfo constructor;
|
||||
|
||||
public virtual string Description { get; private set; }
|
||||
public readonly MissionType type;
|
||||
|
||||
public bool MultiplayerOnly { get; private set; }
|
||||
public bool SingleplayerOnly { get; private set; }
|
||||
public readonly bool MultiplayerOnly, SingleplayerOnly;
|
||||
|
||||
public float Commonness { get; private set; }
|
||||
public readonly string Identifier;
|
||||
|
||||
public int Reward { get; private set; }
|
||||
public readonly string Name;
|
||||
public readonly string Description;
|
||||
public readonly string SuccessMessage;
|
||||
public readonly string FailureMessage;
|
||||
public readonly string SonarLabel;
|
||||
|
||||
public string RadarLabel { get; private set; }
|
||||
public readonly string AchievementIdentifier;
|
||||
|
||||
public List<string> Headers { get; private set; }
|
||||
public List<string> Messages { get; private set; }
|
||||
public readonly int Commonness;
|
||||
|
||||
public string SuccessMessage { get; private set; }
|
||||
public string FailureMessage { get; private set; }
|
||||
public readonly int Reward;
|
||||
|
||||
public XElement ConfigElement { get; private set; }
|
||||
public readonly List<string> Headers;
|
||||
public readonly List<string> Messages;
|
||||
|
||||
//the mission can only be received when travelling from Pair.First to Pair.Second
|
||||
public readonly List<Pair<string, string>> AllowedLocationTypes;
|
||||
|
||||
public readonly XElement ConfigElement;
|
||||
|
||||
public static void Init()
|
||||
{
|
||||
var files = GameMain.SelectedPackage.GetFilesOfType(ContentType.Missions);
|
||||
var files = GameMain.Instance.GetFilesOfType(ContentType.Missions);
|
||||
foreach (string file in files)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(file);
|
||||
@@ -49,13 +65,8 @@ namespace Barotrauma
|
||||
|
||||
foreach (XElement element in doc.Root.Elements())
|
||||
{
|
||||
string missionTypeName = element.Name.ToString();
|
||||
missionTypeName = missionTypeName.Replace("Mission", "");
|
||||
|
||||
List.Add(new MissionPrefab(element));
|
||||
if (!MissionTypes.Contains(missionTypeName)) MissionTypes.Add(missionTypeName);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,56 +74,86 @@ namespace Barotrauma
|
||||
{
|
||||
ConfigElement = element;
|
||||
|
||||
name = element.GetAttributeString("name", "");
|
||||
Description = element.GetAttributeString("description", "");
|
||||
Commonness = element.GetAttributeFloat("commonness", 1.0f);
|
||||
SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
|
||||
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
|
||||
Identifier = element.GetAttributeString("identifier", "");
|
||||
|
||||
Reward = element.GetAttributeInt("reward", 1);
|
||||
Name = TextManager.Get("MissionName." + Identifier, true) ?? element.GetAttributeString("name", "");
|
||||
Description = TextManager.Get("MissionDescription." + Identifier, true) ?? element.GetAttributeString("description", "");
|
||||
Reward = element.GetAttributeInt("reward", 1);
|
||||
|
||||
SuccessMessage = element.GetAttributeString("successmessage", "Mission completed successfully");
|
||||
FailureMessage = element.GetAttributeString("failuremessage", "Mission failed");
|
||||
RadarLabel = element.GetAttributeString("radarlabel", "");
|
||||
Commonness = element.GetAttributeInt("commonness", 1);
|
||||
|
||||
SuccessMessage = TextManager.Get("MissionSuccess." + Identifier, true) ?? element.GetAttributeString("successmessage", "Mission completed successfully");
|
||||
FailureMessage = TextManager.Get("MissionFailure." + Identifier, true) ?? element.GetAttributeString("failuremessage", "Mission failed");
|
||||
|
||||
SonarLabel = TextManager.Get("MissionSonarLabel." + Identifier, true) ?? element.GetAttributeString("sonarlabel", "");
|
||||
|
||||
MultiplayerOnly = element.GetAttributeBool("multiplayeronly", false);
|
||||
SingleplayerOnly = element.GetAttributeBool("singleplayeronly", false);
|
||||
|
||||
AchievementIdentifier = element.GetAttributeString("achievementidentifier", "");
|
||||
|
||||
Messages = new List<string>();
|
||||
Headers = new List<string>();
|
||||
Messages = new List<string>();
|
||||
AllowedLocationTypes = new List<Pair<string, string>>();
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "message") continue;
|
||||
Headers.Add(subElement.GetAttributeString("header", ""));
|
||||
Messages.Add(subElement.GetAttributeString("text", ""));
|
||||
}
|
||||
|
||||
string type = element.Name.ToString();
|
||||
|
||||
try
|
||||
{
|
||||
missionType = Type.GetType("Barotrauma." + type, true, true);
|
||||
if (missionType == null)
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab " + Name + "! Could not find a mission class of the type \"" + type + "\".");
|
||||
return;
|
||||
case "message":
|
||||
int index = Messages.Count;
|
||||
|
||||
Headers.Add(TextManager.Get("MissionHeader" + index + "." + Identifier, true) ?? subElement.GetAttributeString("header", ""));
|
||||
Messages.Add(TextManager.Get("MissionMessage" + index + "." + Identifier, true) ?? subElement.GetAttributeString("text", ""));
|
||||
break;
|
||||
case "locationtype":
|
||||
AllowedLocationTypes.Add(new Pair<string, string>(
|
||||
subElement.GetAttributeString("from", ""),
|
||||
subElement.GetAttributeString("to", "")));
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch
|
||||
|
||||
string missionTypeName = element.GetAttributeString("type", "");
|
||||
if (!Enum.TryParse(missionTypeName, out type))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab " + Name + "! Could not find a mission class of the type \"" + type + "\".");
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - \"" + missionTypeName + "\" is not a valid mission type.");
|
||||
return;
|
||||
}
|
||||
constructor = missionType.GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
|
||||
if (type == MissionType.Random)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be random.");
|
||||
return;
|
||||
}
|
||||
if (type == MissionType.None)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in mission prefab \"" + Name + "\" - mission type cannot be none.");
|
||||
return;
|
||||
}
|
||||
|
||||
constructor = missionClasses[type].GetConstructor(new[] { typeof(MissionPrefab), typeof(Location[]) });
|
||||
}
|
||||
|
||||
public bool IsAllowed(Location from, Location to)
|
||||
{
|
||||
foreach (Pair<string, string> allowedLocationType in AllowedLocationTypes)
|
||||
{
|
||||
if (allowedLocationType.First.ToLowerInvariant() == "any" ||
|
||||
allowedLocationType.First.ToLowerInvariant() == from.Type.Name.ToLowerInvariant())
|
||||
{
|
||||
if (allowedLocationType.Second.ToLowerInvariant() == "any" ||
|
||||
allowedLocationType.Second.ToLowerInvariant() == to.Type.Name.ToLowerInvariant())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Mission Instantiate(Location[] locations)
|
||||
{
|
||||
return constructor?.Invoke(new object[] { this, locations }) as Mission;
|
||||
}
|
||||
|
||||
public bool TypeMatches(string typeName)
|
||||
{
|
||||
//TODO: use enums instead of strings?
|
||||
typeName = typeName.ToLowerInvariant();
|
||||
return missionType.Name.ToString().Replace("Mission", "").ToLowerInvariant() == typeName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -11,18 +10,17 @@ namespace Barotrauma
|
||||
|
||||
private Character monster;
|
||||
|
||||
private Vector2 radarPosition;
|
||||
private Vector2 sonarPosition;
|
||||
|
||||
public override Vector2 RadarPosition
|
||||
public override Vector2 SonarPosition
|
||||
{
|
||||
get { return monster != null && !monster.IsDead ? radarPosition : Vector2.Zero; }
|
||||
get { return monster != null && !monster.IsDead ? sonarPosition : Vector2.Zero; }
|
||||
}
|
||||
|
||||
public MonsterMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
{
|
||||
monsterFile = prefab.ConfigElement.GetAttributeString("monsterfile", "");
|
||||
|
||||
}
|
||||
|
||||
public override void Start(Level level)
|
||||
@@ -30,9 +28,9 @@ namespace Barotrauma
|
||||
Vector2 spawnPos;
|
||||
Level.Loaded.TryGetInterestingPosition(true, Level.PositionType.MainPath, Level.Loaded.Size.X * 0.3f, out spawnPos);
|
||||
|
||||
monster = Character.Create(monsterFile, spawnPos, null, GameMain.Client != null, true, false);
|
||||
monster = Character.Create(monsterFile, spawnPos, ToolBox.RandomSeed(8), null, GameMain.Client != null, true, false);
|
||||
monster.Enabled = false;
|
||||
radarPosition = spawnPos;
|
||||
sonarPosition = spawnPos;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
@@ -42,7 +40,7 @@ namespace Barotrauma
|
||||
case 0:
|
||||
if (monster.Enabled)
|
||||
{
|
||||
radarPosition = monster.Position;
|
||||
sonarPosition = monster.Position;
|
||||
}
|
||||
|
||||
if (!monster.IsDead) return;
|
||||
|
||||
@@ -14,28 +14,38 @@ namespace Barotrauma
|
||||
|
||||
private int state;
|
||||
|
||||
public override Vector2 RadarPosition
|
||||
public override Vector2 SonarPosition
|
||||
{
|
||||
get
|
||||
{
|
||||
return state>0 ? Vector2.Zero : ConvertUnits.ToDisplayUnits(item.SimPosition);
|
||||
return state > 0 ? Vector2.Zero : ConvertUnits.ToDisplayUnits(item.SimPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public SalvageMission(MissionPrefab prefab, Location[] locations)
|
||||
: base(prefab, locations)
|
||||
{
|
||||
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
|
||||
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
if (prefab.ConfigElement.Attribute("itemname") != null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
|
||||
return;
|
||||
DebugConsole.ThrowError("Error in SalvageMission - use item identifier instead of the name of the item.");
|
||||
string itemName = prefab.ConfigElement.GetAttributeString("itemname", "");
|
||||
itemPrefab = MapEntityPrefab.Find(itemName) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SalvageMission: couldn't find an item prefab with the name " + itemName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
string itemIdentifier = prefab.ConfigElement.GetAttributeString("itemidentifier", "");
|
||||
itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in SalvageMission - couldn't find an item prefab with the identifier " + itemIdentifier);
|
||||
}
|
||||
}
|
||||
|
||||
string spawnPositionTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(spawnPositionTypeStr) ||
|
||||
!Enum.TryParse(spawnPositionTypeStr, true, out spawnPositionType))
|
||||
{
|
||||
@@ -77,8 +87,8 @@ namespace Barotrauma
|
||||
{
|
||||
case 0:
|
||||
//item.body.LinearVelocity = Vector2.Zero;
|
||||
if (item.ParentInventory!=null) item.body.FarseerBody.IsKinematic = false;
|
||||
if (item.CurrentHull == null) return;
|
||||
if (item.ParentInventory != null) item.body.FarseerBody.IsKinematic = false;
|
||||
if (item.CurrentHull?.Submarine == null) return;
|
||||
|
||||
#if CLIENT
|
||||
ShowMessage(state);
|
||||
@@ -97,7 +107,7 @@ namespace Barotrauma
|
||||
|
||||
public override void End()
|
||||
{
|
||||
if (item.CurrentHull == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) return;
|
||||
if (item.CurrentHull?.Submarine == null || !item.CurrentHull.Submarine.AtEndPosition || item.Removed) return;
|
||||
item.Remove();
|
||||
|
||||
GiveReward();
|
||||
|
||||
@@ -17,53 +17,61 @@ namespace Barotrauma
|
||||
|
||||
private bool spawnDeep;
|
||||
|
||||
private bool disallowed;
|
||||
private Vector2 spawnPos;
|
||||
|
||||
private bool repeat;
|
||||
|
||||
private bool disallowed;
|
||||
|
||||
private Level.PositionType spawnPosType;
|
||||
|
||||
private bool spawnPending;
|
||||
|
||||
private string characterFileName;
|
||||
|
||||
public override Vector2 DebugDrawPos
|
||||
{
|
||||
get { return spawnPos; }
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "ScriptedEvent (" + characterFile + ")";
|
||||
}
|
||||
|
||||
private bool isActive;
|
||||
public override bool IsActive
|
||||
{
|
||||
get
|
||||
if (maxAmount <= 1)
|
||||
{
|
||||
return isActive;
|
||||
return "MonsterEvent (" + characterFileName + ")";
|
||||
}
|
||||
else if (minAmount < maxAmount)
|
||||
{
|
||||
return "MonsterEvent (" + characterFileName + " x" + minAmount + "-" + maxAmount + ")";
|
||||
}
|
||||
else
|
||||
{
|
||||
return "MonsterEvent (" + characterFileName + " x" + maxAmount + ")";
|
||||
}
|
||||
}
|
||||
|
||||
public MonsterEvent(XElement element)
|
||||
: base (element)
|
||||
public MonsterEvent(ScriptedEventPrefab prefab)
|
||||
: base (prefab)
|
||||
{
|
||||
characterFile = element.GetAttributeString("characterfile", "");
|
||||
characterFile = prefab.ConfigElement.GetAttributeString("characterfile", "");
|
||||
|
||||
int defaultAmount = element.GetAttributeInt("amount", 1);
|
||||
int defaultAmount = prefab.ConfigElement.GetAttributeInt("amount", 1);
|
||||
minAmount = prefab.ConfigElement.GetAttributeInt("minamount", defaultAmount);
|
||||
maxAmount = Math.Max(prefab.ConfigElement.GetAttributeInt("maxamount", 1), minAmount);
|
||||
|
||||
minAmount = element.GetAttributeInt("minamount", defaultAmount);
|
||||
maxAmount = Math.Max(element.GetAttributeInt("maxamount", 1), minAmount);
|
||||
|
||||
var spawnPosTypeStr = element.GetAttributeString("spawntype", "");
|
||||
var spawnPosTypeStr = prefab.ConfigElement.GetAttributeString("spawntype", "");
|
||||
|
||||
if (string.IsNullOrWhiteSpace(spawnPosTypeStr) ||
|
||||
!Enum.TryParse<Level.PositionType>(spawnPosTypeStr, true, out spawnPosType))
|
||||
!Enum.TryParse(spawnPosTypeStr, true, out spawnPosType))
|
||||
{
|
||||
spawnPosType = Level.PositionType.MainPath;
|
||||
}
|
||||
|
||||
spawnDeep = element.GetAttributeBool("spawndeep", false);
|
||||
|
||||
repeat = element.GetAttributeBool("repeat", repeat);
|
||||
spawnDeep = prefab.ConfigElement.GetAttributeBool("spawndeep", false);
|
||||
characterFileName = Path.GetFileName(Path.GetDirectoryName(characterFile)).ToLower();
|
||||
|
||||
if (GameMain.NetworkMember != null)
|
||||
{
|
||||
List<string> monsterNames = GameMain.NetworkMember.monsterEnabled.Keys.ToList();
|
||||
string characterName = Path.GetFileName(Path.GetDirectoryName(characterFile)).ToLower();
|
||||
string tryKey = monsterNames.Find(s => characterName == s.ToLower());
|
||||
string tryKey = monsterNames.Find(s => characterFileName == s.ToLower());
|
||||
if (!string.IsNullOrWhiteSpace(tryKey))
|
||||
{
|
||||
if (!GameMain.NetworkMember.monsterEnabled[tryKey]) disallowed = true; //spawn was disallowed by host
|
||||
@@ -71,56 +79,121 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
public override void Init()
|
||||
public override bool CanAffectSubImmediately(Level level)
|
||||
{
|
||||
base.Init();
|
||||
float maxRange = Items.Components.Sonar.DefaultSonarRange * 0.8f;
|
||||
|
||||
monsters = SpawnMonsters(Rand.Range(minAmount, maxAmount, Rand.RandSync.Server), false);
|
||||
List<Vector2> positions = GetAvailableSpawnPositions();
|
||||
foreach (Vector2 position in positions)
|
||||
{
|
||||
if (Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition) < maxRange * maxRange)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Init(bool affectSubImmediately)
|
||||
{
|
||||
FindSpawnPosition(affectSubImmediately);
|
||||
if (GameSettings.VerboseLogging)
|
||||
{
|
||||
if (monsters != null)
|
||||
{
|
||||
DebugConsole.NewMessage("Initialized MonsterEvent (" + monsters[0]?.SpeciesName + " x" + monsters.Length + ")", Color.White);
|
||||
}
|
||||
DebugConsole.NewMessage("Initialized MonsterEvent (" + characterFile + ")", Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
private Character[] SpawnMonsters(int amount, bool createNetworkEvent)
|
||||
private List<Vector2> GetAvailableSpawnPositions()
|
||||
{
|
||||
if (disallowed) return null;
|
||||
|
||||
Vector2 spawnPos;
|
||||
float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f;
|
||||
if (!Level.Loaded.TryGetInterestingPosition(true, spawnPosType, minDist, out spawnPos))
|
||||
{
|
||||
//no suitable position found, disable the event
|
||||
repeat = false;
|
||||
Finished();
|
||||
return null;
|
||||
}
|
||||
var availablePositions = Level.Loaded.PositionsOfInterest.FindAll(p => spawnPosType.HasFlag(p.PositionType));
|
||||
|
||||
var monsters = new Character[amount];
|
||||
List<Vector2> positions = new List<Vector2>();
|
||||
foreach (var allowedPosition in availablePositions)
|
||||
{
|
||||
positions.Add(allowedPosition.Position.ToVector2());
|
||||
}
|
||||
|
||||
if (spawnDeep)
|
||||
{
|
||||
spawnPos.Y -= Level.Loaded.Size.Y;
|
||||
//disable the event if the ocean floor is too high up to spawn the monster deep
|
||||
if (spawnPos.Y < Level.Loaded.GetBottomPosition(spawnPos.X).Y)
|
||||
for (int i = 0; i < positions.Count; i++)
|
||||
{
|
||||
repeat = false;
|
||||
Finished();
|
||||
return null;
|
||||
positions[i] = new Vector2(positions[i].X, positions[i].Y - Level.Loaded.Size.Y);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
spawnPos.X += Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server);
|
||||
spawnPos.Y += Rand.Range(-0.5f, 0.5f, Rand.RandSync.Server);
|
||||
monsters[i] = Character.Create(characterFile, spawnPos, null, GameMain.Client != null, true, createNetworkEvent);
|
||||
}
|
||||
|
||||
return monsters;
|
||||
positions.RemoveAll(pos => pos.Y < Level.Loaded.GetBottomPosition(pos.X).Y);
|
||||
|
||||
return positions;
|
||||
}
|
||||
|
||||
private void FindSpawnPosition(bool affectSubImmediately)
|
||||
{
|
||||
if (disallowed) return;
|
||||
|
||||
spawnPos = Vector2.Zero;
|
||||
var availablePositions = GetAvailableSpawnPositions();
|
||||
if (affectSubImmediately && spawnPosType != Level.PositionType.Ruin)
|
||||
{
|
||||
if (availablePositions.Count == 0)
|
||||
{
|
||||
//no suitable position found, disable the event
|
||||
Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
float closestDist = float.PositiveInfinity;
|
||||
//find the closest spawnposition that isn't too close to any of the subs
|
||||
foreach (Vector2 position in availablePositions)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (sub.IsOutpost) { continue; }
|
||||
float minDistToSub = GetMinDistanceToSub(sub);
|
||||
if (dist > minDistToSub * minDistToSub && dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
spawnPos = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//only found a spawnpos that's very far from the sub, pick one that's closer
|
||||
//and wait for the sub to move further before spawning
|
||||
if (closestDist > 10000.0f * 10000.0f)
|
||||
{
|
||||
foreach (Vector2 position in availablePositions)
|
||||
{
|
||||
float dist = Vector2.DistanceSquared(position, Submarine.MainSub.WorldPosition);
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestDist = dist;
|
||||
spawnPos = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float minDist = spawnPosType == Level.PositionType.Ruin ? 0.0f : 20000.0f;
|
||||
availablePositions.RemoveAll(p => Vector2.Distance(Submarine.MainSub.WorldPosition, p) < minDist);
|
||||
if (availablePositions.Count == 0)
|
||||
{
|
||||
//no suitable position found, disable the event
|
||||
Finished();
|
||||
return;
|
||||
}
|
||||
|
||||
spawnPos = availablePositions[Rand.Int(availablePositions.Count, Rand.RandSync.Server)];
|
||||
}
|
||||
spawnPending = true;
|
||||
}
|
||||
|
||||
private float GetMinDistanceToSub(Submarine submarine)
|
||||
{
|
||||
//12000 units is slightly more than the default range of the sonar
|
||||
return Math.Max(Math.Max(submarine.Borders.Width, submarine.Borders.Height), 12000.0f);
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
@@ -131,37 +204,37 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
if (repeat)
|
||||
{
|
||||
//clients aren't allowed to spawn more monsters mid-round
|
||||
if (GameMain.Client != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < monsters.Length; i++)
|
||||
{
|
||||
if (monsters[i] == null || monsters[i].Removed || monsters[i].IsDead)
|
||||
{
|
||||
monsters[i] = SpawnMonsters(1, true)[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isFinished) return;
|
||||
|
||||
isActive = false;
|
||||
//isActive = false;
|
||||
|
||||
Entity targetEntity = null;
|
||||
if (Character.Controlled != null)
|
||||
if (spawnPending)
|
||||
{
|
||||
targetEntity = Character.Controlled;
|
||||
}
|
||||
else
|
||||
{
|
||||
targetEntity = Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
|
||||
//wait until there are no submarines at the spawnpos
|
||||
foreach (Submarine submarine in Submarine.Loaded)
|
||||
{
|
||||
if (submarine.IsOutpost) { continue; }
|
||||
float minDist = GetMinDistanceToSub(submarine);
|
||||
if (Vector2.DistanceSquared(submarine.WorldPosition, spawnPos) < minDist * minDist) return;
|
||||
}
|
||||
|
||||
//+1 because Range returns an integer less than the max value
|
||||
int amount = Rand.Range(minAmount, maxAmount + 1, Rand.RandSync.Server);
|
||||
monsters = new Character[amount];
|
||||
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
monsters[i] = Character.Create(
|
||||
characterFile, spawnPos + Rand.Vector(100.0f, Rand.RandSync.Server),
|
||||
i.ToString(), null, GameMain.Client != null, true, true);
|
||||
}
|
||||
|
||||
spawnPending = false;
|
||||
}
|
||||
|
||||
Entity targetEntity = Character.Controlled != null ?
|
||||
(Entity)Character.Controlled : Submarine.FindClosest(GameMain.GameScreen.Cam.WorldViewCenter);
|
||||
|
||||
bool monstersDead = true;
|
||||
foreach (Character monster in monsters)
|
||||
{
|
||||
@@ -171,13 +244,12 @@ namespace Barotrauma
|
||||
|
||||
if (targetEntity != null && Vector2.DistanceSquared(monster.WorldPosition, targetEntity.WorldPosition) < 5000.0f * 5000.0f)
|
||||
{
|
||||
isActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (monstersDead && !repeat) Finished();
|
||||
if (monstersDead) Finished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ScriptedEvent
|
||||
{
|
||||
private static List<ScriptedEvent> prefabs;
|
||||
|
||||
protected readonly string name;
|
||||
protected readonly string description;
|
||||
|
||||
private readonly int minEventCount, maxEventCount;
|
||||
|
||||
{
|
||||
protected bool isFinished;
|
||||
|
||||
private readonly XElement configElement;
|
||||
|
||||
private readonly Dictionary<string, int> overrideMinEventCount;
|
||||
private readonly Dictionary<string, int> overrideMaxEventCount;
|
||||
|
||||
public string Name
|
||||
{
|
||||
get { return name; }
|
||||
}
|
||||
|
||||
public string Description
|
||||
{
|
||||
get { return description; }
|
||||
}
|
||||
|
||||
public string MusicType
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public virtual bool IsActive
|
||||
{
|
||||
get { return true; }
|
||||
}
|
||||
|
||||
private readonly ScriptedEventPrefab prefab;
|
||||
|
||||
public bool IsFinished
|
||||
{
|
||||
get { return isFinished; }
|
||||
@@ -49,43 +15,24 @@ namespace Barotrauma
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return "ScriptedEvent (" + name + ")";
|
||||
return "ScriptedEvent (" + prefab.EventType.ToString() +")";
|
||||
}
|
||||
|
||||
protected ScriptedEvent(XElement element)
|
||||
public virtual Vector2 DebugDrawPos
|
||||
{
|
||||
configElement = element;
|
||||
|
||||
name = element.GetAttributeString("name", "");
|
||||
description = element.GetAttributeString("description", "");
|
||||
|
||||
minEventCount = element.GetAttributeInt("mineventcount", 0);
|
||||
maxEventCount = element.GetAttributeInt("maxeventcount", 0);
|
||||
|
||||
MusicType = element.GetAttributeString("musictype", "default");
|
||||
|
||||
overrideMinEventCount = new Dictionary<string, int>();
|
||||
overrideMaxEventCount = new Dictionary<string, int>();
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
get
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "overrideeventcount":
|
||||
string levelType = subElement.GetAttributeString("leveltype", "");
|
||||
if (!overrideMinEventCount.ContainsKey(levelType))
|
||||
{
|
||||
overrideMinEventCount.Add(levelType, subElement.GetAttributeInt("min", 0));
|
||||
overrideMaxEventCount.Add(levelType, subElement.GetAttributeInt("max", 0));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return Vector2.Zero;
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void Init()
|
||||
|
||||
public ScriptedEvent(ScriptedEventPrefab prefab)
|
||||
{
|
||||
this.prefab = prefab;
|
||||
}
|
||||
|
||||
public virtual void Init(bool affectSubImmediately)
|
||||
{
|
||||
isFinished = false;
|
||||
}
|
||||
|
||||
public virtual void Update(float deltaTime)
|
||||
@@ -96,156 +43,37 @@ namespace Barotrauma
|
||||
{
|
||||
isFinished = true;
|
||||
}
|
||||
|
||||
|
||||
private static void LoadPrefabs()
|
||||
|
||||
public virtual bool CanAffectSubImmediately(Level level)
|
||||
{
|
||||
prefabs = new List<ScriptedEvent>();
|
||||
var configFiles = GameMain.Config.SelectedContentPackage.GetFilesOfType(ContentType.RandomEvents);
|
||||
|
||||
if (configFiles.Count == 0)
|
||||
{
|
||||
DebugConsole.ThrowError("No config files for random events found in the selected content package");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string configFile in configFiles)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile);
|
||||
if (doc == null) continue;
|
||||
|
||||
foreach (XElement element in doc.Root.Elements())
|
||||
{
|
||||
prefabs.Add(new ScriptedEvent(element));
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static List<ScriptedEvent> GenerateLevelEvents(Random random, Level level)
|
||||
/*public static List<ScriptedEvent> GenerateInitialEvents(Random random, Level level)
|
||||
{
|
||||
if (prefabs == null)
|
||||
if (ScriptedEventPrefab.List == null)
|
||||
{
|
||||
LoadPrefabs();
|
||||
ScriptedEventPrefab.LoadPrefabs();
|
||||
}
|
||||
|
||||
List<ScriptedEvent> events = new List<ScriptedEvent>();
|
||||
foreach (ScriptedEvent scriptedEvent in prefabs)
|
||||
foreach (ScriptedEventPrefab scriptedEvent in ScriptedEventPrefab.List)
|
||||
{
|
||||
int minCount = scriptedEvent.overrideMinEventCount.ContainsKey(level.GenerationParams.Name) ?
|
||||
scriptedEvent.overrideMinEventCount[level.GenerationParams.Name] : scriptedEvent.minEventCount;
|
||||
int maxCount = scriptedEvent.overrideMaxEventCount.ContainsKey(level.GenerationParams.Name) ?
|
||||
scriptedEvent.overrideMaxEventCount[level.GenerationParams.Name] : scriptedEvent.maxEventCount;
|
||||
int minCount = scriptedEvent.MinEventCount.ContainsKey(level.GenerationParams.Name) ?
|
||||
scriptedEvent.MinEventCount[level.GenerationParams.Name] : scriptedEvent.MinEventCount[""];
|
||||
int maxCount = scriptedEvent.MaxEventCount.ContainsKey(level.GenerationParams.Name) ?
|
||||
scriptedEvent.MaxEventCount[level.GenerationParams.Name] : scriptedEvent.MaxEventCount[""];
|
||||
|
||||
minCount = Math.Min(minCount, maxCount);
|
||||
|
||||
int count = random.Next(maxCount - minCount) + minCount;
|
||||
|
||||
for (int i = 0; i<count; i++)
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Type t;
|
||||
|
||||
try
|
||||
{
|
||||
t = Type.GetType("Barotrauma." + scriptedEvent.configElement.Name, true, true);
|
||||
if (t == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + scriptedEvent.configElement.Name + "\".");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + scriptedEvent.configElement.Name + "\".");
|
||||
continue;
|
||||
}
|
||||
|
||||
ConstructorInfo constructor = t.GetConstructor(new[] { typeof(XElement) });
|
||||
object instance = null;
|
||||
try
|
||||
{
|
||||
instance = constructor.Invoke(new object[] { scriptedEvent.configElement });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
|
||||
}
|
||||
|
||||
events.Add((ScriptedEvent)instance);
|
||||
ScriptedEvent eventInstance = scriptedEvent.CreateInstance();
|
||||
events.Add(eventInstance);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
||||
|
||||
|
||||
/*public static ScriptedEvent Load(ScriptedEvent scriptedEvent)
|
||||
{
|
||||
if (prefabs == null)
|
||||
{
|
||||
LoadPrefabs();
|
||||
}
|
||||
|
||||
if (prefabs.Count == 0) return null;
|
||||
|
||||
int eventCount = prefabs.Count;
|
||||
float[] eventProbability = new float[eventCount];
|
||||
float probabilitySum = 0.0f;
|
||||
|
||||
int i = 0;
|
||||
foreach (ScriptedEvent scriptedEvent in prefabs)
|
||||
{
|
||||
eventProbability[i] = scriptedEvent.commonness;
|
||||
if (level != null)
|
||||
{
|
||||
scriptedEvent.OverrideCommonness.TryGetValue(level.GenerationParams.Name, out eventProbability[i]);
|
||||
}
|
||||
probabilitySum += eventProbability[i];
|
||||
i++;
|
||||
}
|
||||
|
||||
float randomNumber = (float)rand.NextDouble() * probabilitySum;
|
||||
|
||||
i = 0;
|
||||
foreach (ScriptedEvent scriptedEvent in prefabs)
|
||||
{
|
||||
if (randomNumber <= eventProbability[i])
|
||||
{
|
||||
Type t;
|
||||
|
||||
try
|
||||
{
|
||||
t = Type.GetType("Barotrauma." + scriptedEvent.configElement.Name, true, true);
|
||||
if (t == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + scriptedEvent.configElement.Name + "\".");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + scriptedEvent.configElement.Name + "\".");
|
||||
continue;
|
||||
}
|
||||
|
||||
ConstructorInfo constructor = t.GetConstructor(new[] { typeof(XElement) });
|
||||
object instance = null;
|
||||
try
|
||||
{
|
||||
instance = constructor.Invoke(new object[] { scriptedEvent.configElement });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
|
||||
}
|
||||
|
||||
return (ScriptedEvent)instance;
|
||||
}
|
||||
|
||||
randomNumber -= eventProbability[i];
|
||||
i++;
|
||||
}
|
||||
|
||||
return null;
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ScriptedEventPrefab
|
||||
{
|
||||
public readonly XElement ConfigElement;
|
||||
|
||||
public readonly Type EventType;
|
||||
|
||||
public readonly string MusicType;
|
||||
|
||||
public ScriptedEventPrefab(XElement element)
|
||||
{
|
||||
ConfigElement = element;
|
||||
|
||||
MusicType = element.GetAttributeString("musictype", "default");
|
||||
|
||||
try
|
||||
{
|
||||
EventType = Type.GetType("Barotrauma." + ConfigElement.Name, true, true);
|
||||
if (EventType == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DebugConsole.ThrowError("Could not find an event class of the type \"" + ConfigElement.Name + "\".");
|
||||
}
|
||||
}
|
||||
|
||||
public ScriptedEvent CreateInstance()
|
||||
{
|
||||
ConstructorInfo constructor = EventType.GetConstructor(new[] { typeof(ScriptedEventPrefab) });
|
||||
object instance = null;
|
||||
try
|
||||
{
|
||||
instance = constructor.Invoke(new object[] { this });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DebugConsole.ThrowError(ex.InnerException != null ? ex.InnerException.ToString() : ex.ToString());
|
||||
}
|
||||
|
||||
return (ScriptedEvent)instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class ScriptedEventSet
|
||||
{
|
||||
public static List<ScriptedEventSet> List
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//0-100
|
||||
public readonly float MinLevelDifficulty, MaxLevelDifficulty;
|
||||
|
||||
public readonly bool ChooseRandom;
|
||||
|
||||
public readonly float MinDistanceTraveled;
|
||||
public readonly float MinMissionTime;
|
||||
|
||||
//the events in this set are delayed if the current EventManager intensity is not between these values
|
||||
public readonly float MinIntensity, MaxIntensity;
|
||||
|
||||
public readonly Dictionary<string, float> Commonness;
|
||||
|
||||
public readonly List<ScriptedEventPrefab> EventPrefabs;
|
||||
|
||||
public readonly List<ScriptedEventSet> ChildSets;
|
||||
|
||||
public string DebugIdentifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
} = "";
|
||||
|
||||
private ScriptedEventSet(XElement element, string debugIdentifier)
|
||||
{
|
||||
DebugIdentifier = debugIdentifier;
|
||||
Commonness = new Dictionary<string, float>();
|
||||
EventPrefabs = new List<ScriptedEventPrefab>();
|
||||
ChildSets = new List<ScriptedEventSet>();
|
||||
|
||||
MinLevelDifficulty = element.GetAttributeFloat("minleveldifficulty", 0);
|
||||
MaxLevelDifficulty = Math.Max(element.GetAttributeFloat("maxleveldifficulty", 100), MinLevelDifficulty);
|
||||
|
||||
MinIntensity = element.GetAttributeFloat("minintensity", 0.0f);
|
||||
MaxIntensity = Math.Max(element.GetAttributeFloat("maxintensity", 100.0f), MinIntensity);
|
||||
|
||||
ChooseRandom = element.GetAttributeBool("chooserandom", false);
|
||||
MinDistanceTraveled = element.GetAttributeFloat("mindistancetraveled", 0.0f);
|
||||
MinMissionTime = element.GetAttributeFloat("minmissiontime", 0.0f);
|
||||
|
||||
Commonness[""] = 1.0f;
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "commonness":
|
||||
Commonness[""] = subElement.GetAttributeFloat("commonness", 0.0f);
|
||||
foreach (XElement overrideElement in subElement.Elements())
|
||||
{
|
||||
if (overrideElement.Name.ToString().ToLowerInvariant() == "override")
|
||||
{
|
||||
string levelType = overrideElement.GetAttributeString("leveltype", "");
|
||||
if (!Commonness.ContainsKey(levelType))
|
||||
{
|
||||
Commonness.Add(levelType, overrideElement.GetAttributeFloat("commonness", 0.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "eventset":
|
||||
ChildSets.Add(new ScriptedEventSet(subElement, this.DebugIdentifier + "-" + ChildSets.Count));
|
||||
break;
|
||||
default:
|
||||
EventPrefabs.Add(new ScriptedEventPrefab(subElement));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public float GetCommonness(Level level)
|
||||
{
|
||||
return Commonness.ContainsKey(level.GenerationParams.Name) ?
|
||||
Commonness[level.GenerationParams.Name] : Commonness[""];
|
||||
}
|
||||
|
||||
public static void LoadPrefabs()
|
||||
{
|
||||
List = new List<ScriptedEventSet>();
|
||||
var configFiles = GameMain.Instance.GetFilesOfType(ContentType.RandomEvents);
|
||||
|
||||
if (!configFiles.Any())
|
||||
{
|
||||
DebugConsole.ThrowError("No config files for random events found in the selected content package");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (string configFile in configFiles)
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(configFile);
|
||||
if (doc == null) continue;
|
||||
|
||||
int i = 0;
|
||||
foreach (XElement element in doc.Root.Elements())
|
||||
{
|
||||
if (element.Name.ToString().ToLowerInvariant() != "eventset") continue;
|
||||
List.Add(new ScriptedEventSet(element, i.ToString()));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma.Extensions
|
||||
{
|
||||
public static class IEnumerableExtensions
|
||||
{
|
||||
public static HashSet<T> ToHashSet<T>(this IEnumerable<T> source)
|
||||
{
|
||||
return new HashSet<T>(source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Randomizes the collection.
|
||||
/// </summary>
|
||||
public static IOrderedEnumerable<T> Randomize<T>(this IEnumerable<T> source)
|
||||
{
|
||||
return source.OrderBy(i => Rand.Range(0f, 1f));
|
||||
}
|
||||
|
||||
public static T GetRandom<T>(this IEnumerable<T> source, Func<T, bool> predicate, Rand.RandSync randSync = Rand.RandSync.Unsynced)
|
||||
{
|
||||
return source.Where(predicate).GetRandom(randSync);
|
||||
}
|
||||
|
||||
public static T GetRandom<T>(this IEnumerable<T> source, Rand.RandSync randSync = Rand.RandSync.Unsynced)
|
||||
{
|
||||
int count = source.Count();
|
||||
return count == 0 ? default(T) : source.ElementAt(Rand.Range(0, count, randSync));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes an action that modifies the collection on each element (such as removing items from the list).
|
||||
/// Creates a temporary list.
|
||||
/// </summary>
|
||||
public static void ForEachMod<T>(this IEnumerable<T> source, Action<T> action)
|
||||
{
|
||||
var temp = new List<T>(source);
|
||||
temp.ForEach(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic version of List.ForEach.
|
||||
/// Performs the specified action on each element of the collection (short hand for a foreach loop).
|
||||
/// </summary>
|
||||
public static void ForEach<T>(this IEnumerable<T> source, Action<T> action)
|
||||
{
|
||||
foreach (var item in source)
|
||||
{
|
||||
action(item);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Shorthand for !source.Any(predicate) -> i.e. not any.
|
||||
/// </summary>
|
||||
public static bool None<T>(this IEnumerable<T> source, Func<T, bool> predicate = null)
|
||||
{
|
||||
if (predicate == null)
|
||||
{
|
||||
return !source.Any();
|
||||
}
|
||||
else
|
||||
{
|
||||
return !source.Any(predicate);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool Multiple<T>(this IEnumerable<T> source, Func<T, bool> predicate = null)
|
||||
{
|
||||
if (predicate == null)
|
||||
{
|
||||
return source.Count() > 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return source.Count(predicate) > 1;
|
||||
}
|
||||
}
|
||||
|
||||
// source: https://stackoverflow.com/questions/19237868/get-all-children-to-one-list-recursive-c-sharp
|
||||
public static IEnumerable<T> SelectManyRecursive<T>(this IEnumerable<T> source, Func<T, IEnumerable<T>> selector)
|
||||
{
|
||||
var result = source.SelectMany(selector);
|
||||
if (!result.Any())
|
||||
{
|
||||
return result;
|
||||
}
|
||||
return result.Concat(result.SelectManyRecursive(selector));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Extensions
|
||||
{
|
||||
public static class PointExtensions
|
||||
{
|
||||
public static Point Multiply(this Point p, float f)
|
||||
{
|
||||
return new Point((int)(p.X * f), (int)(p.Y * f));
|
||||
}
|
||||
|
||||
public static Point Multiply(this Point p, int i)
|
||||
{
|
||||
return new Point(p.X * i, p.Y * i);
|
||||
}
|
||||
|
||||
public static Point Multiply(this Point p, Vector2 v)
|
||||
{
|
||||
return new Point((int)(p.X * v.X), (int)(p.Y * v.Y));
|
||||
}
|
||||
|
||||
public static Point Divide(this Point p, int i)
|
||||
{
|
||||
if (i == 0) { return Point.Zero; }
|
||||
return new Point(p.X / i, p.Y / i);
|
||||
}
|
||||
|
||||
public static Point Divide(this Point p, float f)
|
||||
{
|
||||
if (f == 0) { return Point.Zero; }
|
||||
return new Point((int)(p.X / f), (int)(p.Y / f));
|
||||
}
|
||||
|
||||
public static Point Divide(this Point p, Vector2 v)
|
||||
{
|
||||
if (v.X == 0 || v.Y == 0) { return Point.Zero; }
|
||||
return new Point((int)(p.X / v.X), (int)(p.Y / v.Y));
|
||||
}
|
||||
|
||||
public static Point Inverse(this Point p)
|
||||
{
|
||||
return new Point(-p.X, -p.Y);
|
||||
}
|
||||
|
||||
public static Point Clamp(this Point p, Point min, Point max)
|
||||
{
|
||||
return new Point(MathHelper.Clamp(p.X, min.X, max.X), MathHelper.Clamp(p.Y, min.Y, max.Y));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Extensions
|
||||
{
|
||||
public static class RectangleExtensions
|
||||
{
|
||||
public static Point DivideSize(this Rectangle rect, float f)
|
||||
{
|
||||
return new Point((int)(rect.Width / f), (int)(rect.Height / f));
|
||||
}
|
||||
|
||||
public static Point DivideSize(this Rectangle rect, Vector2 f)
|
||||
{
|
||||
return new Point((int)(rect.Width / f.X), (int)(rect.Height / f.Y));
|
||||
}
|
||||
|
||||
public static Point MultiplySize(this Rectangle rect, float f)
|
||||
{
|
||||
return new Point((int)(rect.Width * f), (int)(rect.Height * f));
|
||||
}
|
||||
|
||||
public static Point MultiplySize(this Rectangle rect, Vector2 f)
|
||||
{
|
||||
return new Point((int)(rect.Width * f.X), (int)(rect.Height * f.Y));
|
||||
}
|
||||
|
||||
public static Vector2 CalculateRelativeSize(this Rectangle rect, Rectangle relativeRect)
|
||||
{
|
||||
return new Vector2(rect.Width, rect.Height) / new Vector2(relativeRect.Width, relativeRect.Height);
|
||||
}
|
||||
|
||||
public static Rectangle ScaleSize(this Rectangle rect, Rectangle relativeTo)
|
||||
{
|
||||
return rect.ScaleSize(rect.CalculateRelativeSize(relativeTo));
|
||||
}
|
||||
|
||||
public static Rectangle ScaleSize(this Rectangle rect, Vector2 scale)
|
||||
{
|
||||
var size = rect.MultiplySize(scale);
|
||||
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
|
||||
}
|
||||
|
||||
public static Rectangle ScaleSize(this Rectangle rect, float scale)
|
||||
{
|
||||
var size = rect.MultiplySize(scale);
|
||||
return new Rectangle(rect.X, rect.Y, size.X, size.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public static class StringFormatter
|
||||
{
|
||||
public static string Remove(this string s, Func<char, bool> predicate)
|
||||
{
|
||||
return new string(s.ToCharArray().Where(c => !predicate(c)).ToArray());
|
||||
}
|
||||
|
||||
public static string RemoveWhitespace(this string s)
|
||||
{
|
||||
return s.Remove(c => char.IsWhiteSpace(c));
|
||||
}
|
||||
|
||||
public static string FormatSingleDecimal(this float value)
|
||||
{
|
||||
return value.ToString("F1", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string FormatDoubleDecimal(this float value)
|
||||
{
|
||||
return value.ToString("F2", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string FormatZeroDecimal(this float value)
|
||||
{
|
||||
return value.ToString("F0", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string Format(this float value, int decimalCount)
|
||||
{
|
||||
return value.ToString($"F{decimalCount.ToString()}", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public static string FormatSingleDecimal(this Vector2 value)
|
||||
{
|
||||
return $"({value.X.FormatSingleDecimal()}, {value.Y.FormatSingleDecimal()})";
|
||||
}
|
||||
|
||||
public static string FormatDoubleDecimal(this Vector2 value)
|
||||
{
|
||||
return $"({value.X.FormatDoubleDecimal()}, {value.Y.FormatDoubleDecimal()})";
|
||||
}
|
||||
|
||||
public static string FormatZeroDecimal(this Vector2 value)
|
||||
{
|
||||
return $"({value.X.FormatZeroDecimal()}, {value.Y.FormatZeroDecimal()})";
|
||||
}
|
||||
|
||||
public static string Format(this Vector2 value, int decimalCount)
|
||||
{
|
||||
return $"({value.X.Format(decimalCount)}, {value.Y.Format(decimalCount)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Capitalises the first letter (invariant) and forces the rest to lower case (invariant).
|
||||
/// </summary>
|
||||
public static string CapitaliseFirstInvariant(this string s)
|
||||
{
|
||||
if (string.IsNullOrEmpty(s)) { return string.Empty; }
|
||||
return s.Substring(0, 1).ToUpperInvariant() + s.Substring(1, s.Length - 1).ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds spaces into a CamelCase string.
|
||||
/// </summary>
|
||||
public static string FormatCamelCaseWithSpaces(this string str)
|
||||
{
|
||||
return new string(InsertSpacesBeforeCaps(str).ToArray());
|
||||
IEnumerable<char> InsertSpacesBeforeCaps(IEnumerable<char> input)
|
||||
{
|
||||
int i = 0;
|
||||
int lastChar = input.Count() - 1;
|
||||
foreach (char c in input)
|
||||
{
|
||||
if (char.IsUpper(c) && i > 0)
|
||||
{
|
||||
yield return ' ';
|
||||
}
|
||||
|
||||
yield return c;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using Microsoft.Xna.Framework;
|
||||
|
||||
namespace Barotrauma.Extensions
|
||||
{
|
||||
public static class VectorExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Unity's Angle implementation.
|
||||
/// Returns the angle in degrees.
|
||||
/// 0 - 180.
|
||||
/// </summary>
|
||||
public static float Angle(this Vector2 from, Vector2 to)
|
||||
{
|
||||
return (float)Math.Acos(MathHelper.Clamp(Vector2.Dot(Vector2.Normalize(from), Vector2.Normalize(to)), -1f, 1f)) * 57.29578f;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a forward pointing vector based on the rotation (in radians).
|
||||
/// </summary>
|
||||
public static Vector2 Forward(float radians, float length = 1)
|
||||
{
|
||||
return new Vector2((float)Math.Sin(radians), (float)Math.Cos(radians)) * length;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a backward pointing vector based on the rotation (in radians).
|
||||
/// </summary>
|
||||
public static Vector2 Backward(float radians, float length = 1)
|
||||
{
|
||||
return -Forward(radians, length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a normalized perpendicular vector to the right from a forward vector.
|
||||
/// </summary>
|
||||
public static Vector2 Right(this Vector2 forward)
|
||||
{
|
||||
var normV = Vector2.Normalize(forward);
|
||||
return new Vector2(normV.Y, -normV.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a normalized perpendicular vector to the left from a forward vector.
|
||||
/// </summary>
|
||||
public static Vector2 Left(this Vector2 forward)
|
||||
{
|
||||
return -forward.Right();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transforms a vector relative to the given up vector.
|
||||
/// </summary>
|
||||
public static Vector2 TransformVector(this Vector2 v, Vector2 up)
|
||||
{
|
||||
return (up * v.Y) + (up.Right() * v.X);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flips the x and y components.
|
||||
/// </summary>
|
||||
public static Vector2 Flip(this Vector2 v) => new Vector2(v.Y, v.X);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the sum of the x and y components.
|
||||
/// </summary>
|
||||
public static float Combine(this Vector2 v) => v.X + v.Y;
|
||||
|
||||
public static Vector2 Clamp(this Vector2 v, Vector2 min, Vector2 max)
|
||||
{
|
||||
return Vector2.Clamp(v, min, max);
|
||||
}
|
||||
|
||||
public static bool NearlyEquals(this Vector2 v, Vector2 other)
|
||||
{
|
||||
return MathUtils.NearlyEqual(v.X, other.X) && MathUtils.NearlyEqual(v.Y, other.Y);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,74 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
public class FrameCounter
|
||||
public class PerformanceCounter
|
||||
{
|
||||
public long TotalFrames { get; private set; }
|
||||
public double TotalSeconds { get; private set; }
|
||||
public double AverageFramesPerSecond { get; private set; }
|
||||
public double CurrentFramesPerSecond { get; private set; }
|
||||
|
||||
public const int MaximumSamples = 10;
|
||||
|
||||
private Queue<double> sampleBuffer = new Queue<double>();
|
||||
|
||||
private Dictionary<string, Queue<long>> elapsedTicks = new Dictionary<string, Queue<long>>();
|
||||
private Dictionary<string, long> avgTicksPerFrame = new Dictionary<string, long>();
|
||||
|
||||
#if CLIENT
|
||||
internal Graph UpdateTimeGraph = new Graph(500), UpdateIterationsGraph = new Graph(500), DrawTimeGraph = new Graph(500);
|
||||
#endif
|
||||
|
||||
public IEnumerable<string> GetSavedIdentifiers
|
||||
{
|
||||
public long TotalFrames { get; private set; }
|
||||
public double TotalSeconds { get; private set; }
|
||||
public double AverageFramesPerSecond { get; private set; }
|
||||
public double CurrentFramesPerSecond { get; private set; }
|
||||
get { return avgTicksPerFrame.Keys; }
|
||||
}
|
||||
|
||||
public const int MaximumSamples = 10;
|
||||
public void AddElapsedTicks(string identifier, long ticks)
|
||||
{
|
||||
if (!elapsedTicks.ContainsKey(identifier)) elapsedTicks.Add(identifier, new Queue<long>());
|
||||
elapsedTicks[identifier].Enqueue(ticks);
|
||||
|
||||
private Queue<double> sampleBuffer = new Queue<double>();
|
||||
|
||||
public bool Update(double deltaTime)
|
||||
if (elapsedTicks[identifier].Count > MaximumSamples)
|
||||
{
|
||||
//float deltaTime = stopwatch.ElapsedMilliseconds / 1000.0f;
|
||||
|
||||
if (deltaTime == 0.0f) { return false; }
|
||||
//stopwatch.Restart();
|
||||
|
||||
CurrentFramesPerSecond = (1.0 / deltaTime);
|
||||
|
||||
sampleBuffer.Enqueue(CurrentFramesPerSecond);
|
||||
|
||||
if (sampleBuffer.Count > MaximumSamples)
|
||||
{
|
||||
sampleBuffer.Dequeue();
|
||||
AverageFramesPerSecond = sampleBuffer.Average(i => i);
|
||||
}
|
||||
else
|
||||
{
|
||||
AverageFramesPerSecond = CurrentFramesPerSecond;
|
||||
}
|
||||
|
||||
if (AverageFramesPerSecond < 0 || AverageFramesPerSecond > 500) { }
|
||||
|
||||
TotalFrames++;
|
||||
TotalSeconds += deltaTime;
|
||||
return true;
|
||||
elapsedTicks[identifier].Dequeue();
|
||||
avgTicksPerFrame[identifier] = (long)elapsedTicks[identifier].Average(i => i);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public float GetAverageElapsedMillisecs(string identifier)
|
||||
{
|
||||
if (!avgTicksPerFrame.ContainsKey(identifier)) return 0.0f;
|
||||
return avgTicksPerFrame[identifier] / TimeSpan.TicksPerMillisecond;
|
||||
}
|
||||
|
||||
public bool Update(double deltaTime)
|
||||
{
|
||||
if (deltaTime == 0.0f) { return false; }
|
||||
|
||||
CurrentFramesPerSecond = (1.0 / deltaTime);
|
||||
|
||||
sampleBuffer.Enqueue(CurrentFramesPerSecond);
|
||||
|
||||
if (sampleBuffer.Count > MaximumSamples)
|
||||
{
|
||||
sampleBuffer.Dequeue();
|
||||
AverageFramesPerSecond = sampleBuffer.Average(i => i);
|
||||
}
|
||||
else
|
||||
{
|
||||
AverageFramesPerSecond = CurrentFramesPerSecond;
|
||||
}
|
||||
|
||||
if (AverageFramesPerSecond < 0 || AverageFramesPerSecond > 500) { }
|
||||
|
||||
TotalFrames++;
|
||||
TotalSeconds += deltaTime;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using GameAnalyticsSDK.Net;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@@ -54,11 +55,16 @@ namespace Barotrauma
|
||||
return;
|
||||
}
|
||||
|
||||
string contentPackageName = GameMain.Config?.SelectedContentPackage?.Name;
|
||||
if (!string.IsNullOrEmpty(contentPackageName))
|
||||
if (GameMain.Config?.SelectedContentPackages.Count > 0)
|
||||
{
|
||||
GameAnalytics.AddDesignEvent("ContentPackage:" +
|
||||
contentPackageName.Replace(":", "").Substring(0, Math.Min(32, contentPackageName.Length)));
|
||||
StringBuilder sb = new StringBuilder("ContentPackage:");
|
||||
int i = 0;
|
||||
foreach (ContentPackage cp in GameMain.Config.SelectedContentPackages)
|
||||
{
|
||||
sb.Append(cp.Name.Replace(":", "").Substring(0, Math.Min(32, cp.Name.Length)));
|
||||
if (i < GameMain.Config.SelectedContentPackages.Count - 1) sb.Append(",");
|
||||
}
|
||||
GameAnalytics.AddDesignEvent(sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,13 +8,13 @@ namespace Barotrauma
|
||||
{
|
||||
class PurchasedItem
|
||||
{
|
||||
public ItemPrefab itemPrefab;
|
||||
public int quantity;
|
||||
public readonly ItemPrefab ItemPrefab;
|
||||
public int Quantity;
|
||||
|
||||
public PurchasedItem(ItemPrefab itemPrefab, int quantity)
|
||||
{
|
||||
this.itemPrefab = itemPrefab;
|
||||
this.quantity = quantity;
|
||||
this.ItemPrefab = itemPrefab;
|
||||
this.Quantity = quantity;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,34 +45,31 @@ namespace Barotrauma
|
||||
OnItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void PurchaseItem(ItemPrefab item, int Quantity = 1)
|
||||
public void PurchaseItem(ItemPrefab item, int quantity = 1)
|
||||
{
|
||||
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.itemPrefab == item);
|
||||
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.ItemPrefab == item);
|
||||
|
||||
if(purchasedItem != null && Quantity == 1)
|
||||
if (purchasedItem != null && quantity == 1)
|
||||
{
|
||||
campaign.Money -= item.Price;
|
||||
purchasedItem.quantity += 1;
|
||||
campaign.Money -= item.GetPrice(campaign.Map.CurrentLocation).BuyPrice;
|
||||
purchasedItem.Quantity += 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
campaign.Money -= (item.Price * Quantity);
|
||||
purchasedItem = new PurchasedItem(item, Quantity);
|
||||
campaign.Money -= item.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
|
||||
purchasedItem = new PurchasedItem(item, quantity);
|
||||
purchasedItems.Add(purchasedItem);
|
||||
}
|
||||
|
||||
OnItemsChanged?.Invoke();
|
||||
}
|
||||
|
||||
public void SellItem(ItemPrefab item, int quantity = 1)
|
||||
public void SellItem(PurchasedItem purchasedItem, int quantity = 1)
|
||||
{
|
||||
campaign.Money += (item.Price * quantity);
|
||||
PurchasedItem purchasedItem = PurchasedItems.Find(pi => pi.itemPrefab == item);
|
||||
if (purchasedItem != null && purchasedItem.quantity - quantity > 0)
|
||||
{
|
||||
purchasedItem.quantity -= quantity;
|
||||
}
|
||||
else
|
||||
quantity = Math.Min(purchasedItem.Quantity, quantity);
|
||||
campaign.Money += purchasedItem.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * quantity;
|
||||
purchasedItem.Quantity -= quantity;
|
||||
if (purchasedItem != null && purchasedItem.Quantity <= 0)
|
||||
{
|
||||
PurchasedItems.Remove(purchasedItem);
|
||||
}
|
||||
@@ -82,7 +79,7 @@ namespace Barotrauma
|
||||
|
||||
public int GetTotalItemCost()
|
||||
{
|
||||
return purchasedItems.Sum(i => (i.itemPrefab.Price * i.quantity));
|
||||
return purchasedItems.Sum(i => i.ItemPrefab.GetPrice(campaign.Map.CurrentLocation).BuyPrice * i.Quantity);
|
||||
}
|
||||
|
||||
public void CreateItems()
|
||||
@@ -115,20 +112,20 @@ namespace Barotrauma
|
||||
{
|
||||
Vector2 position = new Vector2(
|
||||
Rand.Range(cargoRoom.Rect.X + 20, cargoRoom.Rect.Right - 20),
|
||||
cargoRoom.Rect.Y - cargoRoom.Rect.Height + pi.itemPrefab.Size.Y / 2);
|
||||
cargoRoom.Rect.Y - cargoRoom.Rect.Height + pi.ItemPrefab.Size.Y / 2);
|
||||
|
||||
ItemContainer itemContainer = null;
|
||||
if (!string.IsNullOrEmpty(pi.itemPrefab.CargoContainerName))
|
||||
if (!string.IsNullOrEmpty(pi.ItemPrefab.CargoContainerIdentifier))
|
||||
{
|
||||
itemContainer = availableContainers.Keys.ToList().Find(ac =>
|
||||
ac.Item.Prefab.NameMatches(pi.itemPrefab.CargoContainerName) ||
|
||||
ac.Item.Prefab.Tags.Contains(pi.itemPrefab.CargoContainerName.ToLowerInvariant()));
|
||||
ac.Item.Prefab.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
|
||||
ac.Item.Prefab.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()));
|
||||
|
||||
if (itemContainer == null)
|
||||
{
|
||||
containerPrefab = MapEntityPrefab.List.Find(ep =>
|
||||
ep.NameMatches(pi.itemPrefab.CargoContainerName) ||
|
||||
(ep.Tags != null && ep.Tags.Contains(pi.itemPrefab.CargoContainerName.ToLowerInvariant()))) as ItemPrefab;
|
||||
ep.Identifier == pi.ItemPrefab.CargoContainerIdentifier ||
|
||||
(ep.Tags != null && ep.Tags.Contains(pi.ItemPrefab.CargoContainerIdentifier.ToLowerInvariant()))) as ItemPrefab;
|
||||
|
||||
if (containerPrefab == null)
|
||||
{
|
||||
@@ -150,18 +147,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < pi.quantity; i++)
|
||||
for (int i = 0; i < pi.Quantity; i++)
|
||||
{
|
||||
if (itemContainer == null)
|
||||
{
|
||||
//no container, place at the waypoint
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.itemPrefab, position, wp.Submarine);
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, position, wp.Submarine);
|
||||
}
|
||||
else
|
||||
{
|
||||
new Item(pi.itemPrefab, position, wp.Submarine);
|
||||
new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -181,11 +178,11 @@ namespace Barotrauma
|
||||
//place in the container
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
Entity.Spawner.AddToSpawnQueue(pi.itemPrefab, itemContainer.Inventory);
|
||||
Entity.Spawner.AddToSpawnQueue(pi.ItemPrefab, itemContainer.Inventory);
|
||||
}
|
||||
else
|
||||
{
|
||||
var item = new Item(pi.itemPrefab, position, wp.Submarine);
|
||||
var item = new Item(pi.ItemPrefab, position, wp.Submarine);
|
||||
itemContainer.Inventory.TryPutItem(item, null);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
using Barotrauma.Networking;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
partial class CrewManager
|
||||
{
|
||||
const float ConversationIntervalMin = 100.0f;
|
||||
const float ConversationIntervalMax = 180.0f;
|
||||
private float conversationTimer, conversationLineTimer;
|
||||
private List<Pair<Character, string>> pendingConversationLines = new List<Pair<Character, string>>();
|
||||
|
||||
//orders that have not been issued to a specific character
|
||||
private List<Pair<Order, float>> activeOrders = new List<Pair<Order, float>>();
|
||||
public List<Pair<Order, float>> ActiveOrders
|
||||
{
|
||||
get { return activeOrders; }
|
||||
}
|
||||
|
||||
private bool isSinglePlayer;
|
||||
public bool IsSinglePlayer
|
||||
{
|
||||
get { return isSinglePlayer; }
|
||||
}
|
||||
|
||||
public CrewManager(bool isSinglePlayer)
|
||||
{
|
||||
this.isSinglePlayer = isSinglePlayer;
|
||||
conversationTimer = 5.0f;
|
||||
|
||||
InitProjectSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjectSpecific();
|
||||
|
||||
public bool AddOrder(Order order, float fadeOutTime)
|
||||
{
|
||||
if (order.TargetEntity == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Attempted to add an order with no target entity to CrewManager!\n" + Environment.StackTrace);
|
||||
return false;
|
||||
}
|
||||
|
||||
Pair<Order, float> existingOrder = activeOrders.Find(o => o.First.Prefab == order.Prefab && o.First.TargetEntity == order.TargetEntity);
|
||||
if (existingOrder != null)
|
||||
{
|
||||
existingOrder.Second = fadeOutTime;
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
activeOrders.Add(new Pair<Order, float>(order, fadeOutTime));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveOrder(Order order)
|
||||
{
|
||||
activeOrders.RemoveAll(o => o.First == order);
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
foreach (Pair<Order, float> order in activeOrders)
|
||||
{
|
||||
order.Second -= deltaTime;
|
||||
}
|
||||
activeOrders.RemoveAll(o => o.Second <= 0.0f);
|
||||
|
||||
UpdateProjectSpecific(deltaTime);
|
||||
}
|
||||
|
||||
#region Dialog
|
||||
|
||||
public void AddConversation(List<Pair<Character, string>> conversationLines)
|
||||
{
|
||||
if (conversationLines == null || conversationLines.Count == 0) { return; }
|
||||
pendingConversationLines.AddRange(conversationLines);
|
||||
}
|
||||
|
||||
private void UpdateConversations(float deltaTime)
|
||||
{
|
||||
conversationTimer -= deltaTime;
|
||||
if (conversationTimer <= 0.0f)
|
||||
{
|
||||
#if CLIENT
|
||||
List<Character> availableSpeakers = GameMain.GameSession.CrewManager.GetCharacters().ToList();
|
||||
availableSpeakers.RemoveAll(c => !(c.AIController is HumanAIController) || c.IsDead || c.SpeechImpediment >= 100.0f);
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
foreach (Client client in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (client.Character != null) availableSpeakers.Remove(client.Character);
|
||||
}
|
||||
if (GameMain.Server.Character != null) availableSpeakers.Remove(GameMain.Server.Character);
|
||||
}
|
||||
#else
|
||||
List<Character> availableSpeakers = Character.CharacterList.FindAll(c =>
|
||||
c.AIController is HumanAIController &&
|
||||
!c.IsDead &&
|
||||
c.SpeechImpediment <= 100.0f);
|
||||
#endif
|
||||
pendingConversationLines.AddRange(NPCConversation.CreateRandom(availableSpeakers));
|
||||
conversationTimer = Rand.Range(ConversationIntervalMin, ConversationIntervalMax);
|
||||
}
|
||||
|
||||
if (pendingConversationLines.Count > 0)
|
||||
{
|
||||
conversationLineTimer -= deltaTime;
|
||||
if (conversationLineTimer <= 0.0f)
|
||||
{
|
||||
//speaker of the next line can't speak, interrupt the conversation
|
||||
if (pendingConversationLines[0].First.SpeechImpediment >= 100.0f)
|
||||
{
|
||||
pendingConversationLines.Clear();
|
||||
return;
|
||||
}
|
||||
|
||||
pendingConversationLines[0].First.Speak(pendingConversationLines[0].Second, null);
|
||||
if (pendingConversationLines.Count > 1)
|
||||
{
|
||||
conversationLineTimer = MathHelper.Clamp(pendingConversationLines[0].Second.Length * 0.1f, 1.0f, 5.0f);
|
||||
}
|
||||
pendingConversationLines.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
partial void UpdateProjectSpecific(float deltaTime);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -9,7 +10,15 @@ namespace Barotrauma
|
||||
{
|
||||
public readonly CargoManager CargoManager;
|
||||
|
||||
const int InitialMoney = 10000;
|
||||
public bool CheatsEnabled;
|
||||
|
||||
const int InitialMoney = 4500;
|
||||
|
||||
private bool watchmenSpawned;
|
||||
private Character startWatchman, endWatchman;
|
||||
|
||||
//key = dialog flag, double = Timing.TotalTime when the line was last said
|
||||
private Dictionary<string, double> dialogLastSpoken = new Dictionary<string, double>();
|
||||
|
||||
protected Map map;
|
||||
public Map Map
|
||||
@@ -21,7 +30,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
return Map.SelectedConnection.Mission;
|
||||
return Map.CurrentLocation?.SelectedMission;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +50,7 @@ namespace Barotrauma
|
||||
|
||||
public void GenerateMap(string seed)
|
||||
{
|
||||
map = new Map(seed, 1000);
|
||||
map = new Map(seed);
|
||||
}
|
||||
|
||||
protected List<Submarine> GetSubsToLeaveBehind(Submarine leavingSub)
|
||||
@@ -50,17 +59,98 @@ namespace Barotrauma
|
||||
return Submarine.Loaded.FindAll(s =>
|
||||
s != leavingSub &&
|
||||
!leavingSub.DockedTo.Contains(s) &&
|
||||
s != Level.Loaded.StartOutpost && s != Level.Loaded.EndOutpost &&
|
||||
(s.AtEndPosition != leavingSub.AtEndPosition || s.AtStartPosition != leavingSub.AtStartPosition));
|
||||
}
|
||||
|
||||
public override void End(string endMessage = "")
|
||||
public override void Start()
|
||||
{
|
||||
base.End(endMessage);
|
||||
base.Start();
|
||||
dialogLastSpoken.Clear();
|
||||
watchmenSpawned = false;
|
||||
startWatchman = null;
|
||||
endWatchman = null;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime)
|
||||
{
|
||||
base.Update(deltaTime);
|
||||
|
||||
if (GameMain.Client != null || !IsRunning) { return; }
|
||||
|
||||
if (!watchmenSpawned)
|
||||
{
|
||||
if (Level.Loaded.StartOutpost != null) { startWatchman = SpawnWatchman(Level.Loaded.StartOutpost); }
|
||||
if (Level.Loaded.EndOutpost != null) { endWatchman = SpawnWatchman(Level.Loaded.EndOutpost); }
|
||||
watchmenSpawned = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
#if SERVER
|
||||
if (string.IsNullOrEmpty(character.OwnerClientIP)) { continue; }
|
||||
#else
|
||||
if (!CrewManager.GetCharacters().Contains(character)) { continue; }
|
||||
#endif
|
||||
if (character.Submarine == Level.Loaded.StartOutpost && character.CurrentHull == startWatchman.CurrentHull)
|
||||
{
|
||||
CreateDialog(new List<Character> { startWatchman }, "EnterStartOutpost", 5 * 60.0f);
|
||||
}
|
||||
else if (character.Submarine == Level.Loaded.EndOutpost && character.CurrentHull == endWatchman.CurrentHull)
|
||||
{
|
||||
CreateDialog(new List<Character> { endWatchman }, "EnterEndOutpost", 5 * 60.0f);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void CreateDialog(List<Character> speakers, string conversationTag, float minInterval)
|
||||
{
|
||||
if (dialogLastSpoken.TryGetValue(conversationTag, out double lastTime))
|
||||
{
|
||||
if (Timing.TotalTime - lastTime < minInterval) { return; }
|
||||
}
|
||||
|
||||
CrewManager.AddConversation(
|
||||
NPCConversation.CreateRandom(speakers, new List<string>() { conversationTag }));
|
||||
dialogLastSpoken[conversationTag] = Timing.TotalTime;
|
||||
}
|
||||
|
||||
private Character SpawnWatchman(Submarine outpost)
|
||||
{
|
||||
WayPoint watchmanSpawnpoint = WayPoint.WayPointList.Find(wp => wp.Submarine == outpost);
|
||||
if (watchmanSpawnpoint == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Failed to spawn a watchman at the outpost. No spawnpoints found inside the outpost.");
|
||||
return null;
|
||||
}
|
||||
|
||||
string seed = outpost == Level.Loaded.StartOutpost ? map.SelectedLocation.Name : map.CurrentLocation.Name;
|
||||
Rand.SetSyncedSeed(ToolBox.StringToInt(seed));
|
||||
|
||||
JobPrefab watchmanJob = JobPrefab.List.Find(jp => jp.Identifier == "watchman");
|
||||
CharacterInfo characterInfo = new CharacterInfo(Character.HumanConfigFile, jobPrefab: watchmanJob);
|
||||
var spawnedCharacter = Character.Create(characterInfo, watchmanSpawnpoint.WorldPosition,
|
||||
Level.Loaded.Seed + (outpost == Level.Loaded.StartOutpost ? "start" : "end"));
|
||||
spawnedCharacter.CharacterHealth.Unkillable = true;
|
||||
spawnedCharacter.CharacterHealth.UseHealthWindow = false;
|
||||
spawnedCharacter.SetCustomInteract(
|
||||
WatchmanInteract,
|
||||
hudText: TextManager.Get("TalkHint").Replace("[key]", GameMain.Config.KeyBind(InputType.Select).ToString()));
|
||||
(spawnedCharacter.AIController as HumanAIController)?.ObjectiveManager.SetOrder(
|
||||
new AIObjectiveGoTo(watchmanSpawnpoint, spawnedCharacter, repeat: true, getDivingGearIfNeeded: false));
|
||||
if (watchmanJob != null)
|
||||
{
|
||||
spawnedCharacter.GiveJobItems();
|
||||
}
|
||||
return spawnedCharacter;
|
||||
}
|
||||
|
||||
protected abstract void WatchmanInteract(Character watchman, Character interactor);
|
||||
|
||||
public abstract void Save(XElement element);
|
||||
|
||||
|
||||
|
||||
public void LogState()
|
||||
{
|
||||
DebugConsole.NewMessage("********* CAMPAIGN STATUS *********", Color.White);
|
||||
@@ -81,11 +171,18 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
if (map.SelectedConnection?.Mission != null)
|
||||
if (map.CurrentLocation?.SelectedMission != null)
|
||||
{
|
||||
DebugConsole.NewMessage(" Selected mission: " + map.SelectedConnection.Mission.Name, Color.White);
|
||||
DebugConsole.NewMessage("\n" + map.SelectedConnection.Mission.Description, Color.White);
|
||||
DebugConsole.NewMessage(" Selected mission: " + map.CurrentLocation.SelectedMission.Name, Color.White);
|
||||
DebugConsole.NewMessage("\n" + map.CurrentLocation.SelectedMission.Description, Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
base.Remove();
|
||||
map?.Remove();
|
||||
map = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
using Barotrauma.Items.Components;
|
||||
using Barotrauma.Networking;
|
||||
using Lidgren.Network;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
class CharacterCampaignData
|
||||
{
|
||||
public readonly CharacterInfo CharacterInfo;
|
||||
|
||||
public readonly string Name;
|
||||
|
||||
public readonly bool IsHostCharacter;
|
||||
|
||||
public readonly string ClientIP;
|
||||
public readonly ulong SteamID;
|
||||
|
||||
private XElement itemData;
|
||||
|
||||
public CharacterCampaignData(Client client)
|
||||
{
|
||||
Name = client.Name;
|
||||
ClientIP = client.Connection.RemoteEndPoint.Address.ToString();
|
||||
SteamID = client.SteamID;
|
||||
CharacterInfo = client.CharacterInfo;
|
||||
|
||||
if (client.Character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
client.Character.SaveInventory(client.Character.Inventory, itemData);
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterCampaignData(GameServer server)
|
||||
{
|
||||
Name = server.Character.Name;
|
||||
CharacterInfo = server.Character.Info;
|
||||
IsHostCharacter = true;
|
||||
|
||||
if (server.Character.Inventory != null)
|
||||
{
|
||||
itemData = new XElement("inventory");
|
||||
server.Character.SaveInventory(server.Character.Inventory, itemData);
|
||||
}
|
||||
}
|
||||
|
||||
public CharacterCampaignData(XElement element)
|
||||
{
|
||||
Name = element.GetAttributeString("name", "Unnamed");
|
||||
IsHostCharacter = element.GetAttributeBool("host", false);
|
||||
if (!IsHostCharacter)
|
||||
{
|
||||
ClientIP = element.GetAttributeString("ip", "");
|
||||
string steamID = element.GetAttributeString("steamid", "");
|
||||
if (!string.IsNullOrEmpty(steamID))
|
||||
{
|
||||
ulong.TryParse(steamID, out SteamID);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "character":
|
||||
case "characterinfo":
|
||||
CharacterInfo = new CharacterInfo(subElement);
|
||||
break;
|
||||
case "inventory":
|
||||
itemData = subElement;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool MatchesClient(Client client)
|
||||
{
|
||||
if (IsHostCharacter) return false;
|
||||
if (SteamID > 0)
|
||||
{
|
||||
return SteamID == client.SteamID;
|
||||
}
|
||||
else
|
||||
{
|
||||
return ClientIP == client.Connection.RemoteEndPoint.Address.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public XElement Save()
|
||||
{
|
||||
XElement element = new XElement("CharacterCampaignData",
|
||||
new XAttribute("name", Name));
|
||||
|
||||
if (IsHostCharacter)
|
||||
{
|
||||
element.Add(new XAttribute("host", true));
|
||||
}
|
||||
else
|
||||
{
|
||||
element.Add(new XAttribute("ip", ClientIP));
|
||||
element.Add(new XAttribute("steamid", SteamID));
|
||||
}
|
||||
|
||||
CharacterInfo?.Save(element);
|
||||
|
||||
if (itemData != null)
|
||||
{
|
||||
element.Add(itemData);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
public void SpawnInventoryItems(CharacterInfo characterInfo, Inventory inventory)
|
||||
{
|
||||
characterInfo.SpawnInventoryItems(inventory, itemData);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ namespace Barotrauma
|
||||
protected GameModePreset preset;
|
||||
|
||||
private string endMessage;
|
||||
|
||||
protected CrewManager CrewManager
|
||||
{
|
||||
get { return GameMain.GameSession?.CrewManager; }
|
||||
}
|
||||
|
||||
public virtual Mission Mission
|
||||
{
|
||||
@@ -60,10 +65,20 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
public virtual void MsgBox() { }
|
||||
|
||||
public virtual void AddToGUIUpdateList()
|
||||
{
|
||||
#if CLIENT
|
||||
if (!isRunning) return;
|
||||
|
||||
GameMain.GameSession?.CrewManager.AddToGUIUpdateList();
|
||||
#endif
|
||||
}
|
||||
|
||||
public virtual void AddToGUIUpdateList() { }
|
||||
|
||||
public virtual void Update(float deltaTime) { }
|
||||
public virtual void Update(float deltaTime)
|
||||
{
|
||||
CrewManager?.Update(deltaTime);
|
||||
}
|
||||
|
||||
public virtual void End(string endMessage = "")
|
||||
{
|
||||
@@ -74,6 +89,6 @@ namespace Barotrauma
|
||||
GameMain.GameSession.EndRound(endMessage);
|
||||
}
|
||||
|
||||
|
||||
public virtual void Remove() { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,25 @@ namespace Barotrauma
|
||||
{
|
||||
class GameModePreset
|
||||
{
|
||||
public static List<GameModePreset> list = new List<GameModePreset>();
|
||||
public static List<GameModePreset> List = new List<GameModePreset>();
|
||||
|
||||
public ConstructorInfo Constructor
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public ConstructorInfo Constructor;
|
||||
public string Name;
|
||||
public string Name
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public string Identifier
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool IsSinglePlayer
|
||||
{
|
||||
@@ -30,16 +45,17 @@ namespace Barotrauma
|
||||
private set;
|
||||
}
|
||||
|
||||
public GameModePreset(string name, Type type, bool isSinglePlayer = false, bool votable = true)
|
||||
public GameModePreset(string identifier, Type type, bool isSinglePlayer = false, bool votable = true)
|
||||
{
|
||||
this.Name = name;
|
||||
Name = TextManager.Get("GameMode." + identifier);
|
||||
Identifier = identifier;
|
||||
|
||||
Constructor = type.GetConstructor(new Type[] { typeof(GameModePreset), typeof(object) });
|
||||
|
||||
IsSinglePlayer = isSinglePlayer;
|
||||
Votable = votable;
|
||||
|
||||
list.Add(this);
|
||||
List.Add(this);
|
||||
}
|
||||
|
||||
public GameMode Instantiate(object param)
|
||||
@@ -51,19 +67,27 @@ namespace Barotrauma
|
||||
public static void Init()
|
||||
{
|
||||
#if CLIENT
|
||||
new GameModePreset("Single Player", typeof(SinglePlayerCampaign), true);
|
||||
new GameModePreset("Tutorial", typeof(TutorialMode), true);
|
||||
new GameModePreset("singleplayercampaign", typeof(SinglePlayerCampaign), true);
|
||||
new GameModePreset("tutorial", typeof(TutorialMode), true);
|
||||
#endif
|
||||
new GameModePreset("devsandbox", typeof(GameMode), true)
|
||||
{
|
||||
Description = "Single player sandbox mode for debugging."
|
||||
};
|
||||
|
||||
var mode = new GameModePreset("SandBox", typeof(GameMode), false);
|
||||
mode.Description = "A game mode with no specific objectives.";
|
||||
|
||||
mode = new GameModePreset("Mission", typeof(MissionMode), false);
|
||||
mode.Description = "The crew must work together to complete a specific task, such as retrieving "
|
||||
new GameModePreset("sandbox", typeof(GameMode), false)
|
||||
{
|
||||
Description = "A game mode with no specific objectives."
|
||||
};
|
||||
|
||||
new GameModePreset("mission", typeof(MissionMode), false)
|
||||
{
|
||||
Description = "The crew must work together to complete a specific task, such as retrieving "
|
||||
+ "an alien artifact or killing a creature that's terrorizing nearby outposts. The game ends "
|
||||
+ "when the task is completed or everyone in the crew has died.";
|
||||
+ "when the task is completed or everyone in the crew has died."
|
||||
};
|
||||
|
||||
new GameModePreset("Campaign", typeof(MultiPlayerCampaign), false, false);
|
||||
//new GameModePreset("multiplayercampaign", typeof(MultiPlayerCampaign), false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
: base(preset, param)
|
||||
{
|
||||
Location[] locations = { GameMain.GameSession.StartLocation, GameMain.GameSession.EndLocation };
|
||||
if (param is string)
|
||||
if (param is MissionType missionType)
|
||||
{
|
||||
mission = Mission.LoadRandom(locations, GameMain.NetLobbyScreen.LevelSeed, (string)param);
|
||||
mission = Mission.LoadRandom(locations, GameMain.NetLobbyScreen.LevelSeed, false, missionType);
|
||||
}
|
||||
else if (param is MissionPrefab)
|
||||
else if (param is MissionPrefab missionPrefab)
|
||||
{
|
||||
mission = ((MissionPrefab)param).Instantiate(locations);
|
||||
mission = missionPrefab.Instantiate(locations);
|
||||
}
|
||||
else if (param is Mission)
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Lidgren.Network;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
@@ -32,6 +33,8 @@ namespace Barotrauma
|
||||
|
||||
private static byte currentCampaignID;
|
||||
|
||||
private List<CharacterCampaignData> characterData = new List<CharacterCampaignData>();
|
||||
|
||||
public byte CampaignID
|
||||
{
|
||||
get; private set;
|
||||
@@ -50,9 +53,55 @@ namespace Barotrauma
|
||||
{
|
||||
CargoManager.OnItemsChanged += () => { LastUpdateID++; };
|
||||
Map.OnLocationSelected += (loc, connection) => { LastUpdateID++; };
|
||||
Map.OnMissionSelected += (loc, mission) => { LastUpdateID++; };
|
||||
}
|
||||
}
|
||||
|
||||
public void DiscardClientCharacterData(Client client)
|
||||
{
|
||||
characterData.RemoveAll(cd => cd.MatchesClient(client));
|
||||
}
|
||||
|
||||
public CharacterCampaignData GetClientCharacterData(Client client)
|
||||
{
|
||||
return characterData.Find(cd => cd.MatchesClient(client));
|
||||
}
|
||||
|
||||
public CharacterCampaignData GetHostCharacterData()
|
||||
{
|
||||
return characterData.Find(cd => cd.IsHostCharacter);
|
||||
}
|
||||
|
||||
public void AssignPlayerCharacterInfos(IEnumerable<Client> connectedClients, bool assignHost)
|
||||
{
|
||||
foreach (Client client in connectedClients)
|
||||
{
|
||||
if (client.SpectateOnly && GameMain.Server.AllowSpectating) continue;
|
||||
var matchingData = GetClientCharacterData(client);
|
||||
if (matchingData != null) client.CharacterInfo = matchingData.CharacterInfo;
|
||||
}
|
||||
|
||||
if (assignHost)
|
||||
{
|
||||
var hostCharacterData = GetHostCharacterData();
|
||||
if (hostCharacterData?.CharacterInfo != null)
|
||||
{
|
||||
GameMain.Server.CharacterInfo = hostCharacterData.CharacterInfo;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<Client, Job> GetAssignedJobs(IEnumerable<Client> connectedClients)
|
||||
{
|
||||
var assignedJobs = new Dictionary<Client, Job>();
|
||||
foreach (Client client in connectedClients)
|
||||
{
|
||||
var matchingData = GetClientCharacterData(client);
|
||||
if (matchingData != null) assignedJobs.Add(client, matchingData.CharacterInfo.Job);
|
||||
}
|
||||
return assignedJobs;
|
||||
}
|
||||
|
||||
public override void Start()
|
||||
{
|
||||
base.Start();
|
||||
@@ -60,6 +109,44 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
protected override void WatchmanInteract(Character watchman, Character interactor)
|
||||
{
|
||||
if ((watchman.Submarine == Level.Loaded.StartOutpost && !Submarine.MainSub.AtStartPosition) ||
|
||||
(watchman.Submarine == Level.Loaded.EndOutpost && !Submarine.MainSub.AtEndPosition))
|
||||
{
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
CreateDialog(new List<Character> { watchman }, "WatchmanInteractNoLeavingSub", 5.0f);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
bool hasPermissions = true;
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
var client = GameMain.Server.ConnectedClients.Find(c => c.Character == interactor);
|
||||
hasPermissions = client != null &&
|
||||
(client.HasPermission(ClientPermissions.EndRound) || client.HasPermission(ClientPermissions.ManageCampaign));
|
||||
CreateDialog(new List<Character> { watchman }, hasPermissions ? "WatchmanInteract" : "WatchmanInteractNotAllowed", 1.0f);
|
||||
}
|
||||
#if CLIENT
|
||||
else if (GameMain.Client != null && interactor == Character.Controlled && hasPermissions)
|
||||
{
|
||||
var msgBox = new GUIMessageBox("", TextManager.Get("CampaignEnterOutpostPrompt")
|
||||
.Replace("[locationname]", Submarine.MainSub.AtStartPosition ? Map.CurrentLocation.Name : Map.SelectedLocation.Name),
|
||||
new string[] { TextManager.Get("Yes"), TextManager.Get("No") });
|
||||
msgBox.Buttons[0].OnClicked = (btn, userdata) =>
|
||||
{
|
||||
GameMain.Client.RequestRoundEnd();
|
||||
return true;
|
||||
};
|
||||
msgBox.Buttons[0].OnClicked += msgBox.Close;
|
||||
msgBox.Buttons[1].OnClicked += msgBox.Close;
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void End(string endMessage = "")
|
||||
{
|
||||
isRunning = false;
|
||||
@@ -92,17 +179,42 @@ namespace Barotrauma
|
||||
}*/
|
||||
|
||||
GameMain.GameSession.EndRound("");
|
||||
|
||||
foreach (Client c in GameMain.Server.ConnectedClients)
|
||||
{
|
||||
if (c.HasSpawned)
|
||||
{
|
||||
//client has spawned this round -> remove old data (and replace with new one if the client still has an alive character)
|
||||
characterData.RemoveAll(cd => cd.MatchesClient(c));
|
||||
}
|
||||
|
||||
if (c.Character?.Info != null && !c.Character.IsDead)
|
||||
{
|
||||
characterData.Add(new CharacterCampaignData(c));
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: save player inventories between mp campaign rounds
|
||||
#if CLIENT
|
||||
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(null);
|
||||
#endif
|
||||
|
||||
if (GameMain.Server.Character != null)
|
||||
{
|
||||
characterData.RemoveAll(cd => cd.IsHostCharacter);
|
||||
if (!GameMain.Server.Character.IsDead)
|
||||
{
|
||||
var hostCharacterData = new CharacterCampaignData(GameMain.Server);
|
||||
characterData.Add(hostCharacterData);
|
||||
#if CLIENT
|
||||
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(hostCharacterData.CharacterInfo);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
//remove all items that are in someone's inventory
|
||||
foreach (Character c in Character.CharacterList)
|
||||
{
|
||||
if (c.Inventory == null) continue;
|
||||
foreach (Item item in c.Inventory.Items)
|
||||
{
|
||||
if (item != null) item.Remove();
|
||||
}
|
||||
c.Inventory?.DeleteAllItems();
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
@@ -128,29 +240,54 @@ namespace Barotrauma
|
||||
|
||||
if (atEndPosition)
|
||||
{
|
||||
Map.MoveToNextLocation();
|
||||
map.MoveToNextLocation();
|
||||
|
||||
//select a random location to make sure we've got some destination
|
||||
//to head towards even if the host/clients don't select anything
|
||||
map.SelectRandomLocation(true);
|
||||
}
|
||||
map.ProgressWorld();
|
||||
|
||||
SaveUtil.SaveGame(GameMain.GameSession.SavePath);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static MultiPlayerCampaign LoadNew(XElement element)
|
||||
{
|
||||
MultiPlayerCampaign campaign = new MultiPlayerCampaign(GameModePreset.list.Find(gm => gm.Name == "Campaign"), null);
|
||||
campaign.Load(element);
|
||||
MultiPlayerCampaign campaign = new MultiPlayerCampaign(GameModePreset.List.Find(gm => gm.Identifier == "multiplayercampaign"), null);
|
||||
campaign.Load(element);
|
||||
campaign.SetDelegates();
|
||||
|
||||
return campaign;
|
||||
}
|
||||
|
||||
public static string GetCharacterDataSavePath(string savePath)
|
||||
{
|
||||
return Path.Combine(SaveUtil.MultiplayerSaveFolder, Path.GetFileNameWithoutExtension(savePath) + "_CharacterData.xml");
|
||||
}
|
||||
|
||||
public string GetCharacterDataSavePath()
|
||||
{
|
||||
return GetCharacterDataSavePath(GameMain.GameSession.SavePath);
|
||||
}
|
||||
|
||||
public void Load(XElement element)
|
||||
{
|
||||
Money = element.GetAttributeInt("money", 0);
|
||||
CheatsEnabled = element.GetAttributeBool("cheatsenabled", false);
|
||||
if (CheatsEnabled)
|
||||
{
|
||||
DebugConsole.CheatsEnabled = true;
|
||||
if (GameMain.Config.UseSteam && !SteamAchievementManager.CheatsEnabled)
|
||||
{
|
||||
SteamAchievementManager.CheatsEnabled = true;
|
||||
#if CLIENT
|
||||
new GUIMessageBox("Cheats enabled", "Cheat commands have been enabled on the server. You will not receive Steam Achievements until you restart the game.");
|
||||
#else
|
||||
DebugConsole.NewMessage("Cheat commands have been enabled.", Color.Red);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
@@ -159,24 +296,64 @@ namespace Barotrauma
|
||||
case "map":
|
||||
if (map == null)
|
||||
{
|
||||
//map not created yet, loading this campaign for the first time
|
||||
map = Map.LoadNew(subElement);
|
||||
}
|
||||
else
|
||||
{
|
||||
map.Load(subElement);
|
||||
//map already created, update it
|
||||
//if we're not downloading the initial save file (LastSaveID > 0),
|
||||
//show notifications about location type changes
|
||||
map.Load(subElement, LastSaveID > 0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
characterData.Clear();
|
||||
string characterDataPath = GetCharacterDataSavePath();
|
||||
var characterDataDoc = XMLExtensions.TryLoadXml(characterDataPath);
|
||||
if (characterDataDoc?.Root == null) return;
|
||||
foreach (XElement subElement in characterDataDoc.Root.Elements())
|
||||
{
|
||||
characterData.Add(new CharacterCampaignData(subElement));
|
||||
}
|
||||
#if CLIENT
|
||||
var hostCharacterData = GetHostCharacterData();
|
||||
if (hostCharacterData?.CharacterInfo != null)
|
||||
{
|
||||
GameMain.NetLobbyScreen.SetCampaignCharacterInfo(hostCharacterData.CharacterInfo);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public override void Save(XElement element)
|
||||
{
|
||||
XElement modeElement = new XElement("MultiPlayerCampaign");
|
||||
modeElement.Add(new XAttribute("money", Money));
|
||||
XElement modeElement = new XElement("MultiPlayerCampaign",
|
||||
new XAttribute("money", Money),
|
||||
new XAttribute("cheatsenabled", CheatsEnabled));
|
||||
Map.Save(modeElement);
|
||||
element.Add(modeElement);
|
||||
|
||||
//save character data to a separate file
|
||||
string characterDataPath = GetCharacterDataSavePath();
|
||||
XDocument characterDataDoc = new XDocument(new XElement("CharacterData"));
|
||||
foreach (CharacterCampaignData cd in characterData)
|
||||
{
|
||||
characterDataDoc.Root.Add(cd.Save());
|
||||
}
|
||||
try
|
||||
{
|
||||
characterDataDoc.Save(characterDataPath);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Saving multiplayer campaign characters to \"" + characterDataPath + "\" failed!", e);
|
||||
}
|
||||
|
||||
lastSaveID++;
|
||||
}
|
||||
|
||||
@@ -190,20 +367,33 @@ namespace Barotrauma
|
||||
msg.Write(map.Seed);
|
||||
msg.Write(map.CurrentLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.CurrentLocationIndex);
|
||||
msg.Write(map.SelectedLocationIndex == -1 ? UInt16.MaxValue : (UInt16)map.SelectedLocationIndex);
|
||||
msg.Write(map.SelectedMissionIndex == -1 ? byte.MaxValue : (byte)map.SelectedMissionIndex);
|
||||
|
||||
msg.Write(Money);
|
||||
|
||||
msg.Write((UInt16)CargoManager.PurchasedItems.Count);
|
||||
foreach (PurchasedItem pi in CargoManager.PurchasedItems)
|
||||
{
|
||||
msg.Write((UInt16)MapEntityPrefab.List.IndexOf(pi.itemPrefab));
|
||||
msg.Write((UInt16)pi.quantity);
|
||||
msg.Write((UInt16)MapEntityPrefab.List.IndexOf(pi.ItemPrefab));
|
||||
msg.Write((UInt16)pi.Quantity);
|
||||
}
|
||||
|
||||
var characterData = GetClientCharacterData(c);
|
||||
if (characterData?.CharacterInfo == null)
|
||||
{
|
||||
msg.Write(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
msg.Write(true);
|
||||
characterData.CharacterInfo.ServerWrite(msg);
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerRead(NetBuffer msg, Client sender)
|
||||
{
|
||||
UInt16 selectedLocIndex = msg.ReadUInt16();
|
||||
byte selectedMissionIndex = msg.ReadByte();
|
||||
UInt16 purchasedItemCount = msg.ReadUInt16();
|
||||
|
||||
List<PurchasedItem> purchasedItems = new List<PurchasedItem>();
|
||||
@@ -216,21 +406,25 @@ namespace Barotrauma
|
||||
|
||||
if (!sender.HasPermission(ClientPermissions.ManageCampaign))
|
||||
{
|
||||
DebugConsole.ThrowError("Client \""+sender.Name+"\" does not have a permission to manage the campaign");
|
||||
DebugConsole.ThrowError("Client \"" + sender.Name + "\" does not have a permission to manage the campaign");
|
||||
return;
|
||||
}
|
||||
|
||||
Map.SelectLocation(selectedLocIndex == UInt16.MaxValue ? -1 : selectedLocIndex);
|
||||
if (Map.SelectedConnection != null)
|
||||
{
|
||||
Map.SelectMission(selectedMissionIndex);
|
||||
}
|
||||
|
||||
List<PurchasedItem> currentItems = new List<PurchasedItem>(CargoManager.PurchasedItems);
|
||||
foreach (PurchasedItem pi in currentItems)
|
||||
{
|
||||
CargoManager.SellItem(pi.itemPrefab, pi.quantity);
|
||||
CargoManager.SellItem(pi, pi.Quantity);
|
||||
}
|
||||
|
||||
foreach (PurchasedItem pi in purchasedItems)
|
||||
{
|
||||
CargoManager.PurchaseItem(pi.itemPrefab, pi.quantity);
|
||||
CargoManager.PurchaseItem(pi.ItemPrefab, pi.Quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,10 @@ namespace Barotrauma
|
||||
var moreAgentsMsgBox = ChatMessage.Create(null, moreAgentsMessage, ChatMessageType.MessageBox, null);
|
||||
|
||||
Client client = server.ConnectedClients.Find(c => c.Character == Character);
|
||||
GameMain.Server.SendChatMessage(greetingChatMsg, client);
|
||||
GameMain.Server.SendChatMessage(moreAgentsChatMsg, client);
|
||||
GameMain.Server.SendChatMessage(greetingMsgBox, client);
|
||||
GameMain.Server.SendChatMessage(moreAgentsMsgBox, client);
|
||||
GameMain.Server.SendDirectChatMessage(greetingChatMsg, client);
|
||||
GameMain.Server.SendDirectChatMessage(moreAgentsChatMsg, client);
|
||||
GameMain.Server.SendDirectChatMessage(greetingMsgBox, client);
|
||||
GameMain.Server.SendDirectChatMessage(moreAgentsMsgBox, client);
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Barotrauma.Items.Components;
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
@@ -18,10 +20,10 @@ namespace Barotrauma
|
||||
private string savePath;
|
||||
|
||||
private Submarine submarine;
|
||||
|
||||
#if CLIENT
|
||||
|
||||
public CrewManager CrewManager;
|
||||
#endif
|
||||
|
||||
public double RoundStartTime;
|
||||
|
||||
private Mission currentMission;
|
||||
|
||||
@@ -44,8 +46,7 @@ namespace Barotrauma
|
||||
{
|
||||
get
|
||||
{
|
||||
CampaignMode mode = (GameMode as CampaignMode);
|
||||
return (mode == null) ? null : mode.Map;
|
||||
return (GameMode as CampaignMode)?.Map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,15 +93,17 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
|
||||
public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, string missionType = "")
|
||||
public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionType missionType = MissionType.None)
|
||||
: this(submarine, savePath)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = gameModePreset.Instantiate(missionType);
|
||||
}
|
||||
|
||||
public GameSession(Submarine submarine, string savePath, GameModePreset gameModePreset, MissionPrefab missionPrefab)
|
||||
: this(submarine, savePath)
|
||||
{
|
||||
CrewManager = new CrewManager(gameModePreset != null && gameModePreset.IsSinglePlayer);
|
||||
GameMode = gameModePreset.Instantiate(missionPrefab);
|
||||
}
|
||||
|
||||
@@ -111,10 +114,11 @@ namespace Barotrauma
|
||||
GameMain.GameSession = this;
|
||||
EventManager = new EventManager(this);
|
||||
this.savePath = savePath;
|
||||
|
||||
#if CLIENT
|
||||
CrewManager = new CrewManager();
|
||||
|
||||
infoButton = new GUIButton(new Rectangle(10, 10, 100, 20), "Info", "", null);
|
||||
int buttonHeight = (int)(HUDLayoutSettings.ButtonAreaTop.Height * 0.6f);
|
||||
infoButton = new GUIButton(HUDLayoutSettings.ToRectTransform(new Rectangle(HUDLayoutSettings.ButtonAreaTop.X, HUDLayoutSettings.ButtonAreaTop.Center.Y - buttonHeight / 2, 100, buttonHeight), GUICanvas.Instance),
|
||||
TextManager.Get("InfoButton"), textAlignment: Alignment.Center);
|
||||
infoButton.OnClicked = ToggleInfoFrame;
|
||||
#endif
|
||||
}
|
||||
@@ -127,10 +131,7 @@ namespace Barotrauma
|
||||
|
||||
GameMain.GameSession = this;
|
||||
selectedSub.Name = doc.Root.GetAttributeString("submarine", selectedSub.Name);
|
||||
#if CLIENT
|
||||
CrewManager = new CrewManager();
|
||||
#endif
|
||||
|
||||
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
@@ -138,10 +139,12 @@ namespace Barotrauma
|
||||
#if CLIENT
|
||||
case "gamemode": //legacy support
|
||||
case "singleplayercampaign":
|
||||
CrewManager = new CrewManager(true);
|
||||
GameMode = SinglePlayerCampaign.Load(subElement);
|
||||
break;
|
||||
#endif
|
||||
case "multiplayercampaign":
|
||||
CrewManager = new CrewManager(false);
|
||||
GameMode = MultiPlayerCampaign.LoadNew(subElement);
|
||||
break;
|
||||
}
|
||||
@@ -165,7 +168,7 @@ namespace Barotrauma
|
||||
MTRandom rand = new MTRandom(ToolBox.StringToInt(seed));
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f));
|
||||
dummyLocations[i] = Location.CreateRandom(new Vector2((float)rand.NextDouble() * 10000.0f, (float)rand.NextDouble() * 10000.0f), null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,9 +178,9 @@ namespace Barotrauma
|
||||
SaveUtil.LoadGame(savePath);
|
||||
}
|
||||
|
||||
public void StartRound(string levelSeed, bool loadSecondSub = false)
|
||||
public void StartRound(string levelSeed, float? difficulty = null, bool loadSecondSub = false)
|
||||
{
|
||||
Level randomLevel = Level.CreateRandom(levelSeed);
|
||||
Level randomLevel = Level.CreateRandom(levelSeed, difficulty);
|
||||
|
||||
StartRound(randomLevel, true, loadSecondSub);
|
||||
}
|
||||
@@ -186,8 +189,8 @@ namespace Barotrauma
|
||||
{
|
||||
#if CLIENT
|
||||
GameMain.LightManager.LosEnabled = GameMain.NetworkMember == null || GameMain.NetworkMember.CharacterInfo != null;
|
||||
if (GameMain.Client == null) GameMain.LightManager.LosMode = GameMain.Config.LosMode;
|
||||
#endif
|
||||
|
||||
this.level = level;
|
||||
|
||||
if (submarine == null)
|
||||
@@ -202,7 +205,7 @@ namespace Barotrauma
|
||||
{
|
||||
if (Submarine.MainSubs[1] == null)
|
||||
{
|
||||
Submarine.MainSubs[1] = new Submarine(Submarine.MainSub.FilePath,Submarine.MainSub.MD5Hash.Hash,true);
|
||||
Submarine.MainSubs[1] = new Submarine(Submarine.MainSub.FilePath, Submarine.MainSub.MD5Hash.Hash, true);
|
||||
Submarine.MainSubs[1].Load(false);
|
||||
}
|
||||
else if (reloadSub)
|
||||
@@ -210,11 +213,61 @@ namespace Barotrauma
|
||||
Submarine.MainSubs[1].Load(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (level != null)
|
||||
{
|
||||
level.Generate(mirrorLevel);
|
||||
submarine.SetPosition(submarine.FindSpawnPos(level.StartPosition - new Vector2(0.0f, 2000.0f)));
|
||||
if (level.StartOutpost != null)
|
||||
{
|
||||
//start by placing the sub below the outpost
|
||||
Rectangle outpostBorders = Level.Loaded.StartOutpost.GetDockedBorders();
|
||||
Rectangle subBorders = submarine.GetDockedBorders();
|
||||
|
||||
Vector2 startOutpostSize = Vector2.Zero;
|
||||
if (Level.Loaded.StartOutpost != null)
|
||||
{
|
||||
startOutpostSize = Level.Loaded.StartOutpost.Borders.Size.ToVector2();
|
||||
}
|
||||
submarine.SetPosition(
|
||||
Level.Loaded.StartOutpost.WorldPosition -
|
||||
new Vector2(0.0f, outpostBorders.Height / 2 + subBorders.Height / 2));
|
||||
|
||||
//find the port that's the nearest to the outpost and dock if one is found
|
||||
float closestDistance = 0.0f;
|
||||
DockingPort myPort = null, outPostPort = null;
|
||||
foreach (DockingPort port in DockingPort.List)
|
||||
{
|
||||
if (port.IsHorizontal) { continue; }
|
||||
if (port.Item.Submarine == level.StartOutpost)
|
||||
{
|
||||
outPostPort = port;
|
||||
continue;
|
||||
}
|
||||
if (port.Item.Submarine != submarine) { continue; }
|
||||
|
||||
//the submarine port has to be at the top of the sub
|
||||
if (port.Item.WorldPosition.Y < submarine.WorldPosition.Y) { continue; }
|
||||
|
||||
float dist = Vector2.DistanceSquared(port.Item.WorldPosition, level.StartOutpost.WorldPosition);
|
||||
if (myPort == null || dist < closestDistance)
|
||||
{
|
||||
myPort = port;
|
||||
closestDistance = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (myPort != null && outPostPort != null)
|
||||
{
|
||||
Vector2 portDiff = myPort.Item.WorldPosition - submarine.WorldPosition;
|
||||
submarine.SetPosition((outPostPort.Item.WorldPosition - portDiff) - Vector2.UnitY * outPostPort.DockedDistance);
|
||||
myPort.Dock(outPostPort);
|
||||
myPort.Lock(true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
submarine.SetPosition(submarine.FindSpawnPos(level.StartPosition));
|
||||
}
|
||||
}
|
||||
|
||||
Entity.Spawner = new EntitySpawner();
|
||||
@@ -224,6 +277,7 @@ namespace Barotrauma
|
||||
if (GameMode.Mission != null) Mission.Start(Level.Loaded);
|
||||
|
||||
EventManager.StartRound(level);
|
||||
SteamAchievementManager.OnStartRound();
|
||||
|
||||
if (GameMode != null)
|
||||
{
|
||||
@@ -237,23 +291,44 @@ namespace Barotrauma
|
||||
GameAnalyticsManager.AddDesignEvent("Submarine:" + submarine.Name);
|
||||
GameAnalyticsManager.AddDesignEvent("Level", ToolBox.StringToInt(level.Seed));
|
||||
GameAnalyticsManager.AddProgressionEvent(GameAnalyticsSDK.Net.EGAProgressionStatus.Start,
|
||||
GameMode.Name, (Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
GameMode.Preset.Identifier, (Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
|
||||
|
||||
#if CLIENT
|
||||
if (GameMode is SinglePlayerCampaign) SteamAchievementManager.OnBiomeDiscovered(level.Biome);
|
||||
roundSummary = new RoundSummary(this);
|
||||
|
||||
GameMain.GameScreen.ColorFade(Color.Black, Color.TransparentBlack, 5.0f);
|
||||
SoundPlayer.SwitchMusic();
|
||||
|
||||
if (!(GameMode is TutorialMode))
|
||||
{
|
||||
GUI.AddMessage("", Color.Transparent, 3.0f, playSound: false);
|
||||
GUI.AddMessage(level.Biome.Name, Color.Lerp(Color.CadetBlue, Color.DarkRed, level.Difficulty / 100.0f), 5.0f, playSound: false);
|
||||
GUI.AddMessage(TextManager.Get("Destination") + ": " + EndLocation.Name, Color.CadetBlue, playSound: false);
|
||||
GUI.AddMessage(TextManager.Get("Mission") + ": " + (Mission == null ? TextManager.Get("None") : Mission.Name), Color.CadetBlue, playSound: false);
|
||||
}
|
||||
#endif
|
||||
|
||||
RoundStartTime = Timing.TotalTime;
|
||||
}
|
||||
|
||||
public void Update(float deltaTime)
|
||||
{
|
||||
EventManager.Update(deltaTime);
|
||||
GameMode?.Update(deltaTime);
|
||||
Mission?.Update(deltaTime);
|
||||
|
||||
UpdateProjSpecific(deltaTime);
|
||||
}
|
||||
|
||||
partial void UpdateProjSpecific(float deltaTime);
|
||||
|
||||
public void EndRound(string endMessage)
|
||||
{
|
||||
if (Mission != null) Mission.End();
|
||||
GameAnalyticsManager.AddProgressionEvent(
|
||||
(Mission == null || Mission.Completed) ? GameAnalyticsSDK.Net.EGAProgressionStatus.Complete : GameAnalyticsSDK.Net.EGAProgressionStatus.Fail,
|
||||
GameMode.Name,
|
||||
GameMode.Preset.Identifier,
|
||||
(Mission == null ? "None" : Mission.GetType().ToString()));
|
||||
|
||||
#if CLIENT
|
||||
@@ -261,12 +336,16 @@ namespace Barotrauma
|
||||
{
|
||||
GUIFrame summaryFrame = roundSummary.CreateSummaryFrame(endMessage);
|
||||
GUIMessageBox.MessageBoxes.Add(summaryFrame);
|
||||
var okButton = new GUIButton(new Rectangle(0, 20, 100, 30), "Ok", Alignment.BottomRight, "", summaryFrame.children[0]);
|
||||
okButton.OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; };
|
||||
var okButton = new GUIButton(new RectTransform(new Vector2(0.2f, 1.0f), summaryFrame.Children.First().Children.First().FindChild("buttonarea").RectTransform),
|
||||
TextManager.Get("OK"))
|
||||
{
|
||||
OnClicked = (GUIButton button, object obj) => { GUIMessageBox.MessageBoxes.Remove(summaryFrame); return true; }
|
||||
};
|
||||
}
|
||||
#endif
|
||||
|
||||
EventManager.EndRound();
|
||||
SteamAchievementManager.OnRoundEnded(this);
|
||||
|
||||
currentMission = null;
|
||||
|
||||
@@ -308,9 +387,9 @@ namespace Barotrauma
|
||||
{
|
||||
doc.Save(filePath);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!");
|
||||
DebugConsole.ThrowError("Saving gamesession to \"" + filePath + "\" failed!", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@ using System.Xml.Linq;
|
||||
using System.Collections.Generic;
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using System.Xml;
|
||||
#if CLIENT
|
||||
using Microsoft.Xna.Framework.Graphics;
|
||||
using Barotrauma.Tutorials;
|
||||
#endif
|
||||
using System;
|
||||
|
||||
@@ -15,22 +17,78 @@ namespace Barotrauma
|
||||
Windowed, Fullscreen, BorderlessWindowed
|
||||
}
|
||||
|
||||
public enum LosMode
|
||||
{
|
||||
None,
|
||||
Transparent,
|
||||
Opaque
|
||||
}
|
||||
|
||||
public partial class GameSettings
|
||||
{
|
||||
const string FilePath = "config.xml";
|
||||
|
||||
public int GraphicsWidth { get; set; }
|
||||
public int GraphicsHeight { get; set; }
|
||||
|
||||
public bool VSyncEnabled { get; set; }
|
||||
|
||||
public bool EnableSplashScreen { get; set; }
|
||||
|
||||
public int ParticleLimit { get; set; }
|
||||
|
||||
//public bool FullScreenEnabled { get; set; }
|
||||
public float LightMapScale { get; set; }
|
||||
public bool SpecularityEnabled { get; set; }
|
||||
public bool ChromaticAberrationEnabled { get; set; }
|
||||
|
||||
private KeyOrMouse[] keyMapping;
|
||||
|
||||
private WindowMode windowMode;
|
||||
|
||||
public List<string> jobNamePreferences;
|
||||
private LosMode losMode;
|
||||
|
||||
public List<string> jobPreferences;
|
||||
|
||||
private bool useSteamMatchmaking;
|
||||
private bool requireSteamAuthentication;
|
||||
|
||||
public string QuickStartSubmarineName
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
//steam functionality can be enabled/disabled in debug builds
|
||||
public bool UseSteam;
|
||||
public bool RequireSteamAuthentication
|
||||
{
|
||||
get { return requireSteamAuthentication && UseSteam; }
|
||||
set { requireSteamAuthentication = value; }
|
||||
}
|
||||
public bool UseSteamMatchmaking
|
||||
{
|
||||
get { return useSteamMatchmaking && UseSteam; }
|
||||
set { useSteamMatchmaking = value; }
|
||||
}
|
||||
|
||||
#else
|
||||
//steam functionality determined at compile time
|
||||
public bool UseSteam
|
||||
{
|
||||
get { return Steam.SteamManager.USE_STEAM; }
|
||||
}
|
||||
public bool RequireSteamAuthentication
|
||||
{
|
||||
get { return requireSteamAuthentication && Steam.SteamManager.USE_STEAM; }
|
||||
set { requireSteamAuthentication = value; }
|
||||
}
|
||||
public bool UseSteamMatchmaking
|
||||
{
|
||||
get { return useSteamMatchmaking && Steam.SteamManager.USE_STEAM; }
|
||||
set { useSteamMatchmaking = value; }
|
||||
}
|
||||
#endif
|
||||
|
||||
public WindowMode WindowMode
|
||||
{
|
||||
@@ -38,47 +96,26 @@ namespace Barotrauma
|
||||
set { windowMode = value; }
|
||||
}
|
||||
|
||||
public List<string> JobNamePreferences
|
||||
public List<string> JobPreferences
|
||||
{
|
||||
get { return jobNamePreferences; }
|
||||
set
|
||||
{
|
||||
// Begin saving coroutine. Remove any existing save coroutines if one is running.
|
||||
if (CoroutineManager.IsCoroutineRunning("saveCoroutine")) { CoroutineManager.StopCoroutines("saveCoroutine"); }
|
||||
CoroutineManager.StartCoroutine(ApplyUnsavedChanges(), "saveCoroutine");
|
||||
|
||||
jobNamePreferences = value;
|
||||
}
|
||||
get { return jobPreferences; }
|
||||
set { jobPreferences = value; }
|
||||
}
|
||||
|
||||
private int characterHeadIndex;
|
||||
public int CharacterHeadIndex
|
||||
public int CharacterHeadIndex { get; set; }
|
||||
public int CharacterHairIndex { get; set; }
|
||||
public int CharacterBeardIndex { get; set; }
|
||||
public int CharacterMoustacheIndex { get; set; }
|
||||
public int CharacterFaceAttachmentIndex { get; set; }
|
||||
|
||||
public Gender CharacterGender { get; set; }
|
||||
public Race CharacterRace { get; set; }
|
||||
|
||||
private float aimAssistAmount;
|
||||
public float AimAssistAmount
|
||||
{
|
||||
get { return characterHeadIndex; }
|
||||
set
|
||||
{
|
||||
if (value == characterHeadIndex) return;
|
||||
// Begin saving coroutine. Remove any existing save coroutines if one is running.
|
||||
if (CoroutineManager.IsCoroutineRunning("saveCoroutine")) { CoroutineManager.StopCoroutines("saveCoroutine"); }
|
||||
CoroutineManager.StartCoroutine(ApplyUnsavedChanges(), "saveCoroutine");
|
||||
|
||||
characterHeadIndex = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Gender characterGender;
|
||||
public Gender CharacterGender
|
||||
{
|
||||
get { return characterGender; }
|
||||
set
|
||||
{
|
||||
if (value == characterGender) return;
|
||||
// Begin saving coroutine. Remove any existing save coroutines if one is running.
|
||||
if (CoroutineManager.IsCoroutineRunning("saveCoroutine")) { CoroutineManager.StopCoroutines("saveCoroutine"); }
|
||||
CoroutineManager.StartCoroutine(ApplyUnsavedChanges(), "saveCoroutine");
|
||||
|
||||
characterGender = value;
|
||||
}
|
||||
get { return aimAssistAmount; }
|
||||
set { aimAssistAmount = MathHelper.Clamp(value, 0.0f, 5.0f); }
|
||||
}
|
||||
|
||||
private bool unsavedSettings;
|
||||
@@ -111,7 +148,12 @@ namespace Barotrauma
|
||||
{
|
||||
soundVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
#if CLIENT
|
||||
Sounds.SoundManager.MasterVolume = soundVolume;
|
||||
if (GameMain.SoundManager != null)
|
||||
{
|
||||
GameMain.SoundManager.SetCategoryGainMultiplier("default", soundVolume);
|
||||
GameMain.SoundManager.SetCategoryGainMultiplier("ui", soundVolume);
|
||||
GameMain.SoundManager.SetCategoryGainMultiplier("waterambience", soundVolume);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -123,19 +165,25 @@ namespace Barotrauma
|
||||
{
|
||||
musicVolume = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
#if CLIENT
|
||||
SoundPlayer.MusicVolume = musicVolume;
|
||||
GameMain.SoundManager?.SetCategoryGainMultiplier("music", musicVolume);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
public ContentPackage SelectedContentPackage { get; set; }
|
||||
public string Language
|
||||
{
|
||||
get { return TextManager.Language; }
|
||||
set { TextManager.Language = value; }
|
||||
}
|
||||
|
||||
public HashSet<ContentPackage> SelectedContentPackages { get; set; }
|
||||
|
||||
public string MasterServerUrl { get; set; }
|
||||
public bool AutoCheckUpdates { get; set; }
|
||||
public bool WasGameUpdated { get; set; }
|
||||
|
||||
private string defaultPlayerName;
|
||||
public string DefaultPlayerName
|
||||
public string DefaultPlayerName
|
||||
{
|
||||
get
|
||||
{
|
||||
@@ -146,11 +194,23 @@ namespace Barotrauma
|
||||
if (defaultPlayerName != value)
|
||||
{
|
||||
defaultPlayerName = value;
|
||||
Save("config.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public LosMode LosMode
|
||||
{
|
||||
get { return losMode; }
|
||||
set { losMode = value; }
|
||||
}
|
||||
|
||||
private const float MinHUDScale = 0.75f, MaxHUDScale = 1.25f;
|
||||
public static float HUDScale { get; set; } = 1.0f;
|
||||
private const float MinInventoryScale = 0.75f, MaxInventoryScale = 1.25f;
|
||||
public static float InventoryScale { get; set; } = 1.0f;
|
||||
|
||||
public List<string> CompletedTutorialNames { get; private set; }
|
||||
|
||||
public static bool VerboseLogging { get; set; }
|
||||
public static bool SaveDebugConsoleLogs { get; set; }
|
||||
|
||||
@@ -161,15 +221,17 @@ namespace Barotrauma
|
||||
set
|
||||
{
|
||||
sendUserStatistics = value;
|
||||
GameMain.Config.Save("config.xml");
|
||||
GameMain.Config.Save();
|
||||
}
|
||||
}
|
||||
public static bool ShowUserStatisticsPrompt { get; set; }
|
||||
|
||||
public GameSettings(string filePath)
|
||||
{
|
||||
ContentPackage.LoadAll(ContentPackage.Folder);
|
||||
SelectedContentPackages = new HashSet<ContentPackage>();
|
||||
|
||||
ContentPackage.LoadAll(ContentPackage.Folder);
|
||||
CompletedTutorialNames = new List<string>();
|
||||
Load(filePath);
|
||||
}
|
||||
|
||||
@@ -177,6 +239,8 @@ namespace Barotrauma
|
||||
{
|
||||
XDocument doc = XMLExtensions.TryLoadXml(filePath);
|
||||
|
||||
Language = doc.Root.GetAttributeString("language", "English");
|
||||
|
||||
MasterServerUrl = doc.Root.GetAttributeString("masterserverurl", "");
|
||||
|
||||
AutoCheckUpdates = doc.Root.GetAttributeBool("autocheckupdates", true);
|
||||
@@ -193,6 +257,11 @@ namespace Barotrauma
|
||||
sendUserStatistics = doc.Root.GetAttributeBool("senduserstatistics", true);
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
UseSteam = doc.Root.GetAttributeBool("usesteam", true);
|
||||
#endif
|
||||
QuickStartSubmarineName = doc.Root.GetAttributeString("quickstartsub", "");
|
||||
|
||||
if (doc == null)
|
||||
{
|
||||
GraphicsWidth = 1024;
|
||||
@@ -200,20 +269,33 @@ namespace Barotrauma
|
||||
|
||||
MasterServerUrl = "";
|
||||
|
||||
SelectedContentPackage = ContentPackage.list.Any() ? ContentPackage.list[0] : new ContentPackage("");
|
||||
SelectedContentPackages.Add(ContentPackage.List.Any() ? ContentPackage.List[0] : new ContentPackage(""));
|
||||
|
||||
JobNamePreferences = new List<string>();
|
||||
jobPreferences = new List<string>();
|
||||
foreach (JobPrefab job in JobPrefab.List)
|
||||
{
|
||||
JobNamePreferences.Add(job.Name);
|
||||
jobPreferences.Add(job.Identifier);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
XElement graphicsMode = doc.Root.Element("graphicsmode");
|
||||
GraphicsWidth = graphicsMode.GetAttributeInt("width", 0);
|
||||
GraphicsHeight = graphicsMode.GetAttributeInt("height", 0);
|
||||
VSyncEnabled = graphicsMode.GetAttributeBool("vsync", true);
|
||||
GraphicsWidth = graphicsMode.GetAttributeInt("width", 0);
|
||||
GraphicsHeight = graphicsMode.GetAttributeInt("height", 0);
|
||||
VSyncEnabled = graphicsMode.GetAttributeBool("vsync", true);
|
||||
|
||||
XElement graphicsSettings = doc.Root.Element("graphicssettings");
|
||||
ParticleLimit = graphicsSettings.GetAttributeInt("particlelimit", 1500);
|
||||
LightMapScale = MathHelper.Clamp(graphicsSettings.GetAttributeFloat("lightmapscale", 0.5f), 0.1f, 1.0f);
|
||||
SpecularityEnabled = graphicsSettings.GetAttributeBool("specularity", true);
|
||||
ChromaticAberrationEnabled = graphicsSettings.GetAttributeBool("chromaticaberration", true);
|
||||
HUDScale = graphicsSettings.GetAttributeFloat("hudscale", 1.0f);
|
||||
InventoryScale = graphicsSettings.GetAttributeFloat("inventoryscale", 1.0f);
|
||||
var losModeStr = graphicsSettings.GetAttributeString("losmode", "Transparent");
|
||||
if (!Enum.TryParse(losModeStr, out losMode))
|
||||
{
|
||||
losMode = LosMode.Transparent;
|
||||
}
|
||||
|
||||
#if CLIENT
|
||||
if (GraphicsWidth == 0 || GraphicsHeight == 0)
|
||||
@@ -234,8 +316,13 @@ namespace Barotrauma
|
||||
SoundVolume = doc.Root.GetAttributeFloat("soundvolume", 1.0f);
|
||||
MusicVolume = doc.Root.GetAttributeFloat("musicvolume", 0.3f);
|
||||
|
||||
useSteamMatchmaking = doc.Root.GetAttributeBool("usesteammatchmaking", true);
|
||||
requireSteamAuthentication = doc.Root.GetAttributeBool("requiresteamauthentication", true);
|
||||
|
||||
EnableSplashScreen = doc.Root.GetAttributeBool("enablesplashscreen", true);
|
||||
|
||||
AimAssistAmount = doc.Root.GetAttributeFloat("aimassistamount", 0.5f);
|
||||
|
||||
keyMapping = new KeyOrMouse[Enum.GetNames(typeof(InputType)).Length];
|
||||
keyMapping[(int)InputType.Up] = new KeyOrMouse(Keys.W);
|
||||
keyMapping[(int)InputType.Down] = new KeyOrMouse(Keys.S);
|
||||
@@ -249,6 +336,9 @@ namespace Barotrauma
|
||||
|
||||
keyMapping[(int)InputType.Select] = new KeyOrMouse(Keys.E);
|
||||
|
||||
keyMapping[(int)InputType.SelectNextCharacter] = new KeyOrMouse(Keys.Tab);
|
||||
keyMapping[(int)InputType.SelectPreviousCharacter] = new KeyOrMouse(Keys.Q);
|
||||
|
||||
keyMapping[(int)InputType.Use] = new KeyOrMouse(0);
|
||||
keyMapping[(int)InputType.Aim] = new KeyOrMouse(1);
|
||||
|
||||
@@ -276,17 +366,33 @@ namespace Barotrauma
|
||||
}
|
||||
break;
|
||||
case "gameplay":
|
||||
JobNamePreferences = new List<string>();
|
||||
jobPreferences = new List<string>();
|
||||
foreach (XElement ele in subElement.Element("jobpreferences").Elements("job"))
|
||||
{
|
||||
JobNamePreferences.Add(ele.GetAttributeString("name", ""));
|
||||
string jobIdentifier = ele.GetAttributeString("identifier", "");
|
||||
if (string.IsNullOrEmpty(jobIdentifier)) continue;
|
||||
jobPreferences.Add(jobIdentifier);
|
||||
}
|
||||
break;
|
||||
case "player":
|
||||
defaultPlayerName = subElement.GetAttributeString("name", "");
|
||||
characterHeadIndex = subElement.GetAttributeInt("headindex", Rand.Int(10));
|
||||
characterGender = subElement.GetAttributeString("gender", Rand.Range(0.0f, 1.0f) < 0.5f ? "male" : "female")
|
||||
CharacterHeadIndex = subElement.GetAttributeInt("headindex", Rand.Int(10));
|
||||
CharacterGender = subElement.GetAttributeString("gender", Rand.Range(0.0f, 1.0f) < 0.5f ? "male" : "female")
|
||||
.ToLowerInvariant() == "male" ? Gender.Male : Gender.Female;
|
||||
if (Enum.TryParse(subElement.GetAttributeString("race", "white"), true, out Race r))
|
||||
{
|
||||
CharacterRace = r;
|
||||
}
|
||||
CharacterHairIndex = subElement.GetAttributeInt("hairindex", -1);
|
||||
CharacterBeardIndex = subElement.GetAttributeInt("beardindex", -1);
|
||||
CharacterMoustacheIndex = subElement.GetAttributeInt("moustacheindex", -1);
|
||||
CharacterFaceAttachmentIndex = subElement.GetAttributeInt("faceattachmentindex", -1);
|
||||
break;
|
||||
case "tutorials":
|
||||
foreach (XElement tutorialElement in subElement.Elements())
|
||||
{
|
||||
CompletedTutorialNames.Add(tutorialElement.GetAttributeString("name", ""));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -302,21 +408,68 @@ namespace Barotrauma
|
||||
|
||||
UnsavedSettings = false;
|
||||
|
||||
bool invalidPackagesFound = false;
|
||||
foreach (XElement subElement in doc.Root.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "contentpackage":
|
||||
string path = subElement.GetAttributeString("path", "");
|
||||
|
||||
SelectedContentPackage = ContentPackage.list.Find(cp => cp.Path == path);
|
||||
if (SelectedContentPackage == null) SelectedContentPackage = new ContentPackage(path);
|
||||
string path = System.IO.Path.GetFullPath(subElement.GetAttributeString("path", ""));
|
||||
var matchingContentPackage = ContentPackage.List.Find(cp => System.IO.Path.GetFullPath(cp.Path) == path);
|
||||
if (matchingContentPackage == null)
|
||||
{
|
||||
DebugConsole.ThrowError(TextManager.Get("ContentPackageNotFound").Replace("[packagepath]", path), createMessageBox: true);
|
||||
}
|
||||
else if (!matchingContentPackage.IsCompatible())
|
||||
{
|
||||
invalidPackagesFound = true;
|
||||
DebugConsole.ThrowError(
|
||||
TextManager.Get(matchingContentPackage.GameVersion <= new Version(0, 0, 0, 0) ? "IncompatibleContentPackageUnknownVersion" : "IncompatibleContentPackage")
|
||||
.Replace("[packagename]", matchingContentPackage.Name)
|
||||
.Replace("[packageversion]", matchingContentPackage.GameVersion.ToString())
|
||||
.Replace("[gameversion]", GameMain.Version.ToString()),
|
||||
createMessageBox: true);
|
||||
}
|
||||
else
|
||||
{
|
||||
invalidPackagesFound = true;
|
||||
SelectedContentPackages.Add(matchingContentPackage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
{
|
||||
foreach (ContentFile file in contentPackage.Files)
|
||||
{
|
||||
if (!System.IO.File.Exists(file.Path))
|
||||
{
|
||||
DebugConsole.ThrowError("Error in content package \"" + contentPackage.Name + "\" - file \"" + file.Path + "\" not found.");
|
||||
continue;
|
||||
}
|
||||
ToolBox.IsProperFilenameCase(file.Path);
|
||||
}
|
||||
}
|
||||
if (!SelectedContentPackages.Any())
|
||||
{
|
||||
var availablePackage = ContentPackage.List.FirstOrDefault(cp => cp.IsCompatible() && cp.CorePackage);
|
||||
if (availablePackage != null)
|
||||
{
|
||||
SelectedContentPackages.Add(availablePackage);
|
||||
}
|
||||
}
|
||||
|
||||
//save to get rid of the invalid selected packages in the config file
|
||||
if (invalidPackagesFound) { Save(); }
|
||||
}
|
||||
|
||||
public void Save(string filePath)
|
||||
public KeyOrMouse KeyBind(InputType inputType)
|
||||
{
|
||||
return keyMapping[(int)inputType];
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
UnsavedSettings = false;
|
||||
|
||||
@@ -328,13 +481,18 @@ namespace Barotrauma
|
||||
}
|
||||
|
||||
doc.Root.Add(
|
||||
new XAttribute("language", TextManager.Language),
|
||||
new XAttribute("masterserverurl", MasterServerUrl),
|
||||
new XAttribute("autocheckupdates", AutoCheckUpdates),
|
||||
new XAttribute("musicvolume", musicVolume),
|
||||
new XAttribute("soundvolume", soundVolume),
|
||||
new XAttribute("verboselogging", VerboseLogging),
|
||||
new XAttribute("savedebugconsolelogs", SaveDebugConsoleLogs),
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen));
|
||||
new XAttribute("enablesplashscreen", EnableSplashScreen),
|
||||
new XAttribute("usesteammatchmaking", useSteamMatchmaking),
|
||||
new XAttribute("quickstartsub", QuickStartSubmarineName),
|
||||
new XAttribute("requiresteamauthentication", requireSteamAuthentication),
|
||||
new XAttribute("aimassistamount", aimAssistAmount));
|
||||
|
||||
if (!ShowUserStatisticsPrompt)
|
||||
{
|
||||
@@ -366,11 +524,26 @@ namespace Barotrauma
|
||||
new XAttribute("displaymode", windowMode));
|
||||
}
|
||||
|
||||
XElement gSettings = doc.Root.Element("graphicssettings");
|
||||
if (gSettings == null)
|
||||
{
|
||||
gSettings = new XElement("graphicssettings");
|
||||
doc.Root.Add(gSettings);
|
||||
}
|
||||
|
||||
if (SelectedContentPackage != null)
|
||||
gSettings.ReplaceAttributes(
|
||||
new XAttribute("particlelimit", ParticleLimit),
|
||||
new XAttribute("lightmapscale", LightMapScale),
|
||||
new XAttribute("specularity", SpecularityEnabled),
|
||||
new XAttribute("chromaticaberration", ChromaticAberrationEnabled),
|
||||
new XAttribute("losmode", LosMode),
|
||||
new XAttribute("hudscale", HUDScale),
|
||||
new XAttribute("inventoryscale", InventoryScale));
|
||||
|
||||
foreach (ContentPackage contentPackage in SelectedContentPackages)
|
||||
{
|
||||
doc.Root.Add(new XElement("contentpackage",
|
||||
new XAttribute("path", SelectedContentPackage.Path)));
|
||||
new XAttribute("path", contentPackage.Path)));
|
||||
}
|
||||
|
||||
var keyMappingElement = new XElement("keymapping");
|
||||
@@ -389,22 +562,57 @@ namespace Barotrauma
|
||||
|
||||
var gameplay = new XElement("gameplay");
|
||||
var jobPreferences = new XElement("jobpreferences");
|
||||
foreach (string jobName in JobNamePreferences)
|
||||
foreach (string jobName in JobPreferences)
|
||||
{
|
||||
jobPreferences.Add(new XElement("job", new XAttribute("name", jobName)));
|
||||
jobPreferences.Add(new XElement("job", new XAttribute("identifier", jobName)));
|
||||
}
|
||||
gameplay.Add(jobPreferences);
|
||||
doc.Root.Add(gameplay);
|
||||
|
||||
var playerElement = new XElement("player",
|
||||
new XAttribute("name", defaultPlayerName ?? ""),
|
||||
new XAttribute("headindex", characterHeadIndex),
|
||||
new XAttribute("gender", characterGender));
|
||||
new XAttribute("headindex", CharacterHeadIndex),
|
||||
new XAttribute("gender", CharacterGender),
|
||||
new XAttribute("race", CharacterRace),
|
||||
new XAttribute("hairindex", CharacterHairIndex),
|
||||
new XAttribute("beardindex", CharacterBeardIndex),
|
||||
new XAttribute("moustacheindex", CharacterMoustacheIndex),
|
||||
new XAttribute("faceattachmentindex", CharacterFaceAttachmentIndex));
|
||||
doc.Root.Add(playerElement);
|
||||
|
||||
#if CLIENT
|
||||
if (Tutorial.Tutorials != null)
|
||||
{
|
||||
foreach (Tutorial tutorial in Tutorial.Tutorials)
|
||||
{
|
||||
if (tutorial.Completed && !CompletedTutorialNames.Contains(tutorial.Name))
|
||||
{
|
||||
CompletedTutorialNames.Add(tutorial.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
var tutorialElement = new XElement("tutorials");
|
||||
foreach (string tutorialName in CompletedTutorialNames)
|
||||
{
|
||||
tutorialElement.Add(new XElement("Tutorial", new XAttribute("name", tutorialName)));
|
||||
}
|
||||
doc.Root.Add(tutorialElement);
|
||||
|
||||
XmlWriterSettings settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
OmitXmlDeclaration = true,
|
||||
NewLineOnAttributes = true
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
doc.Save(filePath);
|
||||
using (var writer = XmlWriter.Create(FilePath, settings))
|
||||
{
|
||||
doc.WriteTo(writer);
|
||||
writer.Flush();
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -413,12 +621,5 @@ namespace Barotrauma
|
||||
"Saving game settings failed.\n" + e.Message + "\n" + e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<object> ApplyUnsavedChanges()
|
||||
{
|
||||
yield return new WaitForSeconds(10.0f);
|
||||
|
||||
Save("config.xml");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,88 +4,110 @@ using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma
|
||||
{
|
||||
[Flags]
|
||||
public enum InvSlotType
|
||||
{
|
||||
None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, Torso = 16, Legs = 32, Face=64, Card=128
|
||||
None = 0, Any = 1, RightHand = 2, LeftHand = 4, Head = 8, InnerClothes = 16, OuterClothes = 32, Headset = 64, Card = 128
|
||||
};
|
||||
|
||||
partial class CharacterInventory : Inventory
|
||||
{
|
||||
private Character character;
|
||||
|
||||
public static InvSlotType[] limbSlots = new InvSlotType[] {
|
||||
InvSlotType.Head, InvSlotType.Torso, InvSlotType.Legs, InvSlotType.LeftHand, InvSlotType.RightHand, InvSlotType.Face, InvSlotType.Card,
|
||||
InvSlotType.Any, InvSlotType.Any, InvSlotType.Any, InvSlotType.Any, InvSlotType.Any,
|
||||
InvSlotType.Any, InvSlotType.Any, InvSlotType.Any, InvSlotType.Any, InvSlotType.Any};
|
||||
|
||||
public CharacterInventory(int capacity, Character character)
|
||||
: base(character, capacity)
|
||||
public InvSlotType[] SlotTypes
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
protected bool[] IsEquipped;
|
||||
|
||||
public bool AccessibleWhenAlive
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public CharacterInventory(XElement element, Character character)
|
||||
: base(character, element.GetAttributeString("slots", "").Split(',').Count())
|
||||
{
|
||||
this.character = character;
|
||||
IsEquipped = new bool[capacity];
|
||||
SlotTypes = new InvSlotType[capacity];
|
||||
|
||||
InitProjSpecific();
|
||||
}
|
||||
AccessibleWhenAlive = element.GetAttributeBool("accessiblewhenalive", true);
|
||||
|
||||
partial void InitProjSpecific();
|
||||
string[] slotTypeNames = element.GetAttributeString("slots", "").Split(',');
|
||||
System.Diagnostics.Debug.Assert(slotTypeNames.Length == capacity);
|
||||
|
||||
private bool UseItemOnSelf(int slotIndex)
|
||||
{
|
||||
if (Items[slotIndex] == null) return false;
|
||||
|
||||
#if CLIENT
|
||||
if (GameMain.Client != null)
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
GameMain.Client.CreateEntityEvent(Items[slotIndex], new object[] { NetEntityEvent.Type.ApplyStatusEffect });
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
GameMain.Server.CreateEntityEvent(Items[slotIndex], new object[] { NetEntityEvent.Type.ApplyStatusEffect, ActionType.OnUse, character.ID });
|
||||
}
|
||||
|
||||
Items[slotIndex].ApplyStatusEffects(ActionType.OnUse, 1.0f, character);
|
||||
|
||||
//item may have been removed by a status effect
|
||||
if (Items[slotIndex] == null) return true;
|
||||
|
||||
foreach (ItemComponent ic in Items[slotIndex].components)
|
||||
{
|
||||
if (ic.DeleteOnUse)
|
||||
InvSlotType parsedSlotType = InvSlotType.Any;
|
||||
slotTypeNames[i] = slotTypeNames[i].Trim();
|
||||
if (!Enum.TryParse(slotTypeNames[i], out parsedSlotType))
|
||||
{
|
||||
Entity.Spawner.AddToRemoveQueue(Items[slotIndex]);
|
||||
DebugConsole.ThrowError("Error in the inventory config of \"" + character.SpeciesName + "\" - " + slotTypeNames[i] + " is not a valid inventory slot type.");
|
||||
}
|
||||
SlotTypes[i] = parsedSlotType;
|
||||
switch (SlotTypes[i])
|
||||
{
|
||||
//case InvSlotType.Head:
|
||||
case InvSlotType.OuterClothes:
|
||||
case InvSlotType.LeftHand:
|
||||
case InvSlotType.RightHand:
|
||||
hideEmptySlot[i] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
InitProjSpecific(element);
|
||||
|
||||
//clients don't create items until the server says so
|
||||
if (GameMain.Client != null) return;
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
if (subElement.Name.ToString().ToLowerInvariant() != "item") continue;
|
||||
|
||||
string itemIdentifier = subElement.GetAttributeString("identifier", "");
|
||||
ItemPrefab itemPrefab = MapEntityPrefab.Find(null, itemIdentifier) as ItemPrefab;
|
||||
if (itemPrefab == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error in character inventory \"" + character.SpeciesName + "\" - item \"" + itemIdentifier + "\" not found.");
|
||||
continue;
|
||||
}
|
||||
|
||||
Entity.Spawner?.AddToSpawnQueue(itemPrefab, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
partial void InitProjSpecific(XElement element);
|
||||
|
||||
public int FindLimbSlot(InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
if (limbSlots[i] == limbSlot) return i;
|
||||
if (SlotTypes[i] == limbSlot) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public bool IsInLimbSlot(Item item, InvSlotType limbSlot)
|
||||
{
|
||||
for (int i = 0; i<Items.Length; i++)
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
if (Items[i] == item && limbSlots[i] == limbSlot) return true;
|
||||
if (Items[i] == item && SlotTypes[i] == limbSlot) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public override bool CanBePut(Item item, int i)
|
||||
{
|
||||
return base.CanBePut(item, i) && item.AllowedSlots.Contains(limbSlots[i]);
|
||||
return base.CanBePut(item, i) && item.AllowedSlots.Contains(SlotTypes[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -95,21 +117,47 @@ namespace Barotrauma
|
||||
{
|
||||
if (allowedSlots == null || !allowedSlots.Any()) return false;
|
||||
|
||||
bool inSuitableSlot = false;
|
||||
bool inWrongSlot = false;
|
||||
int currentSlot = -1;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
//already in the inventory and in a suitable slot
|
||||
if (Items[i] == item && allowedSlots.Any(a => a.HasFlag(limbSlots[i])))
|
||||
if (Items[i] == item)
|
||||
{
|
||||
return true;
|
||||
currentSlot = i;
|
||||
if (allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
|
||||
inSuitableSlot = true;
|
||||
else if (!allowedSlots.Any(a => a.HasFlag(SlotTypes[i])))
|
||||
inWrongSlot = true;
|
||||
}
|
||||
}
|
||||
//all good
|
||||
if (inSuitableSlot && !inWrongSlot) return true;
|
||||
|
||||
//try to place the item in LimBlot.Any slot if that's allowed
|
||||
//try to place the item in a LimbSlot.Any slot if that's allowed
|
||||
if (allowedSlots.Contains(InvSlotType.Any))
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] != null || limbSlots[i] != InvSlotType.Any) continue;
|
||||
if (SlotTypes[i] != InvSlotType.Any) continue;
|
||||
if (Items[i] == item)
|
||||
{
|
||||
PutItem(item, i, user, true, createNetworkEvent);
|
||||
item.Unequip(character);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] != InvSlotType.Any) continue;
|
||||
if (inWrongSlot)
|
||||
{
|
||||
if (Items[i] != item && Items[i] != null) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Items[i] != null) continue;
|
||||
}
|
||||
|
||||
PutItem(item, i, user, true, createNetworkEvent);
|
||||
item.Unequip(character);
|
||||
@@ -117,18 +165,21 @@ namespace Barotrauma
|
||||
}
|
||||
}
|
||||
|
||||
bool placed = false;
|
||||
int placedInSlot = -1;
|
||||
foreach (InvSlotType allowedSlot in allowedSlots)
|
||||
{
|
||||
//check if all the required slots are free
|
||||
bool free = true;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(limbSlots[i]) && Items[i] != null && Items[i] != item)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] != null && Items[i] != item)
|
||||
{
|
||||
free = false;
|
||||
#if CLIENT
|
||||
if (slots != null) slots[i].ShowBorderHighlight(Color.Red, 0.1f, 0.9f);
|
||||
for (int j = 0; j < capacity; j++)
|
||||
{
|
||||
if (slots != null && Items[j] == Items[i]) slots[j].ShowBorderHighlight(Color.Red, 0.1f, 0.9f);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -137,97 +188,71 @@ namespace Barotrauma
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(limbSlots[i]) && Items[i] == null)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] == null)
|
||||
{
|
||||
PutItem(item, i, user, !placed, createNetworkEvent);
|
||||
bool removeFromOtherSlots = item.ParentInventory != this;
|
||||
if (placedInSlot == -1 && inWrongSlot)
|
||||
{
|
||||
if (!hideEmptySlot[i] || SlotTypes[currentSlot] != InvSlotType.Any) removeFromOtherSlots = true;
|
||||
}
|
||||
|
||||
PutItem(item, i, user, removeFromOtherSlots, createNetworkEvent);
|
||||
item.Equip(character);
|
||||
placed = true;
|
||||
placedInSlot = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (placed)
|
||||
if (placedInSlot > -1)
|
||||
{
|
||||
if (item.AllowedSlots.Contains(InvSlotType.Any) && hideEmptySlot[placedInSlot])
|
||||
{
|
||||
bool isInAnySlot = false;
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] == InvSlotType.Any && Items[i]==item)
|
||||
{
|
||||
isInAnySlot = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!isInAnySlot)
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (SlotTypes[i] == InvSlotType.Any && Items[i] == null)
|
||||
{
|
||||
Items[i] = item;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return placed;
|
||||
return placedInSlot > -1;
|
||||
}
|
||||
|
||||
public override bool TryPutItem(Item item, int index, bool allowSwapping, bool allowCombine, Character user, bool createNetworkEvent = true)
|
||||
{
|
||||
if (index < 0 || index >= Items.Length)
|
||||
{
|
||||
string errorMsg = "CharacterInventory.TryPutItem failed: index was out of range(" + index + ").\n" + Environment.StackTrace;
|
||||
GameAnalyticsManager.AddErrorEventOnce("CharacterInventory.TryPutItem:IndexOutOfRange", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
||||
return false;
|
||||
}
|
||||
|
||||
//there's already an item in the slot
|
||||
if (Items[index] != null)
|
||||
{
|
||||
if (Items[index] == item) return false;
|
||||
|
||||
bool combined = false;
|
||||
if (allowCombine && Items[index].Combine(item))
|
||||
{
|
||||
System.Diagnostics.Debug.Assert(Items[index] != null);
|
||||
|
||||
Inventory otherInventory = Items[index].ParentInventory;
|
||||
if (otherInventory != null && otherInventory.Owner!=null)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
combined = true;
|
||||
}
|
||||
//if moving the item between slots in the same inventory
|
||||
else if (item.ParentInventory == this && allowSwapping)
|
||||
{
|
||||
int currentIndex = Array.IndexOf(Items, item);
|
||||
|
||||
Item existingItem = Items[index];
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] == item || Items[i] == existingItem) Items[i] = null;
|
||||
}
|
||||
|
||||
//if the item in the slot can be moved to the slot of the moved item
|
||||
if (TryPutItem(existingItem, currentIndex, false, false, user, createNetworkEvent) &&
|
||||
TryPutItem(item, index, false, false, user, createNetworkEvent))
|
||||
{
|
||||
#if CLIENT
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] == item || Items[i] == existingItem)
|
||||
{
|
||||
slots[i].ShowBorderHighlight(Color.Green, 0.1f, 0.9f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] == item || Items[i] == existingItem) Items[i] = null;
|
||||
}
|
||||
|
||||
//swapping the items failed -> move them back to where they were
|
||||
TryPutItem(item, currentIndex, false, false, user, createNetworkEvent);
|
||||
TryPutItem(existingItem, index, false, false, user, createNetworkEvent);
|
||||
#if CLIENT
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (Items[i] == existingItem)
|
||||
{
|
||||
slots[i].ShowBorderHighlight(Color.Red, 0.1f, 0.9f);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return combined;
|
||||
return base.TryPutItem(item, index, allowSwapping, allowCombine, user, createNetworkEvent);
|
||||
}
|
||||
|
||||
if (limbSlots[index] == InvSlotType.Any)
|
||||
if (SlotTypes[index] == InvSlotType.Any)
|
||||
{
|
||||
if (!item.AllowedSlots.Contains(InvSlotType.Any)) return false;
|
||||
if (Items[index] != null) return Items[index] == item;
|
||||
@@ -242,11 +267,11 @@ namespace Barotrauma
|
||||
List<InvSlotType> allowedSlots = item.AllowedSlots;
|
||||
foreach (InvSlotType allowedSlot in allowedSlots)
|
||||
{
|
||||
if (!allowedSlot.HasFlag(limbSlots[index])) continue;
|
||||
if (!allowedSlot.HasFlag(SlotTypes[index])) continue;
|
||||
|
||||
for (int i = 0; i < capacity; i++)
|
||||
{
|
||||
if (allowedSlot.HasFlag(limbSlots[i]) && Items[i] != null && Items[i] != item)
|
||||
if (allowedSlot.HasFlag(SlotTypes[i]) && Items[i] != null && Items[i] != item)
|
||||
{
|
||||
slotsFree = false;
|
||||
break;
|
||||
|
||||
@@ -14,17 +14,14 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class DockingPort : ItemComponent, IDrawableComponent, IServerSerializable
|
||||
{
|
||||
public static List<DockingPort> list = new List<DockingPort>();
|
||||
|
||||
private static List<DockingPort> list = new List<DockingPort>();
|
||||
public static IEnumerable<DockingPort> List
|
||||
{
|
||||
get { return list; }
|
||||
}
|
||||
|
||||
private Sprite overlaySprite;
|
||||
|
||||
private Vector2 distanceTolerance;
|
||||
|
||||
private DockingPort dockingTarget;
|
||||
|
||||
private float dockingState;
|
||||
private int dockingDir;
|
||||
|
||||
private Joint joint;
|
||||
|
||||
private readonly Hull[] hulls = new Hull[2];
|
||||
@@ -37,18 +34,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool docked;
|
||||
|
||||
public int DockingDir
|
||||
{
|
||||
get { return dockingDir; }
|
||||
set { dockingDir = value; }
|
||||
}
|
||||
private float forceLockTimer;
|
||||
//if the submarine isn't in the correct position to lock within this time after docking has been activated,
|
||||
//force the sub to the correct position
|
||||
const float ForceLockDelay = 1.0f;
|
||||
|
||||
public int DockingDir { get; private set; }
|
||||
|
||||
[Serialize("32.0,32.0", false)]
|
||||
public Vector2 DistanceTolerance
|
||||
{
|
||||
get { return distanceTolerance; }
|
||||
set { distanceTolerance = value; }
|
||||
}
|
||||
public Vector2 DistanceTolerance { get; set; }
|
||||
|
||||
[Serialize(32.0f, false)]
|
||||
public float DockedDistance
|
||||
@@ -64,11 +58,7 @@ namespace Barotrauma.Items.Components
|
||||
set;
|
||||
}
|
||||
|
||||
public DockingPort DockingTarget
|
||||
{
|
||||
get { return dockingTarget; }
|
||||
set { dockingTarget = value; }
|
||||
}
|
||||
public DockingPort DockingTarget { get; private set; }
|
||||
|
||||
public bool Docked
|
||||
{
|
||||
@@ -80,8 +70,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!docked && value)
|
||||
{
|
||||
if (dockingTarget == null) AttemptDock();
|
||||
if (dockingTarget == null) return;
|
||||
if (DockingTarget == null) AttemptDock();
|
||||
if (DockingTarget == null) return;
|
||||
|
||||
docked = true;
|
||||
}
|
||||
@@ -89,11 +79,9 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
Undock();
|
||||
}
|
||||
|
||||
//base.IsActive = value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public DockingPort(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
@@ -114,37 +102,40 @@ namespace Barotrauma.Items.Components
|
||||
list.Add(this);
|
||||
}
|
||||
|
||||
public override void FlipX()
|
||||
public override void FlipX(bool relativeToSub)
|
||||
{
|
||||
base.FlipX();
|
||||
|
||||
if (dockingTarget != null)
|
||||
if (DockingTarget != null)
|
||||
{
|
||||
if (joint != null)
|
||||
{
|
||||
CreateJoint(joint is WeldJoint);
|
||||
LinkHullsToGaps();
|
||||
}
|
||||
else if (dockingTarget.joint != null)
|
||||
else if (DockingTarget.joint != null)
|
||||
{
|
||||
if (!GameMain.World.BodyList.Contains(dockingTarget.joint.BodyA) ||
|
||||
!GameMain.World.BodyList.Contains(dockingTarget.joint.BodyB))
|
||||
if (!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyA) ||
|
||||
!GameMain.World.BodyList.Contains(DockingTarget.joint.BodyB))
|
||||
{
|
||||
dockingTarget.CreateJoint(dockingTarget.joint is WeldJoint);
|
||||
DockingTarget.CreateJoint(DockingTarget.joint is WeldJoint);
|
||||
}
|
||||
dockingTarget.LinkHullsToGaps();
|
||||
DockingTarget.LinkHullsToGaps();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void FlipY(bool relativeToSub)
|
||||
{
|
||||
FlipX(relativeToSub);
|
||||
}
|
||||
|
||||
private DockingPort FindAdjacentPort()
|
||||
{
|
||||
foreach (DockingPort port in list)
|
||||
{
|
||||
if (port == this || port.item.Submarine == item.Submarine) continue;
|
||||
|
||||
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > distanceTolerance.X) continue;
|
||||
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > distanceTolerance.Y) continue;
|
||||
if (Math.Abs(port.item.WorldPosition.X - item.WorldPosition.X) > DistanceTolerance.X) continue;
|
||||
if (Math.Abs(port.item.WorldPosition.Y - item.WorldPosition.Y) > DistanceTolerance.Y) continue;
|
||||
|
||||
return port;
|
||||
}
|
||||
@@ -162,8 +153,10 @@ namespace Barotrauma.Items.Components
|
||||
public void Dock(DockingPort target)
|
||||
{
|
||||
if (item.Submarine.DockedTo.Contains(target.item.Submarine)) return;
|
||||
|
||||
if (dockingTarget != null)
|
||||
|
||||
forceLockTimer = 0.0f;
|
||||
|
||||
if (DockingTarget != null)
|
||||
{
|
||||
Undock();
|
||||
}
|
||||
@@ -171,7 +164,7 @@ namespace Barotrauma.Items.Components
|
||||
if (target.item.Submarine == item.Submarine)
|
||||
{
|
||||
DebugConsole.ThrowError("Error - tried to dock a submarine to itself");
|
||||
dockingTarget = null;
|
||||
DockingTarget = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,27 +178,27 @@ namespace Barotrauma.Items.Components
|
||||
if (!target.item.Submarine.DockedTo.Contains(item.Submarine)) target.item.Submarine.DockedTo.Add(item.Submarine);
|
||||
if (!item.Submarine.DockedTo.Contains(target.item.Submarine)) item.Submarine.DockedTo.Add(target.item.Submarine);
|
||||
|
||||
dockingTarget = target;
|
||||
dockingTarget.dockingTarget = this;
|
||||
DockingTarget = target;
|
||||
DockingTarget.DockingTarget = this;
|
||||
|
||||
docked = true;
|
||||
dockingTarget.Docked = true;
|
||||
DockingTarget.Docked = true;
|
||||
|
||||
if (Character.Controlled != null &&
|
||||
(Character.Controlled.Submarine == dockingTarget.item.Submarine || Character.Controlled.Submarine == item.Submarine))
|
||||
(Character.Controlled.Submarine == DockingTarget.item.Submarine || Character.Controlled.Submarine == item.Submarine))
|
||||
{
|
||||
GameMain.GameScreen.Cam.Shake = Vector2.Distance(dockingTarget.item.Submarine.Velocity, item.Submarine.Velocity);
|
||||
GameMain.GameScreen.Cam.Shake = Vector2.Distance(DockingTarget.item.Submarine.Velocity, item.Submarine.Velocity);
|
||||
}
|
||||
|
||||
dockingDir = IsHorizontal ?
|
||||
Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
|
||||
Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
dockingTarget.dockingDir = -dockingDir;
|
||||
DockingDir = IsHorizontal ?
|
||||
Math.Sign(DockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
|
||||
Math.Sign(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
DockingTarget.DockingDir = -DockingDir;
|
||||
|
||||
if (door != null && dockingTarget.door != null)
|
||||
if (door != null && DockingTarget.door != null)
|
||||
{
|
||||
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
|
||||
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => dockingTarget.door.LinkedGap == wp.ConnectedGap);
|
||||
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
|
||||
|
||||
if (myWayPoint != null && targetWayPoint != null)
|
||||
{
|
||||
@@ -222,11 +215,11 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void Lock(bool isNetworkMessage)
|
||||
public void Lock(bool isNetworkMessage, bool forcePosition = false)
|
||||
{
|
||||
if (GameMain.Client != null && !isNetworkMessage) return;
|
||||
|
||||
if (dockingTarget == null)
|
||||
if (DockingTarget == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Error - attempted to lock a docking port that's not connected to anything");
|
||||
return;
|
||||
@@ -234,15 +227,24 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (!(joint is WeldJoint))
|
||||
{
|
||||
|
||||
dockingDir = IsHorizontal ?
|
||||
Math.Sign(dockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
|
||||
Math.Sign(dockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
dockingTarget.dockingDir = -dockingDir;
|
||||
|
||||
DockingDir = IsHorizontal ?
|
||||
Math.Sign(DockingTarget.item.WorldPosition.X - item.WorldPosition.X) :
|
||||
Math.Sign(DockingTarget.item.WorldPosition.Y - item.WorldPosition.Y);
|
||||
DockingTarget.DockingDir = -DockingDir;
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnSecondaryUse, item.WorldPosition);
|
||||
#endif
|
||||
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
|
||||
if (item.Submarine.PhysicsBody.Mass < DockingTarget.item.Submarine.PhysicsBody.Mass ||
|
||||
DockingTarget.item.Submarine.IsOutpost)
|
||||
{
|
||||
item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position + ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
}
|
||||
else if (DockingTarget.item.Submarine.PhysicsBody.Mass < item.Submarine.PhysicsBody.Mass ||
|
||||
item.Submarine.IsOutpost)
|
||||
{
|
||||
DockingTarget.item.Submarine.SubBody.SetPosition(item.Submarine.SubBody.Position - ConvertUnits.ToDisplayUnits(jointDiff));
|
||||
}
|
||||
|
||||
ConnectWireBetweenPorts();
|
||||
CreateJoint(true);
|
||||
@@ -257,7 +259,7 @@ namespace Barotrauma.Items.Components
|
||||
List<MapEntity> removedEntities = item.linkedTo.Where(e => e.Removed).ToList();
|
||||
foreach (MapEntity removed in removedEntities) item.linkedTo.Remove(removed);
|
||||
|
||||
if (!item.linkedTo.Any(e => e is Hull) && !dockingTarget.item.linkedTo.Any(e => e is Hull))
|
||||
if (!item.linkedTo.Any(e => e is Hull) && !DockingTarget.item.linkedTo.Any(e => e is Hull))
|
||||
{
|
||||
CreateHulls();
|
||||
}
|
||||
@@ -273,18 +275,18 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
Vector2 offset = (IsHorizontal ?
|
||||
Vector2.UnitX * dockingDir :
|
||||
Vector2.UnitY * dockingDir);
|
||||
Vector2.UnitX * DockingDir :
|
||||
Vector2.UnitY * DockingDir);
|
||||
offset *= DockedDistance * 0.5f;
|
||||
|
||||
Vector2 pos1 = item.WorldPosition + offset;
|
||||
|
||||
Vector2 pos2 = dockingTarget.item.WorldPosition - offset;
|
||||
Vector2 pos2 = DockingTarget.item.WorldPosition - offset;
|
||||
|
||||
if (useWeldJoint)
|
||||
{
|
||||
joint = JointFactory.CreateWeldJoint(GameMain.World,
|
||||
item.Submarine.PhysicsBody.FarseerBody, dockingTarget.item.Submarine.PhysicsBody.FarseerBody,
|
||||
item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody,
|
||||
ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true);
|
||||
|
||||
((WeldJoint)joint).FrequencyHz = 1.0f;
|
||||
@@ -292,7 +294,7 @@ namespace Barotrauma.Items.Components
|
||||
else
|
||||
{
|
||||
var distanceJoint = JointFactory.CreateDistanceJoint(GameMain.World,
|
||||
item.Submarine.PhysicsBody.FarseerBody, dockingTarget.item.Submarine.PhysicsBody.FarseerBody,
|
||||
item.Submarine.PhysicsBody.FarseerBody, DockingTarget.item.Submarine.PhysicsBody.FarseerBody,
|
||||
ConvertUnits.ToSimUnits(pos1), FarseerPhysics.ConvertUnits.ToSimUnits(pos2), true);
|
||||
|
||||
distanceJoint.Length = 0.01f;
|
||||
@@ -302,7 +304,6 @@ namespace Barotrauma.Items.Components
|
||||
joint = distanceJoint;
|
||||
}
|
||||
|
||||
|
||||
joint.CollideConnected = true;
|
||||
}
|
||||
|
||||
@@ -319,12 +320,12 @@ namespace Barotrauma.Items.Components
|
||||
var powerConnection = Item.Connections.Find(c => c.IsPower);
|
||||
if (powerConnection == null) return;
|
||||
|
||||
if (dockingTarget == null || dockingTarget.item.Connections == null) return;
|
||||
var recipient = dockingTarget.item.Connections.Find(c => c.IsPower);
|
||||
if (DockingTarget == null || DockingTarget.item.Connections == null) return;
|
||||
var recipient = DockingTarget.item.Connections.Find(c => c.IsPower);
|
||||
if (recipient == null) return;
|
||||
|
||||
wire.RemoveConnection(item);
|
||||
wire.RemoveConnection(dockingTarget.item);
|
||||
wire.RemoveConnection(DockingTarget.item);
|
||||
|
||||
|
||||
powerConnection.TryAddLink(wire);
|
||||
@@ -341,7 +342,7 @@ namespace Barotrauma.Items.Components
|
||||
doorBody = null;
|
||||
}
|
||||
|
||||
Vector2 position = ConvertUnits.ToSimUnits(item.Position + (dockingTarget.door.Item.WorldPosition - item.WorldPosition));
|
||||
Vector2 position = ConvertUnits.ToSimUnits(item.Position + (DockingTarget.door.Item.WorldPosition - item.WorldPosition));
|
||||
if (!MathUtils.IsValid(position))
|
||||
{
|
||||
string errorMsg =
|
||||
@@ -360,11 +361,11 @@ namespace Barotrauma.Items.Components
|
||||
System.Diagnostics.Debug.Assert(doorBody == null);
|
||||
|
||||
doorBody = BodyFactory.CreateRectangle(GameMain.World,
|
||||
dockingTarget.door.Body.width,
|
||||
dockingTarget.door.Body.height,
|
||||
DockingTarget.door.Body.width,
|
||||
DockingTarget.door.Body.height,
|
||||
1.0f,
|
||||
position,
|
||||
dockingTarget.door);
|
||||
DockingTarget.door);
|
||||
|
||||
doorBody.CollisionCategories = Physics.CollisionWall;
|
||||
doorBody.BodyType = BodyType.Static;
|
||||
@@ -372,33 +373,32 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void CreateHulls()
|
||||
{
|
||||
var hullRects = new Rectangle[] { item.WorldRect, dockingTarget.item.WorldRect };
|
||||
var subs = new Submarine[] { item.Submarine, dockingTarget.item.Submarine };
|
||||
var hullRects = new Rectangle[] { item.WorldRect, DockingTarget.item.WorldRect };
|
||||
var subs = new Submarine[] { item.Submarine, DockingTarget.item.Submarine };
|
||||
|
||||
bodies = new Body[4];
|
||||
|
||||
if (dockingTarget.door != null)
|
||||
if (DockingTarget.door != null)
|
||||
{
|
||||
CreateDoorBody();
|
||||
}
|
||||
|
||||
if (door != null)
|
||||
{
|
||||
dockingTarget.CreateDoorBody();
|
||||
DockingTarget.CreateDoorBody();
|
||||
}
|
||||
|
||||
if (IsHorizontal)
|
||||
{
|
||||
if (hullRects[0].Center.X > hullRects[1].Center.X)
|
||||
{
|
||||
hullRects = new Rectangle[] { dockingTarget.item.WorldRect, item.WorldRect };
|
||||
subs = new Submarine[] { dockingTarget.item.Submarine,item.Submarine };
|
||||
hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
|
||||
subs = new Submarine[] { DockingTarget.item.Submarine,item.Submarine };
|
||||
}
|
||||
|
||||
hullRects[0] = new Rectangle(hullRects[0].Center.X, hullRects[0].Y, ((int)DockedDistance / 2), hullRects[0].Height);
|
||||
hullRects[1] = new Rectangle(hullRects[1].Center.X - ((int)DockedDistance / 2), hullRects[1].Y, ((int)DockedDistance / 2), hullRects[1].Height);
|
||||
|
||||
|
||||
|
||||
//expand hulls if needed, so there's no empty space between the sub's hulls and docking port hulls
|
||||
int leftSubRightSide = int.MinValue, rightSubLeftSide = int.MaxValue;
|
||||
foreach (Hull hull in Hull.hullList)
|
||||
@@ -420,11 +420,10 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//expand left hull to the rightmost hull of the sub at the left side
|
||||
//(unless the difference is more than 100 units - if the distance is very large
|
||||
//there's something wrong with the positioning of the docking ports or submarine hulls)
|
||||
int leftHullDiff = hullRects[0].X - leftSubRightSide;
|
||||
int leftHullDiff = (hullRects[0].X - leftSubRightSide) + 5;
|
||||
if (leftHullDiff > 0)
|
||||
{
|
||||
if (leftHullDiff > 100)
|
||||
@@ -438,7 +437,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
int rightHullDiff = rightSubLeftSide - hullRects[1].Right;
|
||||
int rightHullDiff = (rightSubLeftSide - hullRects[1].Right) + 5;
|
||||
if (rightHullDiff > 0)
|
||||
{
|
||||
if (rightHullDiff > 100)
|
||||
@@ -455,7 +454,7 @@ namespace Barotrauma.Items.Components
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find("Hull"), hullRects[i], subs[i]);
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find(null, "Hull"), hullRects[i], subs[i]);
|
||||
hulls[i].AddToGrid(subs[i]);
|
||||
hulls[i].FreeID();
|
||||
|
||||
@@ -473,8 +472,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (hullRects[0].Center.Y > hullRects[1].Center.Y)
|
||||
{
|
||||
hullRects = new Rectangle[] { dockingTarget.item.WorldRect, item.WorldRect };
|
||||
subs = new Submarine[] { dockingTarget.item.Submarine, item.Submarine };
|
||||
hullRects = new Rectangle[] { DockingTarget.item.WorldRect, item.WorldRect };
|
||||
subs = new Submarine[] { DockingTarget.item.Submarine, item.Submarine };
|
||||
}
|
||||
|
||||
hullRects[0] = new Rectangle(hullRects[0].X, hullRects[0].Y + (int)(-hullRects[0].Height + DockedDistance) / 2, hullRects[0].Width, ((int)DockedDistance / 2));
|
||||
@@ -504,7 +503,7 @@ namespace Barotrauma.Items.Components
|
||||
//expand lower hull to the topmost hull of the lower sub
|
||||
//(unless the difference is more than 100 units - if the distance is very large
|
||||
//there's something wrong with the positioning of the docking ports or submarine hulls)
|
||||
int lowerHullDiff = (hullRects[0].Y - hullRects[0].Height) - lowerSubTop;
|
||||
int lowerHullDiff = ((hullRects[0].Y - hullRects[0].Height) - lowerSubTop) + 5;
|
||||
if (lowerHullDiff > 0)
|
||||
{
|
||||
if (lowerHullDiff > 100)
|
||||
@@ -517,7 +516,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
int upperHullDiff = upperSubBottom - hullRects[1].Y;
|
||||
int upperHullDiff = (upperSubBottom - hullRects[1].Y) + 5;
|
||||
if (upperHullDiff > 0)
|
||||
{
|
||||
if (upperHullDiff > 100)
|
||||
@@ -531,10 +530,24 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
//difference between the edges of the hulls (to avoid a gap between the hulls)
|
||||
//0 is lower
|
||||
int midHullDiff = ((hullRects[1].Y - hullRects[1].Height) - hullRects[0].Y) + 2;
|
||||
if (midHullDiff > 100)
|
||||
{
|
||||
DebugConsole.ThrowError("Creating hulls between docking ports failed. The upper hull seems to be very far from the lower hull.");
|
||||
}
|
||||
else if (midHullDiff > 0)
|
||||
{
|
||||
hullRects[0].Height += midHullDiff / 2 + 1;
|
||||
hullRects[1].Y -= midHullDiff / 2 + 1;
|
||||
hullRects[1].Height += midHullDiff / 2 + 1;
|
||||
}
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
hullRects[i].Location -= MathUtils.ToPoint((subs[i].WorldPosition - subs[i].HiddenSubPosition));
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find("Hull"), hullRects[i], subs[i]);
|
||||
hulls[i] = new Hull(MapEntityPrefab.Find(null, "hull"), hullRects[i], subs[i]);
|
||||
hulls[i].AddToGrid(subs[i]);
|
||||
hulls[i].FreeID();
|
||||
}
|
||||
@@ -605,14 +618,14 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
Gap doorGap = i == 0 ? door?.LinkedGap : dockingTarget?.door?.LinkedGap;
|
||||
Gap doorGap = i == 0 ? door?.LinkedGap : DockingTarget?.door?.LinkedGap;
|
||||
if (doorGap == null) continue;
|
||||
doorGap.DisableHullRechecks = true;
|
||||
if (doorGap.linkedTo.Count >= 2) continue;
|
||||
|
||||
if (IsHorizontal)
|
||||
{
|
||||
if (item.WorldPosition.X < dockingTarget.item.WorldPosition.X)
|
||||
if (item.WorldPosition.X < DockingTarget.item.WorldPosition.X)
|
||||
{
|
||||
if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]);
|
||||
}
|
||||
@@ -620,10 +633,17 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]);
|
||||
}
|
||||
//make sure the left hull is linked to the gap first (gap logic assumes that the first hull is the one to the left)
|
||||
if (doorGap.linkedTo[0].Rect.X > doorGap.linkedTo[1].Rect.X)
|
||||
{
|
||||
var temp = doorGap.linkedTo[0];
|
||||
doorGap.linkedTo[0] = doorGap.linkedTo[1];
|
||||
doorGap.linkedTo[1] = temp;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (item.WorldPosition.Y < dockingTarget.item.WorldPosition.Y)
|
||||
if (item.WorldPosition.Y < DockingTarget.item.WorldPosition.Y)
|
||||
{
|
||||
if (!doorGap.linkedTo.Contains(hulls[0])) doorGap.linkedTo.Add(hulls[0]);
|
||||
}
|
||||
@@ -631,25 +651,34 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!doorGap.linkedTo.Contains(hulls[1])) doorGap.linkedTo.Add(hulls[1]);
|
||||
}
|
||||
//make sure the upper hull is linked to the gap first (gap logic assumes that the first hull is above the second one)
|
||||
if (doorGap.linkedTo[0].Rect.Y < doorGap.linkedTo[1].Rect.Y)
|
||||
{
|
||||
var temp = doorGap.linkedTo[0];
|
||||
doorGap.linkedTo[0] = doorGap.linkedTo[1];
|
||||
doorGap.linkedTo[1] = temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Undock()
|
||||
{
|
||||
if (dockingTarget == null || !docked) return;
|
||||
if (DockingTarget == null || !docked) return;
|
||||
|
||||
forceLockTimer = 0.0f;
|
||||
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnUse, item.WorldPosition);
|
||||
#endif
|
||||
|
||||
dockingTarget.item.Submarine.DockedTo.Remove(item.Submarine);
|
||||
item.Submarine.DockedTo.Remove(dockingTarget.item.Submarine);
|
||||
DockingTarget.item.Submarine.DockedTo.Remove(item.Submarine);
|
||||
item.Submarine.DockedTo.Remove(DockingTarget.item.Submarine);
|
||||
|
||||
if (door != null && dockingTarget.door != null)
|
||||
if (door != null && DockingTarget.door != null)
|
||||
{
|
||||
WayPoint myWayPoint = WayPoint.WayPointList.Find(wp => door.LinkedGap == wp.ConnectedGap);
|
||||
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => dockingTarget.door.LinkedGap == wp.ConnectedGap);
|
||||
WayPoint targetWayPoint = WayPoint.WayPointList.Find(wp => DockingTarget.door.LinkedGap == wp.ConnectedGap);
|
||||
|
||||
if (myWayPoint != null && targetWayPoint != null)
|
||||
{
|
||||
@@ -662,8 +691,8 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
docked = false;
|
||||
|
||||
dockingTarget.Undock();
|
||||
dockingTarget = null;
|
||||
DockingTarget.Undock();
|
||||
DockingTarget = null;
|
||||
|
||||
if (doorBody != null)
|
||||
{
|
||||
@@ -710,7 +739,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
if (dockingTarget == null)
|
||||
if (DockingTarget == null)
|
||||
{
|
||||
dockingState = MathHelper.Lerp(dockingState, 0.0f, deltaTime * 10.0f);
|
||||
if (dockingState < 0.01f) docked = false;
|
||||
@@ -723,8 +752,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (!docked)
|
||||
{
|
||||
Dock(dockingTarget);
|
||||
if (dockingTarget == null) return;
|
||||
Dock(DockingTarget);
|
||||
if (DockingTarget == null) { return; }
|
||||
}
|
||||
|
||||
if (joint is DistanceJoint)
|
||||
@@ -732,16 +761,48 @@ namespace Barotrauma.Items.Components
|
||||
item.SendSignal(0, "0", "state_out", null);
|
||||
dockingState = MathHelper.Lerp(dockingState, 0.5f, deltaTime * 10.0f);
|
||||
|
||||
if (Vector2.Distance(joint.WorldAnchorA, joint.WorldAnchorB) < 0.05f)
|
||||
forceLockTimer += deltaTime;
|
||||
|
||||
Vector2 jointDiff = joint.WorldAnchorB - joint.WorldAnchorA;
|
||||
|
||||
if (jointDiff.LengthSquared() > 0.04f * 0.04f && forceLockTimer < ForceLockDelay)
|
||||
{
|
||||
Lock(false);
|
||||
float totalMass = item.Submarine.PhysicsBody.Mass + DockingTarget.item.Submarine.PhysicsBody.Mass;
|
||||
float massRatio1 = 1.0f;
|
||||
float massRatio2 = 1.0f;
|
||||
|
||||
if (item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
|
||||
{
|
||||
massRatio1 = 0.0f;
|
||||
massRatio2 = 1.0f;
|
||||
}
|
||||
else if (DockingTarget.item.Submarine.PhysicsBody.BodyType != BodyType.Dynamic)
|
||||
{
|
||||
massRatio1 = 1.0f;
|
||||
massRatio2 = 0.0f;
|
||||
}
|
||||
else
|
||||
{
|
||||
massRatio1 = DockingTarget.item.Submarine.PhysicsBody.Mass / totalMass;
|
||||
massRatio2 = item.Submarine.PhysicsBody.Mass / totalMass;
|
||||
}
|
||||
|
||||
Vector2 relativeVelocity = DockingTarget.item.Submarine.Velocity - item.Submarine.Velocity;
|
||||
Vector2 desiredRelativeVelocity = Vector2.Normalize(jointDiff);
|
||||
|
||||
item.Submarine.Velocity += (relativeVelocity + desiredRelativeVelocity) * massRatio1;
|
||||
DockingTarget.item.Submarine.Velocity += (-relativeVelocity - desiredRelativeVelocity) * massRatio2;
|
||||
}
|
||||
else
|
||||
{
|
||||
Lock(isNetworkMessage: false, forcePosition: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (dockingTarget.door != null && doorBody != null)
|
||||
if (DockingTarget.door != null && doorBody != null)
|
||||
{
|
||||
doorBody.Enabled = dockingTarget.door.Body.Enabled;
|
||||
doorBody.Enabled = DockingTarget.door.Body.Enabled;
|
||||
}
|
||||
|
||||
item.SendSignal(0, "1", "state_out", null);
|
||||
@@ -780,16 +841,14 @@ namespace Barotrauma.Items.Components
|
||||
List<MapEntity> linked = new List<MapEntity>(item.linkedTo);
|
||||
foreach (MapEntity entity in linked)
|
||||
{
|
||||
var hull = entity as Hull;
|
||||
if (hull != null)
|
||||
if (entity is Hull hull)
|
||||
{
|
||||
hull.Remove();
|
||||
item.linkedTo.Remove(hull);
|
||||
continue;
|
||||
}
|
||||
|
||||
var gap = entity as Gap;
|
||||
if (gap != null)
|
||||
if (entity is Gap gap)
|
||||
{
|
||||
gap.Remove();
|
||||
continue;
|
||||
@@ -806,12 +865,12 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f)
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (GameMain.Client != null) return;
|
||||
|
||||
bool wasDocked = docked;
|
||||
DockingPort prevDockingTarget = dockingTarget;
|
||||
DockingPort prevDockingTarget = DockingTarget;
|
||||
|
||||
switch (connection.Name)
|
||||
{
|
||||
@@ -828,8 +887,8 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (docked)
|
||||
{
|
||||
if (item.Submarine != null && dockingTarget?.item?.Submarine != null)
|
||||
GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + dockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
|
||||
if (item.Submarine != null && DockingTarget?.item?.Submarine != null)
|
||||
GameServer.Log(sender.LogName + " docked " + item.Submarine.Name + " to " + DockingTarget.item.Submarine.Name, ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -845,7 +904,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (docked)
|
||||
{
|
||||
msg.Write(dockingTarget.item.ID);
|
||||
msg.Write(DockingTarget.item.ID);
|
||||
msg.Write(hulls != null && hulls[0] != null && hulls[1] != null && gap != null);
|
||||
}
|
||||
}
|
||||
@@ -882,18 +941,18 @@ namespace Barotrauma.Items.Components
|
||||
return;
|
||||
}
|
||||
|
||||
dockingTarget = (targetEntity as Item).GetComponent<DockingPort>();
|
||||
if (dockingTarget == null)
|
||||
DockingTarget = (targetEntity as Item).GetComponent<DockingPort>();
|
||||
if (DockingTarget == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Invalid docking port network event (" + targetEntity + " doesn't have a docking port component)");
|
||||
return;
|
||||
}
|
||||
|
||||
Dock(dockingTarget);
|
||||
Dock(DockingTarget);
|
||||
|
||||
if (isLocked)
|
||||
{
|
||||
Lock(true);
|
||||
Lock(isNetworkMessage: true, forcePosition: true);
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -30,6 +30,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool isHorizontal;
|
||||
|
||||
private bool createdNewGap;
|
||||
private bool autoOrientGap;
|
||||
|
||||
private bool isStuck;
|
||||
|
||||
private bool? predictedState;
|
||||
@@ -39,6 +42,9 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private bool isBroken;
|
||||
|
||||
//openState when the vertices of the convex hull were last calculated
|
||||
private float lastConvexHullState;
|
||||
|
||||
public bool IsBroken
|
||||
{
|
||||
get { return isBroken; }
|
||||
@@ -63,6 +69,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
private float stuck;
|
||||
[Serialize(0.0f, false)]
|
||||
public float Stuck
|
||||
{
|
||||
get { return stuck; }
|
||||
@@ -70,11 +77,16 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
if (isOpen || isBroken) return;
|
||||
stuck = MathHelper.Clamp(value, 0.0f, 100.0f);
|
||||
if (stuck == 0.0f) isStuck = false;
|
||||
if (stuck == 100.0f) isStuck = true;
|
||||
if (stuck <= 0.0f) isStuck = false;
|
||||
if (stuck >= 100.0f) isStuck = true;
|
||||
}
|
||||
}
|
||||
|
||||
public bool? PredictedState
|
||||
{
|
||||
get { return predictedState; }
|
||||
}
|
||||
|
||||
public Gap LinkedGap
|
||||
{
|
||||
get
|
||||
@@ -102,14 +114,22 @@ namespace Barotrauma.Items.Components
|
||||
rect.Width += 10;
|
||||
}
|
||||
|
||||
linkedGap = new Gap(rect, Item.Submarine);
|
||||
linkedGap.Submarine = item.Submarine;
|
||||
linkedGap.PassAmbientLight = window != Rectangle.Empty;
|
||||
linkedGap.Open = openState;
|
||||
linkedGap = new Gap(rect, !isHorizontal, Item.Submarine)
|
||||
{
|
||||
Submarine = item.Submarine,
|
||||
PassAmbientLight = window != Rectangle.Empty,
|
||||
Open = openState
|
||||
};
|
||||
item.linkedTo.Add(linkedGap);
|
||||
createdNewGap = true;
|
||||
return linkedGap;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsHorizontal
|
||||
{
|
||||
get { return isHorizontal; }
|
||||
}
|
||||
|
||||
[Serialize("0.0,0.0,0.0,0.0", false)]
|
||||
public Rectangle Window
|
||||
@@ -133,23 +153,30 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
get { return openState; }
|
||||
set
|
||||
{
|
||||
|
||||
float prevValue = openState;
|
||||
{
|
||||
openState = MathHelper.Clamp(value, 0.0f, 1.0f);
|
||||
if (openState == prevValue) return;
|
||||
|
||||
#if CLIENT
|
||||
float size = isHorizontal ? item.Rect.Width : item.Rect.Height;
|
||||
if (Math.Abs(lastConvexHullState - openState) * size < 5.0f) { return; }
|
||||
UpdateConvexHulls();
|
||||
lastConvexHullState = openState;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool Impassable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
public Door(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
isHorizontal = element.GetAttributeBool("horizontal", false);
|
||||
canBePicked = element.GetAttributeBool("canbepicked", false);
|
||||
autoOrientGap = element.GetAttributeBool("autoorientgap", false);
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
@@ -171,24 +198,25 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
|
||||
doorRect = new Rectangle(
|
||||
item.Rect.Center.X - (int)(doorSprite.size.X / 2),
|
||||
item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f),
|
||||
(int)doorSprite.size.X,
|
||||
(int)doorSprite.size.Y);
|
||||
item.Rect.Center.X - (int)(doorSprite.size.X / 2 * item.Scale),
|
||||
item.Rect.Y - item.Rect.Height/2 + (int)(doorSprite.size.Y / 2.0f * item.Scale),
|
||||
(int)(doorSprite.size.X * item.Scale),
|
||||
(int)(doorSprite.size.Y * item.Scale));
|
||||
|
||||
body = new PhysicsBody(
|
||||
ConvertUnits.ToSimUnits(Math.Max(doorRect.Width, 1)),
|
||||
ConvertUnits.ToSimUnits(Math.Max(doorRect.Height, 1)),
|
||||
0.0f,
|
||||
1.5f);
|
||||
|
||||
body.UserData = item;
|
||||
body.CollisionCategories = Physics.CollisionWall;
|
||||
body.BodyType = BodyType.Static;
|
||||
1.5f)
|
||||
{
|
||||
UserData = item,
|
||||
CollisionCategories = Physics.CollisionWall,
|
||||
BodyType = BodyType.Static,
|
||||
Friction = 0.5f
|
||||
};
|
||||
body.SetTransform(
|
||||
ConvertUnits.ToSimUnits(new Vector2(doorRect.Center.X, doorRect.Y - doorRect.Height / 2)),
|
||||
0.0f);
|
||||
body.Friction = 0.5f;
|
||||
|
||||
IsActive = true;
|
||||
}
|
||||
@@ -197,7 +225,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
base.Move(amount);
|
||||
|
||||
body.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
||||
body?.SetTransform(body.SimPosition + ConvertUnits.ToSimUnits(amount), 0.0f);
|
||||
|
||||
#if CLIENT
|
||||
UpdateConvexHulls();
|
||||
@@ -223,7 +251,7 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
SetState(predictedState == null ? !isOpen : !predictedState.Value, false, true); //crowbar function
|
||||
#if CLIENT
|
||||
PlaySound(ActionType.OnPicked, item.WorldPosition);
|
||||
PlaySound(ActionType.OnPicked, item.WorldPosition, picker);
|
||||
#endif
|
||||
return false;
|
||||
}
|
||||
@@ -275,7 +303,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
else
|
||||
{
|
||||
body.Enabled = openState < 1.0f;
|
||||
body.Enabled = Impassable || openState < 1.0f;
|
||||
}
|
||||
|
||||
//don't use the predicted state here, because it might set
|
||||
@@ -290,7 +318,10 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
private void EnableBody()
|
||||
{
|
||||
body.FarseerBody.IsSensor = false;
|
||||
if (!Impassable)
|
||||
{
|
||||
body.FarseerBody.IsSensor = false;
|
||||
}
|
||||
#if CLIENT
|
||||
UpdateConvexHulls();
|
||||
#endif
|
||||
@@ -301,7 +332,10 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//change the body to a sensor instead of disabling it completely,
|
||||
//because otherwise repairtool raycasts won't hit it
|
||||
body.FarseerBody.IsSensor = true;
|
||||
if (!Impassable)
|
||||
{
|
||||
body.FarseerBody.IsSensor = true;
|
||||
}
|
||||
linkedGap.Open = 1.0f;
|
||||
IsOpen = false;
|
||||
#if CLIENT
|
||||
@@ -314,6 +348,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
LinkedGap.ConnectedDoor = this;
|
||||
LinkedGap.Open = openState;
|
||||
if (createdNewGap && autoOrientGap) linkedGap.AutoOrient();
|
||||
|
||||
#if CLIENT
|
||||
Vector2[] corners = GetConvexHullCorners(Rectangle.Empty);
|
||||
@@ -365,8 +400,8 @@ namespace Barotrauma.Items.Components
|
||||
Vector2 simPos = ConvertUnits.ToSimUnits(new Vector2(item.Rect.X, item.Rect.Y));
|
||||
|
||||
Vector2 currSize = isHorizontal ?
|
||||
new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y) :
|
||||
new Vector2(doorSprite.size.X, item.Rect.Height * (1.0f - openState));
|
||||
new Vector2(item.Rect.Width * (1.0f - openState), doorSprite.size.Y * item.Scale) :
|
||||
new Vector2(doorSprite.size.X * item.Scale, item.Rect.Height * (1.0f - openState));
|
||||
|
||||
Vector2 simSize = ConvertUnits.ToSimUnits(currSize);
|
||||
|
||||
@@ -447,7 +482,7 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f)
|
||||
public override void ReceiveSignal(int stepsTaken, string signal, Connection connection, Item source, Character sender, float power = 0.0f, float signalStrength = 1.0f)
|
||||
{
|
||||
if (isStuck) return;
|
||||
|
||||
@@ -477,26 +512,24 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (GameMain.Client != null && !isNetworkMessage)
|
||||
{
|
||||
//clients can "predict" that the door opens/closes when a signal is received
|
||||
bool stateChanged = open != predictedState;
|
||||
|
||||
//clients can "predict" that the door opens/closes when a signal is received
|
||||
//the prediction will be reset after 1 second, setting the door to a state
|
||||
//sent by the server, or reverting it back to its old state if no msg from server was received
|
||||
|
||||
#if CLIENT
|
||||
if (open != predictedState) PlaySound(ActionType.OnUse, item.WorldPosition);
|
||||
#endif
|
||||
|
||||
predictedState = open;
|
||||
resetPredictionTimer = CorrectionDelay;
|
||||
|
||||
#if CLIENT
|
||||
if (stateChanged) PlaySound(ActionType.OnUse, item.WorldPosition);
|
||||
#endif
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
isOpen = open;
|
||||
#if CLIENT
|
||||
if (!isNetworkMessage || open != predictedState) PlaySound(ActionType.OnUse, item.WorldPosition);
|
||||
#endif
|
||||
|
||||
isOpen = open;
|
||||
}
|
||||
|
||||
//opening a partially stuck door makes it less stuck
|
||||
@@ -508,14 +541,18 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerWrite(Lidgren.Network.NetBuffer msg, Barotrauma.Networking.Client c, object[] extraData = null)
|
||||
public override void ServerWrite(Lidgren.Network.NetBuffer msg, Client c, object[] extraData = null)
|
||||
{
|
||||
base.ServerWrite(msg, c, extraData);
|
||||
|
||||
msg.Write(isOpen);
|
||||
msg.WriteRangedSingle(stuck, 0.0f, 100.0f, 8);
|
||||
}
|
||||
|
||||
public void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime)
|
||||
public override void ClientRead(ServerNetObject type, Lidgren.Network.NetBuffer msg, float sendingTime)
|
||||
{
|
||||
base.ClientRead(type, msg, sendingTime);
|
||||
|
||||
SetState(msg.ReadBoolean(), true);
|
||||
Stuck = msg.ReadRangedSingle(0.0f, 100.0f, 8);
|
||||
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Xml.Linq;
|
||||
|
||||
namespace Barotrauma.Items.Components
|
||||
{
|
||||
partial class ElectricalDischarger : Powered
|
||||
{
|
||||
private static List<ElectricalDischarger> list = new List<ElectricalDischarger>();
|
||||
public static IEnumerable<ElectricalDischarger> List
|
||||
{
|
||||
get { return list; }
|
||||
}
|
||||
|
||||
const int MaxNodes = 100;
|
||||
const float MaxNodeDistance = 150.0f;
|
||||
|
||||
public struct Node
|
||||
{
|
||||
public Vector2 WorldPosition;
|
||||
public int ParentIndex;
|
||||
public float Length;
|
||||
public float Angle;
|
||||
|
||||
public Node(Vector2 worldPosition, int parentIndex, float length = 0.0f, float angle = 0.0f)
|
||||
{
|
||||
WorldPosition = worldPosition;
|
||||
ParentIndex = parentIndex;
|
||||
Length = length;
|
||||
Angle = angle;
|
||||
}
|
||||
}
|
||||
|
||||
[Serialize(100.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 5000.0f)]
|
||||
public float Range
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(10.0f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1000.0f, ToolTip = "How much further can the discharge be carried when moving across walls.")]
|
||||
public float RangeMultiplierInWalls
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(0.25f, true), Editable(MinValueFloat = 0.0f, MaxValueFloat = 1.0f)]
|
||||
public float Duration
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, true), Editable()]
|
||||
public bool OutdoorsOnly
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
private readonly List<Node> nodes = new List<Node>();
|
||||
public IEnumerable<Node> Nodes
|
||||
{
|
||||
get { return nodes; }
|
||||
}
|
||||
|
||||
private readonly List<Pair<Character,Node>> charactersInRange = new List<Pair<Character, Node>>();
|
||||
|
||||
private bool charging;
|
||||
|
||||
private float timer;
|
||||
|
||||
private Attack attack;
|
||||
|
||||
public ElectricalDischarger(Item item, XElement element) :
|
||||
base(item, element)
|
||||
{
|
||||
list.Add(this);
|
||||
|
||||
foreach (XElement subElement in element.Elements())
|
||||
{
|
||||
switch (subElement.Name.ToString().ToLowerInvariant())
|
||||
{
|
||||
case "attack":
|
||||
attack = new Attack(subElement, item.Name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
InitProjSpecific();
|
||||
}
|
||||
|
||||
partial void InitProjSpecific();
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
//already active, do nothing
|
||||
if (IsActive) { return false; }
|
||||
|
||||
CurrPowerConsumption = powerConsumption;
|
||||
charging = true;
|
||||
timer = Duration;
|
||||
IsActive = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public override void Update(float deltaTime, Camera cam)
|
||||
{
|
||||
#if CLIENT
|
||||
frameOffset = Rand.Int(electricitySprite.FrameCount);
|
||||
#endif
|
||||
if (timer > 0.0f)
|
||||
{
|
||||
if (charging)
|
||||
{
|
||||
if (voltage > minVoltage || powerConsumption <= 0.0f)
|
||||
{
|
||||
Discharge();
|
||||
}
|
||||
}
|
||||
timer -= deltaTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
nodes.Clear();
|
||||
charactersInRange.Clear();
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
voltage = 0.0f;
|
||||
}
|
||||
|
||||
private void Discharge()
|
||||
{
|
||||
ApplyStatusEffects(ActionType.OnUse, 1.0f);
|
||||
FindNodes(item.WorldPosition, Range);
|
||||
if (attack != null)
|
||||
{
|
||||
foreach (Pair<Character, Node> characterInRange in charactersInRange)
|
||||
{
|
||||
characterInRange.First.ApplyAttack(null, characterInRange.Second.WorldPosition, attack, 1.0f);
|
||||
}
|
||||
}
|
||||
DischargeProjSpecific();
|
||||
charging = false;
|
||||
}
|
||||
|
||||
partial void DischargeProjSpecific();
|
||||
|
||||
private void FindNodes(Vector2 worldPosition, float range)
|
||||
{
|
||||
//see which submarines are within range so we can skip structures that are in far-away subs
|
||||
List<Submarine> submarinesInRange = new List<Submarine>();
|
||||
foreach (Submarine sub in Submarine.Loaded)
|
||||
{
|
||||
if (item.Submarine == sub)
|
||||
{
|
||||
submarinesInRange.Add(sub);
|
||||
}
|
||||
else
|
||||
{
|
||||
Rectangle subBorders = new Rectangle(
|
||||
sub.Borders.X - (int)range, sub.Borders.Y + (int)range,
|
||||
sub.Borders.Width + (int)(range * 2), sub.Borders.Height + (int)(range * 2));
|
||||
subBorders.Location += MathUtils.ToPoint(sub.SubBody.Position);
|
||||
if (Submarine.RectContains(subBorders, worldPosition))
|
||||
{
|
||||
submarinesInRange.Add(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//get all walls within range
|
||||
List<Entity> entitiesInRange = new List<Entity>(100);
|
||||
foreach (Structure structure in Structure.WallList)
|
||||
{
|
||||
if (!structure.HasBody || structure.IsPlatform) { continue; }
|
||||
if (structure.Submarine != null&& !submarinesInRange.Contains(structure.Submarine)) { continue; }
|
||||
|
||||
var structureWorldRect = structure.WorldRect;
|
||||
if (worldPosition.X < structureWorldRect.X - range) continue;
|
||||
if (worldPosition.X > structureWorldRect.Right + range) continue;
|
||||
if (worldPosition.Y > structureWorldRect.Y + range) continue;
|
||||
if (worldPosition.Y < structureWorldRect.Y -structureWorldRect.Height - range) continue;
|
||||
|
||||
if (structure.Submarine != null)
|
||||
{
|
||||
if (!submarinesInRange.Contains(structure.Submarine)) { continue; }
|
||||
if (OutdoorsOnly)
|
||||
{
|
||||
//check if the structure is within a hull
|
||||
//add a small offset away from the sub's center so structures right at the edge of a hull are still valid
|
||||
Vector2 offset = Vector2.Normalize(structure.WorldPosition - structure.Submarine.WorldPosition);
|
||||
if (Hull.FindHull(structure.Position + offset * Submarine.GridSize, useWorldCoordinates: false) != null) { continue; }
|
||||
}
|
||||
}
|
||||
|
||||
entitiesInRange.Add(structure);
|
||||
}
|
||||
|
||||
foreach (Character character in Character.CharacterList)
|
||||
{
|
||||
if (!character.Enabled) continue;
|
||||
if (OutdoorsOnly && character.Submarine != null) continue;
|
||||
if (character.Submarine != null && !submarinesInRange.Contains(character.Submarine)) continue;
|
||||
|
||||
if (Vector2.DistanceSquared(character.WorldPosition, worldPosition) < range * range * RangeMultiplierInWalls)
|
||||
{
|
||||
entitiesInRange.Add(character);
|
||||
}
|
||||
}
|
||||
|
||||
nodes.Clear();
|
||||
nodes.Add(new Node(worldPosition, -1));
|
||||
FindNodes(entitiesInRange, worldPosition, 0, range);
|
||||
|
||||
//construct final nodes (w/ lengths and angles so they don't have to be recalculated when rendering the discharge)
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (nodes[i].ParentIndex < 0) continue;
|
||||
Node parentNode = nodes[nodes[i].ParentIndex];
|
||||
float length = Vector2.Distance(nodes[i].WorldPosition, parentNode.WorldPosition) * Rand.Range(1.0f, 1.25f);
|
||||
float angle = MathUtils.VectorToAngle(parentNode.WorldPosition - nodes[i].WorldPosition);
|
||||
nodes[i] = new Node(nodes[i].WorldPosition, nodes[i].ParentIndex, length, angle);
|
||||
}
|
||||
}
|
||||
|
||||
private void FindNodes(List<Entity> entitiesInRange, Vector2 currPos, int parentNodeIndex, float currentRange)
|
||||
{
|
||||
if (currentRange <= 0.0f || nodes.Count >= MaxNodes) return;
|
||||
|
||||
//find the closest structure
|
||||
int closestIndex = -1;
|
||||
float closestDist = float.MaxValue;
|
||||
for (int i = 0; i < entitiesInRange.Count; i++)
|
||||
{
|
||||
float dist = float.MaxValue;
|
||||
|
||||
if (entitiesInRange[i] is Structure structure)
|
||||
{
|
||||
if (structure.IsHorizontal)
|
||||
{
|
||||
dist = Math.Abs(structure.WorldPosition.Y - currPos.Y);
|
||||
if (currPos.X < structure.WorldRect.X)
|
||||
dist += structure.WorldRect.X - currPos.X;
|
||||
else if (currPos.X > structure.WorldRect.Right)
|
||||
dist += currPos.X - structure.WorldRect.Right;
|
||||
}
|
||||
else
|
||||
{
|
||||
dist = Math.Abs(structure.WorldPosition.X - currPos.X);
|
||||
if (currPos.Y < structure.WorldRect.Y - structure.Rect.Height)
|
||||
dist += (structure.WorldRect.Y - structure.Rect.Height) - currPos.Y;
|
||||
else if (currPos.Y > structure.WorldRect.Y)
|
||||
dist += currPos.Y - structure.WorldRect.Y;
|
||||
}
|
||||
}
|
||||
else if (entitiesInRange[i] is Character character)
|
||||
{
|
||||
dist = Vector2.Distance(character.WorldPosition, currPos);
|
||||
}
|
||||
|
||||
if (dist < closestDist)
|
||||
{
|
||||
closestIndex = i;
|
||||
closestDist = dist;
|
||||
}
|
||||
}
|
||||
|
||||
if (closestIndex == -1 || closestDist > currentRange)
|
||||
{
|
||||
int originalParentNodeIndex = parentNodeIndex;
|
||||
//nothing in range, create some arcs to random directions
|
||||
for (int i = 0; i < Rand.Int(4); i++)
|
||||
{
|
||||
Vector2 targetPos = currPos + Rand.Vector(MaxNodeDistance * Rand.Range(0.5f, 1.5f));
|
||||
nodes.Add(new Node(targetPos, parentNodeIndex));
|
||||
}
|
||||
return;
|
||||
}
|
||||
currentRange -= closestDist;
|
||||
|
||||
if (entitiesInRange[closestIndex] is Structure targetStructure)
|
||||
{
|
||||
if (targetStructure.IsHorizontal)
|
||||
{
|
||||
//which side of the structure to add the nodes to
|
||||
//if outside the sub, use the sides that's furthers from the sub's center position
|
||||
//otherwise the side that's closer to the previous node
|
||||
int yDir = OutdoorsOnly && targetStructure.Submarine != null ?
|
||||
Math.Sign(targetStructure.WorldPosition.Y - targetStructure.Submarine.WorldPosition.Y) :
|
||||
Math.Sign(currPos.Y - targetStructure.WorldPosition.Y);
|
||||
|
||||
int sectionIndex = targetStructure.FindSectionIndex(currPos, world: true, clamp: true);
|
||||
if (sectionIndex == -1) { return; }
|
||||
Vector2 sectionPos = targetStructure.SectionPosition(sectionIndex, world: true);
|
||||
Vector2 targetPos =
|
||||
new Vector2(
|
||||
MathHelper.Clamp(sectionPos.X, targetStructure.WorldRect.X, targetStructure.WorldRect.Right),
|
||||
sectionPos.Y + targetStructure.BodyHeight / 2 * yDir);
|
||||
|
||||
//create nodes from the current position to the closest point on the structure
|
||||
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
|
||||
|
||||
//add a node at the closest point
|
||||
nodes.Add(new Node(targetPos, parentNodeIndex));
|
||||
int nodeIndex = nodes.Count - 1;
|
||||
entitiesInRange.RemoveAt(closestIndex);
|
||||
|
||||
float newRange = currentRange - (targetStructure.Rect.Width / 2) * (1.0f / RangeMultiplierInWalls);
|
||||
|
||||
//continue the discharge to the left edge of the structure and extend from there
|
||||
int leftNodeIndex = nodeIndex;
|
||||
Vector2 leftPos = targetStructure.SectionPosition(0, world: true);
|
||||
leftPos.Y += targetStructure.BodyHeight / 2 * yDir;
|
||||
AddNodesBetweenPoints(targetPos, leftPos, 0.05f, ref leftNodeIndex);
|
||||
nodes.Add(new Node(leftPos, leftNodeIndex));
|
||||
FindNodes(entitiesInRange, leftPos, nodes.Count - 1, newRange);
|
||||
|
||||
//continue the discharge to the right edge of the structure and extend from there
|
||||
int rightNodeIndex = nodeIndex;
|
||||
Vector2 rightPos = targetStructure.SectionPosition(targetStructure.SectionCount - 1, world: true);
|
||||
leftPos.Y += targetStructure.BodyHeight / 2 * yDir;
|
||||
AddNodesBetweenPoints(targetPos, rightPos, 0.05f, ref rightNodeIndex);
|
||||
nodes.Add(new Node(rightPos, rightNodeIndex));
|
||||
FindNodes(entitiesInRange, rightPos, nodes.Count - 1, newRange);
|
||||
}
|
||||
else
|
||||
{
|
||||
int xDir = OutdoorsOnly && targetStructure.Submarine != null ?
|
||||
Math.Sign(targetStructure.WorldPosition.X - targetStructure.Submarine.WorldPosition.X) :
|
||||
Math.Sign(currPos.X - targetStructure.WorldPosition.X);
|
||||
|
||||
int sectionIndex = targetStructure.FindSectionIndex(currPos, world: true, clamp: true);
|
||||
if (sectionIndex == -1) { return; }
|
||||
Vector2 sectionPos = targetStructure.SectionPosition(sectionIndex, world: true);
|
||||
|
||||
Vector2 targetPos = new Vector2(
|
||||
sectionPos.X + targetStructure.BodyWidth / 2 * xDir,
|
||||
MathHelper.Clamp(sectionPos.Y, targetStructure.WorldRect.Y - targetStructure.Rect.Height, targetStructure.WorldRect.Y));
|
||||
|
||||
//create nodes from the current position to the closest point on the structure
|
||||
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
|
||||
|
||||
//add a node at the closest point
|
||||
nodes.Add(new Node(targetPos, parentNodeIndex));
|
||||
int nodeIndex = nodes.Count - 1;
|
||||
entitiesInRange.RemoveAt(closestIndex);
|
||||
|
||||
float newRange = currentRange - (targetStructure.Rect.Height / 2) * (1.0f / RangeMultiplierInWalls);
|
||||
|
||||
//continue the discharge to the top edge of the structure and extend from there
|
||||
int topNodeIndex = nodeIndex;
|
||||
Vector2 topPos = targetStructure.SectionPosition(0, world: true);
|
||||
topPos.X += targetStructure.BodyWidth / 2 * xDir;
|
||||
AddNodesBetweenPoints(targetPos, topPos, 0.05f, ref topNodeIndex);
|
||||
nodes.Add(new Node(topPos, topNodeIndex));
|
||||
FindNodes(entitiesInRange, topPos, nodes.Count - 1, newRange);
|
||||
|
||||
//continue the discharge to the bottom edge of the structure and extend from there
|
||||
int bottomNodeIndex = nodeIndex;
|
||||
Vector2 bottomBos = targetStructure.SectionPosition(targetStructure.SectionCount - 1, world: true);
|
||||
bottomBos.X += targetStructure.BodyWidth / 2 * xDir;
|
||||
AddNodesBetweenPoints(targetPos, bottomBos, 0.05f, ref bottomNodeIndex);
|
||||
nodes.Add(new Node(bottomBos, bottomNodeIndex));
|
||||
FindNodes(entitiesInRange, bottomBos, nodes.Count - 1, newRange);
|
||||
}
|
||||
|
||||
//check if any character is close to this structure
|
||||
for (int j = 0; j < entitiesInRange.Count; j++)
|
||||
{
|
||||
var otherEntity = entitiesInRange[j];
|
||||
if (!(otherEntity is Character character)) continue;
|
||||
if (OutdoorsOnly && character.Submarine != null) continue;
|
||||
|
||||
if (targetStructure.IsHorizontal)
|
||||
{
|
||||
if (otherEntity.WorldPosition.X < targetStructure.WorldRect.X) continue;
|
||||
if (otherEntity.WorldPosition.X > targetStructure.WorldRect.Right) continue;
|
||||
if (Math.Abs(otherEntity.WorldPosition.Y - targetStructure.WorldPosition.Y) > currentRange) continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (otherEntity.WorldPosition.Y < targetStructure.WorldRect.Y - targetStructure.Rect.Height) continue;
|
||||
if (otherEntity.WorldPosition.Y > targetStructure.WorldRect.Y) continue;
|
||||
if (Math.Abs(otherEntity.WorldPosition.X - targetStructure.WorldPosition.X) > currentRange) continue;
|
||||
}
|
||||
float closestNodeDistSqr = float.MaxValue;
|
||||
int closestNodeIndex = -1;
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
float distSqr = Vector2.DistanceSquared(character.WorldPosition, nodes[i].WorldPosition);
|
||||
if (distSqr < closestNodeDistSqr)
|
||||
{
|
||||
closestNodeDistSqr = distSqr;
|
||||
closestNodeIndex = i;
|
||||
}
|
||||
}
|
||||
if (closestNodeIndex > -1)
|
||||
{
|
||||
FindNodes(entitiesInRange, nodes[closestNodeIndex].WorldPosition, closestNodeIndex, currentRange - (float)Math.Sqrt(closestNodeDistSqr));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (entitiesInRange[closestIndex] is Character character)
|
||||
{
|
||||
Vector2 targetPos = character.WorldPosition;
|
||||
//create nodes from the current position to the closest point on the character
|
||||
AddNodesBetweenPoints(currPos, targetPos, 0.25f, ref parentNodeIndex);
|
||||
nodes.Add(new Node(targetPos, parentNodeIndex));
|
||||
entitiesInRange.RemoveAt(closestIndex);
|
||||
charactersInRange.Add(new Pair<Character, Node>(character, nodes[parentNodeIndex]));
|
||||
FindNodes(entitiesInRange, targetPos, nodes.Count - 1, currentRange);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddNodesBetweenPoints(Vector2 currPos, Vector2 targetPos, float variance, ref int parentNodeIndex)
|
||||
{
|
||||
Vector2 diff = targetPos - currPos;
|
||||
float dist = diff.Length();
|
||||
Vector2 normal = new Vector2(-diff.Y, diff.X) / dist;
|
||||
for (float x = MaxNodeDistance; x < dist - MaxNodeDistance; x += MaxNodeDistance * Rand.Range(0.5f, 1.5f))
|
||||
{
|
||||
//0 at the edges, 1 at the center
|
||||
float normalOffset = (0.5f - Math.Abs(x / dist - 0.5f)) * 2.0f;
|
||||
normalOffset *= variance * dist * Rand.Range(-1.0f, 1.0f);
|
||||
|
||||
nodes.Add(new Node(currPos + (diff / dist) * x + normal * normalOffset, parentNodeIndex));
|
||||
parentNodeIndex = nodes.Count - 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RemoveComponentSpecific()
|
||||
{
|
||||
list.Remove(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,24 +11,35 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
//the position(s) in the item that the Character grabs
|
||||
protected Vector2[] handlePos;
|
||||
|
||||
private Vector2[] scaledHandlePos;
|
||||
|
||||
private InputType prevPickKey;
|
||||
private string prevMsg;
|
||||
private List<RelatedItem> prevRequiredItems;
|
||||
private Dictionary<RelatedItem.RelationType, List<RelatedItem>> prevRequiredItems;
|
||||
|
||||
//the distance from the holding characters elbow to center of the physics body of the item
|
||||
protected Vector2 holdPos;
|
||||
|
||||
protected Vector2 aimPos;
|
||||
|
||||
//protected bool aimable;
|
||||
private float swingState;
|
||||
|
||||
private bool attachable, attached, attachedByDefault;
|
||||
private PhysicsBody body;
|
||||
public PhysicsBody Pusher
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
//the angle in which the Character holds the item
|
||||
protected float holdAngle;
|
||||
|
||||
public PhysicsBody Body
|
||||
{
|
||||
get { return item.body ?? body; }
|
||||
}
|
||||
|
||||
[Serialize(false, true)]
|
||||
public bool Attached
|
||||
{
|
||||
@@ -36,6 +47,13 @@ namespace Barotrauma.Items.Components
|
||||
set { attached = value; }
|
||||
}
|
||||
|
||||
[Serialize(true, true)]
|
||||
public bool Aimable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool ControlPose
|
||||
{
|
||||
@@ -50,6 +68,13 @@ namespace Barotrauma.Items.Components
|
||||
set { attachable = value; }
|
||||
}
|
||||
|
||||
[Serialize(true, false)]
|
||||
public bool Reattachable
|
||||
{
|
||||
get;
|
||||
set;
|
||||
}
|
||||
|
||||
[Serialize(false, false)]
|
||||
public bool AttachedByDefault
|
||||
{
|
||||
@@ -57,7 +82,7 @@ namespace Barotrauma.Items.Components
|
||||
set { attachedByDefault = value; }
|
||||
}
|
||||
|
||||
[Serialize("0.0,0.0", false)]
|
||||
[Serialize("0.0,0.0", false),Editable]
|
||||
public Vector2 HoldPos
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(holdPos); }
|
||||
@@ -71,25 +96,62 @@ namespace Barotrauma.Items.Components
|
||||
set { aimPos = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false)]
|
||||
[Serialize(0.0f, false), Editable]
|
||||
public float HoldAngle
|
||||
{
|
||||
get { return MathHelper.ToDegrees(holdAngle); }
|
||||
set { holdAngle = MathHelper.ToRadians(value); }
|
||||
}
|
||||
|
||||
private Vector2 swingAmount;
|
||||
[Serialize("0.0,0.0", false), Editable]
|
||||
public Vector2 SwingAmount
|
||||
{
|
||||
get { return ConvertUnits.ToDisplayUnits(swingAmount); }
|
||||
set { swingAmount = ConvertUnits.ToSimUnits(value); }
|
||||
}
|
||||
|
||||
[Serialize(0.0f, false), Editable]
|
||||
public float SwingSpeed { get; set; }
|
||||
|
||||
[Serialize(false, false), Editable]
|
||||
public bool SwingWhenHolding { get; set; }
|
||||
[Serialize(false, false), Editable]
|
||||
public bool SwingWhenAiming { get; set; }
|
||||
[Serialize(false, false), Editable]
|
||||
public bool SwingWhenUsing { get; set; }
|
||||
|
||||
public Holdable(Item item, XElement element)
|
||||
: base(item, element)
|
||||
{
|
||||
body = item.body;
|
||||
|
||||
handlePos = new Vector2[2];
|
||||
Pusher = null;
|
||||
if (element.GetAttributeBool("blocksplayers", false))
|
||||
{
|
||||
Pusher = new PhysicsBody(item.body.width, item.body.height, item.body.radius, item.body.Density)
|
||||
{
|
||||
BodyType = FarseerPhysics.Dynamics.BodyType.Dynamic,
|
||||
CollidesWith = Physics.CollisionCharacter,
|
||||
CollisionCategories = Physics.CollisionItemBlocking,
|
||||
Enabled = false
|
||||
};
|
||||
Pusher.FarseerBody.FixedRotation = false;
|
||||
Pusher.FarseerBody.GravityScale = 0.0f;
|
||||
}
|
||||
|
||||
handlePos = new Vector2[2];
|
||||
scaledHandlePos = new Vector2[2];
|
||||
Vector2 previousValue = Vector2.Zero;
|
||||
for (int i = 1; i < 3; i++)
|
||||
{
|
||||
handlePos[i - 1] = element.GetAttributeVector2("handle" + i, Vector2.Zero);
|
||||
|
||||
handlePos[i - 1] = ConvertUnits.ToSimUnits(handlePos[i - 1]);
|
||||
int index = i - 1;
|
||||
string attributeName = "handle" + i;
|
||||
var attribute = element.Attribute(attributeName);
|
||||
// If no value is defind for handle2, use the value of handle1.
|
||||
var value = attribute != null ? ConvertUnits.ToSimUnits(XMLExtensions.ParseVector2(attribute.Value)) : previousValue;
|
||||
handlePos[index] = value;
|
||||
previousValue = value;
|
||||
}
|
||||
|
||||
canBePicked = true;
|
||||
@@ -98,7 +160,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
prevMsg = Msg;
|
||||
prevPickKey = PickKey;
|
||||
prevRequiredItems = new List<RelatedItem>(requiredItems);
|
||||
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
|
||||
|
||||
if (item.Submarine != null)
|
||||
{
|
||||
@@ -131,7 +193,7 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
prevMsg = Msg;
|
||||
prevPickKey = PickKey;
|
||||
prevRequiredItems = new List<RelatedItem>(requiredItems);
|
||||
prevRequiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(requiredItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,7 +218,8 @@ namespace Barotrauma.Items.Components
|
||||
item.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (Pusher != null) Pusher.Enabled = false;
|
||||
if (item.body != null) item.body.Enabled = true;
|
||||
IsActive = false;
|
||||
|
||||
@@ -177,18 +240,17 @@ namespace Barotrauma.Items.Components
|
||||
{
|
||||
heldHand = picker.AnimController.GetLimb(LimbType.LeftHand);
|
||||
arm = picker.AnimController.GetLimb(LimbType.LeftArm);
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
heldHand = picker.AnimController.GetLimb(LimbType.RightHand);
|
||||
arm = picker.AnimController.GetLimb(LimbType.RightArm);
|
||||
}
|
||||
|
||||
|
||||
float xDif = (heldHand.SimPosition.X - arm.SimPosition.X) / 2f;
|
||||
float yDif = (heldHand.SimPosition.Y - arm.SimPosition.Y) / 2.5f;
|
||||
//hand simPosition is actually in the wrist so need to move the item out from it slightly
|
||||
item.SetTransform(heldHand.SimPosition + new Vector2(xDif,yDif), 0.0f);
|
||||
item.SetTransform(heldHand.SimPosition + new Vector2(xDif, yDif), 0.0f);
|
||||
}
|
||||
|
||||
picker.DeselectItem(item);
|
||||
@@ -242,12 +304,28 @@ namespace Barotrauma.Items.Components
|
||||
IsActive = false;
|
||||
}
|
||||
|
||||
public bool CanBeAttached()
|
||||
{
|
||||
if (!attachable || !Reattachable) return false;
|
||||
|
||||
//can be attached anywhere in sub editor
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) return true;
|
||||
|
||||
//can be attached anywhere inside hulls
|
||||
if (item.CurrentHull != null) return true;
|
||||
|
||||
return Structure.GetAttachTarget(item.WorldPosition) != null;
|
||||
}
|
||||
|
||||
public bool CanBeDeattached()
|
||||
{
|
||||
if (!attachable || !attached) return true;
|
||||
|
||||
//don't allow deattaching if outside hulls and not in sub editor
|
||||
return item.CurrentHull != null || Screen.Selected == GameMain.SubEditorScreen;
|
||||
//allow deattaching everywhere in sub editor
|
||||
if (Screen.Selected == GameMain.SubEditorScreen) return true;
|
||||
|
||||
//don't allow deattaching if part of a sub and outside hulls
|
||||
return item.Submarine == null || item.CurrentHull != null;
|
||||
}
|
||||
|
||||
public override bool Pick(Character picker)
|
||||
@@ -281,7 +359,7 @@ namespace Barotrauma.Items.Components
|
||||
item.CreateServerEvent(this);
|
||||
if (picker != null)
|
||||
{
|
||||
Networking.GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
|
||||
GameServer.Log(picker.LogName + " detached " + item.Name + " from a wall", ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -290,10 +368,25 @@ namespace Barotrauma.Items.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
private void AttachToWall()
|
||||
public void AttachToWall()
|
||||
{
|
||||
if (!attachable) return;
|
||||
|
||||
//outside hulls/subs -> we need to check if the item is being attached on a structure outside the sub
|
||||
if (item.CurrentHull == null && item.Submarine == null)
|
||||
{
|
||||
Structure attachTarget = Structure.GetAttachTarget(item.WorldPosition);
|
||||
if (attachTarget != null)
|
||||
{
|
||||
if (attachTarget.Submarine != null)
|
||||
{
|
||||
//set to submarine-relative position
|
||||
item.SetTransform(ConvertUnits.ToSimUnits(item.WorldPosition - attachTarget.Submarine.Position), 0.0f, false);
|
||||
}
|
||||
item.Submarine = attachTarget.Submarine;
|
||||
}
|
||||
}
|
||||
|
||||
var containedItems = item.ContainedItems;
|
||||
if (containedItems != null)
|
||||
{
|
||||
@@ -309,12 +402,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
Msg = prevMsg;
|
||||
PickKey = prevPickKey;
|
||||
requiredItems = new List<RelatedItem>(prevRequiredItems);
|
||||
requiredItems = new Dictionary<RelatedItem.RelationType, List<RelatedItem>>(prevRequiredItems);
|
||||
|
||||
attached = true;
|
||||
}
|
||||
|
||||
private void DeattachFromWall()
|
||||
public void DeattachFromWall()
|
||||
{
|
||||
if (!attachable) return;
|
||||
|
||||
@@ -328,15 +421,15 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
public override bool Use(float deltaTime, Character character = null)
|
||||
{
|
||||
if (!attachable || item.body == null) return true;
|
||||
if (!attachable || item.body == null) return (character == null || character.IsKeyDown(InputType.Aim));
|
||||
if (character != null)
|
||||
{
|
||||
if (!character.IsKeyDown(InputType.Aim)) return false;
|
||||
if (character.CurrentHull == null) return false;
|
||||
if (!CanBeAttached()) return false;
|
||||
if (GameMain.Server != null)
|
||||
{
|
||||
item.CreateServerEvent(this);
|
||||
GameServer.Log(character.LogName + " attached " + item.Name+" to a wall", ServerLog.MessageType.ItemInteraction);
|
||||
GameServer.Log(character.LogName + " attached " + item.Name + " to a wall", ServerLog.MessageType.ItemInteraction);
|
||||
}
|
||||
item.Drop();
|
||||
}
|
||||
@@ -356,42 +449,57 @@ namespace Barotrauma.Items.Components
|
||||
if (item.body == null || !item.body.Enabled) return;
|
||||
if (picker == null || !picker.HasEquippedItem(item))
|
||||
{
|
||||
if (Pusher != null) Pusher.Enabled = false;
|
||||
IsActive = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Vector2 swing = Vector2.Zero;
|
||||
if (swingAmount != Vector2.Zero)
|
||||
{
|
||||
swingState += deltaTime;
|
||||
swingState %= 1.0f;
|
||||
if (SwingWhenHolding ||
|
||||
(SwingWhenAiming && picker.IsKeyDown(InputType.Aim)) ||
|
||||
(SwingWhenUsing && picker.IsKeyDown(InputType.Aim) && picker.IsKeyDown(InputType.Use)))
|
||||
{
|
||||
swing = swingAmount * new Vector2(
|
||||
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f, swingState * SwingSpeed * 0.1f) - 0.5f,
|
||||
PerlinNoise.GetPerlin(swingState * SwingSpeed * 0.1f + 0.5f, swingState * SwingSpeed * 0.1f + 0.5f) - 0.5f);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyStatusEffects(ActionType.OnActive, deltaTime, picker);
|
||||
|
||||
if (item.body.Dir != picker.AnimController.Dir) Flip(item);
|
||||
|
||||
item.Submarine = picker.Submarine;
|
||||
|
||||
|
||||
if (picker.HasSelectedItem(item))
|
||||
{
|
||||
picker.AnimController.HoldItem(deltaTime, item, handlePos, holdPos, aimPos, picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero, holdAngle);
|
||||
scaledHandlePos[0] = handlePos[0] * item.Scale;
|
||||
scaledHandlePos[1] = handlePos[1] * item.Scale;
|
||||
picker.AnimController.HoldItem(deltaTime, item, scaledHandlePos, holdPos + swing, aimPos + swing, picker.IsKeyDown(InputType.Aim) && aimPos != Vector2.Zero, holdAngle);
|
||||
}
|
||||
else
|
||||
{
|
||||
Limb equipLimb = null;
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Face) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
|
||||
if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Headset) || picker.Inventory.IsInLimbSlot(item, InvSlotType.Head))
|
||||
{
|
||||
equipLimb = picker.AnimController.GetLimb(LimbType.Head);
|
||||
}
|
||||
else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Torso))
|
||||
else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.InnerClothes) ||
|
||||
picker.Inventory.IsInLimbSlot(item, InvSlotType.OuterClothes))
|
||||
{
|
||||
equipLimb = picker.AnimController.GetLimb(LimbType.Torso);
|
||||
}
|
||||
else if (picker.Inventory.IsInLimbSlot(item, InvSlotType.Legs))
|
||||
{
|
||||
equipLimb = picker.AnimController.GetLimb(LimbType.Waist);
|
||||
}
|
||||
|
||||
if (equipLimb != null)
|
||||
{
|
||||
float itemAngle = (equipLimb.Rotation + holdAngle * picker.AnimController.Dir);
|
||||
|
||||
Matrix itemTransfrom = Matrix.CreateRotationZ(equipLimb.Rotation);
|
||||
Vector2 transformedHandlePos = Vector2.Transform(handlePos[0], itemTransfrom);
|
||||
Vector2 transformedHandlePos = Vector2.Transform(handlePos[0] * item.Scale, itemTransfrom);
|
||||
|
||||
item.body.ResetDynamics();
|
||||
item.SetTransform(equipLimb.SimPosition - transformedHandlePos, itemAngle);
|
||||
@@ -434,20 +542,19 @@ namespace Barotrauma.Items.Components
|
||||
}
|
||||
}
|
||||
|
||||
public void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
|
||||
public override void ServerWrite(NetBuffer msg, Client c, object[] extraData = null)
|
||||
{
|
||||
if (!attachable || body == null)
|
||||
{
|
||||
DebugConsole.ThrowError("Sent an attachment event for an item that's not attachable.");
|
||||
}
|
||||
base.ServerWrite(msg, c, extraData);
|
||||
if (!attachable || body == null) return;
|
||||
|
||||
msg.Write(Attached);
|
||||
msg.Write(body.SimPosition.X);
|
||||
msg.Write(body.SimPosition.Y);
|
||||
}
|
||||
|
||||
public void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime)
|
||||
public override void ClientRead(ServerNetObject type, NetBuffer msg, float sendingTime)
|
||||
{
|
||||
base.ClientRead(type, msg, sendingTime);
|
||||
bool shouldBeAttached = msg.ReadBoolean();
|
||||
Vector2 simPosition = new Vector2(msg.ReadFloat(), msg.ReadFloat());
|
||||
|
||||
@@ -459,9 +566,12 @@ namespace Barotrauma.Items.Components
|
||||
|
||||
if (shouldBeAttached)
|
||||
{
|
||||
Drop(false, null);
|
||||
item.SetTransform(simPosition, 0.0f);
|
||||
AttachToWall();
|
||||
if (!attached)
|
||||
{
|
||||
Drop(false, null);
|
||||
item.SetTransform(simPosition, 0.0f);
|
||||
AttachToWall();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user