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

FREE TimedDoorVault : 1.0.0 1.0.0

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

namespace Oxide.Plugins
{
    [Info("TimedDoorVault", "ZIm", "1.0.0")]
    [Description("Swap armored doors for bank vault doors (/vaultdoor). Link a Smart Alarm as a timed unlock trigger (/doortrigger).")]
    public class TimedDoorVault : RustPlugin
    {
        private const string PermUse = "timeddoorvault.use";
        private const string VaultPrefab = "assets/bundled/prefabs/modding/asset_store/bankheist_package/bankheist_vol03/prefabs/door.vault.static.prefab";
        private const string ArmoredPrefab = "assets/prefabs/building/door.hinged/door.hinged.toptier.prefab";
        private const string DoubleArmoredPrefab = "assets/prefabs/building/door.double.hinged/door.double.hinged.toptier.prefab";
        private const string SmartAlarmPrefab = "assets/prefabs/deployable/playerioents/app/smartalarm/smartalarm.prefab";
        private const string LaptopPrefab = "assets/bundled/prefabs/modding/events/twitch/wipe_laptop.prefab";
        private const string AlarmItemShortname = "smart.alarm";
        private const ulong SkinTag = 777000111UL; // entity marker so we recognise our own swaps

        private const string FxBeep = "assets/prefabs/locks/keypad/effects/lock.code.updated.prefab";
        private const string FxUnlock = "assets/prefabs/locks/keypad/effects/lock.code.unlock.prefab";
        private const string FxDenied = "assets/prefabs/locks/keypad/effects/lock.code.denied.prefab";
        private const string UiName = ".VaultUnlockUI";

        // Optional: status bars via the free AdvancedStatus plugin (by IIIaKa) when installed.
        [PluginReference] private Plugin AdvancedStatus;

        private Configuration config;
        private StoredData data = new StoredData();

        private bool HasStatus => config.UseAdvancedStatus && AdvancedStatus != null && AdvancedStatus.IsLoaded;

        private class OpenTicker
        {
            public Timer Tick;
            public BasePlayer Player;
        }

        // doorId -> hold-open countdown display (built-in popup mode only)
        private readonly Dictionary<ulong, OpenTicker> openTickers = new Dictionary<ulong, OpenTicker>();
        private static double UnixNow => System.DateTimeOffset.UtcNow.ToUnixTimeSeconds();

        private class UnlockSequence
        {
            public ulong DoorId;
            public BasePlayer Starter;
            public int SecondsLeft;
            public Timer Tick;
        }

        // doorId -> running sequence
        private readonly Dictionary<ulong, UnlockSequence> active = new Dictionary<ulong, UnlockSequence>();

        #region Config

        private class Configuration
        {
            [JsonProperty("Vault door health - matches an armored wall (0 = leave prefab default)")]
            public float Health = 2000f;

            [JsonProperty("Opening / Cracking - seconds the unlock sequence takes")]
            public int TriggerSeconds = 20;

            [JsonProperty("Holding Open - seconds the vault stays open before closing")]
            public int RelockSeconds = 30;

            [JsonProperty("Lock After - re-lock the code lock when the vault closes (false = shuts but stays unlocked)")]
            public bool LockAfter = true;

            [JsonProperty("Radius from Vault door - max distance to link a trigger terminal (meters)")]
            public float LinkRadius = 10f;

            [JsonProperty("Anyone can use the terminal (false = only players authed on the code lock, owner, or admins)")]
            public bool AllowAnyoneToTrigger = false;

            [JsonProperty("Use AdvancedStatus bars for countdowns (false = built-in on-screen bar)")]
            public bool UseAdvancedStatus = true;

            [JsonProperty("Alignment - forward/back offset (meters)")]
            public float OffsetForward = -0.9f;

            [JsonProperty("Alignment - left/right offset (meters)")]
            public float OffsetRight = 0.54f;

            [JsonProperty("Alignment - up/down offset (meters)")]
            public float OffsetUp = 0.222f;

            [JsonProperty("Alignment - yaw rotation offset (degrees)")]
            public float YawOffset = 0f;

            [JsonProperty("Code lock - forward offset")]
            public float LockOffsetForward = -0.35f;

            [JsonProperty("Code lock - right offset")]
            public float LockOffsetRight = 0.60f;

            [JsonProperty("Code lock - up offset")]
            public float LockOffsetUp = 0f;
        }

        protected override void LoadDefaultConfig() => config = new Configuration();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<Configuration>();
                if (config == null) throw new JsonException();
                SaveConfig();
            }
            catch
            {
                PrintWarning("Config corrupt, loading defaults.");
                LoadDefaultConfig();
            }
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        #endregion

        #region Data

        private class OriginalDoor
        {
            public string Prefab;
            public float X, Y, Z;
            public float RotX, RotY, RotZ, RotW;
            public ulong OwnerId;
            public ulong PartnerId; // legacy double support; kept for old data
        }

        private class StoredData
        {
            public Dictionary<ulong, OriginalDoor> Swapped = new Dictionary<ulong, OriginalDoor>();
            public Dictionary<ulong, ulong> Partner = new Dictionary<ulong, ulong>();
            // smart alarm net id -> vault door net id
            public Dictionary<ulong, ulong> Triggers = new Dictionary<ulong, ulong>();
            // open vault door net id -> unix time it must shut and re-lock
            // (persisted so a plugin reload or restart can never leave a vault open)
            public Dictionary<ulong, double> RelockAt = new Dictionary<ulong, double>();
        }

        private void LoadData()
        {
            try { data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData(); }
            catch { data = new StoredData(); }
            if (data.Swapped == null) data.Swapped = new Dictionary<ulong, OriginalDoor>();
            if (data.Partner == null) data.Partner = new Dictionary<ulong, ulong>();
            if (data.Triggers == null) data.Triggers = new Dictionary<ulong, ulong>();
            if (data.RelockAt == null) data.RelockAt = new Dictionary<ulong, double>();
        }

        private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(Name, data);

        #endregion

        #region Hooks

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            LoadData();
        }

        private void OnServerInitialized()
        {
            if (config.Health > 0f)
            {
                foreach (var ent in BaseNetworkable.serverEntities)
                {
                    var door = ent as Door;
                    if (door == null || door.IsDestroyed) continue;
                    if (door.PrefabName == VaultPrefab && door.skinID == SkinTag)
                        ApplyHealth(door);
                }
            }

            // Sweep pending relocks: overdue ones shut now, future ones re-schedule.
            foreach (var kv in new List<KeyValuePair<ulong, double>>(data.RelockAt))
            {
                double remaining = kv.Value - UnixNow;
                if (remaining <= 0) DoRelock(kv.Key);
                else { ulong id = kv.Key; timer.Once((float)remaining, () => DoRelock(id)); }
            }
        }

        private void Unload()
        {
            foreach (var seq in new List<UnlockSequence>(active.Values))
            {
                seq.Tick?.Destroy();
                if (seq.Starter != null) CuiHelper.DestroyUi(seq.Starter, UiName);
            }
            active.Clear();
            foreach (var id in new List<ulong>(openTickers.Keys)) StopOpenTicker(id);
        }

        // One handler for all our tracked entities: vault doors, alarms, laptops.
        private void OnEntityKill(BaseEntity entity)
        {
            ulong id = entity?.net?.ID.Value ?? 0;
            if (id == 0) return;
            bool changed = false;

            if (entity is Door)
            {
                CancelSequence(id);

                OriginalDoor rec;
                if (data.Swapped.TryGetValue(id, out rec))
                {
                    if (rec.PartnerId != 0) data.Partner.Remove(rec.PartnerId);
                    data.Swapped.Remove(id);
                    changed = true;
                }
                else if (data.Partner.Remove(id))
                {
                    changed = true;
                }

                // Unlink any triggers pointing at this door.
                foreach (var key in new List<ulong>(data.Triggers.Keys))
                    if (data.Triggers[key] == id) { data.Triggers.Remove(key); changed = true; }
            }
            else if (data.Triggers.Remove(id))
            {
                changed = true;
            }

            if (changed) SaveData();
        }

        // E on a linked trigger (laptop terminal, or a legacy alarm) starts the unlock.
        private void OnPlayerInput(BasePlayer player, InputState input)
        {
            if (player == null || input == null || !input.WasJustPressed(BUTTON.USE)) return;
            if (data.Triggers.Count == 0) return;

            var ent = GetLookEntity<BaseEntity>(player, 3f);
            if (ent == null) return;

            ulong doorId;
            if (!data.Triggers.TryGetValue(ent.net.ID.Value, out doorId)) return;

            var door = BaseNetworkable.serverEntities.Find(new NetworkableId(doorId)) as Door;
            if (door == null || door.IsDestroyed)
            {
                data.Triggers.Remove(ent.net.ID.Value);
                SaveData();
                return;
            }

            TryStartSequence(player, door, ent);
        }

        #endregion

        #region Commands

        [ChatCommand("vaultdoor")]
        private void CmdVaultDoor(BasePlayer player, string command, string[] args)
        {
            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage("You don't have permission to do that.");
                return;
            }

            var target = GetLookEntity<Door>(player, 5f) ?? FindNearbyDoor(player, 4f);
            if (target == null)
            {
                player.ChatMessage("Look at a door (within 5m) and try again.");
                return;
            }

            if (target.PrefabName == ArmoredPrefab)
            {
                SwapToVault(player, target);
            }
            else if (target.PrefabName == DoubleArmoredPrefab)
            {
                player.ChatMessage("Double doors aren't supported — only single armored doors can be swapped.");
            }
            else if (target.PrefabName == VaultPrefab && target.skinID == SkinTag)
            {
                RevertToArmored(player, target);
            }
            else
            {
                player.ChatMessage("That's not an armored door (or one of my vault doors). Only <color=#ffa500>armored doors</color> can be swapped.");
            }
        }

        [ChatCommand("doortrigger")]
        private void CmdDoorTrigger(BasePlayer player, string command, string[] args)
        {
            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage("You don't have permission to do that.");
                return;
            }

            var ent = GetLookEntity<BaseEntity>(player, 5f);

            // Looking at one of our laptop terminals: unlink, remove it, refund the alarm.
            if (ent != null && ent.PrefabName == LaptopPrefab && ent.skinID == SkinTag)
            {
                data.Triggers.Remove(ent.net.ID.Value);
                SaveData();
                ent.Kill();
                var refund = ItemManager.CreateByName(AlarmItemShortname, 1);
                if (refund != null) player.GiveItem(refund, BaseEntity.GiveItemReason.PickedUp);
                player.ChatMessage("Terminal removed — Smart Alarm refunded to your inventory.");
                return;
            }

            var alarm = ent as SmartAlarm ?? GetLookEntity<SmartAlarm>(player, 5f);
            if (alarm == null)
            {
                player.ChatMessage("Look at a <color=#ffa500>Smart Alarm</color> (within 5m) and try again.");
                return;
            }

            // Find the nearest vault door to link.
            Door nearest = null;
            float closest = config.LinkRadius;
            foreach (var kv in data.Swapped)
            {
                var door = BaseNetworkable.serverEntities.Find(new NetworkableId(kv.Key)) as Door;
                if (door == null || door.IsDestroyed) continue;
                float d = Vector3.Distance(door.transform.position, alarm.transform.position);
                if (d < closest) { closest = d; nearest = door; }
            }

            if (nearest == null)
            {
                player.ChatMessage($"No vault door within {config.LinkRadius}m of this alarm. Swap one first with /vaultdoor.");
                return;
            }

            // Carrier-pattern: the alarm becomes the hack terminal (wipe laptop).
            Vector3 pos = alarm.transform.position;
            // The laptop model faces the opposite way to the alarm's mount - flip it.
            Quaternion rot = alarm.transform.rotation * Quaternion.Euler(0f, 180f, 0f);
            ulong prevLink;
            data.Triggers.TryGetValue(alarm.net.ID.Value, out prevLink); // legacy alarm links migrate
            data.Triggers.Remove(alarm.net.ID.Value);

            var laptop = GameManager.server.CreateEntity(LaptopPrefab, pos, rot);
            if (laptop == null)
            {
                // Laptop prop unavailable - fall back to the alarm itself as trigger.
                data.Triggers[alarm.net.ID.Value] = nearest.net.ID.Value;
                SaveData();
                player.ChatMessage($"Linked (laptop prop unavailable, alarm kept). Press <color=#ffa500>E</color> on it for the {config.TriggerSeconds}s unlock.");
                return;
            }

            laptop.skinID = SkinTag;
            laptop.OwnerID = player.userID;
            laptop.Spawn();
            alarm.Kill();

            data.Triggers[laptop.net.ID.Value] = nearest.net.ID.Value;
            SaveData();
            player.ChatMessage($"Terminal deployed and linked to the vault door {closest:0.0}m away. Press <color=#ffa500>E</color> on the laptop for the {config.TriggerSeconds}s unlock. /doortrigger it again to remove.");
        }

        [ConsoleCommand("vaultdoor")]
        private void CmdVaultDoorConsole(ConsoleSystem.Arg arg)
        {
            BasePlayer player = arg.Player();
            if (player == null) return;
            CmdVaultDoor(player, "vaultdoor", new string[0]);
        }

        #endregion

        #region Unlock sequence

        private void TryStartSequence(BasePlayer player, Door door, BaseEntity trigger)
        {
            if (active.ContainsKey(door.net.ID.Value)) return; // already running

            var codeLock = door.GetSlot(BaseEntity.Slot.Lock) as CodeLock;

            if (!config.AllowAnyoneToTrigger && !player.IsAdmin)
            {
                bool authed = door.OwnerID == player.userID
                    || (codeLock != null && codeLock.whitelistPlayers != null && codeLock.whitelistPlayers.Contains(player.userID));
                if (!authed)
                {
                    Effect.server.Run(FxDenied, door.transform.position);
                    player.ChatMessage("You're not authorised on this vault.");
                    return;
                }
            }

            if (door.IsOpen())
            {
                player.ChatMessage("The vault is already open.");
                return;
            }

            var seq = new UnlockSequence
            {
                DoorId = door.net.ID.Value,
                Starter = player,
                SecondsLeft = config.TriggerSeconds
            };
            active[seq.DoorId] = seq;

            // Legacy smart-alarm triggers get their light turned on for the duration.
            if (trigger is SmartAlarm)
            {
                trigger.SetFlagLocal(BaseEntity.Flags.On, true);
                trigger.SendNetworkUpdate();
            }

            // AdvancedStatus bars are timestamp-driven - one call, it animates itself.
            if (HasStatus)
            {
                double now = UnixNow;
                AdvancedStatus.Call("CreateBar", player, new Dictionary<string, object>
                {
                    ["Plugin"] = Name,
                    ["Id"] = "vaultcrack",
                    ["BarType"] = "TimeProgressCounter",
                    ["Text"] = "CRACKING VAULT",
                    ["Main_Color"] = "#7F1D1D",
                    ["Progress_Color"] = "#EF4444",
                    ["Text_Color"] = "#FFFFFF",
                    ["SubText_Color"] = "#FFFFFF",
                    ["Image_Color"] = "#FF6B6B",
                    ["TimeStampStart"] = now,
                    ["TimeStamp"] = now + config.TriggerSeconds,
                    ["TimeStampDestroy"] = now + config.TriggerSeconds
                });
            }

            seq.Tick = timer.Every(1f, () =>
            {
                var d = BaseNetworkable.serverEntities.Find(new NetworkableId(seq.DoorId)) as Door;
                if (d == null || d.IsDestroyed)
                {
                    CancelSequence(seq.DoorId);
                    return;
                }

                if (seq.SecondsLeft > 0)
                {
                    Effect.server.Run(FxBeep, d.transform.position);
                    if (!HasStatus) ShowCountdown(seq.Starter, "CRACKING VAULT", "0.94 0.27 0.27 0.9", "1 0.55 0.55 1", seq.SecondsLeft, config.TriggerSeconds);
                    seq.SecondsLeft--;
                    return;
                }

                // Time's up - open the vault.
                CompleteSequence(seq, d, trigger);
            });
        }

        private void CompleteSequence(UnlockSequence seq, Door door, BaseEntity trigger)
        {
            CancelSequence(seq.DoorId);

            var codeLock = door.GetSlot(BaseEntity.Slot.Lock) as CodeLock;
            if (codeLock != null)
            {
                codeLock.SetFlagLocal(BaseEntity.Flags.Locked, false);
                codeLock.SendNetworkUpdate();
            }

            Effect.server.Run(FxUnlock, door.transform.position);
            door.SetOpen(true);

            if (trigger is SmartAlarm && !trigger.IsDestroyed)
            {
                trigger.SetFlagLocal(BaseEntity.Flags.On, false);
                trigger.SendNetworkUpdate();
            }

            // Re-lock countdown as a status bar (no chat spam).
            if (HasStatus && seq.Starter != null)
            {
                double now = UnixNow;
                AdvancedStatus.Call("CreateBar", seq.Starter, new Dictionary<string, object>
                {
                    ["Plugin"] = Name,
                    ["Id"] = "vaultopen",
                    ["BarType"] = "TimeCounter",
                    ["Text"] = "VAULT OPEN",
                    ["Main_Color"] = "#14532D",
                    ["Progress_Color"] = "#22C55E",
                    ["Text_Color"] = "#FFFFFF",
                    ["SubText_Color"] = "#FFFFFF",
                    ["Image_Color"] = "#4ADE80",
                    ["TimeStampStart"] = now,
                    ["TimeStamp"] = now + config.RelockSeconds,
                    ["TimeStampDestroy"] = now + config.RelockSeconds
                });
            }

            // Auto close + re-lock: deadline persisted so reloads/restarts can't lose it.
            ulong doorId = door.net.ID.Value;
            data.RelockAt[doorId] = UnixNow + config.RelockSeconds;
            SaveData();
            timer.Once(config.RelockSeconds, () => DoRelock(doorId));

            // Built-in popup mode: green hold-open countdown for the starter.
            if (!HasStatus && seq.Starter != null)
            {
                var ticker = new OpenTicker { Player = seq.Starter };
                int left = config.RelockSeconds;
                ticker.Tick = timer.Every(1f, () =>
                {
                    if (left <= 0 || ticker.Player == null || !ticker.Player.IsConnected)
                    {
                        StopOpenTicker(doorId);
                        return;
                    }
                    ShowCountdown(ticker.Player, "VAULT OPEN", "0.13 0.77 0.37 0.9", "0.6 1 0.7 1", left, config.RelockSeconds);
                    left--;
                });
                openTickers[doorId] = ticker;
            }
        }

        private void StopOpenTicker(ulong doorId)
        {
            OpenTicker ticker;
            if (!openTickers.TryGetValue(doorId, out ticker)) return;
            ticker.Tick?.Destroy();
            if (ticker.Player != null) CuiHelper.DestroyUi(ticker.Player, UiName);
            openTickers.Remove(doorId);
        }

        private void DoRelock(ulong doorId)
        {
            StopOpenTicker(doorId);
            if (!data.RelockAt.Remove(doorId)) return; // already handled
            SaveData();

            var d = BaseNetworkable.serverEntities.Find(new NetworkableId(doorId)) as Door;
            if (d == null || d.IsDestroyed) return;
            d.SetOpen(false);
            if (config.LockAfter)
            {
                var cl = d.GetSlot(BaseEntity.Slot.Lock) as CodeLock;
                if (cl != null)
                {
                    cl.SetFlagLocal(BaseEntity.Flags.Locked, true);
                    cl.SendNetworkUpdate();
                }
            }
            Effect.server.Run(FxUnlock, d.transform.position);
        }

        private void CancelSequence(ulong doorId)
        {
            UnlockSequence seq;
            if (!active.TryGetValue(doorId, out seq)) return;
            seq.Tick?.Destroy();
            if (seq.Starter != null)
            {
                CuiHelper.DestroyUi(seq.Starter, UiName);
                if (HasStatus) AdvancedStatus.Call("DeleteBar", seq.Starter, "vaultcrack", Name);
            }
            active.Remove(doorId);
        }

        private void ShowCountdown(BasePlayer player, string text, string barColor, string textColor, int seconds, int total)
        {
            if (player == null || !player.IsConnected) return;

            // DestroyUi inside the element = atomic in-place replace, no flicker.
            var container = new CuiElementContainer();
            container.Add(new CuiElement
            {
                Parent = "Overlay",
                Name = UiName,
                DestroyUi = UiName,
                Components =
                {
                    new CuiImageComponent { Color = "0 0 0 0.8" },
                    new CuiRectTransformComponent { AnchorMin = "0.5 0", AnchorMax = "0.5 0", OffsetMin = "-120 120", OffsetMax = "120 150" }
                }
            });

            float progress = 1f - (seconds / (float)total);
            container.Add(new CuiPanel
            {
                RectTransform = { AnchorMin = "0 0", AnchorMax = $"{Mathf.Clamp01(progress)} 0.15" },
                Image = { Color = barColor }
            }, UiName);

            container.Add(new CuiLabel
            {
                RectTransform = { AnchorMin = "0 0.2", AnchorMax = "1 1" },
                Text =
                {
                    Text = $"{text} — {seconds}s",
                    Align = TextAnchor.MiddleCenter,
                    FontSize = 14,
                    Font = "robotocondensed-bold.ttf",
                    Color = textColor
                }
            }, UiName);

            CuiHelper.AddUi(player, container);
        }

        #endregion

        #region Core (swap / revert)

        private void SwapToVault(BasePlayer player, Door armored)
        {
            Vector3 basePos = armored.transform.position;
            Quaternion baseRot = armored.transform.rotation;
            ulong ownerId = armored.OwnerID != 0 ? armored.OwnerID : player.userID;

            var original = new OriginalDoor
            {
                Prefab = armored.PrefabName,
                X = basePos.x, Y = basePos.y, Z = basePos.z,
                RotX = baseRot.x, RotY = baseRot.y, RotZ = baseRot.z, RotW = baseRot.w,
                OwnerId = ownerId
            };

            Quaternion finalRot = baseRot * Quaternion.Euler(0f, config.YawOffset, 0f);
            Vector3 finalPos = basePos
                             + finalRot * Vector3.forward * config.OffsetForward
                             + finalRot * Vector3.right * config.OffsetRight
                             + Vector3.up * config.OffsetUp;

            var vault = GameManager.server.CreateEntity(VaultPrefab, finalPos, finalRot) as Door;
            if (vault == null)
            {
                player.ChatMessage("Vault door prefab failed to spawn - nothing was changed.");
                return;
            }

            vault.skinID = SkinTag;
            vault.OwnerID = ownerId;
            vault.Spawn();
            ApplyHealth(vault);

            TransferLock(armored, vault, toVault: true);

            armored.Kill();
            vault.SendNetworkUpdateImmediate();

            data.Swapped[vault.net.ID.Value] = original;
            SaveData();

            player.ChatMessage("Swapped to a <color=#ffa500>Vault Door</color>. /vaultdoor again to revert. Mount a Smart Alarm nearby and /doortrigger to add a timed unlock.");
        }

        private void RevertToArmored(BasePlayer player, Door vault)
        {
            ulong id = vault.net.ID.Value;
            ulong primaryId;
            if (data.Partner.TryGetValue(id, out primaryId))
            {
                var primary = BaseNetworkable.serverEntities.Find(new NetworkableId(primaryId)) as Door;
                if (primary != null && !primary.IsDestroyed) vault = primary;
            }

            OriginalDoor original;
            Vector3 pos;
            Quaternion rot;
            ulong ownerId;
            string prefab = ArmoredPrefab;
            Door partner = null;

            if (data.Swapped.TryGetValue(vault.net.ID.Value, out original))
            {
                pos = new Vector3(original.X, original.Y, original.Z);
                rot = new Quaternion(original.RotX, original.RotY, original.RotZ, original.RotW);
                ownerId = original.OwnerId;
                if (!string.IsNullOrEmpty(original.Prefab)) prefab = original.Prefab;
                if (original.PartnerId != 0)
                    partner = BaseNetworkable.serverEntities.Find(new NetworkableId(original.PartnerId)) as Door;
            }
            else
            {
                rot = vault.transform.rotation * Quaternion.Euler(0f, -config.YawOffset, 0f);
                pos = vault.transform.position
                    - vault.transform.rotation * Vector3.forward * config.OffsetForward
                    - vault.transform.rotation * Vector3.right * config.OffsetRight
                    - Vector3.up * config.OffsetUp;
                ownerId = vault.OwnerID;
            }

            var armored = GameManager.server.CreateEntity(prefab, pos, rot) as Door;
            if (armored == null)
            {
                player.ChatMessage("Armored door failed to spawn - nothing was changed.");
                return;
            }
            armored.OwnerID = ownerId != 0 ? ownerId : player.userID;
            armored.Spawn();

            TransferLock(vault, armored, toVault: false);

            if (original != null && original.PartnerId != 0) data.Partner.Remove(original.PartnerId);
            data.Swapped.Remove(vault.net.ID.Value);
            SaveData();

            vault.Kill();
            if (partner != null && !partner.IsDestroyed) partner.Kill();
            armored.SendNetworkUpdateImmediate();

            player.ChatMessage("Reverted to an <color=#ffa500>Armored Door</color>.");
        }

        private void TransferLock(Door from, Door to, bool toVault)
        {
            var baseLock = from.GetSlot(BaseEntity.Slot.Lock) as BaseLock;
            if (baseLock == null || baseLock.IsDestroyed) return;

            from.SetSlot(BaseEntity.Slot.Lock, null);
            baseLock.SetParent(to, to.GetSlotAnchorName(BaseEntity.Slot.Lock));
            to.SetSlot(BaseEntity.Slot.Lock, baseLock);

            baseLock.transform.localPosition = Vector3.zero;
            if (toVault)
            {
                baseLock.transform.localPosition += new Vector3(
                    config.LockOffsetRight, config.LockOffsetUp, config.LockOffsetForward);
            }
            baseLock.SendNetworkUpdateImmediate();
        }

        private void ApplyHealth(Door door)
        {
            if (config.Health <= 0f) return;
            SetHealth(door);
            NextTick(() => SetHealth(door));
            timer.Once(0.5f, () => SetHealth(door));
            timer.Once(2f, () => SetHealth(door));
        }

        private void SetHealth(Door door)
        {
            if (door == null || door.IsDestroyed) return;
            door.InitializeHealth(config.Health, config.Health);
            door._maxHealth = config.Health;
            door.health = config.Health;
            door.SendNetworkUpdate();
        }

        // Walk every ray hit (ignoring trigger volumes) and take the closest T.
        private T GetLookEntity<T>(BasePlayer player, float distance) where T : BaseEntity
        {
            var hits = Physics.RaycastAll(player.eyes.HeadRay(), distance, -1, QueryTriggerInteraction.Ignore);
            T best = null;
            float closest = float.MaxValue;
            foreach (var hit in hits)
            {
                var ent = hit.GetEntity() as T;
                if (ent != null && hit.distance < closest)
                {
                    closest = hit.distance;
                    best = ent;
                }
            }
            return best;
        }

        private Door FindNearbyDoor(BasePlayer player, float radius)
        {
            var list = new List<BaseEntity>();
            Vis.Entities(player.transform.position, radius, list, Rust.Layers.Mask.Construction);
            Door best = null;
            float closest = float.MaxValue;
            foreach (var ent in list)
            {
                var door = ent as Door;
                if (door == null || door.IsDestroyed) continue;
                bool swappable = door.PrefabName == ArmoredPrefab
                              || (door.PrefabName == VaultPrefab && door.skinID == SkinTag);
                if (!swappable) continue;
                float d = Vector3.Distance(door.transform.position, player.transform.position);
                if (d < closest) { closest = d; best = door; }
            }
            return best;
        }

        #endregion

        #region Align tuning (admin)

        [ConsoleCommand("timeddoorvault.align")]
        private void CmdAlign(ConsoleSystem.Arg arg)
        {
            BasePlayer player = arg.Player();
            if (player == null || !player.IsAdmin) return;

            if (arg.Args == null || arg.Args.Length < 4)
            {
                arg.ReplyWith("Usage: timeddoorvault.align <forward> <right> <up> <yaw>\n" +
                              $"Current: {config.OffsetForward} {config.OffsetRight} {config.OffsetUp} {config.YawOffset}\n" +
                              "Applies to the NEXT swap (revert + re-swap to see it).");
                return;
            }

            float fwd, right, up, yaw;
            if (!float.TryParse(arg.GetString(0), out fwd) || !float.TryParse(arg.GetString(1), out right) ||
                !float.TryParse(arg.GetString(2), out up) || !float.TryParse(arg.GetString(3), out yaw))
            {
                arg.ReplyWith("All four values must be numbers.");
                return;
            }

            config.OffsetForward = fwd;
            config.OffsetRight = right;
            config.OffsetUp = up;
            config.YawOffset = yaw;
            SaveConfig();
            arg.ReplyWith($"Saved: forward={fwd} right={right} up={up} yaw={yaw}. Revert + re-swap a door to apply.");
        }

        #endregion
    }
}

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