← Back to changelog

What changed

Personal Vault Door · v1.0.6 → v1.0.7 (current)

Diff 315 added · 3 removed · 1048 → 1360 total lines
1 1 /*▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓
2 2 ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒
3 3 ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░
4 4 ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░
5 5 ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░
6 6 ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░
7 7 ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░
8 8 ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
9 9 ░ ░ ░ ░ ░ ░ ░*/
10 10 using Oxide.Game.Rust.Cui;
11 11 using ProtoBuf;
12 12 using Newtonsoft.Json;
13 13 using System.Collections.Generic;
14 14 using System.Linq;
15 15 using UnityEngine;
16 16 using Oxide.Core.Plugins;
17 17
18 18 namespace Oxide.Plugins
19 19 {
20 - [Info("Personal Vault Door", "bmgjet", "1.0.6")]
20 + [Info("Personal Vault Door", "bmgjet", "1.0.7")]
21 21 [Description("Lets you place a vault door")]
22 22 public class PersonalVaultDoor : RustPlugin
23 23 {
24 24 [PluginReference]
25 25 private Plugin ServerRewards, Economics;
26 26 #region Configuration
27 27
28 28 private Configuration config;
29 29
30 30 private class Configuration
31 31 {
32 32 [JsonProperty("Vault Max Health")]
33 33 public float VaultMaxHealth = 2000;
34 34
35 35 [JsonProperty("Show Health % Bar CUI")]
36 36 public bool HealthBarEnabled = false;
37 37
38 38 [JsonProperty(PropertyName = "Range to show health")]
39 39 public float vaultrange = 3f;
40 40
41 41
42 42 [JsonProperty("Slide Door Settings")]
43 43 public SlideSettings SlideDoor = new SlideSettings();
44 44
45 + [JsonProperty("Pie Menu Settings")]
46 + public PieMenuSettings PieMenu = new PieMenuSettings();
47 +
48 + public class PieMenuSettings
49 + {
50 + [JsonProperty("Tool Shortname Required (e.g. hammer)")]
51 + public string ToolShortname = "hammer";
52 +
53 + [JsonProperty("Hold Time To Open Menu (seconds)")]
54 + public float HoldTime = 0.4f;
55 +
56 + [JsonProperty("Raycast Distance (meters)")]
57 + public float RaycastDistance = 3f;
58 + }
59 +
45 60 public class SlideSettings
46 61 {
47 62 [JsonProperty("Enable Slide Mode (false = classic swing)")]
48 63 public bool Enabled = false;
49 64
50 65 [JsonProperty("Slide Distance (meters)")]
51 66 public float Distance = 2.0f;
52 67
53 68 [JsonProperty("Slide Duration (seconds)")]
54 69 public float Duration = 3.5f;
55 70
56 71 [JsonProperty("Auto Close Delay (seconds, 0 = disabled)")]
57 72 public float AutoCloseDelay = 0f;
58 73 }
59 74
60 75 [JsonProperty("Health Bar Settings")]
61 76 public HealthBarSettings HealthBar = new HealthBarSettings();
62 77
63 78 public class HealthBarSettings
64 79 {
65 80 [JsonProperty("Healthy Color (health >= 60%)")]
66 81 public string HighColor = "#4CAF50";
67 82
68 83 [JsonProperty("Damaged Color (health 25-60%)")]
69 84 public string MidColor = "#FFC107";
70 85
71 86 [JsonProperty("Critical Color (health < 25%)")]
72 87 public string LowColor = "#F44336";
73 88
74 89 [JsonProperty("Damage Flash Color")]
75 90 public string FlashColor = "#FF5252";
76 91
77 92 [JsonProperty("Damage Flash Duration (seconds)")]
78 93 public float FlashDuration = 0.3f;
79 94 }
80 95
81 96 [JsonProperty("Repair Delay")]
82 97 public float RepairDelay = 60;
83 98
84 99 [JsonProperty("Repair Ammount")]
85 100 public int RepairAmmount = 25;
86 101
87 102 [JsonProperty("Spawn Cost type (serverrewards,economics,resources)")]
88 103 public string Costtype = "resources";
89 104
90 105 [JsonProperty("Repair Cost type (serverrewards,economics,resources)")]
91 106 public string RepairCosttype = "resources";
92 107
93 108 [JsonProperty("Spawn Cost for (serverrewards,economics)")]
94 109 public float Spawncost = 2000;
95 110
96 111 [JsonProperty("Repair Cost for (serverrewards,economics)")]
97 112 public float Repaircost = 50;
98 113
99 114 [JsonProperty("Currency Symbol for (serverrewards,economics)")]
100 115 public string CurrencySymbol = "$";
101 116
102 117 [JsonProperty("Charge to craft")]
103 118 public bool craftcosts = true;
104 119
105 120 [JsonProperty("Repair Cost")]
106 121 //Items and quanity needed to repair
107 122 public Dictionary<string, int> RepairCost = new Dictionary<string, int>
108 123 {
109 124 {"scrap", 1},
110 125 {"metal.fragments", 100},
111 126 {"metal.refined", 10},
112 127 };
113 128
114 129 [JsonProperty("Craft Cost")]
115 130 //Items and quanity needed per craft
116 131 public Dictionary<string, int> CraftCost = new Dictionary<string, int>
117 132 {
118 133 {"scrap", 10},
119 134 {"metal.fragments", 1000},
120 135 {"metal.refined", 100},
121 136 };
122 137
123 138 public string ToJson() => JsonConvert.SerializeObject(this);
124 139
125 140 public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
126 141 }
127 142
128 143 protected override void LoadDefaultConfig() => config = new Configuration();
129 144
130 145 protected override void LoadConfig()
131 146 {
132 147 base.LoadConfig();
133 148 try
134 149 {
135 150 config = Config.ReadObject<Configuration>();
136 151 if (config == null)
137 152 {
138 153 throw new JsonException();
139 154 }
140 155
141 156 if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
142 157 {
143 158 PrintWarning("Configuration appears to be outdated; updating and saving");
144 159 SaveConfig();
145 160 }
146 161 }
147 162 catch
148 163 {
149 164 PrintWarning($"Configuration file {Name}.json is invalid; using defaults");
150 165 LoadDefaultConfig();
151 166 }
152 167 }
153 168
154 169 protected override void SaveConfig()
155 170 {
156 171 PrintWarning($"Configuration changes saved to {Name}.json");
157 172 Config.WriteObject(config, true);
158 173 }
159 174 #endregion Configuration
160 175
161 176 #region Vars
162 177 //Offset from door slot position to move lock.
163 178 Vector3 codeoffset = new Vector3(0.71f, -0.1f, -0.3f); //Puts it on flat pannel, On handles was too far for it to trigger on door still.
164 179 //Skin of Icon
165 180 private const ulong skinID = 2643584466;
166 181 //Replacement prefab
167 182 private const string prefab = "assets/bundled/prefabs/modding/asset_store/bankheist_package/bankheist_vol03/prefabs/door.vault.static.prefab";
168 183 //Permission
169 184 private const string permUse = "PersonalVaultDoor.use";
170 185 //Show Debug Info
171 186 private bool showDebug = false;
172 187 //Sound Effects to play on vault break.
173 188 static List<string> effects = new List<string>
174 189 {
175 190 "assets/bundled/prefabs/fx/entities/loot_barrel/gib.prefab",
176 191 "assets/bundled/prefabs/fx/building/metal_sheet_gib.prefab"
177 192 };
178 193 private List<Vector3> REPlaced = new List<Vector3>();
179 194 private static PersonalVaultDoor plugin;
180 195 #endregion
181 196
182 197 #region Language
183 198 protected override void LoadDefaultMessages()
184 199 {
185 200 lang.RegisterMessages(new Dictionary<string, string>
186 201 {
187 202 {"Name", "Vault Door"},
188 203 {"Pickup", "You picked up Vault Door!"},
189 204 {"Receive", "You received Vault Door!"},
190 205 {"Repair", "You need more resources:\n{0}"},
191 206 {"Wait", "You must wait: {0} before you can repair."},
192 - {"Permission", "You need permission to do that!"}
207 + {"Permission", "You need permission to do that!"},
208 + {"NotAuthed", "You are not authorized on this vault door's lock."},
209 + {"NeedBuildingAuth", "You need building privilege to pick this up."}
193 210 }, this);
194 211 }
195 212 private static string HexToRustFormat(string hex)
196 213 {
197 214 Color color;
198 215 return ColorUtility.TryParseHtmlString(hex, out color) ? $"{color.r:F2} {color.g:F2} {color.b:F2} {color.a:F2}" : "false";
199 216 }
200 217 private static string HexToRustFormat(string hex, float alpha)
201 218 {
202 219 Color color;
203 220 return ColorUtility.TryParseHtmlString(hex, out color) ? $"{color.r:F2} {color.g:F2} {color.b:F2} {alpha:F2}" : "false";
204 221 }
205 222 //Send player message
206 223 private void message(BasePlayer player, string key, params object[] args)
207 224 {
208 225 if (player == null) { return; }
209 226 var message = string.Format(lang.GetMessage(key, this, player.UserIDString), args);
210 227 player.ChatMessage(message);
211 228 }
212 229 #endregion
213 230
214 231 #region Oxide Hooks
215 232 private void OnServerInitialized(bool initial)
216 233 {
217 234 plugin = this;
218 235 Fstartup(initial ? 30 : 1);
236 + //Attach pie menu controller to anyone already holding the tool (e.g. after a plugin reload)
237 + foreach (BasePlayer player in BasePlayer.activePlayerList)
238 + {
239 + CheckHammer(player);
240 + }
219 241 }
220 242
221 243 private void Fstartup(int delay)
222 244 {
223 245 //Wait to start up vault door componant for slow servers.
224 246 timer.Once(delay, () =>
225 247 {
226 248 CheckVaultDoor();
227 249 });
228 250 }
229 251
230 252 private void Init()
231 253 {
232 254 //Setup Permissions
233 255 permission.RegisterPermission(permUse, this);
234 256 }
235 257
236 258 private void Unload()
237 259 {
238 260 //Clear Vault Door Health CUI
239 261 foreach (BasePlayer player in BasePlayer.activePlayerList.ToArray())
240 262 {
241 263 CuiHelper.DestroyUi(player, "VaultHealthUI");
242 264 }
243 265 //Snap any mid-slide vault doors back to closed before the component is destroyed
244 266 foreach (var slider in VaultSlide.ActiveSliders.ToArray())
245 267 {
246 268 slider?.ResetToClosed();
247 269 }
270 + //Remove pie menu controller from anyone holding the tool
271 + foreach (BasePlayer player in BasePlayer.activePlayerList.ToArray())
272 + {
273 + var comp = player.GetComponent<PersonalVaultDoorController>();
274 + if (comp != null) { UnityEngine.Object.DestroyImmediate(comp); }
275 + }
248 276 int VDS = DestroyVaultDoorScript();
249 277 if (showDebug) Puts("Destroyed " + VDS.ToString() + " VaultDoor Scripts");
250 278 //Unload Statics
251 279 effects = null;
252 280 plugin = null;
253 281 }
254 282
283 + //Attaches the pie menu controller to anyone already holding the tool (e.g. after a plugin reload)
284 + private void OnPlayerConnected(BasePlayer player) { CheckHammer(player); }
285 +
286 + //Standard Oxide hook fired whenever the player's active held item changes
287 + private void OnActiveItemChanged(BasePlayer player, Item oldItem, Item newItem)
288 + {
289 + if (player == null) { return; }
290 + NextTick(() => CheckHammer(player));
291 + }
292 +
293 + //Adds/removes the pie menu watcher component based on whether the player is holding the configured tool
294 + private void CheckHammer(BasePlayer player)
295 + {
296 + if (player == null || player.IsDestroyed) { return; }
297 + var activeItem = player.GetActiveItem();
298 + bool holdingTool = activeItem != null && activeItem.info.shortname == config.PieMenu.ToolShortname;
299 +
300 + var comp = player.GetComponent<PersonalVaultDoorController>();
301 + if (holdingTool)
302 + {
303 + if (comp == null) { player.gameObject.AddComponent<PersonalVaultDoorController>(); }
304 + }
305 + else
306 + {
307 + if (comp != null) { UnityEngine.Object.DestroyImmediate(comp); }
308 + }
309 + }
310 +
255 311 void OnEntityKill(Door entity)
256 312 {
257 313 //Check if vault door
258 314 if (entity.ShortPrefabName == "door.vault.static")
259 315 {
260 316 VaultDoor VD = entity.GetComponent<VaultDoor>();
261 317 if (VD == null) return;
262 318 //Remove Door Frame
263 319 NextTick(() =>
264 320 {
265 321 if (VD.Frame != null)
266 322 {
267 323 VD.Frame.Kill();
268 324 }
269 325 });
270 326 if (VD.PlayersCUIed == null) return;
271 327 //Remove CUI
272 328 foreach (BasePlayer player in VD.PlayersCUIed)
273 329 {
274 330 if (player != null)
275 331 {
276 332 CuiHelper.DestroyUi(player, "VaultHealthUI");
277 333 }
278 334 }
279 335 UnityEngine.Object.DestroyImmediate(VD);
280 336 }
281 337 }
282 338
339 + //Frame was killed directly (e.g. RemoverTool targeted the doorway instead of the door) - kill the paired vault door immediately
340 + void OnEntityKill(BuildingBlock entity)
341 + {
342 + if (entity == null || entity.ShortPrefabName != "wall.doorway") return;
343 +
344 + foreach (var VD in VaultDoor.ActiveVaultDoors.ToArray())
345 + {
346 + if (VD != null && VD.Frame == entity)
347 + {
348 + NextTick(() =>
349 + {
350 + if (VD.vdoor != null)
351 + {
352 + foreach (var effect in effects) { Effect.server.Run(effect, VD.Position); }
353 + VD.vdoor.Kill();
354 + }
355 + });
356 + break;
357 + }
358 + }
359 + }
360 +
283 361 void OnEntitySpawned(CodeLock cl)
284 362 {
285 363 BaseEntity Door = cl.GetParentEntity();
286 364 if (Door != null && Door.ToString().Contains("door.vault.static"))
287 365 {
288 366 //Moves codelock and update
289 367 cl.transform.localPosition += codeoffset;
290 368 cl.SendNetworkUpdateImmediate();
291 369 }
292 370 }
293 371
294 372 void OnEntitySpawned(KeyLock cl)
295 373 {
296 374 BaseEntity Door = cl.GetParentEntity();
297 375 if (Door != null && Door.ToString().Contains("door.vault.static"))
298 376 {
299 377 //Moves codelock and update
300 378 cl.transform.localPosition += codeoffset;
301 379 cl.SendNetworkUpdateImmediate();
302 380 }
303 381 }
304 382
305 383 //Code and KeyLock position Fix
306 384 void OnEntitySpawned(Door cl)
307 385 {
308 386 if (Rust.Application.isLoading) { return; }
309 387 //Add component in spawn here so Copypaste can Work with vaultdoors.
310 388 if (cl is Door && cl.ToString().Contains("door.vault.static"))
311 389 {
312 390 if (cl.GetComponent<VaultDoor>() == null)
313 391 {
314 392 if (REPlaced.Contains(cl.transform.position))
315 393 {
316 394 if (showDebug) Puts("Skipping Rust Edit Placed Vault Door");
317 395 return;
318 396 }
319 397 if (showDebug) Puts("Adding VaultDoor Component");
320 398 //Delay since copypaste might not of spawn door frame yet.
321 399 timer.Once(2f, () =>
322 400 {
323 401 try
324 402 {
325 403 cl.gameObject.AddComponent<VaultDoor>();
326 404 if (config.SlideDoor.Enabled && cl.GetComponent<VaultSlide>() == null)
327 405 {
328 406 cl.gameObject.AddComponent<VaultSlide>();
329 407 }
330 408 }
331 409 catch { };
332 410 });
333 411 }
334 412 }
335 413 }
336 414
337 415 //Hook vault placement to switch in.
338 416 private void OnEntityBuilt(Planner plan, GameObject go) { CheckDeploy(go.ToBaseEntity()); }
339 417
340 418 //Hooks if should pickup
341 419 private void OnHammerHit(BasePlayer player, HitInfo info) { CheckHit(player, info?.HitEntity); }
342 420
343 421 //Flashes the health bar red when the vault door takes damage
344 422 private void OnEntityTakeDamage(Door entity, HitInfo info)
345 423 {
346 424 if (entity == null || !IsVaultDoor(entity.skinID)) { return; }
347 425 entity.GetComponent<VaultDoor>()?.FlashDamage();
348 426 }
349 427
350 428 //Slide Mode: replaces the vault door's native swing with a sliding animation
351 429 private void OnDoorOpened(Door door, BasePlayer player)
352 430 {
353 431 if (!config.SlideDoor.Enabled || door == null || !IsVaultDoor(door.skinID)) { return; }
354 432 VaultSlide slider = door.GetComponent<VaultSlide>();
355 433 if (slider == null) { return; }
356 434 //Cancel the native open flag/animation - the slider tracks open/closed itself
357 435 door.SetOpen(false);
358 436 slider.Toggle();
359 437 }
360 438 #endregion
361 439
362 440 #region Core
363 441 List<Door> FindVaultDoors(Vector3 pos, float radius)
364 442 {
365 443 //Casts a sphere at given position and find all doors there
366 444 var hits = Physics.SphereCastAll(pos, radius, Vector3.one);
367 445 var x = new List<Door>();
368 446 foreach (var hit in hits)
369 447 {
370 448 var entity = hit.GetEntity()?.GetComponent<Door>();
371 449 if (entity && !x.Contains(entity))
372 450 x.Add(entity);
373 451 }
374 452 return x;
375 453 }
376 454
377 455 void DestroyGroundComp(BaseEntity ent)
378 456 {
379 457 UnityEngine.Object.DestroyImmediate(ent.GetComponent<DestroyOnGroundMissing>());
380 458 UnityEngine.Object.DestroyImmediate(ent.GetComponent<GroundWatch>());
381 459 //Stops Decay
382 460 UnityEngine.Object.DestroyImmediate(ent.GetComponent<DeployableDecay>());
383 461 }
384 462
385 463 void DestroyMeshCollider(BaseEntity ent)
386 464 {
387 465 foreach (var mesh in ent.GetComponentsInChildren<MeshCollider>())
388 466 {
389 467 UnityEngine.Object.DestroyImmediate(mesh);
390 468 }
391 469 }
392 470
393 471 //Resets the component after server restart
394 472 private void CheckVaultDoor()
395 473 {
396 474 //Build list of VaultDoors In Map File
397 475 for (int i = World.Serialization.world.prefabs.Count - 1; i >= 0; i--)
398 476 {
399 477 PrefabData prefabdata = World.Serialization.world.prefabs[i];
400 478 if (prefabdata.id == 3595032872)
401 479 {
402 480 REPlaced.Add(prefabdata.position);
403 481 //Check its still there and fix if not
404 482 if (FindVaultDoors(prefabdata.position, 4f).Count == 0)
405 483 {
406 484 Door replacement = GameManager.server.CreateEntity(StringPool.Get(prefabdata.id), prefabdata.position, prefabdata.rotation) as Door;
407 485 if (replacement == null) return;
408 486 DestroyGroundComp(replacement);
409 487 DestroyMeshCollider(replacement);
410 488 replacement.Spawn();
411 489 replacement.transform.position = prefabdata.position;
412 490 replacement.transform.rotation = prefabdata.rotation;
413 491 replacement.pickup.enabled = false;
414 492 replacement.SendNetworkUpdateImmediate();
415 493 }
416 494 }
417 495 }
418 496 if (showDebug) Puts("Founded " + REPlaced.Count.ToString() + " Map Placed Vault Doors");
419 497 int VaultsUpdated = 0;
420 - foreach (var vaultdoor in GameObject.FindObjectsOfType<BaseEntity>())
498 + foreach (var vaultdoor in GameObject.FindObjectsByType<BaseEntity>(FindObjectsSortMode.None))
421 499 {
422 500 //Skip Servers Vault Doors
423 501 if (REPlaced.Contains(vaultdoor.transform.position))
424 502 {
425 503 if (showDebug) Puts("Skipping Map Placed Vault Door @ " + vaultdoor.transform.position.ToString());
426 504 continue;
427 505 }
428 506 if (vaultdoor.ShortPrefabName == "door.vault.static" && vaultdoor.GetComponent<VaultDoor>() == null)
429 507 {
430 508 if (showDebug) Puts("Found vaultdoor " + vaultdoor.ToString() + " " + vaultdoor.OwnerID.ToString() + " Adding Component");
431 509 vaultdoor.gameObject.AddComponent<VaultDoor>();
432 510 if (config.SlideDoor.Enabled && vaultdoor.GetComponent<VaultSlide>() == null)
433 511 {
434 512 vaultdoor.gameObject.AddComponent<VaultSlide>();
435 513 }
436 514 VaultsUpdated++;
437 515 }
438 516 }
439 517 Puts("Updated " + VaultsUpdated.ToString() + " VaultDoors");
440 518 }
441 519
442 520 //Gives player vault door
443 521 private void GiveVaultDoor(BasePlayer player, bool pickup = false)
444 522 {
445 523 var item = CreateItem();
446 524 if (item != null && player != null)
447 525 {
448 526 player.GiveItem(item);
449 527 message(player, pickup ? "Pickup" : "Receive");
450 528 }
451 529 }
452 530 bool ChargePlayer(BasePlayer player)
453 531 {
454 532 object result = null;
455 533 if (!config.craftcosts)
456 534 {
457 535 return true;
458 536 }
459 537 else
460 538 {
461 539 if (config.Costtype == "serverrewards" && ServerRewards != null)
462 540 {
463 541 result = ServerRewards.Call("TakePoints", player.UserIDString, (int)config.Spawncost);
464 542 }
465 543 else if (config.Costtype == "economics" && Economics != null)
466 544 {
467 545 result = Economics.Call("Withdraw", player.UserIDString, (double)config.Spawncost);
468 546 }
469 547 else
470 548 {
471 549 // No supported rewards plugin loaded or configured
472 550 message(player, "Currency Type Not supported on this server");
473 551 return false;
474 552 }
475 553 if (result == null || (result is bool && (bool)result == false))
476 554 {
477 555 message(player, "Looks like you can not afford to buy this");
478 556 return false;
479 557 }
480 558 message(player, "Charged {amount} {currency} for Vault Door".Replace("{amount}", config.Spawncost.ToString()).Replace("{currency}", config.CurrencySymbol));
481 559 return true;
482 560 }
483 561 }
484 562
485 563 bool CanaffordFix(BasePlayer player, Door VD)
486 564 {
487 565 object result = null;
488 566 if (VD.SecondsSinceAttacked < config.RepairDelay)
489 567 {
490 568 message(player, "Wait", (config.RepairDelay - VD.SecondsSinceAttacked).ToString("#.#"));
491 569 return false;
492 570 }
493 571 else
494 572 {
495 573 if (config.RepairCosttype == "serverrewards" && ServerRewards != null)
496 574 {
497 575 result = ServerRewards.Call("TakePoints", player.UserIDString, (int)config.Repaircost);
498 576 }
499 577 else if (config.RepairCosttype == "economics" && Economics != null)
500 578 {
501 579 result = Economics.Call("Withdraw", player.UserIDString, (double)config.Repaircost);
502 580 }
503 581 else
504 582 {
505 583 // No supported rewards plugin loaded or configured
506 584 message(player, "Currency Type Not supported on this server");
507 585 return false;
508 586 }
509 587 if (result == null || (result is bool && (bool)result == false))
510 588 {
511 589 message(player, "Looks like you can not afford to repair this");
512 590 return false;
513 591 }
514 592 message(player, "Charged {amount} {currency} for repairing Vault Door".Replace("{amount}", config.Repaircost.ToString()).Replace("{currency}", config.CurrencySymbol));
515 593 return true;
516 594 }
517 595 }
518 596
519 597 //Checks if has the correct permission
520 598 private bool CanCraft(BasePlayer player)
521 599 {
522 600 if (!permission.UserHasPermission(player.UserIDString, permUse))
523 601 {
524 602 message(player, "Permission");
525 603 return false;
526 604 }
527 605 return CanFix(player, null, true);
528 606 }
529 607
530 608 //Creates vault door
531 609 private Item CreateItem()
532 610 {
533 611 var item = ItemManager.CreateByName("wall.frame.garagedoor", 1, skinID);
534 612 if (item != null)
535 613 {
536 614 item.text = "Vault Door";
537 615 item.name = item.text;
538 616 }
539 617 return item;
540 618 }
541 619
542 620 //Checks if its a vault door when hit with hammer
543 621 private void CheckHit(BasePlayer player, BaseEntity entity)
544 622 {
545 623 if (entity == null) { return; }
546 624 if (!IsVaultDoor(entity.skinID)) { return; }
547 625 //Check if door is open and has no lock to remove Otherwise heal door
548 626 Door VD = entity as Door;
549 627 VaultSlide slider = entity.GetComponent<VaultSlide>();
550 628 bool isOpen = slider != null ? slider.IsOpen : VD.IsOpen();
551 629 if (VD.GetSlot(0) == null && isOpen)
552 630 {
553 631 entity.GetComponent<VaultDoor>()?.TryPickup(player);
554 632 }
555 633 else
556 634 {
557 635 if (VD._health < VD._maxHealth)
558 636 {
559 637 if (config.RepairCosttype != "resources")
560 638 {
561 639 if (CanaffordFix(player, VD))
562 640 {
563 641 Effect.server.Run("assets/bundled/prefabs/fx/build/repair_full_metal.prefab", VD.transform.position);
564 642 VD.health += config.RepairAmmount;
565 643 }
566 644 }
567 645 else
568 646 {
569 647 if (CanFix(player, VD))
570 648 {
571 649 Effect.server.Run("assets/bundled/prefabs/fx/build/repair_full_metal.prefab", VD.transform.position);
572 650 VD.health += config.RepairAmmount;
573 651 }
574 652 }
575 653 }
576 654 else
577 655 {
578 656 message(player, "Vault Door Is full health");
579 657 }
580 658 }
581 659 }
582 660
583 661 //Checks if can fix
584 662 private bool CanFix(BasePlayer player, Door VD, bool craft = false)
585 663 {
586 664 Dictionary<string, int> Needed = new Dictionary<string, int>();
587 665 Dictionary<string, int> Parts = new Dictionary<string, int>();
588 666
589 667 if (!craft)
590 668 {
591 669 Parts = config.RepairCost;
592 670 //Check repair delay
593 671 if (VD.SecondsSinceAttacked < config.RepairDelay)
594 672 {
595 673 message(player, "Wait", (config.RepairDelay - VD.SecondsSinceAttacked).ToString("#.#"));
596 674 return false;
597 675 }
598 676 }
599 677 else
600 678 {
601 679 if (!config.craftcosts) return true;
602 680 Parts = config.CraftCost;
603 681 }
604 682
605 683 //Check has needed meterials
606 684 foreach (var component in Parts)
607 685 {
608 686 string name = component.Key;
609 687 if (player.inventory.GetAmount(ItemManager.FindItemDefinition(component.Key).itemid) < component.Value)
610 688 {
611 689 if (!Needed.ContainsKey(name))
612 690 {
613 691 Needed.Add(name, 0);
614 692 }
615 693 Needed[name] += component.Value;
616 694 }
617 695 }
618 696 //Has everything needed so remove from player and send can fix.
619 697 if (Needed.Count == 0)
620 698 {
621 699 foreach (var item in Parts)
622 700 {
623 701 player.inventory.Take(null, ItemManager.FindItemDefinition(item.Key).itemid, item.Value);
624 702 }
625 703 return true;
626 704 }
627 705 //Doesnt have everything needed to build list and message use. Send cant fix
628 706 else
629 707 {
630 708 string text = "";
631 709 foreach (var item in Needed)
632 710 {
633 711 text += $" * {item.Key} x{item.Value}\n";
634 712 }
635 713 message(player, "Repair", text);
636 714 return false;
637 715 }
638 716 }
639 717
640 718 private bool IsVaultDoor(ulong skin) { return skin != 0 && skin == skinID; }
641 719 //Checks if vault door should be swapped in
642 720 private void CheckDeploy(BaseEntity entity)
643 721 {
644 722 if (entity == null) { return; }
645 723 //Checks if is using vault door skin
646 724 if (!IsVaultDoor(entity.skinID)) { return; }
647 725 //Creates Doorway
648 726 var doorway = GameManager.server.CreateEntity("assets/prefabs/building core/wall.doorway/wall.doorway.prefab", entity.transform.position, entity.transform.rotation);
649 727 if (doorway == null)
650 728 {
651 729 //Something Failed
652 730 return;
653 731 }
654 732 doorway.Spawn();
655 733 doorway.OwnerID = entity.OwnerID;
656 734 //sets up door frame to fill in gaps around vault door
657 735 var buildingBlock = doorway as BuildingBlock;
658 736 if (buildingBlock != null)
659 737 {
660 738 //Upgrade HQM
661 739 buildingBlock.SetGrade((BuildingGrade.Enum)4);
662 740 //Grounded so doesnt fall apart
663 741 buildingBlock.grounded = false;
664 742 //Sets health as max
665 743 buildingBlock.health = buildingBlock.MaxHealth();
666 744 //Rotate from Door Way
667 745 Vector3 rot = buildingBlock.transform.rotation.eulerAngles;
668 746 rot = new Vector3(rot.x, rot.y + 180, rot.z);
669 747 //Create Vault Door
670 748 Door vaultdoor = GameManager.server.CreateEntity(prefab, buildingBlock.transform.position, buildingBlock.transform.rotation) as Door;
671 749 if (vaultdoor == null) { return; }
672 750 //Stupid rotation based move stuff
673 751 Vector3 movepos = buildingBlock.transform.position;
674 752 movepos += buildingBlock.transform.forward * 1.0f;
675 753 movepos += buildingBlock.transform.right * -0.54f;
676 754 movepos += buildingBlock.transform.up * 0.2f;
677 755 //Sets door way as creator so can destory vault on door way being destroyed
678 756 vaultdoor.creatorEntity = doorway;
679 757 vaultdoor.transform.rotation = Quaternion.Euler(rot);
680 758 vaultdoor.transform.position = movepos;
681 759 //Set skin and owner
682 760 vaultdoor.skinID = skinID;
683 761 vaultdoor.OwnerID = entity.OwnerID;
684 762 //Delay setting Max health and health other wise it defaults to 800
685 763 timer.Once(1f, () =>
686 764 {
687 765 vaultdoor.SetMaxHealth(config.VaultMaxHealth);
688 766 vaultdoor.SetHealth(config.VaultMaxHealth);
689 767 if (config.SlideDoor.Enabled && vaultdoor.GetComponent<VaultSlide>() == null)
690 768 {
691 769 vaultdoor.gameObject.AddComponent<VaultSlide>();
692 770 }
693 771 });
694 772 //Sets up functions
695 773 vaultdoor.Spawn();
696 774 vaultdoor.SendNetworkUpdateImmediate();
697 775 }
698 776 //Cleans out placeholder
699 777 NextTick(() => { entity?.Kill(); });
700 778 }
701 779
702 780 //Removes script on unload incase some ones restarting plugin but not restarted server
703 781 int DestroyVaultDoorScript()
704 782 {
705 783 int killed = 0;
706 784 foreach (var vd in VaultDoor.ActiveVaultDoors.ToArray())
707 785 {
708 786 if (vd != null)
709 787 {
710 788 UnityEngine.Object.DestroyImmediate(vd);
711 789 killed++;
712 790 }
713 791 }
714 792 foreach (var vs in VaultSlide.ActiveSliders.ToArray())
715 793 {
716 794 if (vs != null)
717 795 {
718 796 UnityEngine.Object.DestroyImmediate(vs);
719 797 killed++;
720 798 }
721 799 }
722 800 return killed;
723 801 }
802 +
803 + #region Pie Menu
804 +
805 + //Checks if player has access to operate the door's lock. No lock at all = public door, anyone can use it.
806 + //Delegates to the lock's own HasLockPermission (CodeLock checks its whitelist/guest code,
807 + //KeyLock checks ownership and matching key items) instead of reimplementing that logic here.
808 + private bool PlayerAuthedOnLock(BasePlayer player, BaseLock baseLock)
809 + {
810 + if (baseLock == null) { return true; }
811 + return baseLock.HasLockPermission(player);
812 + }
813 +
814 + //Raycasts from the player's eyes and returns a vault door if they're looking at one
815 + private Door FindVaultDoorLookedAt(BasePlayer player)
816 + {
817 + RaycastHit hit;
818 + if (!Physics.Raycast(player.eyes.HeadRay(), out hit, config.PieMenu.RaycastDistance)) { return null; }
819 +
820 + Door door = hit.GetEntity() as Door;
821 + if (door == null || !IsVaultDoor(door.skinID)) { return null; }
822 +
823 + return door;
824 + }
825 +
826 + //Finds a door by its network ID, used by the console commands the pie menu buttons call
827 + private Door FindDoorByNetId(ulong id)
828 + {
829 + BaseNetworkable ent = BaseNetworkable.serverEntities.Find(new NetworkableId(id));
830 + return ent as Door;
831 + }
832 +
833 + public void TryOpenVaultMenu(BasePlayer player)
834 + {
835 + if (player == null) { return; }
836 +
837 + Door door = FindVaultDoorLookedAt(player);
838 + if (door == null) { return; }
839 +
840 + BaseLock baseLock = door.GetSlot(BaseEntity.Slot.Lock) as BaseLock;
841 + if (!PlayerAuthedOnLock(player, baseLock))
842 + {
843 + message(player, "NotAuthed");
844 + return;
845 + }
846 +
847 + SendVaultPie(player, door, baseLock);
848 + }
849 +
850 + //CustomPie/CustomPieMenu fields are public, so we build entries ourselves without needing
851 + //CommunityEntity's private AddPieMenu helper.
852 + private void AddPie(CustomPie pie, string name, string description, string command, string sprite, bool disabled, bool selected)
853 + {
854 + CustomPieMenu menu = Facepunch.Pool.Get<CustomPieMenu>();
855 + menu.name = name;
856 + menu.description = description;
857 + menu.command = command;
858 + menu.sprite = sprite;
859 + menu.disabled = disabled;
860 + menu.selected = selected;
861 + pie.menus.Add(menu);
862 + }
863 +
864 + private void SendVaultPie(BasePlayer player, Door door, BaseLock baseLock)
865 + {
866 + ulong netId = door.net.ID.Value;
867 + VaultSlide slider = door.GetComponent<VaultSlide>();
868 + bool isMoving = slider != null && slider.IsMoving;
869 + bool isOpen = slider != null ? slider.IsOpen : door.IsOpen();
870 + bool canPickup = !isMoving && isOpen && baseLock == null && player.IsBuildingAuthed();
871 +
872 + using (CustomPie pie = Facepunch.Pool.Get<CustomPie>())
873 + {
874 + pie.menus = Facepunch.Pool.Get<List<CustomPieMenu>>();
875 +
876 + AddPie(pie, "Exit", "Close this menu", "", "assets/icons/close.png", false, false);
877 +
878 + //While the door is mid-slide, IsOpen is unreliable (it's between open/closed), so hide
879 + //Open/Close/Pickup until the slide finishes rather than risk showing the wrong button.
880 + if (!isMoving)
881 + {
882 + AddPie(pie,
883 + isOpen ? "Close" : "Open",
884 + isOpen ? "Close the vault door" : "Open the vault door",
885 + $"personalvaultdoor.toggle {netId}",
886 + isOpen ? "assets/icons/lock.png" : "assets/icons/unlock.png",
887 + false, false);
888 +
889 + if (canPickup)
890 + {
891 + AddPie(pie, "Pickup", "Pick up the vault door", $"personalvaultdoor.pickup {netId}", "assets/icons/pickup.png", false, false);
892 + }
893 + }
894 +
895 + CommunityEntity.ServerInstance.SendPie(player, pie);
896 + }
897 + }
898 +
899 + #endregion Pie Menu
724 900 #endregion
725 901
726 902 #region Commands
727 903 //Chat command
728 904 [ChatCommand("vaultdoor")]
729 905 private void Craft(BasePlayer player)
730 906 {
731 907 if (config.Costtype != "resources")
732 908 {
733 909 if (ChargePlayer(player)) { GiveVaultDoor(player); }
734 910 }
735 911 else
736 912 {
737 913 if (CanCraft(player)) { GiveVaultDoor(player); }
738 914 }
739 915 }
740 916
741 917 //Console command
742 918 [ConsoleCommand("vaultdoor.give")]
743 919 private void Cmd(ConsoleSystem.Arg arg)
744 920 {
745 921 if (arg.IsAdmin && arg.Args?.Length > 0)
746 922 {
747 923 var player = BasePlayer.Find(arg.Args[0].ToString()) ?? BasePlayer.FindSleeping(arg.Args[0].ToString());
748 924 if (player == null)
749 925 {
750 926 PrintWarning($"Can't find player with that name/ID! {arg.Args[0]}");
751 927 return;
752 928 }
753 929 GiveVaultDoor(player);
754 930 }
755 931 }
932 +
933 + [ConsoleCommand("personalvaultdoor.toggle")]
934 + private void CmdVaultToggle(ConsoleSystem.Arg arg)
935 + {
936 + BasePlayer player = arg.Player();
937 + if (player == null) { return; }
938 +
939 + ulong netId;
940 + if (arg.Args == null || arg.Args.Length < 1 || !ulong.TryParse(arg.Args[0].ToString(), out netId)) { return; }
941 +
942 + Door door = FindDoorByNetId(netId);
943 + if (door == null || !IsVaultDoor(door.skinID)) { return; }
944 +
945 + BaseLock baseLock = door.GetSlot(BaseEntity.Slot.Lock) as BaseLock;
946 + if (!PlayerAuthedOnLock(player, baseLock))
947 + {
948 + message(player, "NotAuthed");
949 + return;
950 + }
951 +
952 + VaultSlide slider = door.GetComponent<VaultSlide>();
953 + //Ignore the toggle while a slide is already in progress - the button shouldn't have been
954 + //shown, but guard here too in case the command is invoked directly.
955 + if (slider != null && slider.IsMoving) { return; }
956 +
957 + if (config.SlideDoor.Enabled && slider != null)
958 + {
959 + slider.Toggle();
960 + }
961 + else
962 + {
963 + door.SetOpen(!door.IsOpen());
964 + }
965 + }
966 +
967 + [ConsoleCommand("personalvaultdoor.pickup")]
968 + private void CmdVaultPickup(ConsoleSystem.Arg arg)
969 + {
970 + BasePlayer player = arg.Player();
971 + if (player == null) { return; }
972 +
973 + ulong netId;
974 + if (arg.Args == null || arg.Args.Length < 1 || !ulong.TryParse(arg.Args[0].ToString(), out netId)) { return; }
975 +
976 + Door door = FindDoorByNetId(netId);
977 + if (door == null || !IsVaultDoor(door.skinID)) { return; }
978 +
979 + //Only a public door (no lock at all) can be picked up via the menu
980 + if (door.GetSlot(BaseEntity.Slot.Lock) != null) { return; }
981 +
982 + if (!player.IsBuildingAuthed())
983 + {
984 + message(player, "NeedBuildingAuth");
985 + return;
986 + }
987 +
988 + VaultSlide slider = door.GetComponent<VaultSlide>();
989 + //Ignore pickup while a slide is in progress - avoids picking up a door mid-transition.
990 + if (slider != null && slider.IsMoving) { return; }
991 +
992 + bool isOpen = slider != null ? slider.IsOpen : door.IsOpen();
993 + if (!isOpen) { return; }
994 +
995 + VaultDoor vd = door.GetComponent<VaultDoor>();
996 + if (vd != null)
997 + {
998 + vd.PickupViaMenu(player);
999 + }
1000 + }
756 1001 #endregion
757 1002
758 1003 #region Scripts
1004 +
1005 + //Attached to a player while they hold the configured tool. Watches for a held right-click
1006 + //while looking at a vault door and opens the pie menu.
1007 + public class PersonalVaultDoorController : MonoBehaviour
1008 + {
1009 + private BasePlayer player;
1010 + private bool heldPrev;
1011 + private float pressStartTime;
1012 + private bool menuOpenedThisPress;
1013 +
1014 + private void Awake()
1015 + {
1016 + player = GetComponent<BasePlayer>();
1017 + }
1018 +
1019 + private void FixedUpdate()
1020 + {
1021 + try
1022 + {
1023 + if (plugin == null || player == null || player.IsDestroyed) { Destroy(this); return; }
1024 +
1025 + var activeItem = player.GetActiveItem();
1026 + if (activeItem == null || activeItem.info.shortname != plugin.config.PieMenu.ToolShortname)
1027 + {
1028 + Destroy(this);
1029 + return;
1030 + }
1031 +
1032 + bool held = player.serverInput.IsDown(BUTTON.FIRE_SECONDARY);
1033 + if (held && !heldPrev)
1034 + {
1035 + pressStartTime = Time.time;
1036 + menuOpenedThisPress = false;
1037 + }
1038 +
1039 + if (held && !menuOpenedThisPress && Time.time - pressStartTime >= plugin.config.PieMenu.HoldTime)
1040 + {
1041 + menuOpenedThisPress = true;
1042 + plugin.TryOpenVaultMenu(player);
1043 + }
1044 +
1045 + if (!held) { menuOpenedThisPress = false; }
1046 +
1047 + heldPrev = held;
1048 + }
1049 + catch
1050 + {
1051 + Destroy(this);
1052 + }
1053 + }
1054 + }
1055 +
759 1056 public class VaultDoor : MonoBehaviour
760 1057 {
761 1058 //Tracked so DestroyVaultDoorScript doesn't need to scan every Door on the map
762 1059 public static List<VaultDoor> ActiveVaultDoors = new List<VaultDoor>();
763 1060
764 1061 //Hold doors info
765 1062 public Door vdoor = null;
766 1063 //Holds Position that can be used after Vdoors destroyed.
767 1064 public Vector3 Position;
768 1065 //Hold door frames info
769 1066 public BaseEntity Frame = null;
770 1067 //List of players that have CUI active
771 1068 public List<BasePlayer> PlayersCUIed = new List<BasePlayer>();
772 1069 private void Awake()
773 1070 {
774 1071 //Setup
775 1072 vdoor = this.GetComponent<Door>();
776 1073 if (vdoor == null) return;
777 1074 ActiveVaultDoors.Add(this);
778 1075 Position = vdoor.transform.position;
779 1076 //Set Custom Health
780 1077 vdoor.SetMaxHealth(plugin.config.VaultMaxHealth);
781 1078 vdoor.SetHealth(plugin.config.VaultMaxHealth);
782 1079 //Safety net: make sure the slider is attached if slide mode is on
783 1080 if (plugin.config.SlideDoor.Enabled && vdoor.GetComponent<VaultSlide>() == null)
784 1081 {
785 1082 vdoor.gameObject.AddComponent<VaultSlide>();
786 1083 }
787 1084 Frame = vdoor.creatorEntity;
788 1085 //If no creatorEntity most likey a server restart so find it.
789 1086 if (Frame == null)
790 1087 {
791 1088 //Offsets scan position since where vault shows isnt where its position is.
792 1089 Vector3 scanpos = Position;
793 1090 scanpos += vdoor.transform.forward * 1.0f;
794 1091 scanpos += vdoor.transform.right * -0.54f;
795 1092 scanpos += vdoor.transform.up * 0.0f;
796 1093 if (plugin.showDebug) foreach (BasePlayer BP in BasePlayer.activePlayerList) { if (BP.IsAdmin) BP.SendConsoleCommand("ddraw.sphere", 8f, Color.red, scanpos, 0.5f); }
797 1094 List<BaseEntity> BuildingBlock = new List<BaseEntity>();
798 1095 //Scans area for players
799 1096 Vis.Entities<BaseEntity>(scanpos, 0.5f, BuildingBlock);
800 1097 foreach (BaseEntity doorframe in BuildingBlock)
801 1098 {
802 1099 if (doorframe.ShortPrefabName == "wall.doorway")
803 1100 {
804 1101 if (plugin.showDebug) plugin.Puts("Found Creator Doorway " + doorframe.ToString());
805 1102 Frame = doorframe;
806 1103 break;
807 1104 }
808 1105 }
809 1106 }
810 1107 //Setup checking if door frame has been destroyed
811 1108 InvokeRepeating("CheckFrame", 5, 8);
812 1109 if (plugin.config.HealthBarEnabled)
813 1110 {
814 1111 InvokeRepeating("VaultHealthBar", 1, 1);
815 1112 }
816 1113 else
817 1114 { plugin.Unsubscribe(nameof(OnEntityTakeDamage)); }
818 1115 }
819 1116
820 1117 private void OnDestroy()
821 1118 {
822 1119 ActiveVaultDoors.Remove(this);
823 1120 }
824 1121
825 1122 void VaultHealthBar()
826 1123 {
827 1124 List<BasePlayer> PlayersInRange = new List<BasePlayer>();
828 1125 //Scans area for players
829 1126 Vis.Entities<BasePlayer>(vdoor.transform.position, plugin.config.vaultrange, PlayersInRange);
830 1127
831 1128 //Logic to remove CUI from players that have left area
832 1129 foreach (BasePlayer bp in PlayersCUIed.ToArray())
833 1130 {
834 1131 if (!PlayersInRange.Contains(bp))
835 1132 {
836 1133 CuiHelper.DestroyUi(bp, "VaultHealthUI");
837 1134 PlayersCUIed.Remove(bp);
838 1135 }
839 1136 }
840 1137
841 1138 //Shows CUI to each player in range
842 1139 if (PlayersInRange.Count == 0) { return; }
843 1140
844 1141 float pct = Mathf.Clamp01(vdoor._maxHealth > 0 ? vdoor._health / vdoor._maxHealth : 0f);
845 1142 var barsettings = plugin.config.HealthBar;
846 1143 string barColor = HexToRustFormat(pct >= 0.6f ? barsettings.HighColor : pct >= 0.25f ? barsettings.MidColor : barsettings.LowColor);
847 1144 string pctText = Mathf.RoundToInt(pct * 100f) + "%";
848 1145
849 1146 foreach (BasePlayer player in PlayersInRange.ToArray())
850 1147 {
851 1148 if (player.IsSleeping()) { continue; }
852 1149
853 1150 CuiHelper.DestroyUi(player, "VaultHealthUI");
854 1151 var elements = new CuiElementContainer();
855 1152
856 1153 //Backing panel
857 1154 elements.Add(new CuiPanel
858 1155 {
859 1156 Image = { Color = "0.05 0.05 0.05 0.55" },
860 1157 RectTransform = { AnchorMin = "0.79 0.93", AnchorMax = "0.995 0.975" }
861 1158 }, "Overlay", "VaultHealthUI");
862 1159
863 1160 //Title
864 1161 elements.Add(new CuiLabel
865 1162 {
866 1163 Text = { Text = "VAULT DOOR", FontSize = 11, Align = TextAnchor.MiddleLeft, Color = "0.85 0.85 0.85 0.9" },
867 1164 RectTransform = { AnchorMin = "0.04 0.5", AnchorMax = "0.45 1" }
868 1165 }, "VaultHealthUI");
869 1166
870 1167 //Bar background
871 1168 elements.Add(new CuiPanel
872 1169 {
873 1170 Image = { Color = "0.15 0.15 0.15 0.9" },
874 1171 RectTransform = { AnchorMin = "0.04 0.12", AnchorMax = "0.96 0.46" }
875 1172 }, "VaultHealthUI", "VaultHealthUI_BarBG");
876 1173
877 1174 //Bar fill, proportional to remaining health, color shifts green -> yellow -> red
878 1175 elements.Add(new CuiPanel
879 1176 {
880 1177 Image = { Color = barColor },
881 1178 RectTransform = { AnchorMin = "0 0", AnchorMax = $"{pct:F3} 1" }
882 1179 }, "VaultHealthUI_BarBG", "VaultHealthUI_BarFill");
883 1180
884 1181 //Percent readout on top of the bar
885 1182 elements.Add(new CuiLabel
886 1183 {
887 1184 Text = { Text = pctText, FontSize = 10, Align = TextAnchor.MiddleCenter, Color = "1 1 1 0.95" },
888 1185 RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
889 1186 }, "VaultHealthUI_BarBG");
890 1187
891 1188 CuiHelper.AddUi(player, elements);
892 1189 if (!PlayersCUIed.Contains(player))
893 1190 {
894 1191 PlayersCUIed.Add(player);
895 1192 }
896 1193 }
897 1194 }
898 1195
899 1196 //Realistic damage indicator: a brief red flash over the health bar every time the door is hit
900 1197 public void FlashDamage()
901 1198 {
902 1199 if (PlayersCUIed.Count == 0) { return; }
903 1200 var barsettings = plugin.config.HealthBar;
904 1201 foreach (BasePlayer player in PlayersCUIed.ToArray())
905 1202 {
906 1203 if (player == null) { continue; }
907 1204 CuiHelper.DestroyUi(player, "VaultHealthUI_Flash");
908 1205 var elements = new CuiElementContainer();
909 1206 elements.Add(new CuiPanel
910 1207 {
911 1208 Image = { Color = HexToRustFormat(barsettings.FlashColor, 0.55f) },
912 1209 RectTransform = { AnchorMin = "0 0", AnchorMax = "1 1" }
913 1210 }, "VaultHealthUI_BarBG", "VaultHealthUI_Flash");
914 1211 CuiHelper.AddUi(player, elements);
915 1212 }
916 1213 plugin.timer.Once(barsettings.FlashDuration, () =>
917 1214 {
918 1215 foreach (BasePlayer player in PlayersCUIed.ToArray())
919 1216 {
920 1217 if (player != null) { CuiHelper.DestroyUi(player, "VaultHealthUI_Flash"); }
921 1218 }
922 1219 });
923 1220 }
924 1221
925 1222 void CheckFrame()
926 1223 {
927 1224 //Frame has been destoryed so destroy vault
928 1225 try
929 1226 {
930 1227 if (Frame == null && vdoor != null)
931 1228 {
932 1229 foreach (var effect in effects) { Effect.server.Run(effect, Position); }
933 1230 vdoor.Kill();
934 1231 }
935 1232 }
936 1233 catch { }
937 1234 }
938 1235
939 1236 public void TryPickup(BasePlayer player)
940 1237 {
941 1238 //Owner has hit with hammer, Destroy frame and vault door and refund them one.
942 1239 if (vdoor.OwnerID == player.userID)
943 1240 {
944 1241 vdoor.Kill();
945 1242 if (Frame != null)
946 1243 {
947 1244 Frame.Kill();
948 1245 }
949 1246 plugin.GiveVaultDoor(player, true);
950 1247 }
951 1248 }
1249 +
1250 + //Pie menu pickup: caller has already confirmed the door is public (no lock) and the player
1251 + //has building privilege, so this skips the owner-only check TryPickup enforces for hammer hits.
1252 + public void PickupViaMenu(BasePlayer player)
1253 + {
1254 + vdoor.Kill();
1255 + if (Frame != null)
1256 + {
1257 + Frame.Kill();
1258 + }
1259 + plugin.GiveVaultDoor(player, true);
1260 + }
952 1261 }
953 1262
954 1263 //Slide Mode: moves the whole vault door sideways instead of using the native swing animation
955 1264 public class VaultSlide : MonoBehaviour
956 1265 {
957 1266 public static List<VaultSlide> ActiveSliders = new List<VaultSlide>();
958 1267
959 1268 private Door entity;
960 1269 private Transform tr;
961 1270 private Vector3 closedPosition;
962 1271 private Vector3 openPosition;
963 1272 private bool opening;
964 1273 private float elapsed;
965 1274 private float duration;
966 1275
967 1276 public bool IsOpen { get { return tr != null && tr.position != closedPosition; } }
1277 + //True while the slide animation is actively running (mid-transition), used to hide the pie
1278 + //menu's Open/Close/Pickup buttons since IsOpen is unreliable while partway through a slide.
1279 + public bool IsMoving { get { return enabled; } }
968 1280
969 1281 private void Awake()
970 1282 {
971 1283 entity = GetComponent<Door>();
972 1284 if (entity == null) { Destroy(this); return; }
973 1285
974 1286 tr = entity.transform;
975 1287 closedPosition = tr.position;
976 1288 openPosition = closedPosition + (-tr.forward * plugin.config.SlideDoor.Distance);
977 1289 duration = Mathf.Max(0.1f, plugin.config.SlideDoor.Duration);
978 1290
979 1291 enabled = false;
980 1292 ActiveSliders.Add(this);
981 1293 }
982 1294
983 1295 private void OnDestroy()
984 1296 {
985 1297 ActiveSliders.Remove(this);
986 1298 }
987 1299
988 1300 public void Toggle()
989 1301 {
990 1302 if (entity == null || entity.IsDestroyed) { return; }
991 1303
992 1304 CancelInvoke(nameof(AutoClose));
993 1305 elapsed = Mathf.Max(0f, duration - elapsed);
994 1306 opening = !opening;
995 1307 enabled = true;
996 1308
997 1309 Effect.server.Run("assets/prefabs/building/door.hinged/effects/door-metal-open-end.prefab", tr.position);
998 1310 }
999 1311
1000 1312 private void Update()
1001 1313 {
1002 1314 elapsed += Time.deltaTime;
1003 1315 float t = Mathf.InverseLerp(0f, duration, elapsed);
1004 1316
1005 1317 tr.position = opening ? Vector3.Lerp(closedPosition, openPosition, t) : Vector3.Lerp(openPosition, closedPosition, t);
1006 1318 tr.hasChanged = true;
1007 1319 SendPositionUpdate();
1008 1320
1009 1321 if (elapsed >= duration)
1010 1322 {
1011 1323 elapsed = duration;
1012 1324 enabled = false;
1013 1325
1014 1326 if (opening && plugin.config.SlideDoor.AutoCloseDelay > 0f)
1015 1327 {
1016 1328 Invoke(nameof(AutoClose), plugin.config.SlideDoor.AutoCloseDelay);
1017 1329 }
1018 1330 }
1019 1331 }
1020 1332
1021 1333 private void AutoClose()
1022 1334 {
1023 1335 if (opening) { Toggle(); }
1024 1336 }
1025 1337
1026 1338 public void ResetToClosed()
1027 1339 {
1028 1340 CancelInvoke(nameof(AutoClose));
1029 1341 enabled = false;
1030 1342 opening = false;
1031 1343 elapsed = 0f;
1032 1344 if (entity != null && !entity.IsDestroyed && tr != null)
1033 1345 {
1034 1346 tr.position = closedPosition;
1035 1347 tr.hasChanged = true;
1036 1348 SendPositionUpdate();
1037 1349 }
1038 1350 }
1039 1351
1040 1352 private void SendPositionUpdate()
1041 1353 {
1042 1354 if (entity == null || entity.net == null) { return; }
1043 1355 entity.SendNetworkUpdateImmediate();
1044 1356 }
1045 1357 }
1046 1358 #endregion
1047 1359 }
1048 1360 }