Иконка ресурса

FREE TerrustriaInstanceRaids : 0.6.0 0.6.0

Нет прав для скачивания
C#:
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core.Libraries.Covalence;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("TerrustriaInstanceRaids", "Terrustria", "0.6.0")]
    [Description("Private solo/duo lane raids with CopyPaste lane baselines, checkpoint-gated room spawning, boss progression, tier-keycard gating, hard prefab injection, and completion reward crates.")]
    public class TerrustriaInstanceRaids : RustPlugin
    {
        #region Plugin refs

        [PluginReference] private Plugin CopyPaste;
        [PluginReference] private Plugin BotReSpawn;
        [PluginReference] private Plugin BossMonster; // optional for future use

        private ISpawnAdapter _spawnAdapter;

        #endregion

        #region Config

        private PluginConfig _config;

        private class PluginConfig
        {
            [JsonProperty("General")] public GeneralConfig General = new GeneralConfig();
            [JsonProperty("Dungeons")] public Dictionary<string, DungeonConfig> Dungeons = new Dictionary<string, DungeonConfig>();

            public class GeneralConfig
            {
                [JsonProperty("Max Party Size")] public int MaxPartySize = 2;

                [JsonProperty("Lane Bounds Default Radius (meters)")] public float DefaultLaneRadius = 120f;
                [JsonProperty("Instance Hard Timeout Minutes")] public float HardTimeoutMinutes = 180f;

                [JsonProperty("Allow Leaving Instance Command")] public bool AllowLeaveCommand = true;

                [JsonProperty("NPC Tether Tick Seconds")] public float NpcTetherTickSeconds = 1.0f;
                [JsonProperty("NPC Default Leash Radius (meters)")] public float DefaultNpcLeashRadius = 25f;
                [JsonProperty("NPC Default Hard Reset Radius (meters)")] public float DefaultNpcHardResetRadius = 45f;

                [JsonProperty("CopyPaste: Default Paste Batch Collision Radius (blockcollision)")]
                public float CopyPasteBlockCollision = 0f;

                // Trigger scanning (checkpoint gates + completion)
                [JsonProperty("Trigger Tick Seconds")] public float TriggerTickSeconds = 0.5f;

                [JsonProperty("Checkpoint Default Trigger Radius (meters)")]
                public float DefaultCheckpointRadius = 3.5f;

                [JsonProperty("Checkpoint Notification Cooldown Seconds")]
                public float CheckpointNotifyCooldownSeconds = 6f;

                // Boss identification (profile + proximity)
                [JsonProperty("Boss Spawn Detect Radius (meters)")]
                public float BossDetectRadius = 6f;

                // Fallback verification (anti-softlock)
                [JsonProperty("Verification Tick Seconds")]
                public float VerificationTickSeconds = 5f;

                [JsonProperty("Verification: Re-spawn boss if missing (seconds between attempts)")]
                public float BossRespawnAttemptCooldownSeconds = 20f;

                [JsonProperty("Verification: Spawn room if party is in gate and it isn't spawned (seconds between attempts)")]
                public float GateSpawnAttemptCooldownSeconds = 10f;

                // Completion / rewards
                [JsonProperty("Completion Loot Window Seconds")]
                public int CompletionLootWindowSeconds = 600; // 10 minutes

                [JsonProperty("Reward Crate Prefab")]
                public string RewardCratePrefab = "assets/prefabs/deployable/large wood storage/box.wooden.large.prefab";

                [JsonProperty("Reward Crate Offset From Completion Position")]
                public Vector3Data RewardCrateOffset = new Vector3Data { X = 0f, Y = 0f, Z = 1.5f };

                [JsonProperty("Reward Crates To Spawn")]
                public int RewardCratesToSpawn = 1;
            }

            public class DungeonConfig
            {
                [JsonProperty("Tier")] public int Tier = 1;
                [JsonProperty("Lanes")] public List<LaneConfig> Lanes = new List<LaneConfig>();

                // BotReSpawn profiles
                [JsonProperty("BotReSpawn Profile (Packs Default)")] public string BotProfilePacks = "dungeon_packs_t1";
                [JsonProperty("BotReSpawn Profile (Boss Default)")] public string BotProfileBoss = "dungeon_boss_t1";

                // Difficulty multipliers (owned by this core; actual scaling hooks to bots can come later)
                [JsonProperty("Base HP Multiplier")] public float HpMult = 1f;
                [JsonProperty("Base Damage Multiplier")] public float DmgMult = 1f;
                [JsonProperty("Party Scalar (Solo)")] public float PartyScalarSolo = 1.00f;
                [JsonProperty("Party Scalar (Duo)")] public float PartyScalarDuo = 1.45f;

                // Tier keycard gating (used on room gate checkpoints)
                [JsonProperty("Enable Tier Keycard Gates")] public bool EnableTierKeycardGates = true;
                [JsonProperty("Consume Keycard On Gate Use")] public bool ConsumeKeycardOnUse = false;

                // Tier-based reward loot (NOT tied to party size)
                [JsonProperty("Reward Loot (Tier-based)")]
                public List<RewardItem> RewardLoot = new List<RewardItem>();

                public class RewardItem
                {
                    [JsonProperty("Shortname")] public string Shortname;
                    [JsonProperty("Amount")] public int Amount = 1;
                    [JsonProperty("SkinId")] public ulong SkinId = 0;
                }
            }

            public class LaneConfig
            {
                [JsonProperty("Lane Id")] public string LaneId = "lane_01";

                [JsonProperty("Lane Center")] public Vector3Data LaneCenter = new Vector3Data();
                [JsonProperty("Lane Radius (meters)")] public float LaneRadius = 0f;

                // CopyPaste baseline restore
                [JsonProperty("CopyPaste Baseline File")] public string BaselineFile = "containment_lane_t1";
                [JsonProperty("CopyPaste Rotation Correction")] public float RotationCorrection = 0f;
                [JsonProperty("CopyPaste Options")] public PasteOptions PasteOptions = new PasteOptions();

                // "DungeonBases capability" (without dependency): spawn hard prefabs deterministically after baseline paste
                [JsonProperty("Hard Prefabs (spawn after baseline paste)")]
                public List<HardPrefabConfig> HardPrefabs = new List<HardPrefabConfig>();

                // Staging / Exit
                [JsonProperty("Staging Spawn")] public Vector3Data StagingSpawn = new Vector3Data();
                [JsonProperty("Exit Spawn (Return)")] public Vector3Data ExitSpawn = new Vector3Data();

                // Anti-cheese staging boundary (NPCs never allowed into staging sphere)
                [JsonProperty("Staging Center")] public Vector3Data StagingCenter = new Vector3Data();
                [JsonProperty("Staging No-NPC Radius (meters)")] public float StagingNoNpcRadius = 35f;

                // Completion room trigger (after final boss)
                [JsonProperty("Completion Position")] public Vector3Data CompletionPosition = new Vector3Data();
                [JsonProperty("Completion Radius (meters)")] public float CompletionRadius = 6f;

                // Waypoints / checkpoints
                [JsonProperty("Checkpoints")]
                public Dictionary<string, CheckpointConfig> Checkpoints = new Dictionary<string, CheckpointConfig>();

                // Room progression
                [JsonProperty("Rooms")]
                public List<RoomConfig> Rooms = new List<RoomConfig>();
            }

            public class HardPrefabConfig
            {
                [JsonProperty("Enabled")] public bool Enabled = true;
                [JsonProperty("Prefab")] public string Prefab = "";
                [JsonProperty("Position")] public Vector3Data Position = new Vector3Data();
                [JsonProperty("Yaw Degrees")] public float Yaw = 0f;
                [JsonProperty("SkinId")] public ulong SkinId = 0;
            }

            public class RoomConfig
            {
                [JsonProperty("Room Id")] public string RoomId = "room_01";

                // The waypoint room trigger that spawns THIS room when entered (once unlocked).
                [JsonProperty("Gate Checkpoint Id")] public string GateCheckpointId = "cp_gate_01";

                // Optional: when boss dies, automatically set this checkpoint as the party respawn anchor
                [JsonProperty("Checkpoint To Set On Clear (optional)")] public string SetCheckpointOnClear = "";

                // Trash
                [JsonProperty("Pack Profile (optional override)")] public string PackProfileOverride = "";
                [JsonProperty("Pack Spawns")] public List<Vector3Data> PackSpawns = new List<Vector3Data>();

                // Boss
                [JsonProperty("Boss Profile (optional override)")] public string BossProfileOverride = "";
                [JsonProperty("Boss Spawn")] public Vector3Data BossSpawn = new Vector3Data();
            }

            public class CheckpointConfig
            {
                [JsonProperty("Position")] public Vector3Data Position = new Vector3Data();
                [JsonProperty("Radius (meters)")] public float Radius = 0f;

                // Optional: spawn a keycard security door prefab for aesthetics (and consistent entry points).
                // Example prefabs:
                //  assets/bundled/prefabs/static/door.hinged.security.green.prefab
                //  assets/bundled/prefabs/static/door.hinged.security.blue.prefab
                //  assets/bundled/prefabs/static/door.hinged.security.red.prefab
                [JsonProperty("Spawn Gate Door")] public bool SpawnGateDoor = false;
                [JsonProperty("Gate Door Yaw Degrees")] public float GateDoorYaw = 0f;
                [JsonProperty("Gate Door Offset")] public Vector3Data GateDoorOffset = new Vector3Data { X = 0f, Y = 0f, Z = 0f };
            }

            public class PasteOptions
            {
                [JsonProperty("AutoHeight")] public bool AutoHeight = true;
                [JsonProperty("Height")] public float Height = 0f;
                [JsonProperty("Stability")] public bool Stability = false;
                [JsonProperty("Deployables")] public bool Deployables = true;
                [JsonProperty("Inventories")] public bool Inventories = true;
                [JsonProperty("Vending")] public bool Vending = true;
                [JsonProperty("EntityOwner")] public bool EntityOwner = true;
            }

            public class Vector3Data
            {
                [JsonProperty("x")] public float X;
                [JsonProperty("y")] public float Y;
                [JsonProperty("z")] public float Z;

                public Vector3 ToVector3() => new Vector3(X, Y, Z);
                public static Vector3Data From(Vector3 v) => new Vector3Data { X = v.x, Y = v.y, Z = v.z };
            }
        }

        protected override void LoadDefaultConfig()
        {
            _config = new PluginConfig();

            var dungeon = new PluginConfig.DungeonConfig
            {
                Tier = 1,
                BotProfilePacks = "containment_packs_t1",
                BotProfileBoss = "containment_boss_t1",
                HpMult = 1f,
                DmgMult = 1f,
                PartyScalarSolo = 1.0f,
                PartyScalarDuo = 1.45f,
                EnableTierKeycardGates = true,
                ConsumeKeycardOnUse = false,
                RewardLoot = new List<PluginConfig.DungeonConfig.RewardItem>
                {
                    new PluginConfig.DungeonConfig.RewardItem{ Shortname="scrap", Amount=500 },
                    new PluginConfig.DungeonConfig.RewardItem{ Shortname="hq.metal", Amount=25 }
                },
                Lanes = new List<PluginConfig.LaneConfig>
                {
                    new PluginConfig.LaneConfig
                    {
                        LaneId = "lane_01",
                        LaneCenter = PluginConfig.Vector3Data.From(new Vector3(-4000, 50, -4000)),
                        LaneRadius = 160f,
                        BaselineFile = "containment_lane_t1_lane01",

                        StagingSpawn = PluginConfig.Vector3Data.From(new Vector3(-4030, 50, -4020)),
                        ExitSpawn = PluginConfig.Vector3Data.From(new Vector3(0, 10, 0)),
                        StagingCenter = PluginConfig.Vector3Data.From(new Vector3(-4030, 50, -4020)),
                        StagingNoNpcRadius = 35f,

                        CompletionPosition = PluginConfig.Vector3Data.From(new Vector3(-3920, 50, -3920)),
                        CompletionRadius = 7f,

                        Checkpoints = new Dictionary<string, PluginConfig.CheckpointConfig>
                        {
                            { "cp_gate_01", new PluginConfig.CheckpointConfig { Position = PluginConfig.Vector3Data.From(new Vector3(-4015, 50, -4015)), Radius = 4f, SpawnGateDoor=true, GateDoorYaw=90f } },
                            { "cp_gate_02", new PluginConfig.CheckpointConfig { Position = PluginConfig.Vector3Data.From(new Vector3(-3970, 50, -3970)), Radius = 4f, SpawnGateDoor=true, GateDoorYaw=90f } },
                            { "cp_gate_03", new PluginConfig.CheckpointConfig { Position = PluginConfig.Vector3Data.From(new Vector3(-3940, 50, -3940)), Radius = 4f, SpawnGateDoor=true, GateDoorYaw=90f } },
                        },

                        Rooms = new List<PluginConfig.RoomConfig>
                        {
                            new PluginConfig.RoomConfig
                            {
                                RoomId = "room_01",
                                GateCheckpointId = "cp_gate_01",
                                SetCheckpointOnClear = "cp_gate_02",
                                PackSpawns = new List<PluginConfig.Vector3Data>
                                {
                                    PluginConfig.Vector3Data.From(new Vector3(-4005, 50, -4005)),
                                    PluginConfig.Vector3Data.From(new Vector3(-3995, 50, -3995)),
                                },
                                BossSpawn = PluginConfig.Vector3Data.From(new Vector3(-3985, 50, -3985))
                            },
                            new PluginConfig.RoomConfig
                            {
                                RoomId = "room_02",
                                GateCheckpointId = "cp_gate_02",
                                SetCheckpointOnClear = "cp_gate_03",
                                PackSpawns = new List<PluginConfig.Vector3Data>
                                {
                                    PluginConfig.Vector3Data.From(new Vector3(-3960, 50, -3960)),
                                    PluginConfig.Vector3Data.From(new Vector3(-3950, 50, -3950)),
                                },
                                BossSpawn = PluginConfig.Vector3Data.From(new Vector3(-3945, 50, -3945))
                            },
                            new PluginConfig.RoomConfig
                            {
                                RoomId = "room_03_final",
                                GateCheckpointId = "cp_gate_03",
                                SetCheckpointOnClear = "cp_gate_03",
                                PackSpawns = new List<PluginConfig.Vector3Data>
                                {
                                    PluginConfig.Vector3Data.From(new Vector3(-3935, 50, -3935)),
                                    PluginConfig.Vector3Data.From(new Vector3(-3928, 50, -3928)),
                                },
                                BossSpawn = PluginConfig.Vector3Data.From(new Vector3(-3925, 50, -3925))
                            }
                        }
                    }
                }
            };

            _config.Dungeons["containment_t1"] = dungeon;

            SaveConfig();
        }

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try { _config = Config.ReadObject<PluginConfig>(); }
            catch
            {
                PrintWarning("Config invalid; regenerating default.");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig() => Config.WriteObject(_config, true);

        #endregion

        #region Runtime state

        private enum InstancePhase { Staging, InProgress, Completed, Failed, Expired }

        private class InstanceState
        {
            public string InstanceId;
            public string DungeonKey;
            public string LaneId;
            public int Tier;
            public InstancePhase Phase;

            public ulong LeaderId;
            public HashSet<ulong> Members = new HashSet<ulong>();

            public DateTime StartedUtc;
            public DateTime ExpiresUtc;

            // Waypoint checkpoints (whole party)
            public string CurrentCheckpointId;
            public HashSet<string> ClaimedCheckpoints = new HashSet<string>();
            public Dictionary<string, DateTime> LastCheckpointNotifyUtcById = new Dictionary<string, DateTime>();

            // Progression
            public int UnlockedRoomIndex; // highest room index allowed to spawn
            public HashSet<int> SpawnedRooms = new HashSet<int>();
            public HashSet<int> DefeatedRooms = new HashSet<int>();
            public bool CompletionUnlocked;

            // Spawning / tracking
            public string BotGroupKey;
            public Dictionary<uint, NpcTetherInfo> TrackedNpcs = new Dictionary<uint, NpcTetherInfo>();

            // Boss tracking: netID -> roomIndex
            public Dictionary<uint, int> BossNetIds = new Dictionary<uint, int>();

            // Baseline + hard prefabs spawned in lane
            public HashSet<uint> LaneBaselineEntities = new HashSet<uint>();
            public List<uint> LaneHardPrefabEntities = new List<uint>();
            public List<uint> GateDoorEntities = new List<uint>();

            // Rewards
            public bool RewardsSpawned;
            public List<uint> RewardCrateNetIds = new List<uint>();
            public DateTime AutoEndUtc;

            // Verification throttles
            public DateTime LastBossRespawnAttemptUtc = DateTime.MinValue;
            public DateTime LastGateSpawnAttemptUtc = DateTime.MinValue;
        }

        private class NpcTetherInfo
        {
            public Vector3 SpawnPos;
            public float LeashRadius;
            public float HardResetRadius;
            public Vector3 StagingCenter;
            public float StagingNoNpcRadius;
        }

        private class PendingPaste
        {
            public string InstanceId;
            public string LaneId;
            public string Filename;
            public Vector3 StartPos;
        }

        private readonly Dictionary<string, InstanceState> _instancesById = new Dictionary<string, InstanceState>();
        private readonly Dictionary<string, string> _laneToInstanceId = new Dictionary<string, string>();
        private readonly Dictionary<ulong, string> _playerToInstanceId = new Dictionary<ulong, string>();
        private readonly Dictionary<string, PendingPaste> _pendingPasteByLane = new Dictionary<string, PendingPaste>();

        private Timer _tetherTimer;
        private Timer _triggerTimer;
        private Timer _verifyTimer;

        #endregion

        #region Lifecycle

        private void OnServerInitialized()
        {
            _spawnAdapter = BotReSpawn != null ? (ISpawnAdapter)new BotReSpawnAdapter(BotReSpawn) : new NativeSpawnAdapter(this);
            Puts($"SpawnAdapter={_spawnAdapter?.Name ?? "null"} | CopyPaste={(CopyPaste != null ? "OK" : "MISSING")}");

            StartLoops();
        }

        private void Unload()
        {
            _tetherTimer?.Destroy();
            _triggerTimer?.Destroy();
            _verifyTimer?.Destroy();

            foreach (var inst in _instancesById.Values.ToList())
            {
                try { EndInstance(inst, InstancePhase.Expired, "Plugin unload"); }
                catch { /* ignored */ }
            }
        }

        private void StartLoops()
        {
            _tetherTimer?.Destroy();
            _triggerTimer?.Destroy();
            _verifyTimer?.Destroy();

            _tetherTimer = timer.Every(Mathf.Clamp(_config.General.NpcTetherTickSeconds, 0.25f, 5f), TetherTick);
            _triggerTimer = timer.Every(Mathf.Clamp(_config.General.TriggerTickSeconds, 0.1f, 2f), TriggerTick);
            _verifyTimer = timer.Every(Mathf.Clamp(_config.General.VerificationTickSeconds, 1f, 30f), VerificationTick);
        }

        #endregion

        #region Commands

        [ChatCommand("raid")]
        private void CmdRaid(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsConnected) return;

            if (args == null || args.Length == 0)
            {
                Reply(player, "Usage: /raid start <dungeonKey> | /raid leave | /raid status | /raid resetlane (admin)");
                return;
            }

            var sub = args[0].ToLowerInvariant();
            if (sub == "start")
            {
                if (args.Length < 2) { Reply(player, "Usage: /raid start <dungeonKey>"); return; }
                TryStartInstance(player, args[1]);
                return;
            }
            if (sub == "leave")
            {
                if (!_config.General.AllowLeaveCommand) { Reply(player, "Leaving is disabled."); return; }
                TryLeaveInstance(player);
                return;
            }
            if (sub == "status") { ShowStatus(player); return; }

            if (sub == "resetlane")
            {
                if (!player.IsAdmin) { Reply(player, "Admin only."); return; }
                if (!_playerToInstanceId.TryGetValue(player.userID, out var id) || !_instancesById.TryGetValue(id, out var inst))
                {
                    Reply(player, "Not in an instance.");
                    return;
                }
                ResetLaneToBaseline(inst, "Admin reset");
                Reply(player, "Lane reset to baseline.");
                return;
            }

            Reply(player, "Unknown subcommand.");
        }

        private void Reply(BasePlayer player, string msg) =>
            SendReply(player, $"<color=#b7ffb7>[Raid]</color> {msg}");

        #endregion

        #region Instance start/end

        private void TryStartInstance(BasePlayer leader, string dungeonKey)
        {
            if (_playerToInstanceId.ContainsKey(leader.userID)) { Reply(leader, "You are already in an instance."); return; }
            if (!_config.Dungeons.TryGetValue(dungeonKey, out var dungeon)) { Reply(leader, $"Unknown dungeonKey '{dungeonKey}'."); return; }

            var party = ResolveParty(leader);
            if (party.Count == 0) { Reply(leader, "No valid party members found."); return; }
            if (party.Count > _config.General.MaxPartySize) { Reply(leader, $"Party too large. Max {_config.General.MaxPartySize}."); return; }

            var lane = FindFreeLane(dungeon);
            if (lane == null) { Reply(leader, "All lanes are currently busy."); return; }

            var instanceId = $"{dungeonKey}:{lane.LaneId}:{DateTime.UtcNow.Ticks}";
            var inst = new InstanceState
            {
                InstanceId = instanceId,
                DungeonKey = dungeonKey,
                LaneId = lane.LaneId,
                Tier = dungeon.Tier,
                Phase = InstancePhase.Staging,
                LeaderId = leader.userID,
                StartedUtc = DateTime.UtcNow,
                ExpiresUtc = DateTime.UtcNow.AddMinutes(_config.General.HardTimeoutMinutes),
                BotGroupKey = $"raid_{leader.userID}_{lane.LaneId}_{DateTime.UtcNow.Ticks}".ToLowerInvariant(),

                UnlockedRoomIndex = 0,
                CompletionUnlocked = false,
                RewardsSpawned = false
            };

            foreach (var uid in party) inst.Members.Add(uid);

            _instancesById[inst.InstanceId] = inst;
            _laneToInstanceId[lane.LaneId] = inst.InstanceId;
            foreach (var uid in inst.Members) _playerToInstanceId[uid] = inst.InstanceId;

            ResetLaneToBaseline(inst, "Instance start baseline");

            var staging = lane.StagingSpawn.ToVector3();
            foreach (var uid in inst.Members)
            {
                var p = BasePlayer.FindByID(uid);
                if (p != null && p.IsConnected) SafeTeleport(p, staging);
            }

            Reply(leader, $"Instance created: {dungeonKey} ({lane.LaneId}) | Party={inst.Members.Count}.");
            Reply(leader, "Enter the first waypoint room (Gate checkpoint) to spawn the first trash/boss.");
        }

        private void EndInstance(InstanceState inst, InstancePhase finalPhase, string reason)
        {
            if (inst == null) return;

            inst.Phase = finalPhase;

            var dungeon = _config.Dungeons[inst.DungeonKey];
            var lane = dungeon.Lanes.First(l => l.LaneId == inst.LaneId);

            _spawnAdapter?.DespawnGroup(inst);
            inst.TrackedNpcs.Clear();
            inst.BossNetIds.Clear();

            KillEntityNetIds(inst.RewardCrateNetIds);
            inst.RewardCrateNetIds.Clear();

            KillEntityNetIds(inst.LaneHardPrefabEntities);
            inst.LaneHardPrefabEntities.Clear();

            KillEntityNetIds(inst.GateDoorEntities);
            inst.GateDoorEntities.Clear();

            KillEntitySet(inst.LaneBaselineEntities);
            inst.LaneBaselineEntities.Clear();

            var exit = lane.ExitSpawn.ToVector3();
            foreach (var uid in inst.Members.ToList())
            {
                var p = BasePlayer.FindByID(uid);
                if (p != null && p.IsConnected) SafeTeleport(p, exit);
                _playerToInstanceId.Remove(uid);
            }

            _laneToInstanceId.Remove(inst.LaneId);
            _instancesById.Remove(inst.InstanceId);

            Puts($"Instance ended ({finalPhase}) {inst.InstanceId} reason={reason}");
        }

        private void TryLeaveInstance(BasePlayer player)
        {
            if (!_playerToInstanceId.TryGetValue(player.userID, out var instanceId) || !_instancesById.TryGetValue(instanceId, out var inst))
            {
                Reply(player, "You are not in an instance.");
                return;
            }

            var dungeon = _config.Dungeons[inst.DungeonKey];
            var lane = dungeon.Lanes.First(l => l.LaneId == inst.LaneId);

            if (player.userID == inst.LeaderId)
            {
                EndInstance(inst, InstancePhase.Failed, "Leader left");
                Reply(player, "Leader left. The run has ended.");
                return;
            }

            inst.Members.Remove(player.userID);
            _playerToInstanceId.Remove(player.userID);

            SafeTeleport(player, lane.ExitSpawn.ToVector3());
            Reply(player, "You left the instance.");

            if (inst.Members.Count == 0)
                EndInstance(inst, InstancePhase.Expired, "All members left");
        }

        #endregion

        #region Party resolution

        private HashSet<ulong> ResolveParty(BasePlayer leader)
        {
            var party = new HashSet<ulong> { leader.userID };

            if (leader.currentTeam != 0)
            {
                var team = RelationshipManager.ServerInstance.FindTeam(leader.currentTeam);
                if (team != null)
                {
                    foreach (var uid in team.members)
                    {
                        if (uid == leader.userID) continue;
                        var p = BasePlayer.FindByID(uid);
                        if (p == null || !p.IsConnected) continue;
                        party.Add(uid);
                        break; // duo cap
                    }
                }
            }

            return party;
        }

        #endregion

        #region Trigger tick

        private void TriggerTick()
        {
            if (_instancesById.Count == 0) return;

            foreach (var inst in _instancesById.Values.ToList())
            {
                if (DateTime.UtcNow > inst.ExpiresUtc)
                {
                    EndInstance(inst, InstancePhase.Expired, "Hard timeout");
                    continue;
                }

                if (inst.Phase == InstancePhase.Completed && DateTime.UtcNow >= inst.AutoEndUtc)
                {
                    EndInstance(inst, InstancePhase.Completed, "Loot window elapsed");
                    continue;
                }

                if (!_config.Dungeons.TryGetValue(inst.DungeonKey, out var dungeon)) continue;
                var lane = dungeon.Lanes.FirstOrDefault(l => l.LaneId == inst.LaneId);
                if (lane == null) continue;

                ProcessCheckpointsAndGates(inst, lane, dungeon);

                if (inst.CompletionUnlocked && inst.Phase == InstancePhase.InProgress)
                {
                    if (AnyMemberInside(inst, lane.CompletionPosition.ToVector3(), Mathf.Max(1f, lane.CompletionRadius)))
                    {
                        inst.Phase = InstancePhase.Completed;
                        inst.AutoEndUtc = DateTime.UtcNow.AddSeconds(_config.General.CompletionLootWindowSeconds);

                        TrySpawnRewardCrates(inst, dungeon, lane);

                        foreach (var uid in inst.Members)
                        {
                            var p = BasePlayer.FindByID(uid);
                            if (p != null && p.IsConnected)
                                Reply(p, $"Run complete. Rewards spawned. You have {_config.General.CompletionLootWindowSeconds}s to loot before the instance closes. Use /raid leave when done.");
                        }
                    }
                }
            }
        }

        private void ProcessCheckpointsAndGates(InstanceState inst, PluginConfig.LaneConfig lane, PluginConfig.DungeonConfig dungeon)
        {
            foreach (var uid in inst.Members)
            {
                var p = BasePlayer.FindByID(uid);
                if (p == null || !p.IsConnected) continue;

                var pos = p.transform.position;

                foreach (var kvp in lane.Checkpoints)
                {
                    var cpId = kvp.Key;
                    var cp = kvp.Value;
                    if (cp?.Position == null) continue;

                    var r = cp.Radius > 0 ? cp.Radius : _config.General.DefaultCheckpointRadius;
                    if (Vector3.Distance(pos, cp.Position.ToVector3()) > r) continue;

                    // Gate keycard check for unlocked rooms
                    if (IsGateCheckpoint(lane, cpId, out var gateRoomIndex) &&
                        dungeon.EnableTierKeycardGates &&
                        gateRoomIndex <= inst.UnlockedRoomIndex &&
                        !inst.SpawnedRooms.Contains(gateRoomIndex))
                    {
                        if (!CheckGateKeycard(p, dungeon.Tier, dungeon.ConsumeKeycardOnUse))
                            return;
                    }

                    if (!inst.ClaimedCheckpoints.Contains(cpId))
                        ClaimCheckpoint(inst, cpId);

                    TrySpawnRoomFromGate(inst, lane, dungeon, cpId);
                    return;
                }
            }
        }

        private bool IsGateCheckpoint(PluginConfig.LaneConfig lane, string checkpointId, out int roomIndex)
        {
            roomIndex = -1;
            for (var i = 0; i < lane.Rooms.Count; i++)
            {
                if (string.Equals(lane.Rooms[i].GateCheckpointId, checkpointId, StringComparison.OrdinalIgnoreCase))
                {
                    roomIndex = i;
                    return true;
                }
            }
            return false;
        }

        private string GetRequiredKeycardShortnameForTier(int tier)
        {
            if (tier <= 1) return "keycard_green";
            if (tier <= 3) return "keycard_blue";
            return "keycard_red";
        }

        private bool CheckGateKeycard(BasePlayer player, int tier, bool consume)
        {
            var needed = GetRequiredKeycardShortnameForTier(tier);
            var item = FindItemByShortname(player, needed);
            if (item == null)
            {
                Reply(player, $"Access denied. Requires {needed.Replace("keycard_", "").ToUpperInvariant()} keycard for this Tier.");
                return false;
            }

            if (!consume) return true;

            if (item.amount > 1) item.amount -= 1;
            else item.Remove();

            player.inventory?.SendSnapshot();
            Reply(player, $"Keycard consumed: {needed.Replace("keycard_", "").ToUpperInvariant()}");
            return true;
        }

        private Item FindItemByShortname(BasePlayer player, string shortname)
        {
            if (player?.inventory == null) return null;

            Item FindIn(ItemContainer c) =>
                c?.itemList?.FirstOrDefault(i => i?.info != null && i.info.shortname == shortname);

            return FindIn(player.inventory.containerMain) ?? FindIn(player.inventory.containerBelt);
        }

        private void TrySpawnRoomFromGate(InstanceState inst, PluginConfig.LaneConfig lane, PluginConfig.DungeonConfig dungeon, string gateCheckpointId)
        {
            var idx = lane.Rooms.FindIndex(r => string.Equals(r.GateCheckpointId, gateCheckpointId, StringComparison.OrdinalIgnoreCase));
            if (idx < 0) return;

            if (idx > inst.UnlockedRoomIndex) return;
            if (inst.SpawnedRooms.Contains(idx)) return;

            SpawnRoom(inst, lane, dungeon, idx);
        }

        private void ClaimCheckpoint(InstanceState inst, string checkpointId)
        {
            inst.ClaimedCheckpoints.Add(checkpointId);
            inst.CurrentCheckpointId = checkpointId;

            var now = DateTime.UtcNow;
            inst.LastCheckpointNotifyUtcById.TryGetValue(checkpointId, out var last);
            if ((now - last).TotalSeconds < _config.General.CheckpointNotifyCooldownSeconds) return;

            inst.LastCheckpointNotifyUtcById[checkpointId] = now;

            foreach (var uid in inst.Members)
            {
                var p = BasePlayer.FindByID(uid);
                if (p != null && p.IsConnected)
                    Reply(p, $"Checkpoint claimed: {checkpointId}. Respawns will route here.");
            }
        }

        private bool AnyMemberInside(InstanceState inst, Vector3 center, float radius)
        {
            foreach (var uid in inst.Members)
            {
                var p = BasePlayer.FindByID(uid);
                if (p == null || !p.IsConnected) continue;
                if (Vector3.Distance(p.transform.position, center) <= radius) return true;
            }
            return false;
        }

        #endregion

        #region Spawn/progression

        private class SpawnContext
        {
            public string BotGroupKey;
            public int Tier;
            public int PartySize;
            public float HpMult;
            public float DmgMult;
        }

        private void SpawnRoom(InstanceState inst, PluginConfig.LaneConfig lane, PluginConfig.DungeonConfig dungeon, int roomIndex)
        {
            var room = lane.Rooms[roomIndex];

            var partyScalar = inst.Members.Count >= 2 ? dungeon.PartyScalarDuo : dungeon.PartyScalarSolo;
            var ctx = new SpawnContext
            {
                BotGroupKey = inst.BotGroupKey,
                Tier = inst.Tier,
                PartySize = inst.Members.Count,
                HpMult = dungeon.HpMult * partyScalar,
                DmgMult = dungeon.DmgMult * partyScalar
            };

            var packProfile = !string.IsNullOrEmpty(room.PackProfileOverride) ? room.PackProfileOverride : dungeon.BotProfilePacks;
            var bossProfile = !string.IsNullOrEmpty(room.BossProfileOverride) ? room.BossProfileOverride : dungeon.BotProfileBoss;

            inst.SpawnedRooms.Add(roomIndex);
            inst.Phase = InstancePhase.InProgress;

            if (room.PackSpawns != null)
            {
                foreach (var sp in room.PackSpawns)
                    _spawnAdapter?.SpawnPackAt(inst, ctx, sp.ToVector3(), packProfile);
            }

            var bossPos = room.BossSpawn?.ToVector3() ?? Vector3.zero;
            if (bossPos != Vector3.zero && !string.IsNullOrEmpty(bossProfile))
                _spawnAdapter?.SpawnBossAt(inst, ctx, bossPos, bossProfile, roomIndex);

            foreach (var uid in inst.Members)
            {
                var p = BasePlayer.FindByID(uid);
                if (p != null && p.IsConnected)
                    Reply(p, $"Spawned {room.RoomId}.");
            }
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            var netId = entity?.net?.ID ?? 0u;
            if (netId == 0u) return;

            foreach (var inst in _instancesById.Values.ToList())
            {
                if (!inst.BossNetIds.TryGetValue(netId, out var roomIndex)) continue;

                inst.BossNetIds.Remove(netId);
                inst.DefeatedRooms.Add(roomIndex);

                var dungeon = _config.Dungeons[inst.DungeonKey];
                var lane = dungeon.Lanes.First(l => l.LaneId == inst.LaneId);
                var room = lane.Rooms[roomIndex];

                var nextIndex = roomIndex + 1;
                if (nextIndex < lane.Rooms.Count)
                {
                    inst.UnlockedRoomIndex = Math.Max(inst.UnlockedRoomIndex, nextIndex);

                    if (!string.IsNullOrEmpty(room.SetCheckpointOnClear) && lane.Checkpoints.ContainsKey(room.SetCheckpointOnClear))
                        ClaimCheckpoint(inst, room.SetCheckpointOnClear);

                    foreach (var uid in inst.Members)
                    {
                        var p = BasePlayer.FindByID(uid);
                        if (p != null && p.IsConnected)
                            Reply(p, $"Boss down. Next waypoint unlocked. Step into the next waypoint room.");
                    }
                }
                else
                {
                    inst.CompletionUnlocked = true;

                    if (!string.IsNullOrEmpty(room.SetCheckpointOnClear) && lane.Checkpoints.ContainsKey(room.SetCheckpointOnClear))
                        ClaimCheckpoint(inst, room.SetCheckpointOnClear);

                    foreach (var uid in inst.Members)
                    {
                        var p = BasePlayer.FindByID(uid);
                        if (p != null && p.IsConnected)
                            Reply(p, "Final boss down. Proceed to the completion checkpoint room for rewards.");
                    }
                }

                break;
            }
        }

        #endregion

        #region Verification tick

        private void VerificationTick()
        {
            foreach (var inst in _instancesById.Values)
            {
                if (inst.Phase != InstancePhase.InProgress) continue;

                var dungeon = _config.Dungeons[inst.DungeonKey];
                var lane = dungeon.Lanes.First(l => l.LaneId == inst.LaneId);

                if ((DateTime.UtcNow - inst.LastGateSpawnAttemptUtc).TotalSeconds >= _config.General.GateSpawnAttemptCooldownSeconds)
                {
                    if (ForceSpawnIfInsideUnlockedGate(inst, lane, dungeon))
                        inst.LastGateSpawnAttemptUtc = DateTime.UtcNow;
                }

                if ((DateTime.UtcNow - inst.LastBossRespawnAttemptUtc).TotalSeconds >= _config.General.BossRespawnAttemptCooldownSeconds)
                {
                    if (EnsureBossPresentForActiveRoom(inst, lane, dungeon))
                        inst.LastBossRespawnAttemptUtc = DateTime.UtcNow;
                }
            }
        }

        private bool ForceSpawnIfInsideUnlockedGate(InstanceState inst, PluginConfig.LaneConfig lane, PluginConfig.DungeonConfig dungeon)
        {
            for (var idx = 0; idx <= inst.UnlockedRoomIndex && idx < lane.Rooms.Count; idx++)
            {
                if (inst.SpawnedRooms.Contains(idx)) continue;

                var room = lane.Rooms[idx];
                if (!lane.Checkpoints.TryGetValue(room.GateCheckpointId, out var cp)) continue;

                var r = cp.Radius > 0 ? cp.Radius : _config.General.DefaultCheckpointRadius;
                if (!AnyMemberInside(inst, cp.Position.ToVector3(), r)) continue;

                if (dungeon.EnableTierKeycardGates)
                {
                    var first = inst.Members.Select(BasePlayer.FindByID)
                        .FirstOrDefault(pl => pl != null && pl.IsConnected && Vector3.Distance(pl.transform.position, cp.Position.ToVector3()) <= r);

                    if (first != null && !CheckGateKeycard(first, dungeon.Tier, dungeon.ConsumeKeycardOnUse))
                        return false;
                }

                SpawnRoom(inst, lane, dungeon, idx);
                return true;
            }

            return false;
        }

        private bool EnsureBossPresentForActiveRoom(InstanceState inst, PluginConfig.LaneConfig lane, PluginConfig.DungeonConfig dungeon)
        {
            var active = inst.SpawnedRooms.Where(i => !inst.DefeatedRooms.Contains(i)).DefaultIfEmpty(-1).Max();
            if (active < 0) return false;

            // if we already have an alive boss tracked for this active room, OK
            foreach (var kvp in inst.BossNetIds.ToList())
            {
                if (kvp.Value != active) continue;
                var ent = BaseNetworkable.serverEntities.Find(kvp.Key) as BaseCombatEntity;
                if (ent != null && !ent.IsDestroyed && ent.IsAlive()) return false;
            }

            var room = lane.Rooms[active];
            var bossProfile = !string.IsNullOrEmpty(room.BossProfileOverride) ? room.BossProfileOverride : dungeon.BotProfileBoss;
            var bossPos = room.BossSpawn.ToVector3();
            if (string.IsNullOrEmpty(bossProfile)) return false;

            var partyScalar = inst.Members.Count >= 2 ? dungeon.PartyScalarDuo : dungeon.PartyScalarSolo;
            var ctx = new SpawnContext
            {
                BotGroupKey = inst.BotGroupKey,
                Tier = inst.Tier,
                PartySize = inst.Members.Count,
                HpMult = dungeon.HpMult * partyScalar,
                DmgMult = dungeon.DmgMult * partyScalar
            };

            _spawnAdapter?.SpawnBossAt(inst, ctx, bossPos, bossProfile, active);
            return true;
        }

        #endregion

        #region Rewards

        private void TrySpawnRewardCrates(InstanceState inst, PluginConfig.DungeonConfig dungeon, PluginConfig.LaneConfig lane)
        {
            if (inst.RewardsSpawned) return;
            inst.RewardsSpawned = true;

            var prefab = _config.General.RewardCratePrefab;
            if (string.IsNullOrEmpty(prefab)) return;

            var basePos = lane.CompletionPosition.ToVector3();
            var offset = _config.General.RewardCrateOffset.ToVector3();
            var count = Mathf.Clamp(_config.General.RewardCratesToSpawn, 1, 10);

            for (var i = 0; i < count; i++)
            {
                var spawnPos = basePos + offset + new Vector3(i * 1.2f, 0f, 0f);
                var ent = GameManager.server.CreateEntity(prefab, spawnPos, Quaternion.identity, true);
                if (ent == null) continue;

                ent.enableSaving = false;
                ent.Spawn();

                var id = ent.net?.ID ?? 0u;
                if (id != 0u) inst.RewardCrateNetIds.Add(id);

                var storage = ent as StorageContainer;
                if (storage?.inventory == null) continue;

                storage.inventory.Clear();
                foreach (var ri in dungeon.RewardLoot ?? new List<PluginConfig.DungeonConfig.RewardItem>())
                {
                    if (ri == null || string.IsNullOrEmpty(ri.Shortname) || ri.Amount <= 0) continue;
                    var item = ItemManager.CreateByName(ri.Shortname, ri.Amount, ri.SkinId);
                    if (item == null) continue;
                    item.MoveToContainer(storage.inventory);
                }
                storage.SendNetworkUpdateImmediate();
            }
        }

        #endregion

        #region Lane build/reset

        private PluginConfig.LaneConfig FindFreeLane(PluginConfig.DungeonConfig dungeon) =>
            dungeon.Lanes.FirstOrDefault(l => !_laneToInstanceId.ContainsKey(l.LaneId));

        private void ResetLaneToBaseline(InstanceState inst, string reason)
        {
            var dungeon = _config.Dungeons[inst.DungeonKey];
            var lane = dungeon.Lanes.First(l => l.LaneId == inst.LaneId);

            KillEntitySet(inst.LaneBaselineEntities);
            inst.LaneBaselineEntities.Clear();

            KillEntityNetIds(inst.LaneHardPrefabEntities);
            inst.LaneHardPrefabEntities.Clear();

            KillEntityNetIds(inst.GateDoorEntities);
            inst.GateDoorEntities.Clear();

            KillEntityNetIds(inst.RewardCrateNetIds);
            inst.RewardCrateNetIds.Clear();
            inst.RewardsSpawned = false;

            inst.TrackedNpcs.Clear();
            inst.BossNetIds.Clear();
            inst.DefeatedRooms.Clear();
            inst.SpawnedRooms.Clear();
            inst.UnlockedRoomIndex = 0;
            inst.ClaimedCheckpoints.Clear();
            inst.CurrentCheckpointId = null;
            inst.LastCheckpointNotifyUtcById.Clear();
            inst.CompletionUnlocked = false;
            inst.Phase = InstancePhase.Staging;

            if (CopyPaste == null || string.IsNullOrEmpty(lane.BaselineFile)) return;

            var pos = lane.LaneCenter.ToVector3();
            var args = BuildCopyPasteArgs(lane);

            _pendingPasteByLane[lane.LaneId] = new PendingPaste
            {
                InstanceId = inst.InstanceId,
                LaneId = lane.LaneId,
                Filename = lane.BaselineFile,
                StartPos = pos
            };

            CopyPaste.Call("TryPasteFromVector3", pos, lane.RotationCorrection, lane.BaselineFile, args);
        }

        private string[] BuildCopyPasteArgs(PluginConfig.LaneConfig lane)
        {
            var o = lane.PasteOptions ?? new PluginConfig.PasteOptions();
            return new[]
            {
                "stability", o.Stability ? "true" : "false",
                "deployables", o.Deployables ? "true" : "false",
                "inventories", o.Inventories ? "true" : "false",
                "vending", o.Vending ? "true" : "false",
                "entityowner", o.EntityOwner ? "true" : "false",
                "autoheight", o.AutoHeight ? "true" : "false",
                "height", o.Height.ToString("0.###"),
                "blockcollision", _config.General.CopyPasteBlockCollision.ToString("0.###"),
            };
        }

        private void OnPasteFinished(List<BaseEntity> pastedEntities, string filename, IPlayer player, Vector3 startPos)
        {
            if (pastedEntities == null || pastedEntities.Count == 0) return;

            PendingPaste match = null;
            foreach (var kvp in _pendingPasteByL
// ... [Code preview truncated due to length. Full source in attached file] ...

Автор
15FPS
Скачиваний
0
Просмотры
1
Первый выпуск
Обновление
Оценка
0.00 звёзд 0 оценок
Сверху