using System; using System.Collections.Generic; using EntityStates; using HG; using RoR2.UI; using UnityEngine; using UnityEngine.Networking; namespace RoR2; [RequireComponent(typeof(CombatSquad))] public class BossGroup : MonoBehaviour { private struct BossMemory { public NetworkInstanceId masterInstanceId; public float maxObservedMaxHealth; public float lastObservedHealth; public CharacterMaster cachedMaster; public CharacterBody cachedBody; } private class DefeatBossObjectiveTracker : ObjectivePanelController.ObjectiveTracker { public DefeatBossObjectiveTracker() { baseToken = "OBJECTIVE_DEFEAT_BOSS"; } } public class EnableHudHealthBarState : EntityState { private BossGroup bossGroup; public override void OnEnter() { base.OnEnter(); bossGroup = GetComponent(); if ((object)bossGroup != null) { bossGroup.shouldDisplayHealthBarOnHud = true; } } public override void OnExit() { if ((object)bossGroup != null) { bossGroup.shouldDisplayHealthBarOnHud = false; } base.OnExit(); } } public float bossDropChance = 0.15f; public Transform dropPosition; public PickupDropTable dropTable; public bool scaleRewardsByPlayerCount = true; [Tooltip("Whether or not this boss group should display a health bar on the HUD while any of its members are alive. Other scripts can change this at runtime to suppress a health bar until the boss is angered, for example. This field is not networked, so whatever is driving the value should be synchronized over the network.")] public bool shouldDisplayHealthBarOnHud = true; private Xoroshiro128Plus rng; private List bossDropTables; private Run.FixedTimeStamp enabledTime; [Header("Deprecated")] public bool forceTier3Reward; private List bossDrops; private static readonly int initialBossMemoryCapacity = 8; private BossMemory[] bossMemories = new BossMemory[initialBossMemoryCapacity]; private int bossMemoryCount; private static int lastTotalBossCount = 0; private static bool totalBossCountDirty = false; public float fixedAge => combatSquad.awakeTime.timeSince; public float fixedTimeSinceEnabled => enabledTime.timeSince; public int bonusRewardCount { get; set; } public CombatSquad combatSquad { get; private set; } public string bestObservedName { get; private set; } = ""; public string bestObservedSubtitle { get; private set; } = ""; public float totalMaxObservedMaxHealth { get; private set; } public float totalObservedHealth { get; private set; } public static event Action onBossGroupStartServer; public static event Action onBossGroupDefeatedServer; private void Awake() { base.enabled = false; combatSquad = GetComponent(); combatSquad.onMemberDiscovered += OnMemberDiscovered; combatSquad.onMemberLost += OnMemberLost; if (NetworkServer.active) { combatSquad.onDefeatedServer += OnDefeatedServer; combatSquad.onMemberAddedServer += OnMemberAddedServer; combatSquad.onMemberDefeatedServer += OnMemberDefeatedServer; rng = new Xoroshiro128Plus(Run.instance.bossRewardRng.nextUlong); bossDrops = new List(); bossDropTables = new List(); } } private void Start() { if (NetworkServer.active) { BossGroup.onBossGroupStartServer?.Invoke(this); } } private void OnEnable() { InstanceTracker.Add(this); ObjectivePanelController.collectObjectiveSources += ReportObjective; enabledTime = Run.FixedTimeStamp.now; } private void OnDisable() { ObjectivePanelController.collectObjectiveSources -= ReportObjective; InstanceTracker.Remove(this); } private void FixedUpdate() { UpdateBossMemories(); } private void OnDefeatedServer() { DropRewards(); Run.instance.OnServerBossDefeated(this); BossGroup.onBossGroupDefeatedServer?.Invoke(this); } private void OnMemberAddedServer(CharacterMaster memberMaster) { Run.instance.OnServerBossAdded(this, memberMaster); } private void OnMemberDefeatedServer(CharacterMaster memberMaster, DamageReport damageReport) { DeathRewards deathRewards = memberMaster.GetBodyObject()?.GetComponent(); if (!deathRewards) { return; } if ((bool)deathRewards.bossDropTable) { bossDropTables.Add(deathRewards.bossDropTable); return; } PickupIndex pickupIndex = (PickupIndex)deathRewards.bossPickup; if (pickupIndex != PickupIndex.none) { bossDrops.Add(pickupIndex); } } private void OnMemberDiscovered(CharacterMaster memberMaster) { base.enabled = true; memberMaster.isBoss = true; totalBossCountDirty = true; RememberBoss(memberMaster); } private void OnMemberLost(CharacterMaster memberMaster) { memberMaster.isBoss = false; totalBossCountDirty = true; if (combatSquad.memberCount == 0) { base.enabled = false; } } private void DropRewards() { if (!Run.instance) { Debug.LogError("No valid run instance!"); return; } if (rng == null) { Debug.LogError("RNG is null!"); return; } int participatingPlayerCount = Run.instance.participatingPlayerCount; if (participatingPlayerCount == 0) { return; } if ((bool)dropPosition) { PickupIndex none = PickupIndex.none; if ((bool)dropTable) { none = dropTable.GenerateDrop(rng); } else { List list = Run.instance.availableTier2DropList; if (forceTier3Reward) { list = Run.instance.availableTier3DropList; } none = rng.NextElementUniform(list); } int num = 1 + bonusRewardCount; if (scaleRewardsByPlayerCount) { num *= participatingPlayerCount; } float angle = 360f / (float)num; Vector3 vector = Quaternion.AngleAxis(UnityEngine.Random.Range(0, 360), Vector3.up) * (Vector3.up * 40f + Vector3.forward * 5f); Quaternion quaternion = Quaternion.AngleAxis(angle, Vector3.up); bool flag = bossDrops != null && bossDrops.Count > 0; bool flag2 = bossDropTables != null && bossDropTables.Count > 0; int num2 = 0; while (num2 < num) { PickupIndex pickupIndex = none; if (bossDrops != null && (flag || flag2) && rng.nextNormalizedFloat <= bossDropChance) { if (flag2) { PickupDropTable pickupDropTable = rng.NextElementUniform(bossDropTables); if (pickupDropTable != null) { pickupIndex = pickupDropTable.GenerateDrop(rng); } } else { pickupIndex = rng.NextElementUniform(bossDrops); } } PickupDropletController.CreatePickupDroplet(pickupIndex, dropPosition.position, vector); num2++; vector = quaternion * vector; } } else { Debug.LogWarning("dropPosition not set for BossGroup! No item will be spawned."); } } private int FindBossMemoryIndex(NetworkInstanceId id) { for (int i = 0; i < bossMemoryCount; i++) { if (bossMemories[i].masterInstanceId == id) { return i; } } return -1; } private void RememberBoss(CharacterMaster master) { if ((bool)master) { int num = FindBossMemoryIndex(master.netId); if (num == -1) { num = AddBossMemory(master); } ref BossMemory reference = ref bossMemories[num]; reference.cachedMaster = master; reference.cachedBody = master.GetBody(); UpdateObservations(ref reference); } } private void UpdateObservations(ref BossMemory memory) { memory.lastObservedHealth = 0f; if ((bool)memory.cachedMaster && !memory.cachedBody) { memory.cachedBody = memory.cachedMaster.GetBody(); } if (!memory.cachedBody) { return; } if (bestObservedName.Length == 0 && bestObservedSubtitle.Length == 0 && Time.fixedDeltaTime * 3f < memory.cachedBody.localStartTime.timeSince) { bestObservedName = Util.GetBestBodyName(memory.cachedBody.gameObject); bestObservedSubtitle = memory.cachedBody.GetSubtitle(); if (bestObservedSubtitle.Length == 0) { bestObservedSubtitle = Language.GetString("NULL_SUBTITLE"); } bestObservedSubtitle = " " + bestObservedSubtitle + ""; } HealthComponent healthComponent = memory.cachedBody.healthComponent; memory.maxObservedMaxHealth = Mathf.Max(memory.maxObservedMaxHealth, healthComponent.fullCombinedHealth); memory.lastObservedHealth = healthComponent.combinedHealth; } private int AddBossMemory(CharacterMaster master) { BossMemory bossMemory = default(BossMemory); bossMemory.masterInstanceId = master.netId; bossMemory.maxObservedMaxHealth = 0f; bossMemory.cachedMaster = master; BossMemory value = bossMemory; ArrayUtils.ArrayAppend(ref bossMemories, ref bossMemoryCount, in value); return bossMemoryCount - 1; } private void UpdateBossMemories() { totalMaxObservedMaxHealth = 0f; totalObservedHealth = 0f; for (int i = 0; i < bossMemoryCount; i++) { ref BossMemory reference = ref bossMemories[i]; UpdateObservations(ref reference); totalMaxObservedMaxHealth += reference.maxObservedMaxHealth; totalObservedHealth += Mathf.Max(reference.lastObservedHealth, 0f); } } public static int GetTotalBossCount() { if (totalBossCountDirty) { totalBossCountDirty = false; lastTotalBossCount = 0; List instancesList = InstanceTracker.GetInstancesList(); for (int i = 0; i < instancesList.Count; i++) { lastTotalBossCount += instancesList[i].combatSquad.readOnlyMembersList.Count; } } return lastTotalBossCount; } public static BossGroup FindBossGroup(CharacterBody body) { if (!body || !body.isBoss) { return null; } CharacterMaster master = body.master; if (!master) { return null; } List instancesList = InstanceTracker.GetInstancesList(); for (int i = 0; i < instancesList.Count; i++) { BossGroup bossGroup = instancesList[i]; if (bossGroup.combatSquad.ContainsMember(master)) { return bossGroup; } } return null; } public void ReportObjective(CharacterMaster master, List output) { if (combatSquad.readOnlyMembersList.Count != 0) { output.Add(new ObjectivePanelController.ObjectiveSourceDescriptor { source = this, master = master, objectiveType = typeof(DefeatBossObjectiveTracker) }); } } }