diff --git a/Content.Client/Goobstation/Supermatter/Systems/SupermatterSystem.cs b/Content.Client/Goobstation/Supermatter/Systems/SupermatterSystem.cs new file mode 100644 index 0000000000..ba40cbe271 --- /dev/null +++ b/Content.Client/Goobstation/Supermatter/Systems/SupermatterSystem.cs @@ -0,0 +1,21 @@ +using Content.Shared.Supermatter.Components; +using Content.Shared.Supermatter.Systems; +using Robust.Shared.GameStates; + +namespace Content.Client.Supermatter.Systems; + +public sealed class SupermatterSystem : SharedSupermatterSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandleSupermatterState); + } + + private void HandleSupermatterState(EntityUid uid, SupermatterComponent comp, ref ComponentHandleState args) + { + if (args.Current is not SupermatterComponentState state) + return; + } +} diff --git a/Content.Server/Goobstation/Supermatter/Systems/SupermatterSystem.cs b/Content.Server/Goobstation/Supermatter/Systems/SupermatterSystem.cs new file mode 100644 index 0000000000..527fcfcfeb --- /dev/null +++ b/Content.Server/Goobstation/Supermatter/Systems/SupermatterSystem.cs @@ -0,0 +1,605 @@ +using System.Linq; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Containers; +using Robust.Shared.Physics; +using Robust.Shared.Physics.Events; +using Robust.Shared.Timing; +using Robust.Server.GameObjects; +using Content.Shared.Atmos; +using Content.Shared.Interaction; +using Content.Shared.Projectiles; +using Content.Shared.Tag; +using Content.Shared.Mobs.Components; +using Content.Shared.Radiation.Components; +using Content.Server.Audio; +using Content.Server.Atmos.EntitySystems; +using Content.Server.Chat.Systems; +using Content.Server.Explosion.EntitySystems; +using Content.Shared.Supermatter.Components; +using Content.Shared.Supermatter.Systems; +using Content.Server.Lightning; +using Content.Server.AlertLevel; +using Content.Server.Station.Systems; +using System.Text; +using Content.Server.Kitchen.Components; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Server.DoAfter; +using Content.Server.Popups; + +namespace Content.Server.Supermatter.Systems; + +public sealed class SupermatterSystem : SharedSupermatterSystem +{ + [Dependency] private readonly AtmosphereSystem _atmosphere = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; + [Dependency] private readonly ExplosionSystem _explosion = default!; + [Dependency] private readonly TransformSystem _xform = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly AmbientSoundSystem _ambient = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + [Dependency] private readonly LightningSystem _lightning = default!; + [Dependency] private readonly AlertLevelSystem _alert = default!; + [Dependency] private readonly StationSystem _station = default!; + [Dependency] private readonly DoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly PopupSystem _popup = default!; + + private DelamType _delamType = DelamType.Explosion; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentRemove); + SubscribeLocalEvent(OnMapInit); + + SubscribeLocalEvent(OnCollideEvent); + SubscribeLocalEvent(OnHandInteract); + SubscribeLocalEvent(OnItemInteract); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnGetSliver); + } + + private void OnComponentRemove(EntityUid uid, SupermatterComponent component, ComponentRemove args) + { + // turn off any ambient if component is removed (ex. entity deleted) + _ambient.SetAmbience(uid, false); + component.AudioStream = _audio.Stop(component.AudioStream); + } + + private void OnMapInit(EntityUid uid, SupermatterComponent component, MapInitEvent args) + { + // Set the Sound + _ambient.SetAmbience(uid, true); + + //Add Air to the initialized SM in the Map so it doesnt delam on default + var mix = _atmosphere.GetContainingMixture(uid, true, true); + mix?.AdjustMoles(Gas.Oxygen, Atmospherics.OxygenMolesStandard); + mix?.AdjustMoles(Gas.Nitrogen, Atmospherics.NitrogenMolesStandard); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + if (!_gameTiming.IsFirstTimePredicted) + return; + + foreach (var sm in EntityManager.EntityQuery()) + { + if (!sm.Activated) + return; + + var uid = sm.Owner; + sm.UpdateAccumulator += frameTime; + + if (sm.UpdateAccumulator >= sm.UpdateTimer) + { + sm.UpdateAccumulator -= sm.UpdateTimer; + Cycle(uid, sm); + } + } + } + + public void Cycle(EntityUid uid, SupermatterComponent sm) + { + sm.ZapAccumulator++; + sm.YellAccumulator++; + + ProcessAtmos(uid, sm); + HandleDamage(uid, sm); + + if (sm.Damage >= sm.DelaminationPoint || sm.Delamming) + HandleDelamination(uid, sm); + + HandleSoundLoop(uid, sm); + + if (sm.ZapAccumulator >= sm.ZapTimer) + { + sm.ZapAccumulator -= sm.ZapTimer; + SupermatterZap(uid, sm); + } + + if (sm.YellAccumulator >= sm.YellTimer) + { + sm.YellAccumulator -= sm.YellTimer; + HandleAnnouncements(uid, sm); + } + } + + #region Processing + + /// + /// Handle power and radiation output depending on atmospheric things. + /// + private void ProcessAtmos(EntityUid uid, SupermatterComponent sm) + { + var mix = _atmosphere.GetContainingMixture(uid, true, true); + + if (mix is not { }) + return; + + var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles); + var moles = absorbedGas.TotalMoles; + + if (!(moles > 0f)) + return; + + var gases = sm.GasStorage; + var facts = sm.GasDataFields; + + //Lets get the proportions of the gasses in the mix for scaling stuff later + //They range between 0 and 1 + gases = gases.ToDictionary( + gas => gas.Key, + gas => Math.Clamp(absorbedGas.GetMoles(gas.Key) / moles, 0, 1) + ); + + //No less then zero, and no greater then one, we use this to do explosions and heat to power transfer. + var powerRatio = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].PowerMixRatio); + + // Minimum value of -10, maximum value of 23. Affects plasma, o2 and heat output. + var heatModifier = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].HeatPenalty); + + // Minimum value of -10, maximum value of 23. Affects plasma, o2 and heat output. + var transmissionBonus = gases.Sum(gas => gases[gas.Key] * facts[gas.Key].TransmitModifier); + + var h2OBonus = 1 - gases[Gas.WaterVapor] * 0.25f; + + powerRatio = Math.Clamp(powerRatio, 0, 1); + heatModifier = Math.Max(heatModifier, 0.5f); + transmissionBonus *= h2OBonus; + + // Effects the damage heat does to the crystal + sm.DynamicHeatResistance = 1f; + + // more moles of gases are harder to heat than fewer, + // so let's scale heat damage around them + sm.MoleHeatPenaltyThreshold = (float) Math.Max(moles * sm.MoleHeatPenalty, 0.25); + + // Ramps up or down in increments of 0.02 up to the proportion of co2 + // Given infinite time, powerloss_dynamic_scaling = co2comp + // Some value between 0 and 1 + if (moles > sm.PowerlossInhibitionMoleThreshold && gases[Gas.CarbonDioxide] > sm.PowerlossInhibitionGasThreshold) + { + var co2powerloss = Math.Clamp(gases[Gas.CarbonDioxide] - sm.PowerlossDynamicScaling, -0.02f, 0.02f); + sm.PowerlossDynamicScaling = Math.Clamp(sm.PowerlossDynamicScaling + co2powerloss, 0f, 1f); + } + else + { + sm.PowerlossDynamicScaling = Math.Clamp(sm.PowerlossDynamicScaling - 0.05f, 0f, 1f); + } + + // Ranges from 0 to 1(1-(value between 0 and 1 * ranges from 1 to 1.5(mol / 500))) + // We take the mol count, and scale it to be our inhibitor + var powerlossInhibitor = + Math.Clamp( + 1 - sm.PowerlossDynamicScaling * + Math.Clamp(moles / sm.PowerlossInhibitionMoleBoostThreshold, 1f, 1.5f), + 0f, 1f); + + if (sm.MatterPower != 0) //We base our removed power off one 10th of the matter_power. + { + var removedMatter = Math.Max(sm.MatterPower / sm.MatterPowerConversion, 40); + //Adds at least 40 power + sm.Power = Math.Max(sm.Power + removedMatter, 0); + //Removes at least 40 matter power + sm.MatterPower = Math.Max(sm.MatterPower - removedMatter, 0); + } + + //based on gas mix, makes the power more based on heat or less effected by heat + var tempFactor = powerRatio > 0.8 ? 50f : 30f; + + //if there is more pluox and n2 then anything else, we receive no power increase from heat + sm.Power = Math.Max(absorbedGas.Temperature * tempFactor / Atmospherics.T0C * powerRatio + sm.Power, 0); + + //Radiate stuff + if (TryComp(uid, out var rad)) + rad.Intensity = sm.Power * Math.Max(0, 1f + transmissionBonus / 10f) * 0.003f; + + //Power * 0.55 * a value between 1 and 0.8 + var energy = sm.Power * sm.ReactionPowerModifier; + + // Keep in mind we are only adding this temperature to (efficiency)% of the one tile the rock + // is on. An increase of 4*C @ 25% efficiency here results in an increase of 1*C / (#tilesincore) overall. + // Power * 0.55 * (some value between 1.5 and 23) / 5 + absorbedGas.Temperature += energy * heatModifier * sm.ThermalReleaseModifier; + absorbedGas.Temperature = Math.Max(0, + Math.Min(absorbedGas.Temperature, sm.HeatThreshold * heatModifier)); + + // Release the waste + absorbedGas.AdjustMoles(Gas.Plasma, Math.Max(energy * heatModifier * sm.PlasmaReleaseModifier, 0f)); + absorbedGas.AdjustMoles(Gas.Oxygen, Math.Max((energy + absorbedGas.Temperature * heatModifier - Atmospherics.T0C) * sm.OxygenReleaseEfficiencyModifier, 0f)); + + _atmosphere.Merge(mix, absorbedGas); + + var powerReduction = (float) Math.Pow(sm.Power / 500, 3); + + // After this point power is lowered + // This wraps around to the begining of the function + sm.Power = Math.Max(sm.Power - Math.Min(powerReduction * powerlossInhibitor, sm.Power * 0.83f * powerlossInhibitor), 0f); + } + + /// + /// Shoot lightning bolts depensing on accumulated power. + /// + private void SupermatterZap(EntityUid uid, SupermatterComponent sm) + { + // Divide power by it's threshold to get a value from 0 to 1, then multiply by the amount of possible lightnings + // Makes it pretty obvious that if SM is shooting out red lightnings something is wrong. + // And if it shoots too weak lightnings it means that it's underfed. Feed the SM :godo: + var zapPower = sm.Power / sm.PowerPenaltyThreshold * sm.LightningPrototypes.Length; + var zapPowerNorm = (int) Math.Clamp(zapPower, 0, sm.LightningPrototypes.Length - 1); + _lightning.ShootRandomLightnings(uid, 3.5f, sm.Power > sm.PowerPenaltyThreshold ? 3 : 1, sm.LightningPrototypes[zapPowerNorm]); + } + + /// + /// Handles environmental damage. + /// + private void HandleDamage(EntityUid uid, SupermatterComponent sm) + { + var xform = Transform(uid); + var indices = _xform.GetGridOrMapTilePosition(uid, xform); + + sm.DamageArchived = sm.Damage; + + var mix = _atmosphere.GetContainingMixture(uid, true, true); + + // We're in space or there is no gas to process + if (!xform.GridUid.HasValue || mix is not { } || mix.TotalMoles == 0f) + { + sm.Damage += Math.Max(sm.Power / 1000 * sm.DamageIncreaseMultiplier, 0.1f); + return; + } + + // Absorbed gas from surrounding area + var absorbedGas = mix.Remove(sm.GasEfficiency * mix.TotalMoles); + var moles = absorbedGas.TotalMoles; + + var totalDamage = 0f; + + var tempThreshold = Atmospherics.T0C + sm.HeatPenaltyThreshold; + + // Temperature start to have a positive effect on damage after 350 + var tempDamage = Math.Max(Math.Clamp(moles / 200f, .5f, 1f) * absorbedGas.Temperature - tempThreshold * sm.DynamicHeatResistance, 0f) * sm.MoleHeatThreshold / 150f * sm.DamageIncreaseMultiplier; + totalDamage += tempDamage; + + // Power only starts affecting damage when it is above 5000 + var powerDamage = Math.Max(sm.Power - sm.PowerPenaltyThreshold, 0f) / 500f * sm.DamageIncreaseMultiplier; + totalDamage += powerDamage; + + // Molar count only starts affecting damage when it is above 1800 + var moleDamage = Math.Max(moles - sm.MolePenaltyThreshold, 0) / 80 * sm.DamageIncreaseMultiplier; + totalDamage += moleDamage; + + // Healing damage + if (moles < sm.MolePenaltyThreshold) + { + // left there a very small float value so that it doesn't eventually divide by 0. + var healHeatDamage = Math.Min(absorbedGas.Temperature - tempThreshold, 0.001f) / 150; + totalDamage += healHeatDamage; + } + + // Check for space tiles next to SM + // TODO: change moles out for checking if adjacent tiles exist + var enumerator = _atmosphere.GetAdjacentTileMixtures(xform.GridUid.Value, indices, false, false); + while (enumerator.MoveNext(out var ind)) + { + if (ind.TotalMoles != 0) + continue; + + var integrity = GetIntegrity(sm); + + // this is some magic number shit + var factor = integrity switch + { + < 10 => 0.0005f, + < 25 => 0.0009f, + < 45 => 0.005f, + < 75 => 0.002f, + _ => 0f + }; + + totalDamage += Math.Clamp(sm.Power * factor * sm.DamageIncreaseMultiplier, 0, sm.MaxSpaceExposureDamage); + + break; + } + + sm.Damage = Math.Min(sm.DamageArchived + sm.DamageHardcap * sm.DelaminationPoint, totalDamage); + } + + /// + /// Handles announcements. + /// + private void HandleAnnouncements(EntityUid uid, SupermatterComponent sm) + { + var message = string.Empty; + var global = false; + + var integrity = GetIntegrity(sm).ToString("0.00"); + + // Special cases + if (sm.Damage < sm.DelaminationPoint && sm.Delamming) + { + message = Loc.GetString("supermatter-delam-cancel", ("integrity", integrity)); + sm.DelamAnnounced = false; + global = true; + } + if (sm.Delamming && !sm.DelamAnnounced) + { + var sb = new StringBuilder(); + var loc = string.Empty; + var alertLevel = "yellow"; + + switch (_delamType) + { + case DelamType.Explosion: + default: + loc = "supermatter-delam-explosion"; + alertLevel = "altdelta"; + break; + + case DelamType.Cascade: + loc = "supermatter-delam-cascade"; + alertLevel = "C.A.S.C.A.D.E"; + break; + } + + var station = _station.GetOwningStation(uid); + if (station != null) + _alert.SetLevel((EntityUid) station, alertLevel, true, true, true, false); + + sb.AppendLine(Loc.GetString(loc)); + sb.AppendLine(Loc.GetString("supermatter-seconds-before-delam", ("seconds", sm.DelamTimer))); + + message = sb.ToString(); + global = true; + sm.DelamAnnounced = true; + + SupermatterAnnouncement(uid, message, global); + return; + } + + // We are not taking consistent damage. Engis not needed. + if (sm.Damage <= sm.DamageArchived) + return; + + if (sm.Damage >= sm.WarningPoint) + { + message = Loc.GetString("supermatter-warning", ("integrity", integrity)); + if (sm.Damage >= sm.EmergencyPoint) + { + message = Loc.GetString("supermatter-emergency", ("integrity", integrity)); + global = true; + } + } + SupermatterAnnouncement(uid, message, global); + } + + /// + /// Help the SM announce something. + /// + /// If true, does the station announcement. + /// If true, sends the announcement from Central Command. + public void SupermatterAnnouncement(EntityUid uid, string message, bool global = false, string? customSender = null) + { + if (global) + { + var sender = customSender != null ? customSender : Loc.GetString("supermatter-announcer"); + _chat.DispatchStationAnnouncement(uid, message, sender, colorOverride: Color.Yellow); + return; + } + _chat.TrySendInGameICMessage(uid, message, InGameICChatType.Speak, hideChat: false, checkRadioPrefix: true); + } + + /// + /// Returns the integrity rounded to hundreds, e.g. 100.00% + /// + public float GetIntegrity(SupermatterComponent sm) + { + var integrity = sm.Damage / sm.DelaminationPoint; + integrity = (float) Math.Round(100 - integrity * 100, 2); + integrity = integrity < 0 ? 0 : integrity; + return integrity; + } + + /// + /// Decide on how to delaminate. + /// + public DelamType ChooseDelamType(EntityUid uid, SupermatterComponent sm) + { + var mix = _atmosphere.GetContainingMixture(uid, true, true); + + { + return DelamType.Explosion; + } + + // при добавлении анти-ноблиума, добавить реакцию "каскад" + // if (sm.Power >= sm.PowerPenaltyThreshold) + // return DelamType.Tesla; + } + + /// + /// Handle the end of the station. + /// + private void HandleDelamination(EntityUid uid, SupermatterComponent sm) + { + var xform = Transform(uid); + + _delamType = ChooseDelamType(uid, sm); + + if (!sm.Delamming) + { + sm.Delamming = true; + HandleAnnouncements(uid, sm); + } + if (sm.Damage < sm.DelaminationPoint && sm.Delamming) + { + sm.Delamming = false; + HandleAnnouncements(uid, sm); + } + + sm.DelamTimerAccumulator++; + + if (sm.DelamTimerAccumulator < sm.DelamTimer) + return; + + switch (_delamType) + { + case DelamType.Explosion: + default: + _explosion.TriggerExplosive(uid); + break; + + case DelamType.Cascade: + Spawn(sm.SupermatterKudzuPrototypeId, xform.Coordinates); + break; + } + } + + private void HandleSoundLoop(EntityUid uid, SupermatterComponent sm) + { + var isAggressive = sm.Damage > sm.WarningPoint; + var isDelamming = sm.Damage > sm.DelaminationPoint; + + if (!isAggressive && !isDelamming) + { + sm.AudioStream = _audio.Stop(sm.AudioStream); + return; + } + + var smSound = isDelamming ? SuperMatterSound.Delam : SuperMatterSound.Aggressive; + + if (sm.SmSound == smSound) + return; + + sm.AudioStream = _audio.Stop(sm.AudioStream); + sm.SmSound = smSound; + } + + #endregion + + + #region Event Handlers + + private void OnCollideEvent(EntityUid uid, SupermatterComponent sm, ref StartCollideEvent args) + { + if (!sm.Activated) + sm.Activated = true; + + var target = args.OtherEntity; + if (args.OtherBody.BodyType == BodyType.Static + || HasComp(target) + || _container.IsEntityInContainer(uid)) + return; + + if (TryComp(target, out var food)) + sm.Power += food.Energy; + else if (TryComp(target, out var projectile)) + sm.Power += (float) projectile.Damage.GetTotal(); + else + sm.Power++; + + sm.MatterPower += HasComp(target) ? 200 : 0; + + if (!HasComp(target)) + { + EntityManager.SpawnEntity("Ash", Transform(target).Coordinates); + _audio.PlayPvs(sm.DustSound, uid); + } + + EntityManager.QueueDeleteEntity(target); + } + + private void OnHandInteract(EntityUid uid, SupermatterComponent sm, ref InteractHandEvent args) + { + if (!sm.Activated) + sm.Activated = true; + + var target = args.User; + + if (HasComp(target)) + return; + + sm.MatterPower += 200; + + EntityManager.SpawnEntity("Ash", Transform(target).Coordinates); + _audio.PlayPvs(sm.DustSound, uid); + EntityManager.QueueDeleteEntity(target); + } + + private void OnItemInteract(EntityUid uid, SupermatterComponent sm, ref InteractUsingEvent args) + { + if (!sm.Activated) + sm.Activated = true; + + if (sm.SliverRemoved) + return; + + if (!HasComp(args.Used)) + return; + + var dae = new DoAfterArgs(EntityManager, args.User, 30f, new SupermatterDoAfterEvent(), uid) + { + BreakOnDamage = true, + BreakOnHandChange = false, + BreakOnMove = true, + BreakOnWeightlessMove = false, + NeedHand = true, + RequireCanInteract = true, + }; + + _doAfter.TryStartDoAfter(dae); + } + + private void OnGetSliver(EntityUid uid, SupermatterComponent sm, ref SupermatterDoAfterEvent args) + { + if (args.Cancelled) + return; + + // your criminal actions will not go unnoticed + sm.Damage += sm.DelaminationPoint / 10; + sm.DamageArchived += sm.DelaminationPoint / 10; + + var integrity = GetIntegrity(sm).ToString("0.00"); + SupermatterAnnouncement(uid, Loc.GetString("supermatter-announcement-cc-tamper", ("integrity", integrity)), true, "Central Command"); + + Spawn(sm.SliverPrototypeId, _transform.GetMapCoordinates(args.User)); + + sm.DelamTimer /= 2; + } + + private void OnExamine(EntityUid uid, SupermatterComponent sm, ref ExaminedEvent args) + { + // get all close and personal to it + if (args.IsInDetailsRange) + { + args.PushMarkup(Loc.GetString("supermatter-examine-integrity", ("integrity", GetIntegrity(sm).ToString("0.00")))); + } + } + + #endregion +} diff --git a/Content.Shared/Goobstation/Clothing/Components/ClothingGrantComponentComponent.cs b/Content.Shared/Goobstation/Clothing/Components/ClothingGrantComponentComponent.cs new file mode 100644 index 0000000000..b558f697d3 --- /dev/null +++ b/Content.Shared/Goobstation/Clothing/Components/ClothingGrantComponentComponent.cs @@ -0,0 +1,15 @@ +using Robust.Shared.Prototypes; + +namespace Content.Shared.SimpleStation14.Clothing +{ + [RegisterComponent] + public sealed partial class ClothingGrantComponentComponent : Component + { + [DataField("component", required: true)] + [AlwaysPushInheritance] + public ComponentRegistry Components { get; private set; } = new(); + + [ViewVariables(VVAccess.ReadWrite)] + public bool IsActive = false; + } +} diff --git a/Content.Shared/Goobstation/Clothing/Components/ClothingGrantTagComponent.cs b/Content.Shared/Goobstation/Clothing/Components/ClothingGrantTagComponent.cs new file mode 100644 index 0000000000..312430cb73 --- /dev/null +++ b/Content.Shared/Goobstation/Clothing/Components/ClothingGrantTagComponent.cs @@ -0,0 +1,11 @@ +namespace Content.Shared.SimpleStation14.Clothing; + +[RegisterComponent] +public sealed partial class ClothingGrantTagComponent : Component +{ + [DataField("tag", required: true), ViewVariables(VVAccess.ReadWrite)] + public string Tag = ""; + + [ViewVariables(VVAccess.ReadWrite)] + public bool IsActive = false; +} diff --git a/Content.Shared/Goobstation/Clothing/Systems/ClothingGrantingSystem.cs b/Content.Shared/Goobstation/Clothing/Systems/ClothingGrantingSystem.cs new file mode 100644 index 0000000000..5fbc83a4b0 --- /dev/null +++ b/Content.Shared/Goobstation/Clothing/Systems/ClothingGrantingSystem.cs @@ -0,0 +1,92 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Inventory.Events; +using Robust.Shared.Serialization.Manager; +using Content.Shared.Tag; + +namespace Content.Shared.SimpleStation14.Clothing; + +public sealed class ClothingGrantingSystem : EntitySystem +{ + [Dependency] private readonly IComponentFactory _componentFactory = default!; + [Dependency] private readonly ISerializationManager _serializationManager = default!; + [Dependency] private readonly TagSystem _tagSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnCompEquip); + SubscribeLocalEvent(OnCompUnequip); + + SubscribeLocalEvent(OnTagEquip); + SubscribeLocalEvent(OnTagUnequip); + } + + private void OnCompEquip(EntityUid uid, ClothingGrantComponentComponent component, GotEquippedEvent args) + { + if (!TryComp(uid, out var clothing)) return; + + if (!clothing.Slots.HasFlag(args.SlotFlags)) return; + + if (component.Components.Count > 1) + { + Logger.Error("Although a component registry supports multiple components, we cannot bookkeep more than 1 component for ClothingGrantComponent at this time."); + return; + } + + foreach (var (name, data) in component.Components) + { + var newComp = (Component) _componentFactory.GetComponent(name); + + if (HasComp(args.Equipee, newComp.GetType())) + continue; + + newComp.Owner = args.Equipee; + + var temp = (object) newComp; + _serializationManager.CopyTo(data.Component, ref temp); + EntityManager.AddComponent(args.Equipee, (Component)temp!); + + component.IsActive = true; + } + } + + private void OnCompUnequip(EntityUid uid, ClothingGrantComponentComponent component, GotUnequippedEvent args) + { + if (!component.IsActive) return; + + foreach (var (name, data) in component.Components) + { + var newComp = (Component) _componentFactory.GetComponent(name); + + RemComp(args.Equipee, newComp.GetType()); + } + + component.IsActive = false; + } + + + private void OnTagEquip(EntityUid uid, ClothingGrantTagComponent component, GotEquippedEvent args) + { + if (!TryComp(uid, out var clothing)) + return; + + if (!clothing.Slots.HasFlag(args.SlotFlags)) + return; + + EnsureComp(args.Equipee); + _tagSystem.AddTag(args.Equipee, component.Tag); + + component.IsActive = true; + } + + private void OnTagUnequip(EntityUid uid, ClothingGrantTagComponent component, GotUnequippedEvent args) + { + if (!component.IsActive) + return; + + _tagSystem.RemoveTag(args.Equipee, component.Tag); + + component.IsActive = false; + } +} diff --git a/Content.Shared/Goobstation/Supermatter/Components/SupermatterComponent.cs b/Content.Shared/Goobstation/Supermatter/Components/SupermatterComponent.cs new file mode 100644 index 0000000000..9729663b33 --- /dev/null +++ b/Content.Shared/Goobstation/Supermatter/Components/SupermatterComponent.cs @@ -0,0 +1,383 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Audio; +using Content.Shared.Atmos; +using Content.Shared.Supermatter.Systems; +using Content.Shared.Whitelist; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Supermatter.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SupermatterComponent : Component +{ + #region SM Base + + /// + /// The SM will only cycle if activated. + /// + [DataField("activated")] + [ViewVariables(VVAccess.ReadWrite)] + public bool Activated = false; + + [DataField("supermatterSliverPrototype")] + public string SliverPrototypeId = "SupermatterSliver"; + + /// + /// Affects delamination timer. If removed - delamination timer is divided by 2. + /// + [DataField("sliverRemoved")] + [ViewVariables(VVAccess.ReadWrite)] + public bool SliverRemoved = false; + + [DataField("whitelist")] + public EntityWhitelist Whitelist = new(); + public string IdTag = "EmitterBolt"; + + public string[] LightningPrototypes = + { + "Lightning", + "ChargedLightning", + "SuperchargedLightning", + "HyperchargedLightning" + }; + + [DataField("singularitySpawnPrototype")] + public string SingularityPrototypeId = "Singularity"; + + [DataField("teslaSpawnPrototype")] + public string TeslaPrototypeId = "TeslaEnergyBall"; + + [DataField("supermatterKudzuSpawnPrototype")] + public string SupermatterKudzuPrototypeId = "SupermatterKudzu"; + + [ViewVariables(VVAccess.ReadWrite)] + public float Power; + + /// + /// The amount of damage we have currently + /// + [ViewVariables(VVAccess.ReadWrite)] + public float Damage = 0f; + + [ViewVariables(VVAccess.ReadWrite)] + public float MatterPower; + + [ViewVariables(VVAccess.ReadWrite)] + public float MatterPowerConversion = 10f; + + /// + /// The portion of the gasmix we're on + /// + [ViewVariables(VVAccess.ReadWrite)] + public float GasEfficiency = 0.15f; + + /// + /// The amount of heat we apply scaled + /// + [ViewVariables(VVAccess.ReadWrite)] + public float HeatThreshold = 2500f; + + #endregion SM Base + + #region SM Sound + /// + /// Current stream of SM audio. + /// + public EntityUid? AudioStream; + + public SharedSupermatterSystem.SuperMatterSound? SmSound; + + [DataField("dustSound")] + public SoundSpecifier DustSound = new SoundPathSpecifier("/Audio/Effects/Grenades/Supermatter/supermatter_start.ogg"); + + [DataField("delamSound")] + public SoundSpecifier DelamSound = new SoundPathSpecifier("/Audio/Goobstation/Supermatter/delamming.ogg"); + + [DataField("delamAlarm")] + public SoundSpecifier DelamAlarm = new SoundPathSpecifier("/Audio/Machines/alarm.ogg"); + + #endregion SM Sound + + #region SM Calculation + + /// + /// Based on co2 percentage, slowly moves between + /// 0 and 1. We use it to calc the powerloss_inhibitor + /// + [ViewVariables(VVAccess.ReadOnly)] + public float PowerlossDynamicScaling; + + /// + /// Affects the amount of damage and minimum point + /// at which the sm takes heat damage + /// + [ViewVariables(VVAccess.ReadOnly)] + public float DynamicHeatResistance = 1; + + /// + /// Multiplier on damage the core takes from absorbing hot gas + /// Default is ~1/350 + /// + [ViewVariables(VVAccess.ReadOnly)] + public float MoleHeatPenalty = 0.00286f; + + /// + /// Inverse of MoleHeatPenalty + /// + [ViewVariables(VVAccess.ReadOnly)] + public float MoleHeatThreshold = 350f; + + /// + /// Multiplier on power generated by nuclear reactions + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("reactionpowerModifier")] + public float ReactionPowerModifier = 0.55f; + + /// + /// Acts as a multiplier on the amount that nuclear reactions increase the supermatter core temperature + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("thermalreleaseModifier")] + public float ThermalReleaseModifier = 0.2f; + + /// + /// Multiplier on how much plasma is released during supermatter reactions + /// Default is ~1/750 + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("plasmareleaseModifier")] + public float PlasmaReleaseModifier = 0.001333f; + + /// + /// Multiplier on how much oxygen is released during supermatter reactions. + /// Default is ~1/325 + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("oxygenreleaseModifier")] + public float OxygenReleaseEfficiencyModifier = 0.0031f; + + #endregion SM Calculation + + #region SM Timer + + /// + /// The point at which we should start sending messeges + /// about the damage to the engi channels. + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("WarningPoint")] + public float WarningPoint = 50; + + /// + /// The point at which we start sending messages to the common channel + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("emergencyPoint")] + public float EmergencyPoint = 500; + + /// + /// we yell if over 50 damage every YellTimer Seconds + /// + [ViewVariables(VVAccess.ReadWrite)] + public float YellTimer = 60f; + + /// + /// set to YellTimer at first so it doesnt yell a minute after being hit + /// + [ViewVariables(VVAccess.ReadOnly)] + public float YellAccumulator = 60f; + + /// + /// Timer for delam + /// + [ViewVariables(VVAccess.ReadOnly)] + public float DelamTimerAccumulator; + + /// + /// Time until delam + /// + [ViewVariables(VVAccess.ReadWrite)] + [DataField("delamTimer")] + public float DelamTimer = 120f; + + /// + /// The message timer + /// + [ViewVariables(VVAccess.ReadWrite)] + public float SpeakAccumulator = 60f; + + [ViewVariables(VVAccess.ReadOnly)] + public float UpdateAccumulator = 0f; + + [ViewVariables(VVAccess.ReadWrite)] + public float UpdateTimer = 1f; + + [ViewVariables(VVAccess.ReadOnly)] + public float ZapAccumulator = 0f; + + [ViewVariables(VVAccess.ReadWrite)] + public float ZapTimer = 10f; + #endregion SM Timer + + #region SM Threshold + + /// + /// Higher == Higher percentage of inhibitor gas needed + /// before the charge inertia chain reaction effect starts. + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("powerlossinhibitiongasThreshold")] + public float PowerlossInhibitionGasThreshold = 0.20f; + + /// + /// Higher == More moles of the gas are needed before the charge + /// inertia chain reaction effect starts. + /// Scales powerloss inhibition down until this amount of moles is reached + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("powerlossinhibitionmoleThreshold")] + public float PowerlossInhibitionMoleThreshold = 20f; + + /// + /// bonus powerloss inhibition boost if this amount of moles is reached + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("powerlossinhibitionmoleboostThreshold")] + public float PowerlossInhibitionMoleBoostThreshold = 500f; + + /// + /// Above this value we can get lord singulo and independent mol damage, + /// below it we can heal damage + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("molepenaltyThreshold")] + public float MolePenaltyThreshold = 900f; + + /// + /// more moles of gases are harder to heat than fewer, + /// so let's scale heat damage around them + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("moleheatpenaltyThreshold")] + public float MoleHeatPenaltyThreshold; + + /// + /// The cutoff on power properly doing damage, pulling shit around, + /// and delamming into a tesla. Low chance of pyro anomalies, +2 bolts of electricity + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("powerPenaltyThreshold")] + public float PowerPenaltyThreshold = 2500f; + + /// + /// Maximum safe operational temperature in degrees Celsius. Supermatter begins taking damage above this temperature. + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("heatpenaltyThreshold")] + public float HeatPenaltyThreshold = 40f; + + /// + /// The damage we had before this cycle. Used to limit the damage we can take each cycle, and for safe alert + /// + [ViewVariables(VVAccess.ReadWrite)] + public float DamageArchived = 0f; + + /// + /// is multiplied by ExplosionPoint to cap + /// evironmental damage per cycle + /// + [ViewVariables(VVAccess.ReadOnly)] + public float DamageHardcap = 0.002f; + + /// + /// environmental damage is scaled by this + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("damageincreaseMultiplier")] + public float DamageIncreaseMultiplier = 0.25f; + + /// + /// if spaced sm wont take more than 2 damage per cycle + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("maxspaceexposureDamage")] + public float MaxSpaceExposureDamage = 2; + + #endregion SM Threshold + + #region SM Delamm + + public bool DelamAnnounced = false; + + /// + /// The point at which we delamm + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("explosionPoint")] + public int DelaminationPoint = 900; + + //Are we delamming? + [ViewVariables(VVAccess.ReadOnly)] + public bool Delamming = false; + + //Explosion totalIntensity value + [ViewVariables(VVAccess.ReadOnly)] + [DataField("totalIntensity")] + public float TotalIntensity = 50000f; + + //Explosion radius value + [ViewVariables(VVAccess.ReadOnly)] + [DataField("radius")] + public float Radius = 50f; + + /// + /// These would be what you would get at point blank, decreases with distance + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("detonationRads")] + public float DetonationRads = 200f; + + #endregion SM Delamm + + #region SM Gas + /// + /// Is used to store gas + /// + [ViewVariables(VVAccess.ReadOnly)] + [DataField("gasStorage")] + public Dictionary GasStorage = new Dictionary() + { + {Gas.Oxygen, 0f}, + {Gas.Nitrogen, 0f}, + {Gas.CarbonDioxide, 0f}, + {Gas.Plasma, 0f}, + {Gas.Tritium, 0f}, + {Gas.WaterVapor, 0f} + }; + + /// + /// Stores each gas facts + /// + public readonly Dictionary GasDataFields = new() + { + [Gas.Oxygen] = (TransmitModifier: 1.5f, HeatPenalty: 1f, PowerMixRatio: 1f), + [Gas.Nitrogen] = (TransmitModifier: 0f, HeatPenalty: -1.5f, PowerMixRatio: -1f), + [Gas.CarbonDioxide] = (TransmitModifier: 0f, HeatPenalty: 0.1f, PowerMixRatio: 1f), + [Gas.Plasma] = (TransmitModifier: 4f, HeatPenalty: 15f, PowerMixRatio: 1f), + [Gas.Tritium] = (TransmitModifier: 30f, HeatPenalty: 10f, PowerMixRatio: 1f), + [Gas.WaterVapor] = (TransmitModifier: 2f, HeatPenalty: 12f, PowerMixRatio: 1f), + [Gas.Frezon] = (TransmitModifier: 3f, HeatPenalty: -10f, PowerMixRatio: -1f), + [Gas.Ammonia] = (TransmitModifier: 0f, HeatPenalty: .5f, PowerMixRatio: 1f), + [Gas.NitrousOxide] = (TransmitModifier: 0f, HeatPenalty: -5f, PowerMixRatio: -1f), + }; + + #endregion SM Gas +} + +[Serializable, NetSerializable] +public sealed partial class SupermatterDoAfterEvent : SimpleDoAfterEvent +{ + +} diff --git a/Content.Shared/Goobstation/Supermatter/Components/SupermatterFoodComponent.cs b/Content.Shared/Goobstation/Supermatter/Components/SupermatterFoodComponent.cs new file mode 100644 index 0000000000..16ee486bfc --- /dev/null +++ b/Content.Shared/Goobstation/Supermatter/Components/SupermatterFoodComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.Supermatter.Components; + +[RegisterComponent] +public sealed partial class SupermatterFoodComponent : Component +{ + [ViewVariables(VVAccess.ReadWrite)] + [DataField("energy")] + public int Energy { get; set; } = 1; +} diff --git a/Content.Shared/Goobstation/Supermatter/Components/SupermatterImmuneComponent.cs b/Content.Shared/Goobstation/Supermatter/Components/SupermatterImmuneComponent.cs new file mode 100644 index 0000000000..b517115eca --- /dev/null +++ b/Content.Shared/Goobstation/Supermatter/Components/SupermatterImmuneComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Supermatter.Components; + +[RegisterComponent, NetworkedComponent] +public sealed partial class SupermatterImmuneComponent : Component +{ + +} diff --git a/Content.Shared/Goobstation/Supermatter/Systems/SharedSupermatterSystem.cs b/Content.Shared/Goobstation/Supermatter/Systems/SharedSupermatterSystem.cs new file mode 100644 index 0000000000..7f5656c0e1 --- /dev/null +++ b/Content.Shared/Goobstation/Supermatter/Systems/SharedSupermatterSystem.cs @@ -0,0 +1,47 @@ +using Content.Shared.Supermatter.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Supermatter.Systems; + +public abstract class SharedSupermatterSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnSupermatterStartup); + } + + public enum SuperMatterSound : sbyte + { + Aggressive = 0, + Delam = 1 + } + + public enum DelamType : sbyte + { + Explosion = 0, + Cascade = 1 // save for later + } + #region Getters/Setters + + public void OnSupermatterStartup(EntityUid uid, SupermatterComponent comp, ComponentStartup args) + { + } + + #endregion Getters/Setters + + #region Serialization + /// + /// A state wrapper used to sync the supermatter between the server and client. + /// + [Serializable, NetSerializable] + protected sealed class SupermatterComponentState : ComponentState + { + public SupermatterComponentState(SupermatterComponent supermatter) + { + } + } + + #endregion Serialization + +} diff --git a/Resources/Audio/Goobstation/Supermatter/attributions.yml b/Resources/Audio/Goobstation/Supermatter/attributions.yml new file mode 100644 index 0000000000..4bf86a4212 --- /dev/null +++ b/Resources/Audio/Goobstation/Supermatter/attributions.yml @@ -0,0 +1,15 @@ +- files: ["calm.ogg"] + license: "CC-BY-3.0" + copyright: "Unknown" + source: "https://forums.goobstation.com" + +- files: ["delamming.ogg"] + license: "CC-BY-3.0" + copyright: "Unknown" + source: "https://forums.goobstation.com" + +- files: ["dust.ogg"] + license: "CC-BY-3.0" + copyright: "Unknown" + source: "https://forums.goobstation.com" + diff --git a/Resources/Audio/Goobstation/Supermatter/calm.ogg b/Resources/Audio/Goobstation/Supermatter/calm.ogg new file mode 100644 index 0000000000..dc3102e578 Binary files /dev/null and b/Resources/Audio/Goobstation/Supermatter/calm.ogg differ diff --git a/Resources/Audio/Goobstation/Supermatter/delamming.ogg b/Resources/Audio/Goobstation/Supermatter/delamming.ogg new file mode 100644 index 0000000000..a48878ec42 Binary files /dev/null and b/Resources/Audio/Goobstation/Supermatter/delamming.ogg differ diff --git a/Resources/Audio/Stories/Misc/altdelta.ogg b/Resources/Audio/Stories/Misc/altdelta.ogg new file mode 100644 index 0000000000..caeeabc3ff Binary files /dev/null and b/Resources/Audio/Stories/Misc/altdelta.ogg differ diff --git a/Resources/Locale/en-US/Goobstation/supermatter/supermatter.ftl b/Resources/Locale/en-US/Goobstation/supermatter/supermatter.ftl new file mode 100644 index 0000000000..8e75116bca --- /dev/null +++ b/Resources/Locale/en-US/Goobstation/supermatter/supermatter.ftl @@ -0,0 +1,38 @@ +supermatter-announcer = Automatic Supermatter Engine + +supermatter-examine-integrity = + It's integrity is [color=yellow]{$integrity}%[/color]. + +supermatter-warning = + Warning! Crystal hyperstructure integrity faltering! Integrity: {$integrity}%. + +supermatter-emergency = + DANGER! Crystal hyperstructure integrity reaching critical levels! Integrity: {$integrity}%. + +supermatter-delam-explosion = + CRYSTAL DELAMINATION IMMINENT! The crystal has reached critical integrity failure! Emergency causality destabilization field has been engaged. + +supermatter-delam-overmass = + CRYSTAL DELAMINATION IMMINENT! Crystal hyperstructure integrity has reached critical mass failure! Singularity formation imminent! + +supermatter-delam-tesla = + CRYSTAL DELAMINATION IMMINENT! Crystal hyperstructure integrity has reached critical power surge failure! Energy ball formation imminent! + +supermatter-delam-cascade = + CRYSTAL DELAMINATION IMMINENT! Harmonic frequency limits exceeded, casualty destabilization field could not be engaged! + +supermatter-delam-cancel = + Crystalline hyperstructure returning to safe operating parameters. Failsafe has been Disengaged. Integrity: {$integrity}%. + +supermatter-seconds-before-delam = + Estimated time before delamination: {$seconds} seconds. + +supermatter-tamper-begin = + You begin carefully cutting a piece off the supermatter crystal... + +supermatter-tamper-end = + You feel the power of a thousand suns laying on your palms. Or is it all the radiation? + +supermatter-announcement-cc-tamper = + Our automatic casualty system has detected that the supermatter crystal structural integrity was compromised by an external force. + Engineering department, report to the supermatter engine immediately.