Added the new coilhead behaviour to the knight Added treasure room puzzle for bedroom Changed many LogInfo into LogDebug Removed jank animator override stuff (no offense) Changed how treasure rooms lock their doors to work in multiplayer Removed basement scripts
512 lines
19 KiB
C#
512 lines
19 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;
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
|
|
public static class ScarletNetworkManagerUtility {
|
|
|
|
public static int GetCriticalDamageToPlayer(PlayerControllerB player, bool forceKill) {
|
|
if (player.health > 20 && !forceKill) {
|
|
return player.health - 15;
|
|
}
|
|
return player.health;
|
|
}
|
|
|
|
|
|
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);
|
|
}
|
|
|
|
}
|
|
|
|
}
|