Critical damage renamed to marked for death and all share the same mechanic now Revelant now slows down to speed over a fixed short amount instead the nonworking jank before Moved item/enemy injection to DunGenPlus
543 lines
20 KiB
C#
543 lines
20 KiB
C#
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using UnityEngine;
|
|
using Unity.Netcode;
|
|
using GameNetcodeStuff;
|
|
using ScarletMansion.GamePatch.Items;
|
|
using ScarletMansion.GamePatch;
|
|
using ScarletMansion.GamePatch.Components;
|
|
using static LethalLevelLoader.ExtendedEvent;
|
|
using ScarletMansion.GamePatch.Managers;
|
|
|
|
namespace ScarletMansion {
|
|
|
|
public struct ScarletNetworkParams : INetworkSerializeByMemcpy {
|
|
public int scrapValue;
|
|
public int specialValue;
|
|
}
|
|
|
|
public class ItemReference {
|
|
public Item item;
|
|
public int itemId;
|
|
public int value;
|
|
|
|
public ItemReference(Item item, int itemId, int value){
|
|
this.item = item;
|
|
this.itemId = itemId;
|
|
this.value = value;
|
|
}
|
|
|
|
public override string ToString() {
|
|
var itemString = item ? item.name : "NULL";
|
|
return $"{itemString}:{itemId} ({value})";
|
|
}
|
|
}
|
|
|
|
public class EnemyReference {
|
|
public EnemyType enemy;
|
|
public int index;
|
|
|
|
public EnemyReference(EnemyType enemy, int index){
|
|
this.enemy = enemy;
|
|
this.index = index;
|
|
}
|
|
|
|
public override string ToString() {
|
|
var enemyString = enemy ? enemy.name : "NULL";
|
|
return $"{enemyString}:{index}";
|
|
}
|
|
}
|
|
|
|
public class EnemyReferenceSpawnLogic : EnemyReference {
|
|
public enum SpawnLogic { None, Special };
|
|
public SpawnLogic logic;
|
|
|
|
public EnemyReferenceSpawnLogic(EnemyType enemy, int index, SpawnLogic logic) : base(enemy, index) {
|
|
this.logic = logic;
|
|
}
|
|
|
|
public void ApplySpawnLogic(){
|
|
if (logic == SpawnLogic.None) return;
|
|
|
|
var enemyName = enemy.name.ToLowerInvariant();
|
|
if (enemyName == "knight")
|
|
KnightSpawnManager.Instance.disableNextKnightSpecialSpawn = true;
|
|
else if (enemyName == "jester")
|
|
JesterAIPatch.active = true;
|
|
}
|
|
|
|
public override string ToString() {
|
|
var b = base.ToString();
|
|
return $"{b} [{logic.ToString()}]";
|
|
}
|
|
}
|
|
|
|
public class ScarletNetworkManager : NetworkBehaviour {
|
|
|
|
public static ScarletNetworkManager Instance { get; private set; }
|
|
|
|
void Awake(){
|
|
Instance = this;
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void DestroyPlayerItemInSlotServerRpc(NetworkBehaviourReference playerRef, int itemSlot, ServerRpcParams callParams = default(ServerRpcParams)){
|
|
if (playerRef.TryGet<PlayerControllerB>(out var player)){
|
|
Plugin.logger.LogDebug($"P{player.OwnerClientId}, S{callParams.Receive.SenderClientId}");
|
|
|
|
DestroyPlayerItemInSlotClientRpc(playerRef, itemSlot);
|
|
return;
|
|
}
|
|
|
|
Plugin.logger.LogError($"Error trying to get player script (SERVERRPC)");
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void DestroyPlayerItemInSlotClientRpc(NetworkBehaviourReference playerRef, int itemSlot){
|
|
if (playerRef.TryGet<PlayerControllerB>(out var player) && !player.IsOwner){
|
|
player.DestroyPlayerItemInSlot_SDM(itemSlot);
|
|
return;
|
|
}
|
|
|
|
if (player == null)
|
|
Plugin.logger.LogError($"Error trying to get player script (CLIENTRPC)");
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void CreateItemServerRpc(int itemId, Vector3 position, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
CreateItem(itemId, false, position, null, callParams);
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void CreateItemServerRpc(int itemId, Vector3 position, NetworkBehaviourReference playerRef, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
playerRef.TryGet<PlayerControllerB>(out var player);
|
|
CreateItem(itemId, false, position, player, callParams);
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void CreateScrapItemServerRpc(int itemId, Vector3 position, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
CreateItem(itemId, true, position, null, callParams);
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void CreateScrapItemServerRpc(int itemId, Vector3 position, NetworkBehaviourReference playerRef, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
playerRef.TryGet<PlayerControllerB>(out var player);
|
|
CreateItem(itemId, true, position, player, callParams);
|
|
}
|
|
|
|
private void CreateItem(int itemId, bool fromScrapArray, Vector3 position, PlayerControllerB player, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
GameObject prefab;
|
|
if (fromScrapArray) {
|
|
prefab = Utility.GetDungeonItems()[itemId].spawnableItem.spawnPrefab;
|
|
} else {
|
|
prefab = StartOfRound.Instance.allItemsList.itemsList[itemId].spawnPrefab;
|
|
}
|
|
var comp = CreateGrabbableObject(prefab, player, position);
|
|
|
|
StartCoroutine(WaitForEndOfFrameToUpdateItemInitialProperities(comp));
|
|
UpdateItemFallingProperites(comp, position);
|
|
UpdateItemElevator(comp, player);
|
|
|
|
var scarletItem = comp as IScarletItem;
|
|
if (scarletItem != null) scarletItem.UpdateSpecialProperties(callParams.specialValue);
|
|
|
|
var scrapValue = callParams.scrapValue;
|
|
if (scrapValue > 0) {
|
|
UpdateItemValueProperties(comp, scrapValue);
|
|
}
|
|
|
|
comp.NetworkObject.Spawn(false);
|
|
|
|
if (player == null) CreateItemClientRpc(comp, position, callParams);
|
|
else CreateItemClientRpc(comp, position, player, callParams);
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void CreateItemClientRpc(NetworkBehaviourReference itemRef, Vector3 position, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
if (IsServer) return;
|
|
StartCoroutine(WaitForItem(itemRef, (c) => CreateItem(c, position, null, callParams)));
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void CreateItemClientRpc(NetworkBehaviourReference itemRef, Vector3 position, NetworkBehaviourReference playerRef, ScarletNetworkParams callParams = default(ScarletNetworkParams)){
|
|
if (IsServer) return;
|
|
StartCoroutine(WaitForItemAndPlayer(itemRef, playerRef, (c, p) => CreateItem(c, position, p, callParams)));
|
|
}
|
|
|
|
private IEnumerator WaitForItem(NetworkBehaviourReference itemRef, Action<GrabbableObject> action){
|
|
var t = Time.realtimeSinceStartup + 8f;
|
|
|
|
GrabbableObject comp;
|
|
while(!itemRef.TryGet(out comp)){
|
|
yield return null;
|
|
|
|
if (Time.realtimeSinceStartup > t) {
|
|
Plugin.logger.LogError("Failed to find network object (ITEM)");
|
|
yield break;
|
|
}
|
|
}
|
|
yield return new WaitForEndOfFrame();
|
|
action.Invoke(comp);
|
|
}
|
|
|
|
private IEnumerator WaitForItemAndPlayer(NetworkBehaviourReference itemRef, NetworkBehaviourReference playerRef, Action<GrabbableObject, PlayerControllerB> action){
|
|
var t = Time.realtimeSinceStartup + 8f;
|
|
|
|
GrabbableObject comp;
|
|
while(!itemRef.TryGet(out comp)){
|
|
yield return null;
|
|
|
|
if (Time.realtimeSinceStartup > t) {
|
|
Plugin.logger.LogError("Failed to find network object (ITEM)");
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
PlayerControllerB player;
|
|
while(!playerRef.TryGet(out player)){
|
|
yield return null;
|
|
|
|
if (Time.realtimeSinceStartup > t) {
|
|
Plugin.logger.LogError("Failed to find network object (PLAYER)");
|
|
yield break;
|
|
}
|
|
}
|
|
|
|
yield return new WaitForEndOfFrame();
|
|
action.Invoke(comp, player);
|
|
}
|
|
|
|
private void CreateItem(GrabbableObject item, Vector3 position, PlayerControllerB player, ScarletNetworkParams callParams = default(ScarletNetworkParams)) {
|
|
UpdateItemFallingProperites(item, position);
|
|
UpdateItemElevator(item, player);
|
|
|
|
var scarletItem = item as IScarletItem;
|
|
if (scarletItem != null) scarletItem.UpdateSpecialProperties(callParams.specialValue);
|
|
|
|
var scrapValue = callParams.scrapValue;
|
|
if (scrapValue > 0) {
|
|
UpdateItemValueProperties(item, scrapValue);
|
|
}
|
|
}
|
|
|
|
private GrabbableObject CreateGrabbableObject(GameObject prefab, PlayerControllerB player, Vector3 position) {
|
|
var parent = GetItemSpawnTransform(player);
|
|
var gameObject = GameObject.Instantiate(prefab, position, Quaternion.identity, parent);
|
|
return gameObject.GetComponent<GrabbableObject>();
|
|
}
|
|
|
|
private Transform GetItemSpawnTransform(PlayerControllerB player) {
|
|
if (((player != null && player.isInElevator) || StartOfRound.Instance.inShipPhase) && RoundManager.Instance.spawnedScrapContainer != null) {
|
|
return RoundManager.Instance.spawnedScrapContainer;
|
|
}
|
|
return StartOfRound.Instance.elevatorTransform;
|
|
}
|
|
|
|
private IEnumerator WaitForEndOfFrameToUpdateItemInitialProperities(GrabbableObject comp) {
|
|
yield return new WaitForEndOfFrame();
|
|
UpdateItemInitialProperites(comp);
|
|
}
|
|
|
|
private void UpdateItemInitialProperites(GrabbableObject comp) {
|
|
comp.reachedFloorTarget = false;
|
|
comp.hasHitGround = false;
|
|
comp.fallTime = 0f;
|
|
}
|
|
|
|
private void UpdateItemFallingProperites(GrabbableObject comp, Vector3 position) {
|
|
comp.startFallingPosition = position;
|
|
comp.targetFloorPosition = comp.GetItemFloorPosition(position);
|
|
}
|
|
|
|
private void UpdateItemValueProperties(GrabbableObject comp, int value) {
|
|
comp.SetScrapValue(value);
|
|
RoundManager.Instance.totalScrapValueInLevel += value;
|
|
}
|
|
|
|
private void UpdateItemElevator(GrabbableObject comp, PlayerControllerB player){
|
|
if (player != null && player.isInHangarShipRoom) {
|
|
player.SetItemInElevator(true, true, comp);
|
|
}
|
|
}
|
|
|
|
|
|
public void RequestEvilSkinApply(NetworkObjectReference reference, string enemyName) {
|
|
if (PluginConfig.Instance.paintingEnemyEvilSkinValue && ScarletBedroom.ENEMY_EVIL_LIST.Contains(enemyName)) {
|
|
RequestEvilSkinApplyClientRpc(reference);
|
|
}
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void RequestEvilSkinApplyClientRpc(NetworkObjectReference reference){
|
|
|
|
void ApplyMaterialToRenderers<T>(T[] renderers) where T: Renderer{
|
|
foreach(var render in renderers) {
|
|
var mats = render.materials;
|
|
for(var i = 0; i < mats.Length; i++) {
|
|
var material = new Material(Assets.networkObjectList.ghostMaterial);
|
|
material.mainTexture = mats[i].mainTexture;
|
|
mats[i] = material;
|
|
}
|
|
render.materials = mats;
|
|
}
|
|
}
|
|
|
|
if (reference.TryGet(out var obj)){
|
|
var enemy = obj.GetComponentInParent<EnemyAI>();
|
|
if (enemy == null) return;
|
|
|
|
var enemyName = enemy.name;
|
|
try {
|
|
Plugin.logger.LogDebug($"Applying evil material to {enemyName}");
|
|
ApplyMaterialToRenderers(enemy.GetComponentsInChildren<SkinnedMeshRenderer>());
|
|
ApplyMaterialToRenderers(enemy.GetComponentsInChildren<MeshRenderer>());
|
|
} catch (Exception e){
|
|
Plugin.logger.LogWarning($"Failed to apply evil material to {enemyName}");
|
|
Plugin.logger.LogWarning(e.ToString());
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
public void CreateSpawnAudioPrefab(Vector3 positon, ulong playerId) {
|
|
CreateSpawnAudioPrefabServerRpc(positon, playerId);
|
|
CreateSpawnAudioPrefab(positon);
|
|
}
|
|
|
|
[ServerRpc(RequireOwnership = false)]
|
|
public void CreateSpawnAudioPrefabServerRpc(Vector3 position, ulong playerId){
|
|
CreateSpawnAudioPrefabClientRpc(position, playerId);
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void CreateSpawnAudioPrefabClientRpc(Vector3 position, ulong playerId){
|
|
if (StartOfRound.Instance.localPlayerController.actualClientId == playerId) return;
|
|
CreateSpawnAudioPrefab(position);
|
|
}
|
|
|
|
private void CreateSpawnAudioPrefab(Vector3 position) {
|
|
var copy = Instantiate(Assets.networkObjectList.yukariSpawnPrefab, position, Quaternion.identity);
|
|
var audioSource = copy.GetComponentInChildren<AudioSource>();
|
|
audioSource.time = 0.5f;
|
|
Destroy(copy, 5f);
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void SetupBookPathsClientRpc(){
|
|
if (ScarletGenericManager.Instance == null) return;
|
|
|
|
var bookPaths = ScarletGenericManager.Instance.bookPaths;
|
|
foreach(var b in bookPaths) {
|
|
b.ResetPath();
|
|
}
|
|
}
|
|
|
|
[ClientRpc]
|
|
public void CallSelfDestroyTargetClientRpc(int index){
|
|
if (ScarletGenericManager.Instance == null) return;
|
|
|
|
ScarletGenericManager.Instance.selfDestroyTargets[index].Destroy();
|
|
}
|
|
|
|
}
|
|
|
|
public static class ScarletNetworkManagerUtility {
|
|
|
|
public static int GetMarkedDamageToPlayer(PlayerControllerB player) {
|
|
if (IsPlayerMarkedForDeath(player)) return player.health;
|
|
|
|
if (!player.criticallyInjured) {
|
|
return player.health - 5;
|
|
}
|
|
return player.health;
|
|
}
|
|
|
|
public static bool IsPlayerMarkedForDeath(PlayerControllerB player){
|
|
var scarletPlayer = ScarletPlayerControllerB.GetScarletPlayerScript(player);
|
|
if (scarletPlayer) return scarletPlayer.markedForDeath;
|
|
return false;
|
|
}
|
|
|
|
public static void MarkPlayerForDeath(PlayerControllerB player){
|
|
var scarletPlayer = ScarletPlayerControllerB.GetScarletPlayerScript(player);
|
|
if (scarletPlayer) scarletPlayer.markedForDeath = true;
|
|
}
|
|
|
|
|
|
public static int GetFlashlightId(FlashlightItem flashlightItem){
|
|
var flashlight = Assets.GetFlashlight(flashlightItem.itemProperties);
|
|
if (flashlight != null) return flashlight.itemId;
|
|
return -1;
|
|
}
|
|
|
|
public static NetworkObjectReference CreateEnemyWithRef(EnemyReferenceSpawnLogic enemy, Vector3 position, float yRotation) {
|
|
var roundManager = RoundManager.Instance;
|
|
if (roundManager == null || !roundManager.IsHost) return default;
|
|
|
|
return roundManager.SpawnEnemyGameObject(position, yRotation, enemy.index);
|
|
}
|
|
|
|
public static T CreateEnemyWithRef<T>(EnemyReferenceSpawnLogic enemy, Vector3 position, float yRotation) where T: EnemyAI {
|
|
var spawnedEnemy = CreateEnemyWithRef(enemy, position, yRotation);
|
|
if (spawnedEnemy.TryGet(out var obj)) {
|
|
return obj.GetComponentInChildren<T>();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static NetworkObjectReference CreateEnemyWithType(EnemyType enemy, Vector3 position, float yRotation) {
|
|
var roundManager = RoundManager.Instance;
|
|
if (roundManager == null || !roundManager.IsHost) return default;
|
|
|
|
return roundManager.SpawnEnemyGameObject(position, yRotation, -1, enemy);
|
|
}
|
|
|
|
public static T CreateEnemyWithType<T>(EnemyType enemy, Vector3 position, float yRotation) where T: EnemyAI {
|
|
var spawnedEnemy = CreateEnemyWithType(enemy, position, yRotation);
|
|
if (spawnedEnemy.TryGet(out var obj)) {
|
|
return obj.GetComponentInChildren<T>();
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public static bool CreateFlashlight(PlayerControllerB player, FlashlightItem flashLight, FlandreCrystal crystal){
|
|
var color = crystal.colorIndex;
|
|
var position = crystal.transform.position + Vector3.up * 0.25f;
|
|
var id = GetFlashlightId(flashLight);
|
|
var scrapValue = crystal.scrapValue;
|
|
var flashlightValue = scrapValue;
|
|
var nextCrystal = crystal.targetTransformation;
|
|
|
|
if (nextCrystal) {
|
|
flashlightValue = Mathf.RoundToInt(flashlightValue * 0.5f);
|
|
}
|
|
|
|
if (id == -1) {
|
|
var flashString = flashLight ? flashLight.itemProperties.itemName : "NULL";
|
|
Plugin.logger.LogError($"Failed to find a corresponding flashlight for {flashString}");
|
|
return false;
|
|
}
|
|
|
|
player.DestroyPlayerItemAndSync_SDM(flashLight);
|
|
player.DestroyPlayerItemAndSync_SDM(crystal);
|
|
|
|
ScarletNetworkParams callParams = new ScarletNetworkParams() { scrapValue = flashlightValue, specialValue = color};
|
|
ScarletNetworkManager.Instance.CreateItemServerRpc(id, position, player, callParams);
|
|
|
|
if (nextCrystal) {
|
|
var brokenCrystal = Assets.GetGlobalItem(nextCrystal);
|
|
if (brokenCrystal == null) {
|
|
Plugin.logger.LogError($"Failed to find a corresponding global item for {nextCrystal.itemName}");
|
|
return true;
|
|
}
|
|
|
|
var brokenCrystalId= brokenCrystal.itemId;
|
|
if (brokenCrystalId == -1) {
|
|
Plugin.logger.LogError($"Failed to find a corresponding id for {nextCrystal.itemName}");
|
|
return true;
|
|
}
|
|
|
|
var colorLength = FlandreCrystal.colorVariants.Length;
|
|
var nextScrapColorValue = scrapValue % colorLength;
|
|
var nextScrapValue = flashlightValue - (flashlightValue % colorLength) + nextScrapColorValue;
|
|
ScarletNetworkParams nextCallParams = new ScarletNetworkParams() { scrapValue = nextScrapValue};
|
|
ScarletNetworkManager.Instance.CreateItemServerRpc(brokenCrystalId, position, player, nextCallParams);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static void DestroyPlayerItemAndSync_SDM(this PlayerControllerB player, GrabbableObject obj){
|
|
if (!player.IsOwner) return;
|
|
|
|
var itemSlots = player.ItemSlots;
|
|
for(var i = 0; i < itemSlots.Length; ++i){
|
|
if (itemSlots[i] == obj) {
|
|
DestroyPlayerItemInSlotAndSync_SDM(player, i);
|
|
return;
|
|
}
|
|
}
|
|
|
|
var grabObjString = obj ? obj.itemProperties.itemName : "NULL";
|
|
Plugin.logger.LogError($"Player {player.playerUsername}:{player.actualClientId} tried to destroy item {grabObjString} which is empty or not owned by player");
|
|
}
|
|
|
|
public static void DestroyPlayerItemInSlotAndSync_SDM(this PlayerControllerB player, int itemSlot){
|
|
if (!player.IsOwner) return;
|
|
|
|
var itemSlots = player.ItemSlots;
|
|
if (itemSlot >= itemSlots.Length || itemSlots[itemSlot] == null) {
|
|
Plugin.logger.LogError($"Player {player.playerUsername}:{player.actualClientId} tried to destroy item in slot {itemSlot} which is empty or overflowed");
|
|
return;
|
|
}
|
|
|
|
player.timeSinceSwitchingSlots = 0f;
|
|
player.DestroyPlayerItemInSlot_SDM(itemSlot);
|
|
ScarletNetworkManager.Instance.DestroyPlayerItemInSlotServerRpc(player, itemSlot);
|
|
}
|
|
|
|
public static void DestroyPlayerItemInSlot_SDM(this PlayerControllerB player, int itemSlot){
|
|
// base game does this
|
|
// better safe and slow then sorry
|
|
if (GameNetworkManager.Instance.localPlayerController == null || NetworkManager.Singleton == null || NetworkManager.Singleton.ShutdownInProgress) return;
|
|
|
|
Plugin.logger.LogDebug($"Destroying player {player.playerUsername}:{player.actualClientId}'s item in slot {itemSlot}");
|
|
|
|
var heldObj = player.currentlyHeldObjectServer;
|
|
var heldObjString = heldObj ? heldObj.itemProperties.itemName : "NULL";
|
|
var grabObj = player.ItemSlots[itemSlot];
|
|
var grabObjString = grabObj ? grabObj.itemProperties.itemName : "NULL";
|
|
Plugin.logger.LogDebug($"Held item {player.currentItemSlot}:{heldObjString}");
|
|
Plugin.logger.LogDebug($"Target item {itemSlot}:{grabObjString}");
|
|
|
|
// fix weight and values
|
|
player.carryWeight -= Mathf.Clamp(grabObj.itemProperties.weight - 1f, 0f, 10f);
|
|
RoundManager.Instance.totalScrapValueInLevel -= grabObj.scrapValue;
|
|
|
|
// destroy radar
|
|
if (grabObj.radarIcon) {
|
|
GameObject.Destroy(grabObj.radarIcon.gameObject);
|
|
}
|
|
|
|
// fix character animations and UI
|
|
// if target item is the same as held item
|
|
if (player.isHoldingObject) {
|
|
if (player.currentItemSlot == itemSlot) {
|
|
player.isHoldingObject = false;
|
|
player.twoHanded = false;
|
|
if (player.IsOwner) {
|
|
player.playerBodyAnimator.SetBool("cancelHolding", true);
|
|
player.playerBodyAnimator.SetTrigger("Throw");
|
|
HUDManager.Instance.holdingTwoHandedItem.enabled = false;
|
|
HUDManager.Instance.ClearControlTips();
|
|
player.activatingItem = false;
|
|
}
|
|
}
|
|
|
|
if (heldObj != null && heldObj == grabObj) {
|
|
if (player.IsOwner) {
|
|
player.SetSpecialGrabAnimationBool(false, heldObj);
|
|
}
|
|
player.currentlyHeldObjectServer = null;
|
|
}
|
|
}
|
|
|
|
// no matter what, we need these to be called
|
|
if (player.IsOwner) {
|
|
HUDManager.Instance.itemSlotIcons[itemSlot].enabled = false;
|
|
grabObj.DiscardItemOnClient();
|
|
}
|
|
|
|
player.ItemSlots[itemSlot] = null;
|
|
|
|
if (player.IsServer) grabObj.NetworkObject.Despawn(true);
|
|
}
|
|
|
|
}
|
|
|
|
}
|