first commit
This commit is contained in:
567
NitroxClient/GameLogic/RemotePlayer.cs
Normal file
567
NitroxClient/GameLogic/RemotePlayer.cs
Normal file
@@ -0,0 +1,567 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using NitroxClient.GameLogic.HUD;
|
||||
using NitroxClient.GameLogic.PlayerLogic;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel;
|
||||
using NitroxClient.GameLogic.PlayerLogic.PlayerModel.Abstract;
|
||||
using NitroxClient.MonoBehaviours;
|
||||
using NitroxClient.MonoBehaviours.Cyclops;
|
||||
using NitroxClient.MonoBehaviours.Gui.HUD;
|
||||
using NitroxClient.MonoBehaviours.Vehicles;
|
||||
using NitroxClient.Unity.Helper;
|
||||
using NitroxModel.GameLogic.FMOD;
|
||||
using NitroxModel.MultiplayerSession;
|
||||
using NitroxModel.Server;
|
||||
using UnityEngine;
|
||||
using UWE;
|
||||
|
||||
namespace NitroxClient.GameLogic;
|
||||
|
||||
public class RemotePlayer : INitroxPlayer
|
||||
{
|
||||
/// <summary>
|
||||
/// Marks <see cref="Player.mainObject"/> and every <see cref="Body"/> so they can be precisely queried (e.g. by sea dragons).
|
||||
/// The value (5050) is determined arbitrarily and should not be used already.
|
||||
/// </summary>
|
||||
public const EcoTargetType PLAYER_ECO_TARGET_TYPE = (EcoTargetType)5050;
|
||||
|
||||
private static readonly int animatorPlayerIn = Animator.StringToHash("player_in");
|
||||
|
||||
private readonly PlayerModelManager playerModelManager;
|
||||
private readonly PlayerVitalsManager playerVitalsManager;
|
||||
private readonly FMODWhitelist fmodWhitelist;
|
||||
|
||||
public PlayerContext PlayerContext { get; }
|
||||
public GameObject Body { get; private set; }
|
||||
public GameObject PlayerModel { get; private set; }
|
||||
public Rigidbody RigidBody { get; private set; }
|
||||
public CapsuleCollider Collider { get; private set; }
|
||||
public ArmsController ArmsController { get; private set; }
|
||||
public AnimationController AnimationController { get; private set; }
|
||||
public ItemsContainer Inventory { get; private set; }
|
||||
public Transform ItemAttachPoint { get; private set; }
|
||||
public RemotePlayerVitals vitals { get; private set; }
|
||||
|
||||
public ushort PlayerId => PlayerContext.PlayerId;
|
||||
public string PlayerName => PlayerContext.PlayerName;
|
||||
public PlayerSettings PlayerSettings => PlayerContext.PlayerSettings;
|
||||
|
||||
public Vehicle Vehicle { get; private set; }
|
||||
public SubRoot SubRoot { get; private set; }
|
||||
public EscapePod EscapePod { get; private set; }
|
||||
public PilotingChair PilotingChair { get; private set; }
|
||||
public InfectedMixin InfectedMixin { get; private set; }
|
||||
public LiveMixin LiveMixin { get; private set; }
|
||||
|
||||
public readonly Event<RemotePlayer> PlayerDeathEvent = new();
|
||||
|
||||
public readonly Event<RemotePlayer> PlayerDisconnectEvent = new();
|
||||
|
||||
public CyclopsPawn Pawn { get; set; }
|
||||
|
||||
public RemotePlayer(PlayerContext playerContext, PlayerModelManager playerModelManager, PlayerVitalsManager playerVitalsManager, FMODWhitelist fmodWhitelist)
|
||||
{
|
||||
PlayerContext = playerContext;
|
||||
this.playerModelManager = playerModelManager;
|
||||
this.playerVitalsManager = playerVitalsManager;
|
||||
this.fmodWhitelist = fmodWhitelist;
|
||||
}
|
||||
|
||||
public void InitializeGameObject(GameObject playerBody)
|
||||
{
|
||||
Body = playerBody;
|
||||
Body.name = PlayerName;
|
||||
|
||||
RigidBody = Body.AddComponent<Rigidbody>();
|
||||
RigidBody.useGravity = false;
|
||||
RigidBody.interpolation = RigidbodyInterpolation.Interpolate;
|
||||
|
||||
NitroxEntity.SetNewId(Body, PlayerContext.PlayerNitroxId);
|
||||
|
||||
// Get player
|
||||
PlayerModel = Body.RequireGameObject("player_view");
|
||||
// Move variables to keep player animations from mirroring and for identification
|
||||
ArmsController = PlayerModel.GetComponent<ArmsController>();
|
||||
ArmsController.smoothSpeedUnderWater = 0;
|
||||
ArmsController.smoothSpeedAboveWater = 0;
|
||||
|
||||
// ConditionRules has Player.Main based conditions from ArmsController
|
||||
PlayerModel.GetComponent<ConditionRules>().enabled = false;
|
||||
|
||||
AnimationController = PlayerModel.AddComponent<AnimationController>();
|
||||
|
||||
Transform inventoryTransform = new GameObject("Inventory").transform;
|
||||
inventoryTransform.SetParent(Body.transform);
|
||||
Inventory = new ItemsContainer(6, 8, inventoryTransform, $"NitroxInventoryStorage_{PlayerName}", null);
|
||||
|
||||
ItemAttachPoint = PlayerModel.transform.Find(PlayerEquipmentConstants.ITEM_ATTACH_POINT_GAME_OBJECT_NAME);
|
||||
|
||||
CoroutineUtils.StartCoroutineSmart(playerModelManager.AttachPing(this));
|
||||
playerModelManager.BeginApplyPlayerColor(this);
|
||||
playerModelManager.RegisterEquipmentVisibilityHandler(PlayerModel);
|
||||
SetupBody();
|
||||
SetupSkyAppliers();
|
||||
SetupPlayerSounds();
|
||||
SetupMixins();
|
||||
|
||||
vitals = playerVitalsManager.CreateOrFindForPlayer(this);
|
||||
RefreshVitalsVisibility();
|
||||
|
||||
PlayerDisconnectEvent.AddHandler(Body, _ =>
|
||||
{
|
||||
Pawn?.Unregister();
|
||||
Pawn = null;
|
||||
});
|
||||
|
||||
PlayerDeathEvent.AddHandler(Body, _ =>
|
||||
{
|
||||
ResetStates();
|
||||
});
|
||||
}
|
||||
|
||||
public void Attach(Transform transform, bool keepWorldTransform = false)
|
||||
{
|
||||
Body.transform.SetParent(transform);
|
||||
|
||||
if (!keepWorldTransform)
|
||||
{
|
||||
UWE.Utils.ZeroTransform(Body);
|
||||
}
|
||||
|
||||
SkyEnvironmentChanged.Broadcast(Body, transform);
|
||||
}
|
||||
|
||||
public void Detach()
|
||||
{
|
||||
Body.transform.SetParent(null);
|
||||
SkyEnvironmentChanged.Broadcast(Body, (GameObject)null);
|
||||
}
|
||||
|
||||
public void UpdatePosition(Vector3 position, Vector3 velocity, Quaternion bodyRotation, Quaternion aimingRotation)
|
||||
{
|
||||
// It might happen that we get movement packets before the body is actually initialized which is not too bad
|
||||
if (!Body)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Body.SetActive(true);
|
||||
|
||||
// When receiving movement packets, a player can not be controlling a vehicle (they can walk through subroots though).
|
||||
SetVehicle(null);
|
||||
SetPilotingChair(null);
|
||||
|
||||
AnimationController.AimingRotation = aimingRotation;
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Velocity = MovementHelper.GetCorrectedVelocity(position, velocity, Body, Time.fixedDeltaTime);
|
||||
|
||||
// If in a subroot the position will be relative to the subroot
|
||||
if (SubRoot && SubRoot.isBase)
|
||||
{
|
||||
Quaternion vehicleAngle = SubRoot.transform.rotation;
|
||||
position = vehicleAngle * position;
|
||||
position += SubRoot.transform.position;
|
||||
bodyRotation = vehicleAngle * bodyRotation;
|
||||
aimingRotation = vehicleAngle * aimingRotation;
|
||||
}
|
||||
|
||||
RigidBody.velocity = AnimationController.Velocity;
|
||||
RigidBody.angularVelocity = MovementHelper.GetCorrectedAngularVelocity(bodyRotation, Vector3.zero, Body, Time.fixedDeltaTime);
|
||||
}
|
||||
|
||||
public void UpdatePositionInCyclops(Vector3 localPosition, Quaternion localRotation)
|
||||
{
|
||||
if (Pawn == null || PilotingChair)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SetVehicle(null);
|
||||
|
||||
AnimationController.AimingRotation = localRotation;
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Velocity = (localPosition - Pawn.Handle.transform.localPosition) / Time.fixedDeltaTime;
|
||||
|
||||
Pawn.Handle.transform.localPosition = localPosition;
|
||||
Pawn.Handle.transform.localRotation = localRotation;
|
||||
}
|
||||
|
||||
public void SetPilotingChair(PilotingChair newPilotingChair)
|
||||
{
|
||||
if (PilotingChair != newPilotingChair)
|
||||
{
|
||||
PilotingChair = newPilotingChair;
|
||||
|
||||
CyclopsMovementReplicator cyclopsMovementReplicator = null;
|
||||
|
||||
// For unexpected and expected cases, for example when a player is driving a cyclops but the cyclops is destroyed
|
||||
if (!SubRoot)
|
||||
{
|
||||
Log.Error("Player changed PilotingChair but is not in SubRoot!");
|
||||
}
|
||||
else
|
||||
{
|
||||
cyclopsMovementReplicator = SubRoot.GetComponent<CyclopsMovementReplicator>();
|
||||
}
|
||||
|
||||
if (PilotingChair)
|
||||
{
|
||||
Attach(PilotingChair.sittingPosition.transform);
|
||||
ArmsController.SetWorldIKTarget(PilotingChair.leftHandPlug, PilotingChair.rightHandPlug);
|
||||
|
||||
if (cyclopsMovementReplicator)
|
||||
{
|
||||
cyclopsMovementReplicator.Enter(this);
|
||||
}
|
||||
|
||||
if (SubRoot)
|
||||
{
|
||||
SkyEnvironmentChanged.Broadcast(Body, SubRoot);
|
||||
}
|
||||
|
||||
AnimationController.UpdatePlayerAnimations = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SetSubRoot(SubRoot, true);
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
|
||||
if (cyclopsMovementReplicator)
|
||||
{
|
||||
cyclopsMovementReplicator.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
bool isKinematic = newPilotingChair;
|
||||
UWE.Utils.SetIsKinematicAndUpdateInterpolation(RigidBody, isKinematic, true);
|
||||
AnimationController["cyclops_steering"] = newPilotingChair;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetSubRoot(SubRoot newSubRoot, bool force = false)
|
||||
{
|
||||
if (SubRoot != newSubRoot || force)
|
||||
{
|
||||
// Unregister from previous cyclops
|
||||
Pawn?.Unregister();
|
||||
Pawn = null;
|
||||
|
||||
if (newSubRoot)
|
||||
{
|
||||
Attach(newSubRoot.transform, true);
|
||||
|
||||
// Register in new cyclops
|
||||
if (newSubRoot.TryGetComponent(out NitroxCyclops nitroxCyclops))
|
||||
{
|
||||
nitroxCyclops.OnPlayerEnter(this);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Detach();
|
||||
}
|
||||
|
||||
SubRoot = newSubRoot;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetEscapePod(EscapePod newEscapePod)
|
||||
{
|
||||
if (EscapePod != newEscapePod)
|
||||
{
|
||||
if (newEscapePod)
|
||||
{
|
||||
Attach(newEscapePod.transform, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
Detach();
|
||||
}
|
||||
|
||||
EscapePod = newEscapePod;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetVehicle(Vehicle newVehicle)
|
||||
{
|
||||
if (Vehicle != newVehicle)
|
||||
{
|
||||
if (Vehicle)
|
||||
{
|
||||
Vehicle.mainAnimator.SetBool(animatorPlayerIn, false);
|
||||
|
||||
Detach();
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
|
||||
if (Vehicle.TryGetComponent(out VehicleMovementReplicator vehicleMovementReplicator))
|
||||
{
|
||||
vehicleMovementReplicator.Exit();
|
||||
}
|
||||
}
|
||||
|
||||
if (newVehicle)
|
||||
{
|
||||
newVehicle.mainAnimator.SetBool(animatorPlayerIn, true);
|
||||
|
||||
Attach(newVehicle.playerPosition.transform);
|
||||
ArmsController.SetWorldIKTarget(newVehicle.leftHandPlug, newVehicle.rightHandPlug);
|
||||
|
||||
// From here, a basic issue can happen.
|
||||
// When a vehicle is docked since we joined a game and another player undocks him before the local player does,
|
||||
// no VehicleMovementReplicator can be found on the vehicle because they are only created when receiving SimulationOwnership packets
|
||||
// Therefore we need to make sure that the VehicleMovementReplicator component exists before using it
|
||||
switch (newVehicle)
|
||||
{
|
||||
case SeaMoth:
|
||||
newVehicle.gameObject.EnsureComponent<SeamothMovementReplicator>().Enter(this);
|
||||
break;
|
||||
case Exosuit:
|
||||
newVehicle.gameObject.EnsureComponent<ExosuitMovementReplicator>().Enter(this);
|
||||
break;
|
||||
}
|
||||
|
||||
AnimationController.UpdatePlayerAnimations = false;
|
||||
}
|
||||
|
||||
bool isKinematic = newVehicle;
|
||||
UWE.Utils.SetIsKinematicAndUpdateInterpolation(RigidBody, isKinematic, true);
|
||||
|
||||
Vehicle = newVehicle;
|
||||
|
||||
AnimationController["in_seamoth"] = newVehicle is SeaMoth;
|
||||
AnimationController["in_exosuit"] = AnimationController["using_mechsuit"] = newVehicle is Exosuit;
|
||||
|
||||
// In case we are dismissing the current seamoth to enter the cyclops through a docking,
|
||||
// we need to setup the player back in the cyclops
|
||||
if (!newVehicle && SubRoot)
|
||||
{
|
||||
SetSubRoot(SubRoot, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops the remote player, swimming where he is. Resets its animator.
|
||||
/// </summary>
|
||||
public void ResetStates()
|
||||
{
|
||||
SetPilotingChair(null);
|
||||
SetVehicle(null);
|
||||
SetSubRoot(null);
|
||||
AnimationController.UpdatePlayerAnimations = true;
|
||||
AnimationController.Reset();
|
||||
ArmsController.SetWorldIKTarget(null, null);
|
||||
}
|
||||
|
||||
public void Destroy()
|
||||
{
|
||||
Log.Info($"{PlayerName} left the game");
|
||||
Log.InGame(Language.main.Get("Nitrox_PlayerLeft").Replace("{PLAYER}", PlayerName));
|
||||
NitroxEntity.RemoveFrom(Body);
|
||||
Object.DestroyImmediate(Body);
|
||||
}
|
||||
|
||||
public void UpdateAnimationAndCollider(AnimChangeType type, AnimChangeState state)
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case AnimChangeType.UNDERWATER:
|
||||
AnimationController["is_underwater"] = state != AnimChangeState.OFF;
|
||||
break;
|
||||
case AnimChangeType.BENCH:
|
||||
AnimationController["cinematics_enabled"] = state != AnimChangeState.UNSET;
|
||||
AnimationController["bench_sit"] = state == AnimChangeState.ON;
|
||||
AnimationController["bench_stand_up"] = state == AnimChangeState.OFF;
|
||||
break;
|
||||
case AnimChangeType.INFECTION_REVEAL:
|
||||
AnimationController["player_infected"] = state != AnimChangeState.UNSET;
|
||||
break;
|
||||
}
|
||||
|
||||
// Rough estimation for different collider boxes in different animation stages
|
||||
if (AnimationController["is_underwater"])
|
||||
{
|
||||
Collider.center = new(0f, -0.3f, 0f);
|
||||
Collider.height = 0.5f;
|
||||
}
|
||||
else
|
||||
{
|
||||
Collider.center = new(0f, -0.8f, 0f);
|
||||
Collider.height = 1.5f;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateEquipmentVisibility(List<TechType> equippedItems)
|
||||
{
|
||||
playerModelManager.UpdateEquipmentVisibility(new ReadOnlyCollection<TechType>(equippedItems));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes the RemotePlayer recognizable as an obstacle for buildings, and as a target for creatures
|
||||
/// </summary>
|
||||
private void SetupBody()
|
||||
{
|
||||
// set as a target for reapers
|
||||
EcoTarget sharkEcoTarget = Body.AddComponent<EcoTarget>();
|
||||
sharkEcoTarget.SetTargetType(EcoTargetType.Shark);
|
||||
|
||||
EcoTarget playerEcoTarget = Body.AddComponent<EcoTarget>();
|
||||
playerEcoTarget.SetTargetType(PLAYER_ECO_TARGET_TYPE);
|
||||
|
||||
TechTag techTag = Body.AddComponent<TechTag>();
|
||||
techTag.type = TechType.Player;
|
||||
|
||||
RemotePlayerIdentifier identifier = Body.AddComponent<RemotePlayerIdentifier>();
|
||||
identifier.RemotePlayer = this;
|
||||
|
||||
if (Player.mainCollider is CapsuleCollider refCollider)
|
||||
{
|
||||
// This layer lets us have a collider as a trigger without preventing its detection as an obstacle
|
||||
Body.layer = LayerID.Useable;
|
||||
Collider = Body.AddComponent<CapsuleCollider>();
|
||||
|
||||
Collider.center = Vector3.zero;
|
||||
Collider.radius = refCollider.radius;
|
||||
Collider.direction = refCollider.direction;
|
||||
Collider.contactOffset = refCollider.contactOffset;
|
||||
Collider.isTrigger = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warn("The main collider of the main Player couldn't be found or is not a CapsuleCollider. Collisions for the RemotePlayer won't be created");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allows the remote player model to have its lighting dynamically adjusted
|
||||
/// </summary>
|
||||
private void SetupSkyAppliers()
|
||||
{
|
||||
// SkyAppliers apply the light effects of a lighting source on a set of renderers
|
||||
SkyApplier skyApplier = Body.AddComponent<SkyApplier>();
|
||||
skyApplier.anchorSky = Skies.Auto;
|
||||
skyApplier.emissiveFromPower = false;
|
||||
skyApplier.dynamic = true;
|
||||
skyApplier.renderers = Body.GetComponentsInChildren<SkinnedMeshRenderer>(true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up all bubbles, breathing and diving sounds to be multiplayer ready
|
||||
/// </summary>
|
||||
private void SetupPlayerSounds()
|
||||
{
|
||||
GameObject remotePlayerSoundsRoot = new("RemotePlayerSounds");
|
||||
remotePlayerSoundsRoot.transform.SetParent(Body.transform);
|
||||
FMODEmitterController emitterController = Body.AddComponent<FMODEmitterController>();
|
||||
|
||||
static void CopyEmitter(FMOD_CustomEmitter src, FMOD_CustomEmitter dst)
|
||||
{
|
||||
dst.asset = src.asset;
|
||||
dst.playOnAwake = src.playOnAwake;
|
||||
dst.stopImmediatelyOnDisable = src.stopImmediatelyOnDisable;
|
||||
dst.followParent = src.followParent;
|
||||
dst.restartOnPlay = src.restartOnPlay;
|
||||
}
|
||||
|
||||
// Bubbles
|
||||
PlayerBreathBubbles localPlayerBubbles = Player.main.GetComponentInChildren<PlayerBreathBubbles>(true);
|
||||
FMOD_CustomEmitter bubblesCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
CopyEmitter(localPlayerBubbles.bubbleSound, bubblesCustomEmitter);
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(bubblesCustomEmitter.asset.path, out float bubblesSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(bubblesCustomEmitter.asset.path, bubblesCustomEmitter, bubblesSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(PlayerBreathBubbles)} but linked sound is not whitelisted: ({bubblesCustomEmitter.asset.path})");
|
||||
}
|
||||
|
||||
// Breathing
|
||||
BreathingSound breathingSound = Player.main.GetComponentInChildren<BreathingSound>(true);
|
||||
FMOD_CustomEmitter breathingSoundCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
breathingSoundCustomEmitter.asset = breathingSound.loopingBreathingSound.asset;
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(breathingSoundCustomEmitter.asset.path, out float breathingSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(breathingSoundCustomEmitter.asset.path, breathingSoundCustomEmitter, breathingSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(BreathingSound)} but linked sound is not whitelisted: ({breathingSoundCustomEmitter.asset.path})");
|
||||
}
|
||||
|
||||
// Diving
|
||||
WaterAmbience waterAmbience = Player.main.GetComponentInChildren<WaterAmbience>(true);
|
||||
FMOD_CustomEmitter diveStartCustomEmitter = remotePlayerSoundsRoot.AddComponent<FMOD_CustomEmitter>();
|
||||
CopyEmitter(waterAmbience.diveStartSplash, diveStartCustomEmitter);
|
||||
|
||||
if (fmodWhitelist.IsWhitelisted(diveStartCustomEmitter.asset.path, out float diveSoundRadius))
|
||||
{
|
||||
emitterController.AddEmitter(diveStartCustomEmitter.asset.path, diveStartCustomEmitter, diveSoundRadius);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Error($"[{nameof(RemotePlayer)}] Manual created FMOD emitter for {nameof(WaterAmbience)} but linked sound is not whitelisted: ({diveStartCustomEmitter.asset.path})");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An InfectedMixin is required for behaviours like <see cref="AggressiveWhenSeeTarget"/> which look for this on the target they find
|
||||
/// </summary>
|
||||
private void SetupMixins()
|
||||
{
|
||||
InfectedMixin = Body.AddComponent<InfectedMixin>();
|
||||
InfectedMixin.shaderKeyWord = InfectedMixin.uwe_playerinfection;
|
||||
Renderer renderer = PlayerModel.transform.Find("male_geo/diveSuit/diveSuit_hands_geo").GetComponent<Renderer>();
|
||||
InfectedMixin.renderers = [renderer];
|
||||
|
||||
LiveMixin = Body.AddComponent<LiveMixin>();
|
||||
LiveMixin.data = new()
|
||||
{
|
||||
maxHealth = 100,
|
||||
broadcastKillOnDeath = false
|
||||
};
|
||||
LiveMixin.health = 100;
|
||||
// We set the remote player to invincible because we only want this component to be detectable but not to work
|
||||
LiveMixin.invincible = true;
|
||||
}
|
||||
|
||||
public void UpdateHealthAndInfection(float health, float infection)
|
||||
{
|
||||
if (LiveMixin)
|
||||
{
|
||||
LiveMixin.health = health;
|
||||
}
|
||||
|
||||
if (InfectedMixin)
|
||||
{
|
||||
InfectedMixin.infectedAmount = infection;
|
||||
InfectedMixin.UpdateInfectionShading();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetGameMode(NitroxGameMode gameMode)
|
||||
{
|
||||
PlayerContext.GameMode = gameMode;
|
||||
RefreshVitalsVisibility();
|
||||
}
|
||||
|
||||
private void RefreshVitalsVisibility()
|
||||
{
|
||||
if (vitals)
|
||||
{
|
||||
bool visible = PlayerContext.GameMode != NitroxGameMode.CREATIVE;
|
||||
vitals.SetStatsVisible(visible);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adaptation of <see cref="Player.CanBeAttacked"/> for remote players.
|
||||
/// NB: This doesn't check for other player's use of 'invisible' command
|
||||
/// </summary>
|
||||
public bool CanBeAttacked()
|
||||
{
|
||||
return !SubRoot && !EscapePod && PlayerContext.GameMode != NitroxGameMode.CREATIVE;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user