3969 lines
160 KiB
C#
3969 lines
160 KiB
C#
using Barotrauma.Networking;
|
|
using FarseerPhysics;
|
|
using Microsoft.Xna.Framework;
|
|
using System;
|
|
using Barotrauma.IO;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Xml.Linq;
|
|
using Barotrauma.Items.Components;
|
|
using FarseerPhysics.Dynamics;
|
|
using Barotrauma.Extensions;
|
|
#if SERVER
|
|
using System.Text;
|
|
#endif
|
|
|
|
namespace Barotrauma
|
|
{
|
|
public enum CharacterTeamType
|
|
{
|
|
None = 0,
|
|
Team1 = 1,
|
|
Team2 = 2,
|
|
FriendlyNPC = 3
|
|
}
|
|
|
|
partial class Character : Entity, IDamageable, ISerializableEntity, IClientSerializable, IServerSerializable
|
|
{
|
|
public static List<Character> CharacterList = new List<Character>();
|
|
|
|
partial void UpdateLimbLightSource(Limb limb);
|
|
|
|
private bool enabled = true;
|
|
public bool Enabled
|
|
{
|
|
get
|
|
{
|
|
return enabled && !Removed;
|
|
}
|
|
set
|
|
{
|
|
if (value == enabled) return;
|
|
|
|
if (Removed)
|
|
{
|
|
enabled = false;
|
|
return;
|
|
}
|
|
|
|
enabled = value;
|
|
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
if (limb.body != null)
|
|
{
|
|
limb.body.Enabled = enabled;
|
|
}
|
|
UpdateLimbLightSource(limb);
|
|
}
|
|
AnimController.Collider.Enabled = value;
|
|
}
|
|
}
|
|
|
|
public Hull PreviousHull = null;
|
|
public Hull CurrentHull = null;
|
|
|
|
/// <summary>
|
|
/// Is the character controlled remotely (either by another player, or a server-side AIController)
|
|
/// </summary>
|
|
public bool IsRemotelyControlled
|
|
{
|
|
get
|
|
{
|
|
if (GameMain.NetworkMember == null)
|
|
{
|
|
return false;
|
|
}
|
|
else if (GameMain.NetworkMember.IsClient)
|
|
{
|
|
//all characters except the client's own character are controlled by the server
|
|
return this != Controlled;
|
|
}
|
|
else
|
|
{
|
|
return IsRemotePlayer;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Is the character controlled by another human player (should always be false in single player)
|
|
/// </summary>
|
|
public bool IsRemotePlayer { get; set; }
|
|
|
|
public bool IsLocalPlayer => Controlled == this;
|
|
public bool IsPlayer => Controlled == this || IsRemotePlayer;
|
|
public bool IsBot => !IsPlayer && AIController is HumanAIController humanAI && humanAI.Enabled;
|
|
|
|
public readonly Dictionary<string, SerializableProperty> Properties;
|
|
public Dictionary<string, SerializableProperty> SerializableProperties
|
|
{
|
|
get { return Properties; }
|
|
}
|
|
|
|
public Key[] Keys
|
|
{
|
|
get { return keys; }
|
|
}
|
|
|
|
protected Key[] keys;
|
|
|
|
private CharacterTeamType teamID;
|
|
public CharacterTeamType TeamID
|
|
{
|
|
get { return teamID; }
|
|
set
|
|
{
|
|
teamID = value;
|
|
if (info != null) { info.TeamID = value; }
|
|
}
|
|
}
|
|
|
|
public bool IsOnPlayerTeam => TeamID == CharacterTeamType.Team1 || TeamID == CharacterTeamType.Team2;
|
|
|
|
public bool IsInstigator => CombatAction != null && CombatAction.IsInstigator;
|
|
public CombatAction CombatAction;
|
|
|
|
public AnimController AnimController;
|
|
|
|
private Vector2 cursorPosition;
|
|
|
|
protected float oxygenAvailable;
|
|
|
|
//seed used to generate this character
|
|
public readonly string Seed;
|
|
protected Item focusedItem;
|
|
private Character selectedCharacter, selectedBy;
|
|
|
|
private const int maxLastAttackerCount = 4;
|
|
|
|
public class Attacker
|
|
{
|
|
public Character Character;
|
|
public float Damage;
|
|
}
|
|
|
|
private readonly List<Attacker> lastAttackers = new List<Attacker>();
|
|
public IEnumerable<Attacker> LastAttackers
|
|
{
|
|
get { return lastAttackers; }
|
|
}
|
|
public Character LastAttacker
|
|
{
|
|
get { return lastAttackers.Count > 0 ? lastAttackers[lastAttackers.Count - 1].Character : null; }
|
|
}
|
|
|
|
public Entity LastDamageSource;
|
|
|
|
public AttackResult LastDamage;
|
|
|
|
public float InvisibleTimer;
|
|
|
|
private CharacterPrefab prefab;
|
|
|
|
public readonly CharacterParams Params;
|
|
public string SpeciesName => Params.SpeciesName;
|
|
public bool IsHumanoid => Params.Humanoid;
|
|
public bool IsHusk => Params.Husk;
|
|
|
|
public string BloodDecalName => Params.BloodDecal;
|
|
|
|
public bool CanSpeak
|
|
{
|
|
get => Params.CanSpeak;
|
|
set => Params.CanSpeak = value;
|
|
}
|
|
|
|
public bool NeedsAir
|
|
{
|
|
get => Params.NeedsAir;
|
|
set => Params.NeedsAir = value;
|
|
}
|
|
|
|
public bool NeedsWater
|
|
{
|
|
get => Params.NeedsWater;
|
|
set => Params.NeedsWater = value;
|
|
}
|
|
|
|
public bool NeedsOxygen => NeedsAir || NeedsWater && !AnimController.InWater;
|
|
|
|
public float Noise
|
|
{
|
|
get => Params.Noise;
|
|
set => Params.Noise = value;
|
|
}
|
|
|
|
public float Visibility
|
|
{
|
|
get => Params.Visibility;
|
|
set => Params.Visibility = value;
|
|
}
|
|
|
|
public bool IsTraitor
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public string TraitorCurrentObjective = "";
|
|
public bool IsHuman => SpeciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase);
|
|
public bool IsMale => Info != null && Info.HasGenders && Info.Gender == Gender.Male;
|
|
public bool IsFemale => Info != null && Info.HasGenders && Info.Gender == Gender.Female;
|
|
|
|
private float attackCoolDown;
|
|
|
|
public List<OrderInfo> CurrentOrders => Info?.CurrentOrders;
|
|
public bool IsDismissed => !GetCurrentOrderWithTopPriority().HasValue;
|
|
|
|
private readonly List<StatusEffect> statusEffects = new List<StatusEffect>();
|
|
|
|
public Entity ViewTarget
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public Vector2 AimRefPosition
|
|
{
|
|
get
|
|
{
|
|
if (ViewTarget == null) { return AnimController.AimSourcePos; }
|
|
|
|
Vector2 viewTargetWorldPos = ViewTarget.WorldPosition;
|
|
if (ViewTarget is Item targetItem)
|
|
{
|
|
Turret turret = targetItem.GetComponent<Turret>();
|
|
if (turret != null)
|
|
{
|
|
viewTargetWorldPos = new Vector2(
|
|
targetItem.WorldRect.X + turret.TransformedBarrelPos.X,
|
|
targetItem.WorldRect.Y - turret.TransformedBarrelPos.Y);
|
|
}
|
|
}
|
|
return Position + (viewTargetWorldPos - WorldPosition);
|
|
}
|
|
}
|
|
|
|
private CharacterInfo info;
|
|
public CharacterInfo Info
|
|
{
|
|
get
|
|
{
|
|
return info;
|
|
}
|
|
set
|
|
{
|
|
if (info != null && info != value) info.Remove();
|
|
|
|
info = value;
|
|
if (info != null) info.Character = this;
|
|
}
|
|
}
|
|
|
|
public string VariantOf { get; private set; }
|
|
|
|
public string Name
|
|
{
|
|
get
|
|
{
|
|
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name : SpeciesName;
|
|
}
|
|
}
|
|
|
|
public string DisplayName
|
|
{
|
|
get
|
|
{
|
|
if (IsPet)
|
|
{
|
|
string petName = (AIController as EnemyAIController).PetBehavior.GetTagName();
|
|
if (!string.IsNullOrEmpty(petName)) { return petName; }
|
|
}
|
|
|
|
if (info != null && !string.IsNullOrWhiteSpace(info.Name)) { return info.Name; }
|
|
var displayName = Params.DisplayName;
|
|
if (string.IsNullOrWhiteSpace(displayName))
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Params.SpeciesTranslationOverride))
|
|
{
|
|
displayName = TextManager.Get($"Character.{SpeciesName}", returnNull: true);
|
|
}
|
|
else
|
|
{
|
|
displayName = TextManager.Get($"Character.{Params.SpeciesTranslationOverride}", returnNull: true);
|
|
}
|
|
}
|
|
return string.IsNullOrWhiteSpace(displayName) ? Name : displayName;
|
|
}
|
|
}
|
|
|
|
//Only used by server logs to determine "true identity" of the player for cases when they're disguised
|
|
public string LogName
|
|
{
|
|
get
|
|
{
|
|
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowDisguises) return Name;
|
|
return info != null && !string.IsNullOrWhiteSpace(info.Name) ? info.Name + (info.DisplayName != info.Name ? " (as " + info.DisplayName + ")" : "") : SpeciesName;
|
|
}
|
|
}
|
|
|
|
private float hideFaceTimer;
|
|
public bool HideFace
|
|
{
|
|
get
|
|
{
|
|
return hideFaceTimer > 0.0f;
|
|
}
|
|
set
|
|
{
|
|
hideFaceTimer = MathHelper.Clamp(hideFaceTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
|
|
if (info != null && info.IsDisguisedAsAnother != HideFace) info.CheckDisguiseStatus(true);
|
|
}
|
|
}
|
|
|
|
public string ConfigPath => Params.File;
|
|
|
|
public float Mass
|
|
{
|
|
get { return AnimController.Mass; }
|
|
}
|
|
|
|
public CharacterInventory Inventory { get; private set; }
|
|
|
|
private Color speechBubbleColor;
|
|
private float speechBubbleTimer;
|
|
|
|
public bool ResetInteract;
|
|
|
|
//text displayed when the character is highlighted if custom interact is set
|
|
public string customInteractHUDText;
|
|
private Action<Character, Character> onCustomInteract;
|
|
public ConversationAction ActiveConversation;
|
|
|
|
public bool AllowCustomInteract
|
|
{
|
|
get { return !IsIncapacitated && Stun <= 0.0f && !Removed; }
|
|
}
|
|
|
|
private float lockHandsTimer;
|
|
public bool LockHands
|
|
{
|
|
get
|
|
{
|
|
return lockHandsTimer > 0.0f;
|
|
}
|
|
set
|
|
{
|
|
lockHandsTimer = MathHelper.Clamp(lockHandsTimer + (value ? 1.0f : -0.5f), 0.0f, 10.0f);
|
|
#if CLIENT
|
|
HintManager.OnHandcuffed(this);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public bool AllowInput
|
|
{
|
|
get { return Stun <= 0.0f && !IsDead && !IsIncapacitated; }
|
|
}
|
|
|
|
public bool CanMove
|
|
{
|
|
get
|
|
{
|
|
if (!AnimController.InWater && !AnimController.CanWalk) { return false; }
|
|
if (!AllowInput) { return false; }
|
|
return true;
|
|
}
|
|
}
|
|
|
|
public bool CanInteract
|
|
{
|
|
get { return AllowInput && IsHumanoid && !LockHands && !Removed && !IsIncapacitated; }
|
|
}
|
|
|
|
public Vector2 CursorPosition
|
|
{
|
|
get { return cursorPosition; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
cursorPosition = value;
|
|
}
|
|
}
|
|
|
|
public Vector2 SmoothedCursorPosition
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public Vector2 CursorWorldPosition
|
|
{
|
|
get { return Submarine == null ? cursorPosition : cursorPosition + Submarine.Position; }
|
|
}
|
|
|
|
public Character FocusedCharacter { get; set; }
|
|
|
|
public Character SelectedCharacter
|
|
{
|
|
get { return selectedCharacter; }
|
|
set
|
|
{
|
|
if (value == selectedCharacter) return;
|
|
if (selectedCharacter != null)
|
|
selectedCharacter.selectedBy = null;
|
|
selectedCharacter = value;
|
|
if (selectedCharacter != null)
|
|
selectedCharacter.selectedBy = this;
|
|
|
|
#if CLIENT
|
|
CharacterHealth.SetHealthBarVisibility(value == null);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public Character SelectedBy
|
|
{
|
|
get { return selectedBy; }
|
|
set
|
|
{
|
|
if (selectedBy != null)
|
|
selectedBy.selectedCharacter = null;
|
|
selectedBy = value;
|
|
if (selectedBy != null)
|
|
selectedBy.selectedCharacter = this;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Items the character has in their hand slots. Doesn't return nulls and only returns items held in both hands once.
|
|
/// </summary>
|
|
public IEnumerable<Item> HeldItems
|
|
{
|
|
get
|
|
{
|
|
var item1 = Inventory?.GetItemInLimbSlot(InvSlotType.RightHand);
|
|
var item2 = Inventory?.GetItemInLimbSlot(InvSlotType.LeftHand);
|
|
if (item1 != null) { yield return item1; }
|
|
if (item2 != null && item2 != item1) { yield return item2; }
|
|
}
|
|
}
|
|
|
|
private float lowPassMultiplier;
|
|
public float LowPassMultiplier
|
|
{
|
|
get { return lowPassMultiplier; }
|
|
set { lowPassMultiplier = MathHelper.Clamp(value, 0.0f, 1.0f); }
|
|
}
|
|
|
|
private float obstructVisionAmount;
|
|
public bool ObstructVision
|
|
{
|
|
get
|
|
{
|
|
return obstructVisionAmount > 0.5f;
|
|
}
|
|
set
|
|
{
|
|
obstructVisionAmount = value ? 1.0f : 0.0f;
|
|
}
|
|
}
|
|
|
|
private float pressureProtection;
|
|
public float PressureProtection
|
|
{
|
|
get { return pressureProtection; }
|
|
set
|
|
{
|
|
pressureProtection = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public const float KnockbackCooldown = 30.0f;
|
|
public float KnockbackCooldownTimer;
|
|
|
|
private float ragdollingLockTimer;
|
|
public bool IsRagdolled;
|
|
public bool IsForceRagdolled;
|
|
public bool dontFollowCursor;
|
|
|
|
public bool IsIncapacitated
|
|
{
|
|
get
|
|
{
|
|
if (IsUnconscious) { return true; }
|
|
return CharacterHealth.Afflictions.Any(a => a.Prefab.AfflictionType == "paralysis" && a.Strength >= a.Prefab.MaxStrength);
|
|
}
|
|
}
|
|
|
|
public bool IsUnconscious
|
|
{
|
|
get { return CharacterHealth.IsUnconscious; }
|
|
}
|
|
|
|
public bool IsPet
|
|
{
|
|
get { return AIController is EnemyAIController enemyController && enemyController.PetBehavior != null; }
|
|
}
|
|
|
|
public float Oxygen
|
|
{
|
|
get { return CharacterHealth.OxygenAmount; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
CharacterHealth.OxygenAmount = MathHelper.Clamp(value, -100.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float OxygenAvailable
|
|
{
|
|
get { return oxygenAvailable; }
|
|
set { oxygenAvailable = MathHelper.Clamp(value, 0.0f, 100.0f); }
|
|
}
|
|
|
|
public bool UseHullOxygen { get; set; } = true;
|
|
|
|
public float Stun
|
|
{
|
|
get { return IsRagdolled ? 1.0f : CharacterHealth.StunTimer; }
|
|
set
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
|
|
SetStun(value, true);
|
|
}
|
|
}
|
|
|
|
public CharacterHealth CharacterHealth { get; private set; }
|
|
|
|
public float Vitality
|
|
{
|
|
get { return CharacterHealth.Vitality; }
|
|
}
|
|
|
|
public float Health
|
|
{
|
|
get { return CharacterHealth.Vitality; }
|
|
}
|
|
|
|
public float HealthPercentage => CharacterHealth.HealthPercentage;
|
|
|
|
public float MaxVitality
|
|
{
|
|
get { return CharacterHealth.MaxVitality; }
|
|
}
|
|
|
|
public float Bloodloss
|
|
{
|
|
get { return CharacterHealth.BloodlossAmount; }
|
|
set
|
|
{
|
|
if (!MathUtils.IsValid(value)) return;
|
|
CharacterHealth.BloodlossAmount = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float Bleeding
|
|
{
|
|
get { return CharacterHealth.GetAfflictionStrength("bleeding", true); }
|
|
}
|
|
|
|
private bool speechImpedimentSet;
|
|
|
|
//value between 0-100 (50 = speech range is reduced by 50%)
|
|
private float speechImpediment;
|
|
public float SpeechImpediment
|
|
{
|
|
get
|
|
{
|
|
if (!CanSpeak || IsUnconscious || Stun > 0.0f || IsDead) return 100.0f;
|
|
return speechImpediment;
|
|
}
|
|
set
|
|
{
|
|
if (value < speechImpediment) return;
|
|
speechImpedimentSet = true;
|
|
speechImpediment = MathHelper.Clamp(value, 0.0f, 100.0f);
|
|
}
|
|
}
|
|
|
|
public float PressureTimer
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
public float DisableImpactDamageTimer
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Current speed of the character's collider. Can be used by status effects to check if the character is moving.
|
|
/// </summary>
|
|
public float CurrentSpeed
|
|
{
|
|
get { return AnimController?.Collider?.LinearVelocity.Length() ?? 0.0f; }
|
|
}
|
|
|
|
private Item _selectedConstruction;
|
|
public Item SelectedConstruction
|
|
{
|
|
get => _selectedConstruction;
|
|
set
|
|
{
|
|
#if CLIENT
|
|
HintManager.OnSetSelectedConstruction(this, _selectedConstruction, value);
|
|
#endif
|
|
_selectedConstruction = value;
|
|
#if CLIENT
|
|
if (Controlled == this)
|
|
{
|
|
if (_selectedConstruction == null)
|
|
{
|
|
GameMain.GameSession?.CrewManager?.ResetCrewList();
|
|
}
|
|
else if (_selectedConstruction.GetComponent<Ladder>() == null)
|
|
{
|
|
GameMain.GameSession?.CrewManager?.AutoHideCrewList();
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public Item FocusedItem
|
|
{
|
|
get { return focusedItem; }
|
|
set { focusedItem = value; }
|
|
}
|
|
|
|
public Item PickingItem
|
|
{
|
|
get;
|
|
set;
|
|
}
|
|
|
|
public virtual AIController AIController
|
|
{
|
|
get { return null; }
|
|
}
|
|
|
|
private bool isDead;
|
|
public bool IsDead
|
|
{
|
|
get { return isDead; }
|
|
set
|
|
{
|
|
if (isDead == value) { return; }
|
|
if (value)
|
|
{
|
|
Kill(CauseOfDeathType.Unknown, causeOfDeathAffliction: null);
|
|
}
|
|
else
|
|
{
|
|
Revive();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool IsObserving => AIController is EnemyAIController enemyAI && enemyAI.Enabled && enemyAI.State == AIState.Observe;
|
|
|
|
public bool EnableDespawn { get; set; } = true;
|
|
|
|
public CauseOfDeath CauseOfDeath
|
|
{
|
|
get;
|
|
private set;
|
|
}
|
|
|
|
//can other characters select (= grab) this character
|
|
public bool CanBeSelected
|
|
{
|
|
get
|
|
{
|
|
return !Removed;
|
|
}
|
|
}
|
|
|
|
private bool canBeDragged = true;
|
|
public bool CanBeDragged
|
|
{
|
|
get
|
|
{
|
|
if (!canBeDragged) { return false; }
|
|
if (Removed || !AnimController.Draggable) { return false; }
|
|
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated || IsPet;
|
|
}
|
|
set { canBeDragged = value; }
|
|
}
|
|
|
|
//can other characters access the inventory of this character
|
|
private bool canInventoryBeAccessed = true;
|
|
public bool CanInventoryBeAccessed
|
|
{
|
|
get
|
|
{
|
|
if (!canInventoryBeAccessed || Removed || Inventory == null) { return false; }
|
|
if (!Inventory.AccessibleWhenAlive)
|
|
{
|
|
return IsDead;
|
|
}
|
|
else
|
|
{
|
|
return IsDead || Stun > 0.0f || LockHands || IsIncapacitated;
|
|
}
|
|
}
|
|
set { canInventoryBeAccessed = value; }
|
|
}
|
|
|
|
public bool CanAim
|
|
{
|
|
get
|
|
{
|
|
return SelectedConstruction == null || SelectedConstruction.GetComponent<Ladder>() != null || (SelectedConstruction.GetComponent<Controller>()?.AllowAiming ?? false);
|
|
}
|
|
}
|
|
|
|
public bool InWater => AnimController?.InWater ?? false;
|
|
|
|
public bool GodMode = false;
|
|
|
|
public CampaignMode.InteractionType CampaignInteractionType;
|
|
|
|
private bool accessRemovedCharacterErrorShown;
|
|
public override Vector2 SimPosition
|
|
{
|
|
get
|
|
{
|
|
if (AnimController?.Collider == null)
|
|
{
|
|
if (!accessRemovedCharacterErrorShown)
|
|
{
|
|
string errorMsg = "Attempted to access a potentially removed character. Character: " + Name + ", id: " + ID + ", removed: " + Removed + ".";
|
|
if (AnimController == null)
|
|
{
|
|
errorMsg += " AnimController == null";
|
|
}
|
|
else if (AnimController.Collider == null)
|
|
{
|
|
errorMsg += " AnimController.Collider == null";
|
|
}
|
|
errorMsg += '\n' + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.NewMessage(errorMsg, Color.Red);
|
|
GameAnalyticsManager.AddErrorEventOnce(
|
|
"Character.SimPosition:AccessRemoved",
|
|
GameAnalyticsSDK.Net.EGAErrorSeverity.Error,
|
|
errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
|
|
accessRemovedCharacterErrorShown = true;
|
|
}
|
|
return Vector2.Zero;
|
|
}
|
|
|
|
return AnimController.Collider.SimPosition;
|
|
}
|
|
}
|
|
|
|
public override Vector2 Position
|
|
{
|
|
get { return ConvertUnits.ToDisplayUnits(SimPosition); }
|
|
}
|
|
|
|
public override Vector2 DrawPosition
|
|
{
|
|
get
|
|
{
|
|
if (AnimController.MainLimb == null) { return Vector2.Zero; }
|
|
return AnimController.MainLimb.body.DrawPosition;
|
|
}
|
|
}
|
|
|
|
public delegate void OnDeathHandler(Character character, CauseOfDeath causeOfDeath);
|
|
public OnDeathHandler OnDeath;
|
|
|
|
public delegate void OnAttackedHandler(Character attacker, AttackResult attackResult);
|
|
public OnAttackedHandler OnAttacked;
|
|
|
|
/// <summary>
|
|
/// Create a new character
|
|
/// </summary>
|
|
/// <param name="characterInfo">The name, gender, config file, etc of the character.</param>
|
|
/// <param name="position">Position in display units.</param>
|
|
/// <param name="seed">RNG seed to use if the character config has randomizable parameters.</param>
|
|
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
|
|
/// <param name="hasAi">Is the character controlled by AI.</param>
|
|
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
|
|
public static Character Create(CharacterInfo characterInfo, Vector2 position, string seed, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, RagdollParams ragdoll = null)
|
|
{
|
|
return Create(characterInfo.SpeciesName, position, seed, characterInfo, id, isRemotePlayer, hasAi, true, ragdoll);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new character
|
|
/// </summary>
|
|
/// <param name="speciesName">Name of the species (or the path to the config file)</param>
|
|
/// <param name="position">Position in display units.</param>
|
|
/// <param name="seed">RNG seed to use if the character config has randomizable parameters.</param>
|
|
/// <param name="characterInfo">The name, gender, etc of the character. Only used for humans, and if the parameter is not given, a random CharacterInfo is generated.</param>
|
|
/// <param name="id">ID to assign to the character. If set to 0, automatically find an available ID.</param>
|
|
/// <param name="isRemotePlayer">Is the character controlled by a remote player.</param>
|
|
/// <param name="hasAi">Is the character controlled by AI.</param>
|
|
/// <param name="createNetworkEvent">Should clients receive a network event about the creation of this character?</param>
|
|
/// <param name="ragdoll">Ragdoll configuration file. If null, will select the default.</param>
|
|
public static Character Create(string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, bool hasAi = true, bool createNetworkEvent = true, RagdollParams ragdoll = null)
|
|
{
|
|
if (speciesName.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
speciesName = Path.GetFileNameWithoutExtension(speciesName).ToLowerInvariant();
|
|
}
|
|
|
|
var prefab = CharacterPrefab.FindBySpeciesName(speciesName);
|
|
if (prefab == null)
|
|
{
|
|
DebugConsole.ThrowError($"Failed to create character \"{speciesName}\". Matching prefab not found.\n" + Environment.StackTrace);
|
|
return null;
|
|
}
|
|
|
|
Character newCharacter = null;
|
|
if (!speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
|
|
var ai = new EnemyAIController(aiCharacter, seed);
|
|
aiCharacter.SetAI(ai);
|
|
newCharacter = aiCharacter;
|
|
}
|
|
else if (hasAi)
|
|
{
|
|
var aiCharacter = new AICharacter(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
|
|
var ai = new HumanAIController(aiCharacter);
|
|
aiCharacter.SetAI(ai);
|
|
newCharacter = aiCharacter;
|
|
}
|
|
else
|
|
{
|
|
newCharacter = new Character(prefab, speciesName, position, seed, characterInfo, id, isRemotePlayer, ragdoll);
|
|
}
|
|
|
|
float healthRegen = newCharacter.Params.Health.ConstantHealthRegeneration;
|
|
if (healthRegen > 0)
|
|
{
|
|
AddDamageReduction("damage", healthRegen);
|
|
}
|
|
float eatingRegen = newCharacter.Params.Health.HealthRegenerationWhenEating;
|
|
if (eatingRegen > 0)
|
|
{
|
|
AddDamageReduction("damage", eatingRegen, ActionType.OnEating);
|
|
}
|
|
float burnReduction = newCharacter.Params.Health.BurnReduction;
|
|
if (burnReduction > 0)
|
|
{
|
|
AddDamageReduction("burn", burnReduction);
|
|
}
|
|
float bleedReduction = newCharacter.Params.Health.BleedingReduction;
|
|
if (bleedReduction > 0)
|
|
{
|
|
AddDamageReduction("bleeding", bleedReduction);
|
|
}
|
|
|
|
void AddDamageReduction(string affliction, float amount, ActionType actionType = ActionType.Always)
|
|
{
|
|
newCharacter.statusEffects.Add(StatusEffect.Load(
|
|
new XElement("StatusEffect", new XAttribute("type", actionType), new XAttribute("target", "Character"),
|
|
new XElement("ReduceAffliction", new XAttribute("identifier", affliction), new XAttribute("amount", amount))), $"automatic damage reduction ({affliction})"));
|
|
}
|
|
|
|
#if SERVER
|
|
if (GameMain.Server != null && Spawner != null && createNetworkEvent)
|
|
{
|
|
Spawner.CreateNetworkEvent(newCharacter, false);
|
|
}
|
|
#endif
|
|
return newCharacter;
|
|
}
|
|
|
|
protected Character(CharacterPrefab prefab, string speciesName, Vector2 position, string seed, CharacterInfo characterInfo = null, ushort id = Entity.NullEntityID, bool isRemotePlayer = false, RagdollParams ragdollParams = null)
|
|
: base(null, id)
|
|
{
|
|
VariantOf = prefab.VariantOf;
|
|
this.Seed = seed;
|
|
this.prefab = prefab;
|
|
MTRandom random = new MTRandom(ToolBox.StringToInt(seed));
|
|
|
|
IsRemotePlayer = isRemotePlayer;
|
|
|
|
oxygenAvailable = 100.0f;
|
|
aiTarget = new AITarget(this);
|
|
|
|
lowPassMultiplier = 1.0f;
|
|
|
|
Properties = SerializableProperty.GetProperties(this);
|
|
|
|
Params = new CharacterParams(prefab.FilePath);
|
|
|
|
Info = characterInfo;
|
|
|
|
speciesName = VariantOf ?? speciesName;
|
|
|
|
if (speciesName.Equals(CharacterPrefab.HumanSpeciesName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (VariantOf != null)
|
|
{
|
|
DebugConsole.ThrowError("The variant system does not yet support humans, sorry. It does support other humanoids though!");
|
|
}
|
|
if (characterInfo == null)
|
|
{
|
|
Info = new CharacterInfo(CharacterPrefab.HumanSpeciesName);
|
|
}
|
|
}
|
|
if (Info != null)
|
|
{
|
|
teamID = Info.TeamID;
|
|
}
|
|
|
|
keys = new Key[Enum.GetNames(typeof(InputType)).Length];
|
|
for (int i = 0; i < Enum.GetNames(typeof(InputType)).Length; i++)
|
|
{
|
|
keys[i] = new Key((InputType)i);
|
|
}
|
|
|
|
var rootElement = prefab.XDocument.Root;
|
|
if (VariantOf != null)
|
|
{
|
|
rootElement = CharacterPrefab.FindBySpeciesName(VariantOf)?.XDocument?.Root;
|
|
}
|
|
var mainElement = rootElement.IsOverride() ? rootElement.FirstElement() : rootElement;
|
|
InitProjSpecific(mainElement);
|
|
|
|
List<XElement> inventoryElements = new List<XElement>();
|
|
List<float> inventoryCommonness = new List<float>();
|
|
List<XElement> healthElements = new List<XElement>();
|
|
List<float> healthCommonness = new List<float>();
|
|
foreach (XElement subElement in mainElement.Elements())
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "inventory":
|
|
inventoryElements.Add(subElement);
|
|
inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
|
break;
|
|
case "health":
|
|
healthElements.Add(subElement);
|
|
healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
|
break;
|
|
case "statuseffect":
|
|
statusEffects.Add(StatusEffect.Load(subElement, Name));
|
|
break;
|
|
}
|
|
}
|
|
if (Params.VariantFile != null)
|
|
{
|
|
XElement overrideElement = Params.VariantFile.Root;
|
|
// Only override if the override file contains matching elements
|
|
if (overrideElement.GetChildElement("inventory") != null)
|
|
{
|
|
inventoryElements.Clear();
|
|
inventoryCommonness.Clear();
|
|
foreach (XElement subElement in overrideElement.GetChildElements("inventory"))
|
|
{
|
|
switch (subElement.Name.ToString().ToLowerInvariant())
|
|
{
|
|
case "inventory":
|
|
inventoryElements.Add(subElement);
|
|
inventoryCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (overrideElement.GetChildElement("health") != null)
|
|
{
|
|
healthElements.Clear();
|
|
healthCommonness.Clear();
|
|
foreach (XElement subElement in overrideElement.GetChildElements("health"))
|
|
{
|
|
healthElements.Add(subElement);
|
|
healthCommonness.Add(subElement.GetAttributeFloat("commonness", 1.0f));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (inventoryElements.Count > 0)
|
|
{
|
|
Inventory = new CharacterInventory(
|
|
inventoryElements.Count == 1 ? inventoryElements[0] : ToolBox.SelectWeightedRandom(inventoryElements, inventoryCommonness, random),
|
|
this);
|
|
}
|
|
if (healthElements.Count == 0)
|
|
{
|
|
CharacterHealth = new CharacterHealth(this);
|
|
}
|
|
else
|
|
{
|
|
var selectedHealthElement = healthElements.Count == 1 ? healthElements[0] : ToolBox.SelectWeightedRandom(healthElements, healthCommonness, random);
|
|
// If there's no limb elements defined in the override variant, let's use the limb health definitions of the original file.
|
|
var limbHealthElement = selectedHealthElement;
|
|
if (Params.VariantFile != null && limbHealthElement.GetChildElement("limb") == null)
|
|
{
|
|
limbHealthElement = Params.OriginalElement.GetChildElement("health");
|
|
}
|
|
CharacterHealth = new CharacterHealth(selectedHealthElement, this, limbHealthElement);
|
|
}
|
|
|
|
if (Params.Husk)
|
|
{
|
|
// Get the non husked name and find the ragdoll with it
|
|
var matchingAffliction = AfflictionPrefab.List
|
|
.Where(p => p.AfflictionType == "huskinfection")
|
|
.Select(p => p as AfflictionPrefabHusk)
|
|
.FirstOrDefault(p => p.TargetSpecies.Any(t => t.Equals(AfflictionHusk.GetNonHuskedSpeciesName(speciesName, p), StringComparison.OrdinalIgnoreCase)));
|
|
string nonHuskedSpeciesName = string.Empty;
|
|
if (matchingAffliction == null)
|
|
{
|
|
DebugConsole.ThrowError("Cannot find a husk infection that matches this species! Please add the speciesnames as 'targets' in the husk affliction prefab definition!");
|
|
// Crashes if we fail to create a ragdoll -> Let's just use some ragdoll so that the user sees the error msg.
|
|
nonHuskedSpeciesName = IsHumanoid ? CharacterPrefab.HumanSpeciesName : "crawler";
|
|
speciesName = nonHuskedSpeciesName;
|
|
}
|
|
else
|
|
{
|
|
nonHuskedSpeciesName = AfflictionHusk.GetNonHuskedSpeciesName(speciesName, matchingAffliction);
|
|
}
|
|
if (ragdollParams == null)
|
|
{
|
|
string name = Params.UseHuskAppendage ? nonHuskedSpeciesName : speciesName;
|
|
ragdollParams = IsHumanoid ? RagdollParams.GetDefaultRagdollParams<HumanRagdollParams>(name) : RagdollParams.GetDefaultRagdollParams<FishRagdollParams>(name) as RagdollParams;
|
|
}
|
|
if (Params.HasInfo && info == null)
|
|
{
|
|
info = new CharacterInfo(nonHuskedSpeciesName);
|
|
}
|
|
}
|
|
|
|
if (IsHumanoid)
|
|
{
|
|
AnimController = new HumanoidAnimController(this, seed, ragdollParams as HumanRagdollParams)
|
|
{
|
|
TargetDir = Direction.Right
|
|
};
|
|
}
|
|
else
|
|
{
|
|
AnimController = new FishAnimController(this, seed, ragdollParams as FishRagdollParams);
|
|
PressureProtection = 100.0f;
|
|
}
|
|
|
|
AnimController.SetPosition(ConvertUnits.ToSimUnits(position));
|
|
|
|
AnimController.FindHull(null);
|
|
if (AnimController.CurrentHull != null) { Submarine = AnimController.CurrentHull.Submarine; }
|
|
|
|
CharacterList.Add(this);
|
|
|
|
//characters start disabled in the multiplayer mode, and are enabled if/when
|
|
// - controlled by the player
|
|
// - client receives a position update from the server
|
|
// - server receives an input message from the client controlling the character
|
|
// - if an AICharacter, the server enables it when close enough to any of the players
|
|
Enabled = GameMain.NetworkMember == null;
|
|
|
|
if (info != null)
|
|
{
|
|
LoadHeadAttachments();
|
|
}
|
|
}
|
|
partial void InitProjSpecific(XElement mainElement);
|
|
|
|
public void ReloadHead(int? headId = null, int hairIndex = -1, int beardIndex = -1, int moustacheIndex = -1, int faceAttachmentIndex = -1)
|
|
{
|
|
if (Info == null) { return; }
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
if (head == null) { return; }
|
|
Info.RecreateHead(headId ?? Info.HeadSpriteId, Info.Race, Info.Gender, hairIndex, beardIndex, moustacheIndex, faceAttachmentIndex);
|
|
#if CLIENT
|
|
head.RecreateSprites();
|
|
#endif
|
|
LoadHeadAttachments();
|
|
}
|
|
|
|
public void LoadHeadAttachments()
|
|
{
|
|
if (Info == null) { return; }
|
|
if (AnimController == null) { return; }
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
if (head == null) { return; }
|
|
// Note that if there are any other wearables on the head, they are removed here.
|
|
head.OtherWearables.ForEach(w => w.Sprite.Remove());
|
|
head.OtherWearables.Clear();
|
|
|
|
//if the element has not been set at this point, the character has no hair and the index should be zero (= no hair)
|
|
if (info.FaceAttachment == null) { info.FaceAttachmentIndex = 0; }
|
|
Info.FaceAttachment?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.FaceAttachment)));
|
|
if (info.BeardElement == null) { info.BeardIndex = 0; }
|
|
Info.BeardElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Beard)));
|
|
if (info.MoustacheElement == null) { info.MoustacheIndex = 0; }
|
|
Info.MoustacheElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Moustache)));
|
|
if (info.HairElement == null) { info.HairIndex = 0; }
|
|
Info.HairElement?.Elements("sprite").ForEach(s => head.OtherWearables.Add(new WearableSprite(s, WearableType.Hair)));
|
|
|
|
#if CLIENT
|
|
head.EnableHuskSprite = Params.Husk;
|
|
head.LoadHerpesSprite();
|
|
head.UpdateWearableTypesToHide();
|
|
#endif
|
|
}
|
|
|
|
public bool IsKeyHit(InputType inputType)
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null && IsRemotePlayer)
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case InputType.Left:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Left)) && (prevDequeuedInput.HasFlag(InputNetFlags.Left));
|
|
case InputType.Right:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Right)) && (prevDequeuedInput.HasFlag(InputNetFlags.Right));
|
|
case InputType.Up:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Up)) && (prevDequeuedInput.HasFlag(InputNetFlags.Up));
|
|
case InputType.Down:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Down)) && (prevDequeuedInput.HasFlag(InputNetFlags.Down));
|
|
case InputType.Run:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Run)) && (prevDequeuedInput.HasFlag(InputNetFlags.Run));
|
|
case InputType.Crouch:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Crouch)) && (prevDequeuedInput.HasFlag(InputNetFlags.Crouch));
|
|
case InputType.Select:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Select); //TODO: clean up the way this input is registered
|
|
case InputType.Deselect:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Deselect);
|
|
case InputType.Health:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Health);
|
|
case InputType.Grab:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Grab);
|
|
case InputType.Use:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Use)) && (prevDequeuedInput.HasFlag(InputNetFlags.Use));
|
|
case InputType.Shoot:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Shoot)) && (prevDequeuedInput.HasFlag(InputNetFlags.Shoot));
|
|
case InputType.Ragdoll:
|
|
return !(dequeuedInput.HasFlag(InputNetFlags.Ragdoll)) && (prevDequeuedInput.HasFlag(InputNetFlags.Ragdoll));
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return keys[(int)inputType].Hit;
|
|
}
|
|
|
|
public bool IsKeyDown(InputType inputType)
|
|
{
|
|
#if SERVER
|
|
if (GameMain.Server != null && IsRemotePlayer)
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case InputType.Left:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Left);
|
|
case InputType.Right:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Right);
|
|
case InputType.Up:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Up);
|
|
case InputType.Down:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Down);
|
|
case InputType.Run:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Run);
|
|
case InputType.Crouch:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Crouch);
|
|
case InputType.Select:
|
|
return false; //TODO: clean up the way this input is registered
|
|
case InputType.Deselect:
|
|
return false;
|
|
case InputType.Aim:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Aim);
|
|
case InputType.Use:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Use);
|
|
case InputType.Shoot:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Shoot);
|
|
case InputType.Attack:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Attack);
|
|
case InputType.Ragdoll:
|
|
return dequeuedInput.HasFlag(InputNetFlags.Ragdoll);
|
|
}
|
|
return false;
|
|
}
|
|
#endif
|
|
if (inputType == InputType.Up || inputType == InputType.Down ||
|
|
inputType == InputType.Left || inputType == InputType.Right)
|
|
{
|
|
var invertControls = CharacterHealth.GetAffliction("invertcontrols");
|
|
if (invertControls != null)
|
|
{
|
|
switch (inputType)
|
|
{
|
|
case InputType.Left:
|
|
inputType = InputType.Right;
|
|
break;
|
|
case InputType.Right:
|
|
inputType = InputType.Left;
|
|
break;
|
|
case InputType.Up:
|
|
inputType = InputType.Down;
|
|
break;
|
|
case InputType.Down:
|
|
inputType = InputType.Up;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return keys[(int)inputType].Held;
|
|
}
|
|
|
|
public void SetInput(InputType inputType, bool hit, bool held)
|
|
{
|
|
keys[(int)inputType].Hit = hit;
|
|
keys[(int)inputType].Held = held;
|
|
keys[(int)inputType].SetState(hit, held);
|
|
}
|
|
|
|
public void ClearInput(InputType inputType)
|
|
{
|
|
keys[(int)inputType].Hit = false;
|
|
keys[(int)inputType].Held = false;
|
|
}
|
|
|
|
public void ClearInputs()
|
|
{
|
|
if (keys == null) return;
|
|
foreach (Key key in keys)
|
|
{
|
|
key.Hit = false;
|
|
key.Held = false;
|
|
}
|
|
}
|
|
|
|
public override string ToString()
|
|
{
|
|
return (info != null && !string.IsNullOrWhiteSpace(info.Name)) ? info.Name : SpeciesName;
|
|
}
|
|
|
|
public void GiveJobItems(WayPoint spawnPoint = null)
|
|
{
|
|
if (info?.Job == null) { return; }
|
|
info.Job.GiveJobItems(this, spawnPoint);
|
|
}
|
|
|
|
public void GiveIdCardTags(WayPoint spawnPoint)
|
|
{
|
|
if (info?.Job == null || spawnPoint == null) { return; }
|
|
|
|
foreach (Item item in Inventory.AllItems)
|
|
{
|
|
if (item?.Prefab.Identifier != "idcard") { continue; }
|
|
foreach (string s in spawnPoint.IdCardTags)
|
|
{
|
|
item.AddTag(s);
|
|
}
|
|
}
|
|
}
|
|
|
|
public float GetSkillLevel(string skillIdentifier)
|
|
{
|
|
return (Info == null || Info.Job == null) ? 0.0f : Info.Job.GetSkillLevel(skillIdentifier);
|
|
}
|
|
|
|
// TODO: reposition? there's also the overrideTargetMovement variable, but it's not in the same manner
|
|
public Vector2? OverrideMovement { get; set; }
|
|
public bool ForceRun { get; set; }
|
|
|
|
public bool IsClimbing => AnimController.Anim == AnimController.Animation.Climbing;
|
|
|
|
public Vector2 GetTargetMovement()
|
|
{
|
|
Vector2 targetMovement = Vector2.Zero;
|
|
if (OverrideMovement.HasValue)
|
|
{
|
|
targetMovement = OverrideMovement.Value;
|
|
}
|
|
else
|
|
{
|
|
if (IsKeyDown(InputType.Left)) { targetMovement.X -= 1.0f; }
|
|
if (IsKeyDown(InputType.Right)) { targetMovement.X += 1.0f; }
|
|
if (IsKeyDown(InputType.Up)) { targetMovement.Y += 1.0f; }
|
|
if (IsKeyDown(InputType.Down)) { targetMovement.Y -= 1.0f; }
|
|
}
|
|
bool run = false;
|
|
if ((IsKeyDown(InputType.Run) && AnimController.ForceSelectAnimationType == AnimationType.NotDefined) || ForceRun)
|
|
{
|
|
|
|
run = CanRun;
|
|
}
|
|
return ApplyMovementLimits(targetMovement, AnimController.GetCurrentSpeed(run));
|
|
}
|
|
|
|
//can't run if
|
|
// - dragging someone
|
|
// - crouching
|
|
// - moving backwards
|
|
public bool CanRun => (SelectedCharacter == null || !SelectedCharacter.CanBeDragged) &&
|
|
(!(AnimController is HumanoidAnimController) || !((HumanoidAnimController)AnimController).Crouching) &&
|
|
!AnimController.IsMovingBackwards;
|
|
|
|
public Vector2 ApplyMovementLimits(Vector2 targetMovement, float currentSpeed)
|
|
{
|
|
//the vertical component is only used for falling through platforms and climbing ladders when not in water,
|
|
//so the movement can't be normalized or the Character would walk slower when pressing down/up
|
|
if (AnimController.InWater)
|
|
{
|
|
float length = targetMovement.Length();
|
|
if (length > 0.0f)
|
|
{
|
|
targetMovement /= length;
|
|
}
|
|
}
|
|
targetMovement *= currentSpeed;
|
|
float maxSpeed = ApplyTemporarySpeedLimits(currentSpeed);
|
|
targetMovement.X = MathHelper.Clamp(targetMovement.X, -maxSpeed, maxSpeed);
|
|
targetMovement.Y = MathHelper.Clamp(targetMovement.Y, -maxSpeed, maxSpeed);
|
|
SpeedMultiplier = greatestPositiveSpeedMultiplier - (1f - greatestNegativeSpeedMultiplier);
|
|
targetMovement *= SpeedMultiplier;
|
|
// Reset, status effects will set the value before the next update
|
|
ResetSpeedMultiplier();
|
|
return targetMovement;
|
|
}
|
|
|
|
private float greatestNegativeSpeedMultiplier = 1f;
|
|
private float greatestPositiveSpeedMultiplier = 1f;
|
|
|
|
/// <summary>
|
|
/// Can be used to modify the character's speed via StatusEffects
|
|
/// </summary>
|
|
public float SpeedMultiplier { get; private set; } = 1;
|
|
|
|
public void StackSpeedMultiplier(float val)
|
|
{
|
|
if (val < 1f)
|
|
{
|
|
if (val < greatestNegativeSpeedMultiplier)
|
|
{
|
|
greatestNegativeSpeedMultiplier = val;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (val > greatestPositiveSpeedMultiplier)
|
|
{
|
|
greatestPositiveSpeedMultiplier = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ResetSpeedMultiplier()
|
|
{
|
|
greatestPositiveSpeedMultiplier = 1f;
|
|
greatestNegativeSpeedMultiplier = 1f;
|
|
}
|
|
|
|
private float greatestNegativeHealthMultiplier = 1f;
|
|
private float greatestPositiveHealthMultiplier = 1f;
|
|
|
|
/// <summary>
|
|
/// Can be used to modify the character's health via StatusEffects
|
|
/// </summary>
|
|
public float HealthMultiplier { get; private set; } = 1;
|
|
|
|
public void StackHealthMultiplier(float val)
|
|
{
|
|
if (val < 1f)
|
|
{
|
|
if (val < greatestNegativeHealthMultiplier)
|
|
{
|
|
greatestNegativeHealthMultiplier = val;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (val > greatestPositiveHealthMultiplier)
|
|
{
|
|
greatestPositiveHealthMultiplier = val;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CalculateHealthMultiplier()
|
|
{
|
|
HealthMultiplier = greatestPositiveHealthMultiplier - (1f - greatestNegativeHealthMultiplier);
|
|
// Reset, status effects should set the values again, if the conditions match
|
|
greatestPositiveHealthMultiplier = 1f;
|
|
greatestNegativeHealthMultiplier = 1f;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Speed reduction from the current limb specific damage. Min 0, max 1.
|
|
/// </summary>
|
|
public float GetTemporarySpeedReduction()
|
|
{
|
|
float reduction = 0;
|
|
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightFoot, excludeSevered: false), reduction);
|
|
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftFoot, excludeSevered: false), reduction);
|
|
if (AnimController is HumanoidAnimController)
|
|
{
|
|
if (AnimController.InWater)
|
|
{
|
|
// Currently only humans use hands for swimming.
|
|
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), reduction);
|
|
reduction = CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), reduction);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
int totalTailLimbs = 0;
|
|
int destroyedTailLimbs = 0;
|
|
foreach (var limb in AnimController.Limbs)
|
|
{
|
|
if (limb.type == LimbType.Tail)
|
|
{
|
|
totalTailLimbs++;
|
|
if (limb.IsSevered)
|
|
{
|
|
destroyedTailLimbs++;
|
|
}
|
|
}
|
|
}
|
|
if (destroyedTailLimbs > 0)
|
|
{
|
|
reduction += MathHelper.Lerp(0, AnimController.InWater ? 1f : 0.5f, (float)destroyedTailLimbs / totalTailLimbs);
|
|
}
|
|
}
|
|
return Math.Clamp(reduction, 0, 1f);
|
|
}
|
|
|
|
private float CalculateMovementPenalty(Limb limb, float sum, float max = 0.4f)
|
|
{
|
|
if (limb != null)
|
|
{
|
|
sum += MathHelper.Lerp(0, max, CharacterHealth.GetLimbDamage(limb, afflictionType: "damage"));
|
|
}
|
|
return Math.Clamp(sum, 0, 1f);
|
|
}
|
|
|
|
public float GetRightHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.RightHand, excludeSevered: false), 0, max: 1);
|
|
public float GetLeftHandPenalty() => CalculateMovementPenalty(AnimController.GetLimb(LimbType.LeftHand, excludeSevered: false), 0, max: 1);
|
|
|
|
public float GetLegPenalty(float startSum = 0)
|
|
{
|
|
float sum = startSum;
|
|
foreach (var limb in AnimController.Limbs)
|
|
{
|
|
switch (limb.type)
|
|
{
|
|
case LimbType.RightFoot:
|
|
case LimbType.LeftFoot:
|
|
sum += CalculateMovementPenalty(limb, sum, max: 0.5f);
|
|
break;
|
|
}
|
|
}
|
|
return Math.Clamp(sum, 0, 1f);
|
|
}
|
|
|
|
public float ApplyTemporarySpeedLimits(float speed)
|
|
{
|
|
float max;
|
|
if (AnimController is HumanoidAnimController)
|
|
{
|
|
max = AnimController.InWater ? 0.5f : 0.7f;
|
|
}
|
|
else
|
|
{
|
|
max = AnimController.InWater ? 0.9f : 0.5f;
|
|
}
|
|
speed *= 1f - MathHelper.Lerp(0, max, GetTemporarySpeedReduction());
|
|
return speed;
|
|
}
|
|
|
|
public void Control(float deltaTime, Camera cam)
|
|
{
|
|
ViewTarget = null;
|
|
if (!AllowInput) { return; }
|
|
|
|
if (Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer))
|
|
{
|
|
SmoothedCursorPosition = cursorPosition;
|
|
}
|
|
else
|
|
{
|
|
//apply some smoothing to the cursor positions of remote players when playing as a client
|
|
//to make aiming look a little less choppy
|
|
Vector2 smoothedCursorDiff = cursorPosition - SmoothedCursorPosition;
|
|
smoothedCursorDiff = NetConfig.InterpolateCursorPositionError(smoothedCursorDiff);
|
|
SmoothedCursorPosition = cursorPosition - smoothedCursorDiff;
|
|
}
|
|
|
|
bool aiControlled = this is AICharacter && Controlled != this && !IsRemotelyControlled;
|
|
if (!aiControlled)
|
|
{
|
|
Vector2 targetMovement = GetTargetMovement();
|
|
AnimController.TargetMovement = targetMovement;
|
|
AnimController.IgnorePlatforms = AnimController.TargetMovement.Y < -0.1f;
|
|
}
|
|
|
|
if (AnimController is HumanoidAnimController)
|
|
{
|
|
((HumanoidAnimController)AnimController).Crouching = IsKeyDown(InputType.Crouch);
|
|
}
|
|
|
|
if (!aiControlled &&
|
|
AnimController.onGround &&
|
|
!AnimController.InWater &&
|
|
AnimController.Anim != AnimController.Animation.UsingConstruction &&
|
|
AnimController.Anim != AnimController.Animation.CPR &&
|
|
(GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient || Controlled == this))
|
|
{
|
|
//Limb head = AnimController.GetLimb(LimbType.Head);
|
|
// Values lower than this seem to cause constantious flipping when the mouse is near the player and the player is running, because the root collider moves after flipping.
|
|
float followMargin = 40;
|
|
if (dontFollowCursor)
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
else if (cursorPosition.X < AnimController.Collider.Position.X - followMargin)
|
|
{
|
|
AnimController.TargetDir = Direction.Left;
|
|
}
|
|
else if (cursorPosition.X > AnimController.Collider.Position.X + followMargin)
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
}
|
|
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
if (GameMain.NetworkMember.IsServer)
|
|
{
|
|
if (!aiControlled)
|
|
{
|
|
if (dequeuedInput.HasFlag(InputNetFlags.FacingLeft))
|
|
{
|
|
AnimController.TargetDir = Direction.Left;
|
|
}
|
|
else
|
|
{
|
|
AnimController.TargetDir = Direction.Right;
|
|
}
|
|
}
|
|
}
|
|
else if (GameMain.NetworkMember.IsClient && Controlled != this)
|
|
{
|
|
if (memState.Count > 0)
|
|
{
|
|
AnimController.TargetDir = memState[0].Direction;
|
|
}
|
|
}
|
|
}
|
|
|
|
#if DEBUG && CLIENT
|
|
if (PlayerInput.KeyHit(Microsoft.Xna.Framework.Input.Keys.F))
|
|
{
|
|
AnimController.ReleaseStuckLimbs();
|
|
if (AIController != null && AIController is EnemyAIController enemyAI)
|
|
{
|
|
enemyAI.LatchOntoAI?.DeattachFromBody(reset: true);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (attackCoolDown > 0.0f)
|
|
{
|
|
attackCoolDown -= deltaTime;
|
|
}
|
|
else if (IsKeyDown(InputType.Attack) && (IsRemotePlayer || Controlled == this || (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)))
|
|
{
|
|
Vector2 attackPos = SimPosition + ConvertUnits.ToSimUnits(cursorPosition - Position);
|
|
List<Body> ignoredBodies = AnimController.Limbs.Select(l => l.body.FarseerBody).ToList();
|
|
ignoredBodies.Add(AnimController.Collider.FarseerBody);
|
|
|
|
var body = Submarine.PickBody(
|
|
SimPosition,
|
|
attackPos,
|
|
ignoredBodies,
|
|
Physics.CollisionCharacter | Physics.CollisionWall);
|
|
|
|
IDamageable attackTarget = null;
|
|
if (body != null)
|
|
{
|
|
attackPos = Submarine.LastPickedPosition;
|
|
|
|
if (body.UserData is Submarine sub)
|
|
{
|
|
body = Submarine.PickBody(
|
|
SimPosition - ((Submarine)body.UserData).SimPosition,
|
|
attackPos - ((Submarine)body.UserData).SimPosition,
|
|
ignoredBodies,
|
|
Physics.CollisionWall);
|
|
|
|
if (body != null)
|
|
{
|
|
attackPos = Submarine.LastPickedPosition + sub.SimPosition;
|
|
attackTarget = body.UserData as IDamageable;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (body.UserData is IDamageable)
|
|
{
|
|
attackTarget = (IDamageable)body.UserData;
|
|
}
|
|
else if (body.UserData is Limb)
|
|
{
|
|
attackTarget = ((Limb)body.UserData).character;
|
|
}
|
|
}
|
|
}
|
|
var currentContexts = GetAttackContexts();
|
|
var validLimbs = AnimController.Limbs.Where(l =>
|
|
{
|
|
if (l.IsSevered || l.IsStuck) { return false; }
|
|
if (l.Disabled) { return false; }
|
|
var attack = l.attack;
|
|
if (attack == null) { return false; }
|
|
if (attack.CoolDownTimer > 0) { return false; }
|
|
if (!attack.IsValidContext(currentContexts)) { return false; }
|
|
if (attackTarget != null)
|
|
{
|
|
if (!attack.IsValidTarget(attackTarget)) { return false; }
|
|
if (attackTarget is ISerializableEntity se && attackTarget is Character)
|
|
{
|
|
if (attack.Conditionals.Any(c => !c.Matches(se))) { return false; }
|
|
}
|
|
}
|
|
if (attack.Conditionals.Any(c => c.TargetSelf && !c.Matches(this))) { return false; }
|
|
return true;
|
|
});
|
|
var sortedLimbs = validLimbs.OrderBy(l => Vector2.DistanceSquared(ConvertUnits.ToDisplayUnits(l.SimPosition), cursorPosition));
|
|
// Select closest
|
|
var attackLimb = sortedLimbs.FirstOrDefault();
|
|
if (attackLimb != null)
|
|
{
|
|
attackLimb.UpdateAttack(deltaTime, attackPos, attackTarget, out AttackResult attackResult);
|
|
if (!attackLimb.attack.IsRunning)
|
|
{
|
|
attackCoolDown = 1.0f;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SelectedConstruction == null || !SelectedConstruction.Prefab.DisableItemUsageWhenSelected)
|
|
{
|
|
foreach (Item item in HeldItems)
|
|
{
|
|
if (IsKeyDown(InputType.Aim) || !item.RequireAimToSecondaryUse)
|
|
{
|
|
item.SecondaryUse(deltaTime, this);
|
|
}
|
|
if (IsKeyDown(InputType.Use) && !item.IsShootable)
|
|
{
|
|
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
|
|
{
|
|
item.Use(deltaTime, this);
|
|
}
|
|
}
|
|
if (IsKeyDown(InputType.Shoot) && item.IsShootable)
|
|
{
|
|
if (!item.RequireAimToUse || IsKeyDown(InputType.Aim))
|
|
{
|
|
item.Use(deltaTime, this);
|
|
}
|
|
#if CLIENT
|
|
else if (item.RequireAimToUse && !IsKeyDown(InputType.Aim))
|
|
{
|
|
HintManager.OnShootWithoutAiming(this, item);
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SelectedConstruction != null)
|
|
{
|
|
if (IsKeyDown(InputType.Aim) || !SelectedConstruction.RequireAimToSecondaryUse)
|
|
{
|
|
SelectedConstruction.SecondaryUse(deltaTime, this);
|
|
}
|
|
if (IsKeyDown(InputType.Use) && SelectedConstruction != null && !SelectedConstruction.IsShootable)
|
|
{
|
|
if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim))
|
|
{
|
|
SelectedConstruction.Use(deltaTime, this);
|
|
}
|
|
}
|
|
if (IsKeyDown(InputType.Shoot) && SelectedConstruction != null && SelectedConstruction.IsShootable)
|
|
{
|
|
if (!SelectedConstruction.RequireAimToUse || IsKeyDown(InputType.Aim))
|
|
{
|
|
SelectedConstruction.Use(deltaTime, this);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (SelectedCharacter != null)
|
|
{
|
|
if (Vector2.DistanceSquared(SelectedCharacter.WorldPosition, WorldPosition) > 90000.0f || !SelectedCharacter.CanBeSelected)
|
|
{
|
|
DeselectCharacter();
|
|
}
|
|
}
|
|
|
|
if (IsRemotelyControlled && keys != null)
|
|
{
|
|
foreach (Key key in keys)
|
|
{
|
|
key.ResetHit();
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CanSeeCharacter(Character target)
|
|
{
|
|
if (target.Removed) { return false; }
|
|
Limb seeingLimb = GetSeeingLimb();
|
|
if (CanSeeTarget(target, seeingLimb)) { return true; }
|
|
if (!target.AnimController.SimplePhysicsEnabled)
|
|
{
|
|
//find the limbs that are furthest from the target's position (from the viewer's point of view)
|
|
Limb leftExtremity = null, rightExtremity = null;
|
|
float leftMostDot = 0.0f, rightMostDot = 0.0f;
|
|
Vector2 dir = target.WorldPosition - WorldPosition;
|
|
Vector2 leftDir = new Vector2(dir.Y, -dir.X);
|
|
Vector2 rightDir = new Vector2(-dir.Y, dir.X);
|
|
foreach (Limb limb in target.AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered || limb == target.AnimController.MainLimb) { continue; }
|
|
if (limb.Hidden) { continue; }
|
|
Vector2 limbDir = limb.WorldPosition - WorldPosition;
|
|
float leftDot = Vector2.Dot(limbDir, leftDir);
|
|
if (leftDot > leftMostDot)
|
|
{
|
|
leftMostDot = leftDot;
|
|
leftExtremity = limb;
|
|
continue;
|
|
}
|
|
float rightDot = Vector2.Dot(limbDir, rightDir);
|
|
if (rightDot > rightMostDot)
|
|
{
|
|
rightMostDot = rightDot;
|
|
rightExtremity = limb;
|
|
continue;
|
|
}
|
|
}
|
|
if (leftExtremity != null && CanSeeTarget(leftExtremity, seeingLimb)) { return true; }
|
|
if (rightExtremity != null && CanSeeTarget(rightExtremity, seeingLimb)) { return true; }
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private Limb GetSeeingLimb()
|
|
{
|
|
return AnimController.GetLimb(LimbType.Head) ?? AnimController.GetLimb(LimbType.Torso) ?? AnimController.MainLimb;
|
|
}
|
|
|
|
public bool CanSeeTarget(ISpatialEntity target, Limb seeingLimb = null)
|
|
{
|
|
seeingLimb ??= GetSeeingLimb();
|
|
if (seeingLimb == null) { return false; }
|
|
ISpatialEntity seeingEntity = AnimController.SimplePhysicsEnabled ? this : seeingLimb as ISpatialEntity;
|
|
// TODO: Could we just use the method below? If not, let's refactor it so that we can.
|
|
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - seeingEntity.WorldPosition);
|
|
Body closestBody;
|
|
//both inside the same sub (or both outside)
|
|
//OR the we're inside, the other character outside
|
|
if (target.Submarine == Submarine || target.Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
|
}
|
|
//we're outside, the other character inside
|
|
else if (Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
|
|
}
|
|
//both inside different subs
|
|
else
|
|
{
|
|
closestBody = Submarine.CheckVisibility(seeingEntity.SimPosition, seeingEntity.SimPosition + diff);
|
|
if (!IsBlocking(closestBody))
|
|
{
|
|
closestBody = Submarine.CheckVisibility(target.SimPosition, target.SimPosition - diff);
|
|
}
|
|
}
|
|
return !IsBlocking(closestBody);
|
|
|
|
bool IsBlocking(Body body)
|
|
{
|
|
if (body == null) { return false; }
|
|
if (body.UserData is Structure wall && wall.CastShadow)
|
|
{
|
|
return wall != target;
|
|
}
|
|
else if (body.UserData is Item item && item != target)
|
|
{
|
|
// TODO: The door collider should be disabled, so this check is probably unnecessary.
|
|
var door = item.GetComponent<Door>();
|
|
if (door != null)
|
|
{
|
|
return !door.CanBeTraversed;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// TODO: ensure that works. CheckVisibility takes positions in sim space, but this method uses world positions
|
|
/// </summary>
|
|
public bool CanSeeCharacter(Character target, Vector2 sourceWorldPos)
|
|
{
|
|
Vector2 diff = ConvertUnits.ToSimUnits(target.WorldPosition - sourceWorldPos);
|
|
Body closestBody;
|
|
if (target.Submarine == null)
|
|
{
|
|
closestBody = Submarine.CheckVisibility(sourceWorldPos, sourceWorldPos + diff);
|
|
if (closestBody == null) { return true; }
|
|
}
|
|
else
|
|
{
|
|
closestBody = Submarine.CheckVisibility(target.WorldPosition, target.WorldPosition - diff);
|
|
if (closestBody == null) { return true; }
|
|
}
|
|
Structure wall = closestBody.UserData as Structure;
|
|
Item item = closestBody.UserData as Item;
|
|
Door door = item?.GetComponent<Door>();
|
|
return (wall == null || !wall.CastShadow) && (door == null || door.CanBeTraversed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// A simple check if the character Dir is towards the target or not. Uses the world coordinates.
|
|
/// </summary>
|
|
public bool IsFacing(Vector2 targetWorldPos) => AnimController.Dir > 0 && targetWorldPos.X > WorldPosition.X || AnimController.Dir < 0 && targetWorldPos.X < WorldPosition.X;
|
|
|
|
public bool HasItem(Item item, bool requireEquipped = false) => requireEquipped ? HasEquippedItem(item) : item.IsOwnedBy(this);
|
|
|
|
public bool HasEquippedItem(Item item)
|
|
{
|
|
if (Inventory == null) { return false; }
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.SlotTypes[i] != InvSlotType.Any && Inventory.GetItemAt(i) == item) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public bool HasEquippedItem(string tagOrIdentifier, bool allowBroken = true)
|
|
{
|
|
if (Inventory == null) { return false; }
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
|
|
var item = Inventory.GetItemAt(i);
|
|
if (item == null) { continue; }
|
|
if (!allowBroken && item.Condition <= 0.0f) { continue; }
|
|
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return true; }
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public Item GetEquippedItem(string tagOrIdentifier)
|
|
{
|
|
if (Inventory == null) { return null; }
|
|
for (int i = 0; i < Inventory.Capacity; i++)
|
|
{
|
|
if (Inventory.SlotTypes[i] == InvSlotType.Any) { continue; }
|
|
var item = Inventory.GetItemAt(i);
|
|
if (item == null) { continue; }
|
|
if (item.Prefab.Identifier == tagOrIdentifier || item.HasTag(tagOrIdentifier)) { return item; }
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public bool CanAccessInventory(Inventory inventory)
|
|
{
|
|
if (!CanInteract || inventory.Locked) { return false; }
|
|
|
|
//the inventory belongs to some other character
|
|
if (inventory.Owner is Character && inventory.Owner != this)
|
|
{
|
|
var owner = (Character)inventory.Owner;
|
|
|
|
//can only be accessed if the character is incapacitated and has been selected
|
|
return SelectedCharacter == owner && owner.CanInventoryBeAccessed;
|
|
}
|
|
|
|
if (inventory.Owner is Item)
|
|
{
|
|
var owner = (Item)inventory.Owner;
|
|
if (!CanInteractWith(owner) && !owner.linkedTo.Any(lt => lt is Item item && item.DisplaySideBySideWhenLinked && CanInteractWith(item))) { return false; }
|
|
ItemContainer container = owner.GetComponents<ItemContainer>().FirstOrDefault(ic => ic.Inventory == inventory);
|
|
if (container != null && !container.HasRequiredItems(this, addMessage: false)) { return false; }
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private float _selectedItemPriority;
|
|
private Item _foundItem;
|
|
/// <summary>
|
|
/// Finds the closest item seeking by identifiers or tags from the world.
|
|
/// Ignores items that are outside or in another team's submarine or in a submarine that is not connected to this submarine.
|
|
/// Also ignores non-interactable items and items that are taken by someone else.
|
|
/// The method is run in steps for performance reasons. So you'll have to provide the reference to the itemIndex.
|
|
/// Returns false while running and true when done.
|
|
/// </summary>
|
|
public bool FindItem(ref int itemIndex, out Item targetItem, IEnumerable<string> identifiers = null, bool ignoreBroken = true,
|
|
IEnumerable<Item> ignoredItems = null, IEnumerable<string> ignoredContainerIdentifiers = null,
|
|
Func<Item, bool> customPredicate = null, Func<Item, float> customPriorityFunction = null, float maxItemDistance = 10000, ISpatialEntity positionalReference = null)
|
|
{
|
|
if (itemIndex == 0)
|
|
{
|
|
_foundItem = null;
|
|
_selectedItemPriority = 0;
|
|
}
|
|
for (int i = 0; i < 10 && itemIndex < Item.ItemList.Count - 1; i++)
|
|
{
|
|
itemIndex++;
|
|
var item = Item.ItemList[itemIndex];
|
|
if (!item.IsInteractable(this)) { continue; }
|
|
if (ignoredItems != null && ignoredItems.Contains(item)) { continue; }
|
|
if (item.Submarine == null) { continue; }
|
|
if (item.Submarine.TeamID != TeamID) { continue; }
|
|
if (item.CurrentHull == null) { continue; }
|
|
if (ignoreBroken && item.Condition <= 0) { continue; }
|
|
if (Submarine != null)
|
|
{
|
|
if (!Submarine.IsEntityFoundOnThisSub(item, true)) { continue; }
|
|
}
|
|
if (customPredicate != null && !customPredicate(item)) { continue; }
|
|
if (identifiers != null && identifiers.None(id => item.Prefab.Identifier == id || item.HasTag(id))) { continue; }
|
|
if (ignoredContainerIdentifiers != null && item.Container != null)
|
|
{
|
|
if (ignoredContainerIdentifiers.Contains(item.ContainerIdentifier)) { continue; }
|
|
}
|
|
if (IsItemTakenBySomeoneElse(item)) { continue; }
|
|
float itemPriority = customPriorityFunction != null ? customPriorityFunction(item) : 1;
|
|
if (itemPriority <= 0) { continue; }
|
|
Entity rootInventoryOwner = item.GetRootInventoryOwner();
|
|
if (rootInventoryOwner is Item ownerItem)
|
|
{
|
|
if (!ownerItem.IsInteractable(this)) { continue; }
|
|
}
|
|
Vector2 itemPos = (rootInventoryOwner ?? item).WorldPosition;
|
|
Vector2 refPos = positionalReference != null ? positionalReference.WorldPosition : WorldPosition;
|
|
float yDist = Math.Abs(refPos.Y - itemPos.Y);
|
|
yDist = yDist > 100 ? yDist * 5 : 0;
|
|
float dist = Math.Abs(refPos.X - itemPos.X) + yDist;
|
|
float distanceFactor = MathHelper.Lerp(1, 0, MathUtils.InverseLerp(0, maxItemDistance, dist));
|
|
itemPriority *= distanceFactor;
|
|
if (itemPriority > _selectedItemPriority)
|
|
{
|
|
_selectedItemPriority = itemPriority;
|
|
_foundItem = item;
|
|
}
|
|
}
|
|
targetItem = _foundItem;
|
|
return itemIndex >= Item.ItemList.Count - 1;
|
|
}
|
|
|
|
public bool IsItemTakenBySomeoneElse(Item item) => item.FindParentInventory(i => i.Owner != this && i.Owner is Character owner && !owner.IsDead && !owner.Removed) != null;
|
|
|
|
public bool CanInteractWith(Character c, float maxDist = 200.0f, bool checkVisibility = true, bool skipDistanceCheck = false)
|
|
{
|
|
if (c == this || Removed || !c.Enabled || !c.CanBeSelected || c.InvisibleTimer > 0.0f) { return false; }
|
|
if (!c.CharacterHealth.UseHealthWindow && !c.CanBeDragged && (c.onCustomInteract == null || !c.AllowCustomInteract)) { return false; }
|
|
|
|
if (!skipDistanceCheck)
|
|
{
|
|
maxDist = ConvertUnits.ToSimUnits(maxDist);
|
|
if (Vector2.DistanceSquared(SimPosition, c.SimPosition) > maxDist * maxDist) { return false; }
|
|
}
|
|
|
|
return checkVisibility ? CanSeeCharacter(c) : true;
|
|
}
|
|
|
|
public bool CanInteractWith(Item item, bool checkLinked = true)
|
|
{
|
|
return CanInteractWith(item, out _, checkLinked);
|
|
}
|
|
|
|
public bool CanInteractWith(Item item, out float distanceToItem, bool checkLinked)
|
|
{
|
|
distanceToItem = -1.0f;
|
|
|
|
bool hidden = item.HiddenInGame;
|
|
#if CLIENT
|
|
if (Screen.Selected == GameMain.SubEditorScreen) { hidden = false; }
|
|
#endif
|
|
if (!CanInteract || hidden || !item.IsInteractable(this)) { return false; }
|
|
|
|
if (item.ParentInventory != null)
|
|
{
|
|
return CanAccessInventory(item.ParentInventory);
|
|
}
|
|
|
|
Wire wire = item.GetComponent<Wire>();
|
|
if (wire != null && item.GetComponent<ConnectionPanel>() == null)
|
|
{
|
|
//locked wires are never interactable
|
|
if (wire.Locked) { return false; }
|
|
if (wire.HiddenInGame && Screen.Selected == GameMain.GameScreen) { return false; }
|
|
|
|
//wires are interactable if the character has selected an item the wire is connected to,
|
|
//and it's disconnected from the other end
|
|
if (wire.Connections[0]?.Item != null && SelectedConstruction == wire.Connections[0].Item)
|
|
{
|
|
return wire.Connections[1] == null;
|
|
}
|
|
if (wire.Connections[1]?.Item != null && SelectedConstruction == wire.Connections[1].Item)
|
|
{
|
|
return wire.Connections[0] == null;
|
|
}
|
|
}
|
|
|
|
if (checkLinked && item.DisplaySideBySideWhenLinked)
|
|
{
|
|
foreach (MapEntity linked in item.linkedTo)
|
|
{
|
|
if (linked is Item linkedItem)
|
|
{
|
|
if (CanInteractWith(linkedItem, out float distToLinked, checkLinked: false))
|
|
{
|
|
distanceToItem = distToLinked;
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (item.InteractDistance == 0.0f && !item.Prefab.Triggers.Any()) { return false; }
|
|
|
|
Pickable pickableComponent = item.GetComponent<Pickable>();
|
|
if (pickableComponent != null && pickableComponent.Picker != this && pickableComponent.Picker != null && !pickableComponent.Picker.IsDead) { return false; }
|
|
|
|
Vector2 characterDirection = Vector2.Transform(Vector2.UnitY, Matrix.CreateRotationZ(AnimController.Collider.Rotation));
|
|
|
|
Vector2 upperBodyPosition = Position + (characterDirection * 20.0f);
|
|
Vector2 lowerBodyPosition = Position - (characterDirection * 60.0f);
|
|
|
|
if (Submarine != null)
|
|
{
|
|
upperBodyPosition += Submarine.Position;
|
|
lowerBodyPosition += Submarine.Position;
|
|
}
|
|
|
|
bool insideTrigger = item.IsInsideTrigger(upperBodyPosition) || item.IsInsideTrigger(lowerBodyPosition);
|
|
if (item.Prefab.Triggers.Count > 0 && !insideTrigger && item.Prefab.RequireBodyInsideTrigger) { return false; }
|
|
|
|
Rectangle itemDisplayRect = new Rectangle(item.InteractionRect.X, item.InteractionRect.Y - item.InteractionRect.Height, item.InteractionRect.Width, item.InteractionRect.Height);
|
|
|
|
// Get the point along the line between lowerBodyPosition and upperBodyPosition which is closest to the center of itemDisplayRect
|
|
Vector2 playerDistanceCheckPosition = Vector2.Clamp(itemDisplayRect.Center.ToVector2(), lowerBodyPosition, upperBodyPosition);
|
|
|
|
// If playerDistanceCheckPosition is inside the itemDisplayRect then we consider the character to within 0 distance of the item
|
|
if (itemDisplayRect.Contains(playerDistanceCheckPosition))
|
|
{
|
|
distanceToItem = 0.0f;
|
|
}
|
|
else
|
|
{
|
|
// Here we get the point on the itemDisplayRect which is closest to playerDistanceCheckPosition
|
|
Vector2 rectIntersectionPoint = new Vector2(
|
|
MathHelper.Clamp(playerDistanceCheckPosition.X, itemDisplayRect.X, itemDisplayRect.Right),
|
|
MathHelper.Clamp(playerDistanceCheckPosition.Y, itemDisplayRect.Y, itemDisplayRect.Bottom));
|
|
distanceToItem = Vector2.Distance(rectIntersectionPoint, playerDistanceCheckPosition);
|
|
}
|
|
|
|
if (distanceToItem > item.InteractDistance && item.InteractDistance > 0.0f) { return false; }
|
|
|
|
if (!item.Prefab.InteractThroughWalls && Screen.Selected != GameMain.SubEditorScreen && !insideTrigger)
|
|
{
|
|
Vector2 itemPosition = item.SimPosition;
|
|
if (Submarine == null && item.Submarine != null)
|
|
{
|
|
//character is outside, item inside
|
|
itemPosition += item.Submarine.SimPosition;
|
|
}
|
|
else if (Submarine != null && item.Submarine == null)
|
|
{
|
|
//character is inside, item outside
|
|
itemPosition -= Submarine.SimPosition;
|
|
}
|
|
else if (Submarine != item.Submarine)
|
|
{
|
|
//character and the item are inside different subs
|
|
itemPosition += item.Submarine.SimPosition;
|
|
itemPosition -= Submarine.SimPosition;
|
|
}
|
|
var body = Submarine.CheckVisibility(SimPosition, itemPosition, ignoreLevel: true);
|
|
if (body != null && body.UserData as Item != item) { return false; }
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Set an action that's invoked when another character interacts with this one.
|
|
/// </summary>
|
|
/// <param name="onCustomInteract">Action invoked when another character interacts with this one. T1 = this character, T2 = the interacting character</param>
|
|
/// <param name="hudText">Displayed on the character when highlighted.</param>
|
|
public void SetCustomInteract(Action<Character, Character> onCustomInteract, string hudText)
|
|
{
|
|
this.onCustomInteract = onCustomInteract;
|
|
customInteractHUDText = hudText;
|
|
}
|
|
|
|
private void TransformCursorPos()
|
|
{
|
|
if (Submarine == null)
|
|
{
|
|
//character is outside but cursor position inside
|
|
if (cursorPosition.Y > Level.Loaded.Size.Y)
|
|
{
|
|
var sub = Submarine.FindContaining(cursorPosition);
|
|
if (sub != null) cursorPosition += sub.Position;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
//character is inside but cursor position is outside
|
|
if (cursorPosition.Y < Level.Loaded.Size.Y)
|
|
{
|
|
cursorPosition -= Submarine.Position;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SelectCharacter(Character character)
|
|
{
|
|
if (character == null) return;
|
|
|
|
SelectedCharacter = character;
|
|
}
|
|
|
|
public void DeselectCharacter()
|
|
{
|
|
if (SelectedCharacter == null) return;
|
|
SelectedCharacter.AnimController?.ResetPullJoints();
|
|
SelectedCharacter = null;
|
|
}
|
|
|
|
public void DoInteractionUpdate(float deltaTime, Vector2 mouseSimPos)
|
|
{
|
|
bool isLocalPlayer = Controlled == this;
|
|
|
|
if (!isLocalPlayer && (this is AICharacter && !IsRemotePlayer))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (ResetInteract)
|
|
{
|
|
ResetInteract = false;
|
|
return;
|
|
}
|
|
|
|
if (!CanInteract)
|
|
{
|
|
SelectedConstruction = null;
|
|
focusedItem = null;
|
|
if (!AllowInput)
|
|
{
|
|
FocusedCharacter = null;
|
|
if (SelectedCharacter != null) DeselectCharacter();
|
|
return;
|
|
}
|
|
}
|
|
|
|
#if CLIENT
|
|
if (isLocalPlayer)
|
|
{
|
|
if (GUI.MouseOn == null &&
|
|
(!CharacterInventory.IsMouseOnInventory() || CharacterInventory.DraggingItemToWorld))
|
|
{
|
|
if (findFocusedTimer <= 0.0f || Screen.Selected == GameMain.SubEditorScreen)
|
|
{
|
|
FocusedCharacter = CanInteract ? FindCharacterAtPosition(mouseSimPos) : null;
|
|
if (FocusedCharacter != null && !CanSeeCharacter(FocusedCharacter)) { FocusedCharacter = null; }
|
|
float aimAssist = GameMain.Config.AimAssistAmount * (AnimController.InWater ? 1.5f : 1.0f);
|
|
if (HeldItems.Any(it => it?.GetComponent<Wire>()?.IsActive ?? false))
|
|
{
|
|
//disable aim assist when rewiring to make it harder to accidentally select items when adding wire nodes
|
|
aimAssist = 0.0f;
|
|
}
|
|
|
|
var item = FindItemAtPosition(mouseSimPos, aimAssist);
|
|
|
|
focusedItem = CanInteract ? item : null;
|
|
findFocusedTimer = 0.05f;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
focusedItem = null;
|
|
}
|
|
findFocusedTimer -= deltaTime;
|
|
}
|
|
#endif
|
|
var head = AnimController.GetLimb(LimbType.Head);
|
|
bool headInWater = head == null ?
|
|
AnimController.InWater :
|
|
head.inWater;
|
|
//climb ladders automatically when pressing up/down inside their trigger area
|
|
Ladder currentLadder = SelectedConstruction?.GetComponent<Ladder>();
|
|
if ((SelectedConstruction == null || currentLadder != null) &&
|
|
!headInWater && Screen.Selected != GameMain.SubEditorScreen)
|
|
{
|
|
bool climbInput = IsKeyDown(InputType.Up) || IsKeyDown(InputType.Down);
|
|
bool isControlled = Controlled == this;
|
|
|
|
Ladder nearbyLadder = null;
|
|
if (isControlled || climbInput)
|
|
{
|
|
float minDist = float.PositiveInfinity;
|
|
foreach (Ladder ladder in Ladder.List)
|
|
{
|
|
if (ladder == currentLadder)
|
|
{
|
|
continue;
|
|
}
|
|
else if (currentLadder != null)
|
|
{
|
|
//only switch from ladder to another if the ladders are above the current ladders and pressing up, or vice versa
|
|
if (ladder.Item.WorldPosition.Y > currentLadder.Item.WorldPosition.Y != IsKeyDown(InputType.Up))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
if (CanInteractWith(ladder.Item, out float dist, checkLinked: false) && dist < minDist)
|
|
{
|
|
minDist = dist;
|
|
nearbyLadder = ladder;
|
|
if (isControlled) ladder.Item.IsHighlighted = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nearbyLadder != null && climbInput)
|
|
{
|
|
if (nearbyLadder.Select(this)) SelectedConstruction = nearbyLadder.Item;
|
|
}
|
|
}
|
|
|
|
if (SelectedCharacter != null && (IsKeyHit(InputType.Grab) || IsKeyHit(InputType.Health))) //Let people use ladders and buttons and stuff when dragging chars
|
|
{
|
|
DeselectCharacter();
|
|
}
|
|
else if (FocusedCharacter != null && IsKeyHit(InputType.Grab) && FocusedCharacter.CanBeDragged && CanInteract)
|
|
{
|
|
SelectCharacter(FocusedCharacter);
|
|
}
|
|
else if (FocusedCharacter != null && !FocusedCharacter.IsIncapacitated && IsKeyHit(InputType.Use) && FocusedCharacter.IsPet && CanInteract)
|
|
{
|
|
(FocusedCharacter.AIController as EnemyAIController).PetBehavior.Play(this);
|
|
}
|
|
else if (FocusedCharacter != null && IsKeyHit(InputType.Health) && FocusedCharacter.CharacterHealth.UseHealthWindow && CanInteract && CanInteractWith(FocusedCharacter, 160f, false))
|
|
{
|
|
if (FocusedCharacter == SelectedCharacter)
|
|
{
|
|
DeselectCharacter();
|
|
#if CLIENT
|
|
if (Controlled == this) CharacterHealth.OpenHealthWindow = null;
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
SelectCharacter(FocusedCharacter);
|
|
#if CLIENT
|
|
if (Controlled == this) CharacterHealth.OpenHealthWindow = FocusedCharacter.CharacterHealth;
|
|
#endif
|
|
}
|
|
}
|
|
else if (FocusedCharacter != null && IsKeyHit(InputType.Use) && FocusedCharacter.onCustomInteract != null && FocusedCharacter.AllowCustomInteract)
|
|
{
|
|
FocusedCharacter.onCustomInteract(FocusedCharacter, this);
|
|
}
|
|
else if (IsKeyHit(InputType.Deselect) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
|
|
{
|
|
SelectedConstruction = null;
|
|
#if CLIENT
|
|
CharacterHealth.OpenHealthWindow = null;
|
|
#endif
|
|
}
|
|
else if (IsKeyHit(InputType.Health) && SelectedConstruction != null && SelectedConstruction.GetComponent<Ladder>() == null)
|
|
{
|
|
SelectedConstruction = null;
|
|
}
|
|
else if (focusedItem != null)
|
|
{
|
|
#if CLIENT
|
|
if (CharacterInventory.DraggingItemToWorld) { return; }
|
|
#endif
|
|
bool canInteract = focusedItem.TryInteract(this);
|
|
#if CLIENT
|
|
if (Controlled == this)
|
|
{
|
|
focusedItem.IsHighlighted = true;
|
|
if (canInteract)
|
|
{
|
|
CharacterHealth.OpenHealthWindow = null;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
public static void UpdateAnimAll(float deltaTime)
|
|
{
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (!c.Enabled || c.AnimController.Frozen) continue;
|
|
|
|
c.AnimController.UpdateAnim(deltaTime);
|
|
}
|
|
}
|
|
|
|
public static void UpdateAll(float deltaTime, Camera cam)
|
|
{
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (!(c is AICharacter) && !c.IsRemotePlayer) continue;
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
//disable AI characters that are far away from all clients and the host's character and not controlled by anyone
|
|
if (c.IsPlayer || (c.IsBot && !c.IsDead))
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
else
|
|
{
|
|
float closestPlayerDist = c.GetDistanceToClosestPlayer();
|
|
if (closestPlayerDist > c.Params.DisableDistance)
|
|
{
|
|
c.Enabled = false;
|
|
if (c.IsDead && c.AIController is EnemyAIController)
|
|
{
|
|
Spawner?.AddToRemoveQueue(c);
|
|
}
|
|
}
|
|
else if (closestPlayerDist < c.Params.DisableDistance * 0.9f)
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
}
|
|
}
|
|
else if (Submarine.MainSub != null)
|
|
{
|
|
//disable AI characters that are far away from the sub and the controlled character
|
|
float distSqr = Vector2.DistanceSquared(Submarine.MainSub.WorldPosition, c.WorldPosition);
|
|
if (Controlled != null)
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, c.WorldPosition));
|
|
}
|
|
else
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.GetPosition(), c.WorldPosition));
|
|
}
|
|
|
|
if (distSqr > MathUtils.Pow2(c.Params.DisableDistance))
|
|
{
|
|
c.Enabled = false;
|
|
if (c.IsDead && c.AIController is EnemyAIController)
|
|
{
|
|
Entity.Spawner?.AddToRemoveQueue(c);
|
|
}
|
|
}
|
|
else if (distSqr < MathUtils.Pow2(c.Params.DisableDistance * 0.9f))
|
|
{
|
|
c.Enabled = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < CharacterList.Count; i++)
|
|
{
|
|
CharacterList[i].Update(deltaTime, cam);
|
|
}
|
|
}
|
|
|
|
public virtual void Update(float deltaTime, Camera cam)
|
|
{
|
|
UpdateProjSpecific(deltaTime, cam);
|
|
|
|
KnockbackCooldownTimer -= deltaTime;
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && this == Controlled && !isSynced) { return; }
|
|
|
|
UpdateDespawn(deltaTime);
|
|
|
|
if (!Enabled) { return; }
|
|
|
|
if (Level.Loaded != null && WorldPosition.Y < Level.MaxEntityDepth ||
|
|
(Submarine != null && Submarine.WorldPosition.Y < Level.MaxEntityDepth))
|
|
{
|
|
Enabled = false;
|
|
Kill(CauseOfDeathType.Pressure, null);
|
|
return;
|
|
}
|
|
|
|
ApplyStatusEffects(ActionType.Always, deltaTime);
|
|
|
|
PreviousHull = CurrentHull;
|
|
CurrentHull = Hull.FindHull(WorldPosition, CurrentHull, true);
|
|
|
|
speechBubbleTimer = Math.Max(0.0f, speechBubbleTimer - deltaTime);
|
|
|
|
obstructVisionAmount = Math.Max(obstructVisionAmount - deltaTime, 0.0f);
|
|
|
|
if (Inventory != null)
|
|
{
|
|
foreach (Item item in Inventory.AllItems)
|
|
{
|
|
if (item.body == null || item.body.Enabled) { continue; }
|
|
item.SetTransform(SimPosition, 0.0f);
|
|
item.Submarine = Submarine;
|
|
}
|
|
}
|
|
|
|
HideFace = false;
|
|
|
|
UpdateSightRange(deltaTime);
|
|
UpdateSoundRange(deltaTime);
|
|
|
|
UpdateAttackers(deltaTime);
|
|
|
|
if (IsDead) { return; }
|
|
|
|
if (GameMain.NetworkMember != null)
|
|
{
|
|
UpdateNetInput();
|
|
}
|
|
else
|
|
{
|
|
AnimController.Frozen = false;
|
|
}
|
|
|
|
DisableImpactDamageTimer -= deltaTime;
|
|
|
|
if (!speechImpedimentSet)
|
|
{
|
|
//if no statuseffect or anything else has set a speech impediment, allow speaking normally
|
|
speechImpediment = 0.0f;
|
|
}
|
|
speechImpedimentSet = false;
|
|
|
|
if (NeedsAir)
|
|
{
|
|
bool protectedFromPressure = PressureProtection > 0.0f;
|
|
//cannot be protected from pressure when below crush depth
|
|
protectedFromPressure = protectedFromPressure && WorldPosition.Y > CharacterHealth.CrushDepth;
|
|
//implode if not protected from pressure, and either outside or in a high-pressure hull
|
|
if (!protectedFromPressure &&
|
|
(AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f))
|
|
{
|
|
if (CharacterHealth.PressureKillDelay <= 0.0f)
|
|
{
|
|
PressureTimer = 100.0f;
|
|
}
|
|
else
|
|
{
|
|
PressureTimer += ((AnimController.CurrentHull == null) ?
|
|
100.0f : AnimController.CurrentHull.LethalPressure) / CharacterHealth.PressureKillDelay * deltaTime;
|
|
}
|
|
|
|
if (PressureTimer >= 100.0f)
|
|
{
|
|
if (Controlled == this) { cam.Zoom = 5.0f; }
|
|
if (GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient)
|
|
{
|
|
Implode();
|
|
if (IsDead) { return; }
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
PressureTimer = 0.0f;
|
|
}
|
|
}
|
|
else if ((GameMain.NetworkMember == null || !GameMain.NetworkMember.IsClient) && WorldPosition.Y < CharacterHealth.CrushDepth)
|
|
{
|
|
//implode if below crush depth, and either outside or in a high-pressure hull
|
|
if (AnimController.CurrentHull == null || AnimController.CurrentHull.LethalPressure >= 80.0f)
|
|
{
|
|
Implode();
|
|
if (IsDead) { return; }
|
|
}
|
|
}
|
|
|
|
ApplyStatusEffects(AnimController.InWater ? ActionType.InWater : ActionType.NotInWater, deltaTime);
|
|
ApplyStatusEffects(ActionType.OnActive, deltaTime);
|
|
|
|
UpdateControlled(deltaTime, cam);
|
|
|
|
//Health effects
|
|
if (NeedsOxygen)
|
|
{
|
|
UpdateOxygen(deltaTime);
|
|
}
|
|
|
|
CalculateHealthMultiplier();
|
|
CharacterHealth.Update(deltaTime);
|
|
|
|
if (IsIncapacitated)
|
|
{
|
|
Stun = Math.Max(5.0f, Stun);
|
|
AnimController.ResetPullJoints();
|
|
SelectedConstruction = null;
|
|
return;
|
|
}
|
|
|
|
UpdateAIChatMessages(deltaTime);
|
|
|
|
//Do ragdoll shenanigans before Stun because it's still technically a stun, innit? Less network updates for us!
|
|
bool allowRagdoll = GameMain.NetworkMember != null ? GameMain.NetworkMember.ServerSettings.AllowRagdollButton : true;
|
|
bool tooFastToUnragdoll = AnimController.Collider.LinearVelocity.LengthSquared() > 1f;
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
|
{
|
|
tooFastToUnragdoll = false;
|
|
}
|
|
if (IsForceRagdolled)
|
|
{
|
|
IsRagdolled = IsForceRagdolled;
|
|
}
|
|
else if (IsRemotePlayer)
|
|
{
|
|
IsRagdolled = IsKeyDown(InputType.Ragdoll);
|
|
}
|
|
//Keep us ragdolled if we were forced or we're too speedy to unragdoll
|
|
else if (allowRagdoll && (!IsRagdolled || !tooFastToUnragdoll))
|
|
{
|
|
if (ragdollingLockTimer > 0.0f)
|
|
{
|
|
ragdollingLockTimer -= deltaTime;
|
|
}
|
|
else
|
|
{
|
|
bool wasRagdolled = IsRagdolled;
|
|
IsRagdolled = IsKeyDown(InputType.Ragdoll); //Handle this here instead of Control because we can stop being ragdolled ourselves
|
|
if (wasRagdolled != IsRagdolled) { ragdollingLockTimer = 0.25f; }
|
|
}
|
|
}
|
|
|
|
lowPassMultiplier = MathHelper.Lerp(lowPassMultiplier, 1.0f, 0.1f);
|
|
|
|
//ragdoll button
|
|
if (IsRagdolled || !CanMove)
|
|
{
|
|
if (AnimController is HumanoidAnimController)
|
|
{
|
|
((HumanoidAnimController)AnimController).Crouching = false;
|
|
}
|
|
AnimController.ResetPullJoints();
|
|
SelectedConstruction = null;
|
|
return;
|
|
}
|
|
|
|
//AI and control stuff
|
|
|
|
Control(deltaTime, cam);
|
|
|
|
bool isNotControlled = Controlled != this;
|
|
|
|
if (isNotControlled && (!(this is AICharacter) || IsRemotePlayer))
|
|
{
|
|
Vector2 mouseSimPos = ConvertUnits.ToSimUnits(cursorPosition);
|
|
DoInteractionUpdate(deltaTime, mouseSimPos);
|
|
}
|
|
|
|
if (SelectedConstruction != null && !CanInteractWith(SelectedConstruction))
|
|
{
|
|
SelectedConstruction = null;
|
|
}
|
|
|
|
if (!IsDead) { LockHands = false; }
|
|
}
|
|
|
|
partial void UpdateControlled(float deltaTime, Camera cam);
|
|
|
|
partial void UpdateProjSpecific(float deltaTime, Camera cam);
|
|
|
|
partial void SetOrderProjSpecific(Order order, string orderOption, int priority);
|
|
|
|
|
|
public void AddAttacker(Character character, float damage)
|
|
{
|
|
Attacker attacker = lastAttackers.FirstOrDefault(a => a.Character == character);
|
|
if (attacker != null)
|
|
{
|
|
lastAttackers.Remove(attacker);
|
|
}
|
|
else
|
|
{
|
|
attacker = new Attacker { Character = character };
|
|
}
|
|
|
|
if (lastAttackers.Count > maxLastAttackerCount)
|
|
{
|
|
lastAttackers.RemoveRange(0, lastAttackers.Count - maxLastAttackerCount);
|
|
}
|
|
|
|
attacker.Damage += damage;
|
|
lastAttackers.Add(attacker);
|
|
}
|
|
|
|
public void ForgiveAttacker(Character character)
|
|
{
|
|
int index;
|
|
if ((index = lastAttackers.FindIndex(a => a.Character == character)) >= 0)
|
|
{
|
|
lastAttackers.RemoveAt(index);
|
|
}
|
|
}
|
|
|
|
private void UpdateAttackers(float deltaTime)
|
|
{
|
|
//slowly forget about damage done by attackers
|
|
foreach (Attacker enemy in LastAttackers)
|
|
{
|
|
float cumulativeDamage = enemy.Damage;
|
|
if (cumulativeDamage > 0)
|
|
{
|
|
float reduction = deltaTime;
|
|
if (cumulativeDamage < 2)
|
|
{
|
|
// If the damage is very low, let's not forget so quickly, or we can't cumulate the damage from repair tools (high frequency, low damage)
|
|
reduction *= 0.5f;
|
|
}
|
|
enemy.Damage = Math.Max(0.0f, enemy.Damage-reduction);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void UpdateOxygen(float deltaTime)
|
|
{
|
|
if (NeedsAir)
|
|
{
|
|
PressureProtection -= deltaTime * 100.0f;
|
|
}
|
|
if (NeedsWater)
|
|
{
|
|
float waterAvailable = 100;
|
|
if (!AnimController.InWater && CurrentHull != null)
|
|
{
|
|
waterAvailable = CurrentHull.WaterPercentage;
|
|
}
|
|
OxygenAvailable += MathHelper.Clamp(waterAvailable - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
|
|
}
|
|
else
|
|
{
|
|
float hullAvailableOxygen = 0.0f;
|
|
if (!AnimController.HeadInWater && AnimController.CurrentHull != null)
|
|
{
|
|
//don't decrease the amount of oxygen in the hull if the character has more oxygen available than the hull
|
|
//(i.e. if the character has some external source of oxygen)
|
|
if (OxygenAvailable * 0.98f < AnimController.CurrentHull.OxygenPercentage && UseHullOxygen)
|
|
{
|
|
AnimController.CurrentHull.Oxygen -= Hull.OxygenConsumptionSpeed * deltaTime;
|
|
}
|
|
hullAvailableOxygen = AnimController.CurrentHull.OxygenPercentage;
|
|
|
|
}
|
|
OxygenAvailable += MathHelper.Clamp(hullAvailableOxygen - oxygenAvailable, -deltaTime * 50.0f, deltaTime * 50.0f);
|
|
}
|
|
UseHullOxygen = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// How far the character is from the closest human player (including spectators)
|
|
/// </summary>
|
|
protected float GetDistanceToClosestPlayer()
|
|
{
|
|
return (float)Math.Sqrt(GetDistanceSqrToClosestPlayer());
|
|
}
|
|
|
|
/// <summary>
|
|
/// How far the character is from the closest human player (including spectators)
|
|
/// </summary>
|
|
protected float GetDistanceSqrToClosestPlayer()
|
|
{
|
|
float distSqr = float.MaxValue;
|
|
foreach (Character otherCharacter in CharacterList)
|
|
{
|
|
if (otherCharacter == this || !otherCharacter.IsRemotePlayer) { continue; }
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.WorldPosition, WorldPosition));
|
|
if (otherCharacter.ViewTarget != null)
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(otherCharacter.ViewTarget.WorldPosition, WorldPosition));
|
|
}
|
|
}
|
|
#if SERVER
|
|
for (int i = 0; i < GameMain.Server.ConnectedClients.Count; i++)
|
|
{
|
|
var spectatePos = GameMain.Server.ConnectedClients[i].SpectatePos;
|
|
if (spectatePos != null)
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(spectatePos.Value, WorldPosition));
|
|
}
|
|
}
|
|
#else
|
|
if (this == Controlled) { return 0.0f; }
|
|
if (controlled != null)
|
|
{
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(Controlled.WorldPosition, WorldPosition));
|
|
}
|
|
distSqr = Math.Min(distSqr, Vector2.DistanceSquared(GameMain.GameScreen.Cam.Position, WorldPosition));
|
|
#endif
|
|
return distSqr;
|
|
}
|
|
|
|
private float despawnTimer;
|
|
private void UpdateDespawn(float deltaTime, bool ignoreThresholds = false)
|
|
{
|
|
if (!EnableDespawn) { return; }
|
|
|
|
//clients don't despawn characters unless the server says so
|
|
if (GameMain.NetworkMember != null && !GameMain.NetworkMember.IsServer) { return; }
|
|
|
|
if (!IsDead || (CauseOfDeath?.Type == CauseOfDeathType.Disconnected && GameMain.GameSession?.Campaign != null)) { return; }
|
|
|
|
int subCorpseCount = 0;
|
|
|
|
if (Submarine != null && !ignoreThresholds)
|
|
{
|
|
subCorpseCount = CharacterList.Count(c => c.IsDead && c.Submarine == Submarine);
|
|
if (subCorpseCount < GameMain.Config.CorpsesPerSubDespawnThreshold) { return; }
|
|
}
|
|
|
|
if (SelectedBy != null)
|
|
{
|
|
despawnTimer = 0.0f;
|
|
return;
|
|
}
|
|
|
|
float distToClosestPlayer = GetDistanceToClosestPlayer();
|
|
if (distToClosestPlayer > Params.DisableDistance)
|
|
{
|
|
//despawn in 1 minute if very far from all human players
|
|
despawnTimer = Math.Max(despawnTimer, GameMain.Config.CorpseDespawnDelay - 60.0f);
|
|
}
|
|
|
|
float despawnPriority = 1.0f;
|
|
if (subCorpseCount > GameMain.Config.CorpsesPerSubDespawnThreshold)
|
|
{
|
|
//despawn faster if there are lots of corpses in the sub (twice as many as the threshold -> despawn twice as fast)
|
|
despawnPriority += (subCorpseCount - GameMain.Config.CorpsesPerSubDespawnThreshold) / (float)GameMain.Config.CorpsesPerSubDespawnThreshold;
|
|
}
|
|
if (AIController is EnemyAIController)
|
|
{
|
|
//enemies despawn faster
|
|
despawnPriority *= 2.0f;
|
|
}
|
|
|
|
despawnTimer += deltaTime * despawnPriority;
|
|
if (despawnTimer < GameMain.Config.CorpseDespawnDelay) { return; }
|
|
|
|
if (IsHuman)
|
|
{
|
|
var containerPrefab =
|
|
ItemPrefab.Prefabs.Find(me => me.Tags.Contains("despawncontainer")) ??
|
|
(MapEntityPrefab.Find(null, identifier: "metalcrate") as ItemPrefab);
|
|
if (containerPrefab == null)
|
|
{
|
|
DebugConsole.NewMessage("Could not spawn a container for a despawned character's items. No item with the tag \"despawncontainer\" or the identifier \"metalcrate\" found.", Color.Red);
|
|
}
|
|
else
|
|
{
|
|
Spawner?.AddToSpawnQueue(containerPrefab, WorldPosition, onSpawned: onItemContainerSpawned);
|
|
}
|
|
|
|
void onItemContainerSpawned(Item item)
|
|
{
|
|
if (Inventory == null) { return; }
|
|
|
|
item.UpdateTransform();
|
|
item.AddTag("name:" + Name);
|
|
if (info?.Job != null) { item.AddTag("job:" + info.Job.Name); }
|
|
|
|
var itemContainer = item?.GetComponent<ItemContainer>();
|
|
if (itemContainer == null) { return; }
|
|
foreach (Item inventoryItem in Inventory.AllItemsMod)
|
|
{
|
|
if (!itemContainer.Inventory.TryPutItem(inventoryItem, user: null))
|
|
{
|
|
//if the item couldn't be put inside the despawn container, just drop it
|
|
inventoryItem.Drop(dropper: this);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Spawner.AddToRemoveQueue(this);
|
|
}
|
|
|
|
public void DespawnNow()
|
|
{
|
|
despawnTimer = GameMain.Config.CorpseDespawnDelay;
|
|
UpdateDespawn(1.0f, ignoreThresholds: true);
|
|
Spawner.Update();
|
|
}
|
|
|
|
public static void RemoveByPrefab(CharacterPrefab prefab)
|
|
{
|
|
if (CharacterList == null) { return; }
|
|
List<Character> list = new List<Character>(CharacterList);
|
|
foreach (Character character in list)
|
|
{
|
|
if (character.prefab == prefab)
|
|
{
|
|
character.Remove();
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly float maxAIRange = 20000;
|
|
private readonly float aiTargetChangeSpeed = 5;
|
|
|
|
private void UpdateSightRange(float deltaTime)
|
|
{
|
|
if (aiTarget == null) { return; }
|
|
float minRange = Math.Clamp((float)Math.Sqrt(Mass) * Visibility, 250, 1000);
|
|
float massFactor = (float)Math.Sqrt(Mass / 20);
|
|
float targetRange = Math.Min(minRange + massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Visibility, maxAIRange);
|
|
float newRange = MathHelper.SmoothStep(aiTarget.SightRange, targetRange, deltaTime * aiTargetChangeSpeed);
|
|
if (!float.IsNaN(newRange))
|
|
{
|
|
aiTarget.SightRange = newRange;
|
|
}
|
|
}
|
|
|
|
private void UpdateSoundRange(float deltaTime)
|
|
{
|
|
if (aiTarget == null) { return; }
|
|
if (IsDead)
|
|
{
|
|
aiTarget.SoundRange = 0;
|
|
}
|
|
else
|
|
{
|
|
float massFactor = (float)Math.Sqrt(Mass / 10);
|
|
float targetRange = Math.Min(massFactor * AnimController.Collider.LinearVelocity.Length() * 2 * Noise, maxAIRange);
|
|
float newRange = MathHelper.SmoothStep(aiTarget.SoundRange, targetRange, deltaTime * aiTargetChangeSpeed);
|
|
if (!float.IsNaN(newRange))
|
|
{
|
|
aiTarget.SoundRange = newRange;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool CanHearCharacter(Character speaker)
|
|
{
|
|
if (speaker == null || speaker.SpeechImpediment > 100.0f) { return false; }
|
|
if (speaker == this) { return true; }
|
|
ChatMessageType messageType = ChatMessage.CanUseRadio(speaker) && ChatMessage.CanUseRadio(this) ?
|
|
ChatMessageType.Radio :
|
|
ChatMessageType.Default;
|
|
return !string.IsNullOrEmpty(ChatMessage.ApplyDistanceEffect("message", messageType, speaker, this));
|
|
}
|
|
|
|
public void SetOrder(Order order, string orderOption, int priority, Character orderGiver, bool speak = true)
|
|
{
|
|
//set the character order only if the character is close enough to hear the message
|
|
if (orderGiver != null && !CanHearCharacter(orderGiver)) { return; }
|
|
|
|
// If there's another character operating the same device, make them dismiss themself
|
|
if (order != null && order.Category == OrderCategory.Operate && order.TargetEntity != null)
|
|
{
|
|
foreach (var character in CharacterList)
|
|
{
|
|
if (character == this) { continue; }
|
|
if (character.TeamID != TeamID) { continue; }
|
|
if (!HumanAIController.IsActive(character)) { continue; }
|
|
foreach (var currentOrder in character.CurrentOrders)
|
|
{
|
|
if (currentOrder.Order == null) { continue; }
|
|
if (currentOrder.Order.Category != OrderCategory.Operate) { continue; }
|
|
if (currentOrder.Order.Identifier != order.Identifier) { continue; }
|
|
if (currentOrder.Order.TargetEntity != order.TargetEntity) { continue; }
|
|
character.SetOrder(Order.GetPrefab("dismissed"), Order.GetDismissOrderOption(currentOrder), currentOrder.ManualPriority, character);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Prevent adding duplicate orders (same identifier and same option)
|
|
RemoveDuplicateOrders(order, orderOption);
|
|
|
|
OrderInfo newOrderInfo = new OrderInfo(order, orderOption, priority);
|
|
AddCurrentOrder(newOrderInfo);
|
|
if (AIController is HumanAIController humanAI)
|
|
{
|
|
humanAI.SetOrder(order, orderOption, priority, orderGiver, speak);
|
|
}
|
|
SetOrderProjSpecific(order, orderOption, priority);
|
|
}
|
|
|
|
private void AddCurrentOrder(OrderInfo newOrder)
|
|
{
|
|
if (newOrder.Order == null || newOrder.Order.Identifier == "dismissed")
|
|
{
|
|
if (!string.IsNullOrEmpty(newOrder.OrderOption))
|
|
{
|
|
if (CurrentOrders.Any(o => o.MatchesDismissedOrder(newOrder.OrderOption)))
|
|
{
|
|
var dismissedOrderInfo = CurrentOrders.First(o => o.MatchesDismissedOrder(newOrder.OrderOption));
|
|
int dismissedOrderPriority = dismissedOrderInfo.ManualPriority;
|
|
CurrentOrders.Remove(dismissedOrderInfo);
|
|
for (int i = 0; i < CurrentOrders.Count; i++)
|
|
{
|
|
var orderInfo = CurrentOrders[i];
|
|
if (orderInfo.ManualPriority < dismissedOrderPriority)
|
|
{
|
|
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CurrentOrders.Clear();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int i = 0; i < CurrentOrders.Count; i++)
|
|
{
|
|
var orderInfo = CurrentOrders[i];
|
|
if (orderInfo.ManualPriority <= newOrder.ManualPriority)
|
|
{
|
|
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority - 1);
|
|
}
|
|
}
|
|
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
|
|
CurrentOrders.Add(newOrder);
|
|
// Sort the current orders so the one with the highest priority comes first
|
|
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
|
|
}
|
|
}
|
|
|
|
private void RemoveDuplicateOrders(Order order, string option)
|
|
{
|
|
int? priorityOfRemoved = null;
|
|
for (int i = CurrentOrders.Count - 1; i >= 0; i--)
|
|
{
|
|
var orderInfo = CurrentOrders[i];
|
|
if (orderInfo.MatchesOrder(order, option))
|
|
{
|
|
priorityOfRemoved = orderInfo.ManualPriority;
|
|
CurrentOrders.RemoveAt(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!priorityOfRemoved.HasValue) { return; }
|
|
|
|
for (int i = 0; i < CurrentOrders.Count; i++)
|
|
{
|
|
var orderInfo = CurrentOrders[i];
|
|
if (orderInfo.ManualPriority < priorityOfRemoved.Value)
|
|
{
|
|
CurrentOrders[i] = new OrderInfo(orderInfo, orderInfo.ManualPriority + 1);
|
|
}
|
|
}
|
|
|
|
CurrentOrders.RemoveAll(order => order.ManualPriority <= 0);
|
|
// Sort the current orders so the one with the highest priority comes first
|
|
CurrentOrders.Sort((x, y) => y.ManualPriority.CompareTo(x.ManualPriority));
|
|
}
|
|
|
|
public OrderInfo? GetCurrentOrderWithTopPriority()
|
|
{
|
|
return GetCurrentOrder(orderInfo =>
|
|
{
|
|
if (orderInfo.Order == null) { return false; }
|
|
if (orderInfo.Order.Identifier == "dismissed") { return false; }
|
|
if (orderInfo.ManualPriority < 1) { return false; }
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public OrderInfo? GetCurrentOrder(Order order, string option)
|
|
{
|
|
return GetCurrentOrder(orderInfo =>
|
|
{
|
|
return orderInfo.MatchesOrder(order, option);
|
|
});
|
|
}
|
|
|
|
private OrderInfo? GetCurrentOrder(Func<OrderInfo, bool> predicate)
|
|
{
|
|
if (CurrentOrders != null && CurrentOrders.Any(predicate))
|
|
{
|
|
return CurrentOrders.First(predicate);
|
|
}
|
|
else
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
|
|
private readonly List<AIChatMessage> aiChatMessageQueue = new List<AIChatMessage>();
|
|
|
|
//key = identifier, value = time the message was sent
|
|
private readonly Dictionary<string, float> prevAiChatMessages = new Dictionary<string, float>();
|
|
|
|
public void DisableLine(string identifier)
|
|
{
|
|
if (!string.IsNullOrEmpty(identifier))
|
|
{
|
|
prevAiChatMessages[identifier] = (float)Timing.TotalTime;
|
|
}
|
|
}
|
|
|
|
public void Speak(string message, ChatMessageType? messageType = null, float delay = 0.0f, string identifier = "", float minDurationBetweenSimilar = 0.0f)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
if (string.IsNullOrEmpty(message)) { return; }
|
|
|
|
if (prevAiChatMessages.ContainsKey(identifier) &&
|
|
prevAiChatMessages[identifier] < Timing.TotalTime - minDurationBetweenSimilar)
|
|
{
|
|
prevAiChatMessages.Remove(identifier);
|
|
}
|
|
|
|
//already sent a similar message a moment ago
|
|
if (!string.IsNullOrEmpty(identifier) && minDurationBetweenSimilar > 0.0f &&
|
|
(aiChatMessageQueue.Any(m => m.Identifier == identifier) || prevAiChatMessages.ContainsKey(identifier)))
|
|
{
|
|
return;
|
|
}
|
|
aiChatMessageQueue.Add(new AIChatMessage(message, messageType, identifier, delay));
|
|
}
|
|
|
|
private void UpdateAIChatMessages(float deltaTime)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) return;
|
|
|
|
List<AIChatMessage> sentMessages = new List<AIChatMessage>();
|
|
foreach (AIChatMessage message in aiChatMessageQueue)
|
|
{
|
|
message.SendDelay -= deltaTime;
|
|
if (message.SendDelay > 0.0f) continue;
|
|
|
|
if (message.MessageType == null)
|
|
{
|
|
message.MessageType = ChatMessage.CanUseRadio(this) ? ChatMessageType.Radio : ChatMessageType.Default;
|
|
}
|
|
#if CLIENT
|
|
if (GameMain.GameSession?.CrewManager != null && GameMain.GameSession.CrewManager.IsSinglePlayer)
|
|
{
|
|
string modifiedMessage = ChatMessage.ApplyDistanceEffect(message.Message, message.MessageType.Value, this, Controlled);
|
|
if (!string.IsNullOrEmpty(modifiedMessage))
|
|
{
|
|
GameMain.GameSession.CrewManager.AddSinglePlayerChatMessage(info.Name, modifiedMessage, message.MessageType.Value, this);
|
|
}
|
|
}
|
|
#endif
|
|
#if SERVER
|
|
if (GameMain.Server != null && message.MessageType != ChatMessageType.Order)
|
|
{
|
|
GameMain.Server.SendChatMessage(message.Message, message.MessageType.Value, null, this);
|
|
}
|
|
#endif
|
|
ShowSpeechBubble(2.0f, ChatMessage.MessageColor[(int)message.MessageType.Value]);
|
|
sentMessages.Add(message);
|
|
}
|
|
|
|
foreach (AIChatMessage sent in sentMessages)
|
|
{
|
|
sent.SendTime = Timing.TotalTime;
|
|
aiChatMessageQueue.Remove(sent);
|
|
if (!string.IsNullOrEmpty(sent.Identifier))
|
|
{
|
|
prevAiChatMessages[sent.Identifier] = (float)sent.SendTime;
|
|
}
|
|
}
|
|
|
|
if (prevAiChatMessages.Count > 100)
|
|
{
|
|
List<string> toRemove = new List<string>();
|
|
foreach (KeyValuePair<string,float> prevMessage in prevAiChatMessages)
|
|
{
|
|
if (prevMessage.Value < Timing.TotalTime - 60.0f)
|
|
{
|
|
toRemove.Add(prevMessage.Key);
|
|
}
|
|
}
|
|
foreach (string identifier in toRemove)
|
|
{
|
|
prevAiChatMessages.Remove(identifier);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
public void ShowSpeechBubble(float duration, Color color)
|
|
{
|
|
speechBubbleTimer = Math.Max(speechBubbleTimer, duration);
|
|
speechBubbleColor = color;
|
|
}
|
|
|
|
public void SetAllDamage(float damageAmount, float bleedingDamageAmount, float burnDamageAmount)
|
|
{
|
|
CharacterHealth.SetAllDamage(damageAmount, bleedingDamageAmount, burnDamageAmount);
|
|
}
|
|
|
|
public AttackResult AddDamage(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = true)
|
|
{
|
|
return ApplyAttack(attacker, worldPosition, attack, deltaTime, playSound, null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Apply the specified attack to this character. If the targetLimb is not specified, the limb closest to worldPosition will receive the damage.
|
|
/// </summary>
|
|
public AttackResult ApplyAttack(Character attacker, Vector2 worldPosition, Attack attack, float deltaTime, bool playSound = false, Limb targetLimb = null)
|
|
{
|
|
if (Removed)
|
|
{
|
|
string errorMsg = "Tried to apply an attack to a removed character (" + Name + ").\n" + Environment.StackTrace.CleanupStackTrace();
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Character.ApplyAttack:RemovedCharacter", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
return new AttackResult();
|
|
}
|
|
|
|
Limb limbHit = targetLimb;
|
|
|
|
float attackImpulse = attack.TargetImpulse + attack.TargetForce * deltaTime;
|
|
|
|
var attackResult = targetLimb == null ?
|
|
AddDamage(worldPosition, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, out limbHit, attacker, attack.DamageMultiplier) :
|
|
DamageLimb(worldPosition, targetLimb, attack.Afflictions.Keys, attack.Stun, playSound, attackImpulse, attacker, attack.DamageMultiplier);
|
|
|
|
if (limbHit == null) { return new AttackResult(); }
|
|
Vector2 forceWorld = attack.TargetImpulseWorld + attack.TargetForceWorld;
|
|
if (attacker != null)
|
|
{
|
|
forceWorld.X *= attacker.AnimController.Dir;
|
|
}
|
|
limbHit.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
var mainLimb = limbHit.character.AnimController.MainLimb;
|
|
if (limbHit != mainLimb)
|
|
{
|
|
// Always add force to mainlimb
|
|
mainLimb.body?.ApplyLinearImpulse(forceWorld * deltaTime, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
#if SERVER
|
|
if (attacker is Character attackingCharacter && attackingCharacter.AIController == null)
|
|
{
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.Append(GameServer.CharacterLogName(this) + " attacked by " + GameServer.CharacterLogName(attackingCharacter) + ".");
|
|
if (attackResult.Afflictions != null)
|
|
{
|
|
foreach (Affliction affliction in attackResult.Afflictions)
|
|
{
|
|
if (affliction.Strength == 0.0f) continue;
|
|
sb.Append($" {affliction.Prefab.Name}: {affliction.Strength}");
|
|
}
|
|
}
|
|
GameServer.Log(sb.ToString(), ServerLog.MessageType.Attack);
|
|
}
|
|
#endif
|
|
// Don't allow beheading for monster attacks, because it happens too frequently (crawlers/tigerthreshers etc attacking each other -> they will most often target to the head)
|
|
TrySeverLimbJoints(limbHit, attack.SeverLimbsProbability, attackResult.Damage, allowBeheading: attacker == null || attacker.IsHuman || attacker.IsPlayer);
|
|
|
|
return attackResult;
|
|
}
|
|
|
|
public void TrySeverLimbJoints(Limb targetLimb, float severLimbsProbability, float damage, bool allowBeheading)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
#if DEBUG
|
|
if (targetLimb.character != this)
|
|
{
|
|
DebugConsole.ThrowError($"{Name} is attempting to sever joints of {targetLimb.character.Name}!");
|
|
return;
|
|
}
|
|
#endif
|
|
if (damage < targetLimb.Params.MinSeveranceDamage) { return; }
|
|
if (!IsDead)
|
|
{
|
|
if (!allowBeheading && targetLimb.type == LimbType.Head) { return; }
|
|
if (!targetLimb.CanBeSeveredAlive) { return; }
|
|
}
|
|
bool wasSevered = false;
|
|
float random = Rand.Value();
|
|
foreach (LimbJoint joint in AnimController.LimbJoints)
|
|
{
|
|
if (!joint.CanBeSevered) { continue; }
|
|
// Limb A is where we usually create the joints from. Let's not allow severing when the "parent" limb is hit, or the head can pop off when we hit the torso, for example.
|
|
if (joint.LimbB != targetLimb) { continue; }
|
|
float probability = severLimbsProbability;
|
|
if (!IsDead)
|
|
{
|
|
probability *= joint.Params.SeveranceProbabilityModifier;
|
|
}
|
|
if (probability <= 0) { continue; }
|
|
if (random > probability) { continue; }
|
|
bool severed = AnimController.SeverLimbJoint(joint);
|
|
if (!wasSevered)
|
|
{
|
|
wasSevered = severed;
|
|
}
|
|
if (severed)
|
|
{
|
|
Limb otherLimb = joint.LimbA == targetLimb ? joint.LimbB : joint.LimbA;
|
|
otherLimb.body.ApplyLinearImpulse(targetLimb.LinearVelocity * targetLimb.Mass, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
|
ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
|
targetLimb.ApplyStatusEffects(ActionType.OnSevered, 1.0f);
|
|
}
|
|
}
|
|
if (wasSevered && targetLimb.character.AIController is EnemyAIController enemyAI)
|
|
{
|
|
enemyAI.ReevaluateAttacks();
|
|
}
|
|
}
|
|
|
|
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse = 0.0f, Character attacker = null)
|
|
{
|
|
return AddDamage(worldPosition, afflictions, stun, playSound, attackImpulse, out _, attacker);
|
|
}
|
|
|
|
public AttackResult AddDamage(Vector2 worldPosition, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, out Limb hitLimb, Character attacker = null, float damageMultiplier = 1)
|
|
{
|
|
hitLimb = null;
|
|
|
|
if (Removed) { return new AttackResult(); }
|
|
|
|
if (attacker != null && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
|
|
{
|
|
if (attacker.TeamID == TeamID) { return new AttackResult(); }
|
|
}
|
|
|
|
float closestDistance = 0.0f;
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
float distance = Vector2.DistanceSquared(worldPosition, limb.WorldPosition);
|
|
if (hitLimb == null || distance < closestDistance)
|
|
{
|
|
hitLimb = limb;
|
|
closestDistance = distance;
|
|
}
|
|
}
|
|
|
|
return DamageLimb(worldPosition, hitLimb, afflictions, stun, playSound, attackImpulse, attacker, damageMultiplier);
|
|
}
|
|
|
|
public void RecordKill(Character target)
|
|
{
|
|
if (!IsOnPlayerTeam) { return; }
|
|
if (GameMain.Config.KilledCreatures.Any(name => name.Equals(target.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
|
|
GameMain.Config.KilledCreatures.Add(target.SpeciesName);
|
|
AddEncounter(target);
|
|
}
|
|
|
|
public void AddEncounter(Character other)
|
|
{
|
|
if (!IsOnPlayerTeam) { return; }
|
|
if (GameMain.Config.EncounteredCreatures.Any(name => name.Equals(other.SpeciesName, StringComparison.OrdinalIgnoreCase))) { return; }
|
|
GameMain.Config.EncounteredCreatures.Add(other.SpeciesName);
|
|
GameMain.Config.RecentlyEncounteredCreatures.Add(other.SpeciesName);
|
|
}
|
|
|
|
public AttackResult DamageLimb(Vector2 worldPosition, Limb hitLimb, IEnumerable<Affliction> afflictions, float stun, bool playSound, float attackImpulse, Character attacker = null, float damageMultiplier = 1)
|
|
{
|
|
if (Removed) { return new AttackResult(); }
|
|
|
|
//character inside the sub received damage from a monster outside the sub
|
|
//can happen during normal gameplay if someone for example fires a ranged weapon from outside,
|
|
//the intention of this error message is to diagnose an issue with monsters being able to damage characters from outside
|
|
|
|
// Disabled, because this happens every now and then when the monsters can get in and out of the sub.
|
|
|
|
// if (attacker?.AIController is EnemyAIController && Submarine != null && attacker.Submarine == null)
|
|
// {
|
|
// string errorMsg = $"Character {Name} received damage from outside the sub while inside (attacker: {attacker.Name})";
|
|
// GameAnalyticsManager.AddErrorEventOnce("Character.DamageLimb:DamageFromOutside" + Name + attacker.Name,
|
|
// GameAnalyticsSDK.Net.EGAErrorSeverity.Warning,
|
|
// errorMsg + "\n" + Environment.StackTrace.CleanupStackTrace());
|
|
//#if DEBUG
|
|
// DebugConsole.ThrowError(errorMsg);
|
|
//#endif
|
|
// }
|
|
|
|
SetStun(stun);
|
|
|
|
if (attacker != null && attacker != this && GameMain.NetworkMember != null && !GameMain.NetworkMember.ServerSettings.AllowFriendlyFire)
|
|
{
|
|
if (attacker.TeamID == TeamID) { return new AttackResult(); }
|
|
}
|
|
|
|
Vector2 dir = hitLimb.WorldPosition - worldPosition;
|
|
if (Math.Abs(attackImpulse) > 0.0f)
|
|
{
|
|
Vector2 diff = dir;
|
|
if (diff == Vector2.Zero) { diff = Rand.Vector(1.0f); }
|
|
Vector2 impulse = Vector2.Normalize(diff) * attackImpulse;
|
|
Vector2 hitPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(diff);
|
|
hitLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity * 0.5f);
|
|
var mainLimb = hitLimb.character.AnimController.MainLimb;
|
|
if (hitLimb != mainLimb)
|
|
{
|
|
// Always add force to mainlimb
|
|
mainLimb.body.ApplyLinearImpulse(impulse, hitPos, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
}
|
|
bool wasDead = IsDead;
|
|
Vector2 simPos = hitLimb.SimPosition + ConvertUnits.ToSimUnits(dir);
|
|
AttackResult attackResult = hitLimb.AddDamage(simPos, afflictions, playSound, damageMultiplier: damageMultiplier);
|
|
CharacterHealth.ApplyDamage(hitLimb, attackResult);
|
|
if (attacker != this)
|
|
{
|
|
OnAttacked?.Invoke(attacker, attackResult);
|
|
OnAttackedProjSpecific(attacker, attackResult, stun);
|
|
if (!wasDead)
|
|
{
|
|
TryAdjustAttackerSkill(attacker, -attackResult.Damage);
|
|
if (IsDead)
|
|
{
|
|
attacker?.RecordKill(this);
|
|
}
|
|
}
|
|
};
|
|
if (attackResult.Damage > 0)
|
|
{
|
|
LastDamage = attackResult;
|
|
ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
|
|
hitLimb.ApplyStatusEffects(ActionType.OnDamaged, 1.0f);
|
|
if (attacker != null)
|
|
{
|
|
AddAttacker(attacker, attackResult.Damage);
|
|
AddEncounter(attacker);
|
|
attacker.AddEncounter(this);
|
|
}
|
|
}
|
|
return attackResult;
|
|
}
|
|
|
|
partial void OnAttackedProjSpecific(Character attacker, AttackResult attackResult, float stun);
|
|
|
|
public void TryAdjustAttackerSkill(Character attacker, float healthChange)
|
|
{
|
|
if (attacker == null) { return; }
|
|
|
|
bool isEnemy = AIController is EnemyAIController || TeamID != attacker.TeamID;
|
|
if (isEnemy)
|
|
{
|
|
if (healthChange < 0.0f)
|
|
{
|
|
float attackerSkillLevel = attacker.GetSkillLevel("weapons");
|
|
attacker.Info?.IncreaseSkillLevel("weapons",
|
|
-healthChange * SkillSettings.Current.SkillIncreasePerHostileDamage / Math.Max(attackerSkillLevel, 1.0f),
|
|
attacker.Position + Vector2.UnitY * 100.0f);
|
|
}
|
|
}
|
|
else if (healthChange > 0.0f)
|
|
{
|
|
float attackerSkillLevel = attacker.GetSkillLevel("medical");
|
|
attacker.Info?.IncreaseSkillLevel("medical",
|
|
healthChange * SkillSettings.Current.SkillIncreasePerFriendlyHealed / Math.Max(attackerSkillLevel, 1.0f),
|
|
attacker.Position + Vector2.UnitY * 100.0f);
|
|
}
|
|
}
|
|
|
|
public void SetStun(float newStun, bool allowStunDecrease = false, bool isNetworkMessage = false)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && !isNetworkMessage) { return; }
|
|
if (Screen.Selected != GameMain.GameScreen) { return; }
|
|
if ((newStun <= Stun && !allowStunDecrease) || !MathUtils.IsValid(newStun)) { return; }
|
|
if (Math.Sign(newStun) != Math.Sign(Stun))
|
|
{
|
|
AnimController.ResetPullJoints();
|
|
}
|
|
CharacterHealth.StunTimer = newStun;
|
|
if (newStun > 0.0f)
|
|
{
|
|
SelectedConstruction = null;
|
|
}
|
|
HealthUpdateInterval = 0.0f;
|
|
}
|
|
|
|
private readonly List<ISerializableEntity> targets = new List<ISerializableEntity>();
|
|
public void ApplyStatusEffects(ActionType actionType, float deltaTime)
|
|
{
|
|
foreach (StatusEffect statusEffect in statusEffects)
|
|
{
|
|
if (statusEffect.type != actionType) { continue; }
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.NearbyItems) ||
|
|
statusEffect.HasTargetType(StatusEffect.TargetType.NearbyCharacters))
|
|
{
|
|
targets.Clear();
|
|
statusEffect.GetNearbyTargets(WorldPosition, targets);
|
|
statusEffect.Apply(actionType, deltaTime, this, targets);
|
|
}
|
|
else
|
|
{
|
|
statusEffect.Apply(actionType, deltaTime, this, this);
|
|
if (statusEffect.targetLimbs != null)
|
|
{
|
|
foreach (var limbType in statusEffect.targetLimbs)
|
|
{
|
|
if (statusEffect.HasTargetType(StatusEffect.TargetType.AllLimbs))
|
|
{
|
|
// Target all matching limbs
|
|
foreach (var limb in AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
if (limb.type == limbType)
|
|
{
|
|
statusEffect.Apply(actionType, deltaTime, this, limb);
|
|
}
|
|
}
|
|
}
|
|
else if (statusEffect.HasTargetType(StatusEffect.TargetType.Limb))
|
|
{
|
|
// Target just the first matching limb
|
|
Limb limb = AnimController.GetLimb(limbType);
|
|
statusEffect.Apply(actionType, deltaTime, this, limb);
|
|
}
|
|
else if (statusEffect.HasTargetType(StatusEffect.TargetType.LastLimb))
|
|
{
|
|
// Target just the last matching limb
|
|
Limb limb = AnimController.Limbs.LastOrDefault(l => l.type == limbType && !l.IsSevered && !l.Hidden);
|
|
statusEffect.Apply(actionType, deltaTime, this, limb);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (actionType != ActionType.OnDamaged && actionType != ActionType.OnSevered)
|
|
{
|
|
// OnDamaged is called only for the limb that is hit.
|
|
AnimController.Limbs.ForEach(l => l.ApplyStatusEffects(actionType, deltaTime));
|
|
}
|
|
}
|
|
|
|
private void Implode(bool isNetworkMessage = false)
|
|
{
|
|
if (CharacterHealth.Unkillable || GodMode || IsDead) { return; }
|
|
|
|
if (!isNetworkMessage)
|
|
{
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient) { return; }
|
|
}
|
|
|
|
CharacterHealth.ApplyAffliction(null, new Affliction(AfflictionPrefab.Pressure, AfflictionPrefab.Pressure.MaxStrength));
|
|
if (isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient && Vitality <= CharacterHealth.MinVitality) { Kill(CauseOfDeathType.Pressure, null, isNetworkMessage: true); }
|
|
if (IsDead)
|
|
{
|
|
BreakJoints();
|
|
}
|
|
}
|
|
|
|
public void BreakJoints()
|
|
{
|
|
Vector2 centerOfMass = AnimController.GetCenterOfMass();
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
if (limb.IsSevered) { continue; }
|
|
limb.AddDamage(limb.SimPosition, 500.0f, 0.0f, 0.0f, false);
|
|
|
|
Vector2 diff = centerOfMass - limb.SimPosition;
|
|
|
|
if (!MathUtils.IsValid(diff))
|
|
{
|
|
string errorMsg = "Attempted to apply an invalid impulse to a limb in Character.BreakJoints (" + diff + "). Limb position: " + limb.SimPosition + ", center of mass: " + centerOfMass + ".";
|
|
DebugConsole.ThrowError(errorMsg);
|
|
GameAnalyticsManager.AddErrorEventOnce("Ragdoll.GetCenterOfMass", GameAnalyticsSDK.Net.EGAErrorSeverity.Error, errorMsg);
|
|
return;
|
|
}
|
|
|
|
if (diff == Vector2.Zero) { continue; }
|
|
limb.body.ApplyLinearImpulse(diff * 50.0f, maxVelocity: NetConfig.MaxPhysicsBodyVelocity);
|
|
}
|
|
|
|
ImplodeFX();
|
|
|
|
foreach (var joint in AnimController.LimbJoints)
|
|
{
|
|
if (joint.revoluteJoint != null)
|
|
{
|
|
joint.revoluteJoint.LimitEnabled = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
partial void ImplodeFX();
|
|
|
|
public void Kill(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool isNetworkMessage = false, bool log = true)
|
|
{
|
|
if (IsDead || CharacterHealth.Unkillable || GodMode) { return; }
|
|
|
|
HealthUpdateInterval = 0.0f;
|
|
|
|
//clients aren't allowed to kill characters unless they receive a network message
|
|
if (!isNetworkMessage && GameMain.NetworkMember != null && GameMain.NetworkMember.IsClient)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(this, new object[] { NetEntityEvent.Type.Status });
|
|
}
|
|
|
|
isDead = true;
|
|
|
|
ApplyStatusEffects(ActionType.OnDeath, 1.0f);
|
|
|
|
AnimController.Frozen = false;
|
|
|
|
if (GameSettings.SendUserStatistics)
|
|
{
|
|
string characterType = "Unknown";
|
|
|
|
if (this == Controlled)
|
|
characterType = "Player";
|
|
else if (IsRemotePlayer)
|
|
characterType = "RemotePlayer";
|
|
else if (AIController is EnemyAIController)
|
|
characterType = "Enemy";
|
|
else if (AIController is HumanAIController)
|
|
characterType = "AICrew";
|
|
|
|
string causeOfDeathStr = causeOfDeathAffliction == null ?
|
|
causeOfDeath.ToString() : causeOfDeathAffliction.Prefab.Name.Replace(" ", "");
|
|
GameAnalyticsManager.AddDesignEvent("Kill:" + characterType + ":" + SpeciesName + ":" + causeOfDeathStr);
|
|
}
|
|
|
|
CauseOfDeath = new CauseOfDeath(
|
|
causeOfDeath, causeOfDeathAffliction?.Prefab,
|
|
causeOfDeathAffliction?.Source ?? LastAttacker, LastDamageSource);
|
|
OnDeath?.Invoke(this, CauseOfDeath);
|
|
|
|
if (GameMain.GameSession != null && Screen.Selected == GameMain.GameScreen)
|
|
{
|
|
SteamAchievementManager.OnCharacterKilled(this, CauseOfDeath);
|
|
}
|
|
|
|
KillProjSpecific(causeOfDeath, causeOfDeathAffliction, log);
|
|
|
|
if (info != null) { info.CauseOfDeath = CauseOfDeath; }
|
|
AnimController.movement = Vector2.Zero;
|
|
AnimController.TargetMovement = Vector2.Zero;
|
|
|
|
foreach (Item heldItem in HeldItems.ToList())
|
|
{
|
|
heldItem.Drop(this);
|
|
}
|
|
|
|
SelectedConstruction = null;
|
|
|
|
AnimController.ResetPullJoints();
|
|
|
|
foreach (var joint in AnimController.LimbJoints)
|
|
{
|
|
if (joint.revoluteJoint != null)
|
|
{
|
|
joint.revoluteJoint.MotorEnabled = false;
|
|
}
|
|
}
|
|
|
|
if (GameMain.GameSession != null)
|
|
{
|
|
GameMain.GameSession.KillCharacter(this);
|
|
}
|
|
}
|
|
partial void KillProjSpecific(CauseOfDeathType causeOfDeath, Affliction causeOfDeathAffliction, bool log);
|
|
|
|
public void Revive()
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError("Attempting to revive an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
|
|
return;
|
|
}
|
|
|
|
if (aiTarget != null)
|
|
{
|
|
aiTarget.Remove();
|
|
}
|
|
|
|
aiTarget = new AITarget(this);
|
|
CharacterHealth.RemoveAllAfflictions();
|
|
SetAllDamage(0.0f, 0.0f, 0.0f);
|
|
Oxygen = 100.0f;
|
|
Bloodloss = 0.0f;
|
|
SetStun(0.0f, true);
|
|
isDead = false;
|
|
|
|
foreach (LimbJoint joint in AnimController.LimbJoints)
|
|
{
|
|
var revoluteJoint = joint.revoluteJoint;
|
|
if (revoluteJoint != null)
|
|
{
|
|
revoluteJoint.MotorEnabled = true;
|
|
}
|
|
joint.Enabled = true;
|
|
joint.IsSevered = false;
|
|
}
|
|
|
|
foreach (Limb limb in AnimController.Limbs)
|
|
{
|
|
#if CLIENT
|
|
if (limb.LightSource != null) limb.LightSource.Color = limb.InitialLightSourceColor;
|
|
#endif
|
|
limb.body.Enabled = true;
|
|
limb.IsSevered = false;
|
|
}
|
|
|
|
if (GameMain.GameSession != null)
|
|
{
|
|
GameMain.GameSession.ReviveCharacter(this);
|
|
}
|
|
}
|
|
|
|
public override void Remove()
|
|
{
|
|
if (Removed)
|
|
{
|
|
DebugConsole.ThrowError("Attempting to remove an already removed character\n" + Environment.StackTrace.CleanupStackTrace());
|
|
return;
|
|
}
|
|
DebugConsole.Log("Removing character " + Name + " (ID: " + ID + ")");
|
|
|
|
base.Remove();
|
|
|
|
foreach (Item heldItem in HeldItems.ToList())
|
|
{
|
|
heldItem.Drop(this);
|
|
}
|
|
|
|
info?.Remove();
|
|
|
|
#if CLIENT
|
|
GameMain.GameSession?.CrewManager?.KillCharacter(this);
|
|
#endif
|
|
|
|
CharacterList.Remove(this);
|
|
|
|
if (Controlled == this) { Controlled = null; }
|
|
|
|
if (Inventory != null)
|
|
{
|
|
foreach (Item item in Inventory.AllItems)
|
|
{
|
|
Spawner?.AddToRemoveQueue(item);
|
|
}
|
|
}
|
|
|
|
DisposeProjSpecific();
|
|
|
|
aiTarget?.Remove();
|
|
AnimController?.Remove();
|
|
CharacterHealth?.Remove();
|
|
|
|
foreach (Character c in CharacterList)
|
|
{
|
|
if (c.FocusedCharacter == this) { c.FocusedCharacter = null; }
|
|
if (c.SelectedCharacter == this) { c.SelectedCharacter = null; }
|
|
}
|
|
}
|
|
partial void DisposeProjSpecific();
|
|
|
|
public void TeleportTo(Vector2 worldPos)
|
|
{
|
|
AnimController.CurrentHull = null;
|
|
Submarine = null;
|
|
AnimController.SetPosition(ConvertUnits.ToSimUnits(worldPos), false);
|
|
AnimController.FindHull(worldPos, true);
|
|
}
|
|
|
|
public void SaveInventory(Inventory inventory, XElement parentElement)
|
|
{
|
|
var items = inventory.AllItems.Distinct();
|
|
foreach (Item item in items)
|
|
{
|
|
item.Submarine = inventory.Owner.Submarine;
|
|
var itemElement = item.Save(parentElement);
|
|
|
|
List<int> slotIndices = inventory.FindIndices(item);
|
|
itemElement.Add(new XAttribute("i", string.Join(",", slotIndices)));
|
|
|
|
foreach (ItemContainer container in item.GetComponents<ItemContainer>())
|
|
{
|
|
XElement childInvElement = new XElement("inventory");
|
|
itemElement.Add(childInvElement);
|
|
SaveInventory(container.Inventory, childInvElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SpawnInventoryItems(Inventory inventory, XElement itemData)
|
|
{
|
|
SpawnInventoryItemsRecursive(inventory, itemData, new List<Item>());
|
|
}
|
|
|
|
private void SpawnInventoryItemsRecursive(Inventory inventory, XElement element, List<Item> extraDuffelBags)
|
|
{
|
|
foreach (XElement itemElement in element.Elements())
|
|
{
|
|
var newItem = Item.Load(itemElement, inventory.Owner.Submarine, createNetworkEvent: true, idRemap: IdRemap.DiscardId);
|
|
if (newItem == null) { continue; }
|
|
|
|
if (!MathUtils.NearlyEqual(newItem.Condition, newItem.MaxCondition) &&
|
|
GameMain.NetworkMember != null && GameMain.NetworkMember.IsServer)
|
|
{
|
|
GameMain.NetworkMember.CreateEntityEvent(newItem, new object[] { NetEntityEvent.Type.Status });
|
|
}
|
|
#if SERVER
|
|
newItem.GetComponent<Terminal>()?.SyncHistory();
|
|
#endif
|
|
int[] slotIndices = itemElement.GetAttributeIntArray("i", new int[] { 0 });
|
|
if (!slotIndices.Any())
|
|
{
|
|
DebugConsole.ThrowError("Invalid inventory data in character \"" + Name + "\" - no slot indices found");
|
|
continue;
|
|
}
|
|
|
|
//make sure there's no other item in the slot
|
|
//this should not happen normally, but can occur if the character is accidentally given new job items while also loading previous items in the campaign
|
|
for (int i = 0; i < inventory.Capacity; i++)
|
|
{
|
|
if (slotIndices.Contains(i))
|
|
{
|
|
var existingItem = inventory.GetItemAt(i);
|
|
if (existingItem != null && existingItem != newItem && (existingItem.prefab != newItem.prefab || existingItem.Prefab.MaxStackSize == 1))
|
|
{
|
|
DebugConsole.ThrowError($"Error while loading character inventory data. The slot {i} was already occupied by the item \"{existingItem.Name} ({existingItem.ID})\" when loading the item \"{newItem.Name} ({newItem.ID})\"");
|
|
existingItem.Drop(null, createNetworkEvent: false);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool canBePutInOriginalInventory = true;
|
|
if (slotIndices[0] >= inventory.Capacity)
|
|
{
|
|
canBePutInOriginalInventory = false;
|
|
//legacy support: before item stacking was implemented, revolver for example had a separate slot for each bullet
|
|
//now there's just one, try to put the extra items where they fit (= stack them)
|
|
for (int i = 0; i < inventory.Capacity; i++)
|
|
{
|
|
if (inventory.CanBePut(newItem, i))
|
|
{
|
|
slotIndices[0] = i;
|
|
canBePutInOriginalInventory = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (canBePutInOriginalInventory)
|
|
{
|
|
inventory.TryPutItem(newItem, slotIndices[0], false, false, null);
|
|
newItem.ParentInventory = inventory;
|
|
|
|
//force the item to the correct slots
|
|
// e.g. putting the item in a hand slot will also put it in the first available Any-slot,
|
|
// which may not be where it actually was
|
|
for (int i = 0; i < inventory.Capacity; i++)
|
|
{
|
|
if (slotIndices.Contains(i))
|
|
{
|
|
if (!inventory.GetItemsAt(i).Contains(newItem)) { inventory.ForceToSlot(newItem, i); }
|
|
}
|
|
else if (inventory.FindIndices(newItem).Contains(i))
|
|
{
|
|
inventory.ForceRemoveFromSlot(newItem, i);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// In case the inventory capacity is smaller than it was when saving:
|
|
// 1) Spawn a new duffel bag if none yet spawned or if the existing ones aren't enough
|
|
if (extraDuffelBags.None(i => i.OwnInventory.CanBePut(newItem)) && ItemPrefab.Find(null, "duffelbag") is ItemPrefab duffelBagPrefab)
|
|
{
|
|
var hull = Hull.FindHull(WorldPosition, guess: CurrentHull);
|
|
var mainSub = Submarine.MainSubs.FirstOrDefault(s => s.TeamID == TeamID);
|
|
if ((hull == null || hull.Submarine != mainSub) && mainSub != null)
|
|
{
|
|
var wp = WayPoint.GetRandom(spawnType: SpawnType.Cargo, sub: mainSub) ?? WayPoint.GetRandom(sub: mainSub);
|
|
if (wp != null)
|
|
{
|
|
hull = Hull.FindHull(wp.WorldPosition);
|
|
}
|
|
}
|
|
var newDuffelBag = new Item(duffelBagPrefab,
|
|
hull != null ? CargoManager.GetCargoPos(hull, duffelBagPrefab) : Position,
|
|
hull?.Submarine ?? Submarine);
|
|
extraDuffelBags.Add(newDuffelBag);
|
|
#if SERVER
|
|
Spawner.CreateNetworkEvent(newDuffelBag, false);
|
|
#endif
|
|
}
|
|
|
|
// 2) Find a slot for the new item
|
|
for (int i = 0; i < extraDuffelBags.Count; i++)
|
|
{
|
|
var duffelBag = extraDuffelBags[i];
|
|
for (int j = 0; j < duffelBag.OwnInventory.Capacity; j++)
|
|
{
|
|
if (duffelBag.OwnInventory.TryPutItem(newItem, j, false, false, null))
|
|
{
|
|
newItem.ParentInventory = duffelBag.OwnInventory;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
int itemContainerIndex = 0;
|
|
var itemContainers = newItem.GetComponents<ItemContainer>().ToList();
|
|
foreach (XElement childInvElement in itemElement.Elements())
|
|
{
|
|
if (itemContainerIndex >= itemContainers.Count) break;
|
|
if (!childInvElement.Name.ToString().Equals("inventory", StringComparison.OrdinalIgnoreCase)) { continue; }
|
|
SpawnInventoryItemsRecursive(itemContainers[itemContainerIndex].Inventory, childInvElement, extraDuffelBags);
|
|
itemContainerIndex++;
|
|
}
|
|
}
|
|
}
|
|
|
|
private readonly HashSet<AttackContext> currentContexts = new HashSet<AttackContext>();
|
|
|
|
public IEnumerable<AttackContext> GetAttackContexts()
|
|
{
|
|
currentContexts.Clear();
|
|
if (AnimController.InWater)
|
|
{
|
|
currentContexts.Add(AttackContext.Water);
|
|
}
|
|
else
|
|
{
|
|
currentContexts.Add(AttackContext.Ground);
|
|
}
|
|
if (CurrentHull == null)
|
|
{
|
|
currentContexts.Add(AttackContext.Outside);
|
|
}
|
|
else
|
|
{
|
|
currentContexts.Add(AttackContext.Inside);
|
|
}
|
|
return currentContexts;
|
|
}
|
|
|
|
private readonly List<Hull> visibleHulls = new List<Hull>();
|
|
private readonly HashSet<Hull> tempList = new HashSet<Hull>();
|
|
/// <summary>
|
|
/// Returns hulls that are visible to the player, including the current hull.
|
|
/// Can be heavy if used every frame.
|
|
/// </summary>
|
|
public List<Hull> GetVisibleHulls()
|
|
{
|
|
visibleHulls.Clear();
|
|
tempList.Clear();
|
|
if (CurrentHull != null)
|
|
{
|
|
visibleHulls.Add(CurrentHull);
|
|
var adjacentHulls = CurrentHull.GetConnectedHulls(true, 1);
|
|
float maxDistance = 1000f;
|
|
foreach (var hull in adjacentHulls)
|
|
{
|
|
if (hull.ConnectedGaps.Any(g => g.Open > 0.9f && g.linkedTo.Contains(CurrentHull) &&
|
|
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2)))
|
|
{
|
|
if (Vector2.DistanceSquared(hull.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2))
|
|
{
|
|
visibleHulls.Add(hull);
|
|
}
|
|
}
|
|
}
|
|
visibleHulls.AddRange(CurrentHull.GetLinkedEntities(tempList, filter: h =>
|
|
{
|
|
// Ignore adjacent hulls because they were already handled above
|
|
if (adjacentHulls.Contains(h))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
if (h.ConnectedGaps.Any(g =>
|
|
g.Open > 0.9f &&
|
|
Vector2.DistanceSquared(g.WorldPosition, WorldPosition) < Math.Pow(maxDistance / 2, 2) &&
|
|
CanSeeTarget(g)))
|
|
{
|
|
return Vector2.DistanceSquared(h.WorldPosition, WorldPosition) < Math.Pow(maxDistance, 2);
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}));
|
|
}
|
|
return visibleHulls;
|
|
}
|
|
|
|
public Vector2 GetRelativeSimPosition(ISpatialEntity target, Vector2? worldPos = null)
|
|
{
|
|
Vector2 targetPos = target.SimPosition;
|
|
if (worldPos.HasValue)
|
|
{
|
|
Vector2 wp = worldPos.Value;
|
|
if (target.Submarine != null)
|
|
{
|
|
wp -= target.Submarine.Position;
|
|
}
|
|
targetPos = ConvertUnits.ToSimUnits(wp);
|
|
}
|
|
if (Submarine == null && target.Submarine != null)
|
|
{
|
|
if (AIController == null || !(AIController.SteeringManager is IndoorsSteeringManager))
|
|
{
|
|
// outside and targeting inside
|
|
// doesn't work with inside steering
|
|
targetPos += target.Submarine.SimPosition;
|
|
}
|
|
}
|
|
else if (Submarine != null && target.Submarine == null)
|
|
{
|
|
// inside and targeting outside
|
|
targetPos -= Submarine.SimPosition;
|
|
}
|
|
else if (Submarine != target.Submarine)
|
|
{
|
|
if (Submarine != null && target.Submarine != null)
|
|
{
|
|
// both inside, but in different subs
|
|
Vector2 diff = Submarine.SimPosition - target.Submarine.SimPosition;
|
|
targetPos -= diff;
|
|
}
|
|
}
|
|
return targetPos;
|
|
}
|
|
|
|
public bool IsCaptain => HasJob("captain");
|
|
public bool IsEngineer => HasJob("engineer");
|
|
public bool IsMechanic => HasJob("mechanic");
|
|
public bool IsMedic => HasJob("medicaldoctor");
|
|
public bool IsSecurity => HasJob("securityofficer");
|
|
public bool IsAssistant => HasJob("assistant");
|
|
public bool IsWatchman => HasJob("watchman");
|
|
|
|
public bool HasJob(string identifier) => Info?.Job?.Prefab.Identifier == identifier;
|
|
}
|
|
}
|