diff --git a/Content.Client/Communications/CommsHackerSystem.cs b/Content.Client/Communications/CommsHackerSystem.cs new file mode 100644 index 0000000000..3459dd1cd4 --- /dev/null +++ b/Content.Client/Communications/CommsHackerSystem.cs @@ -0,0 +1,10 @@ +using Content.Shared.Communications; + +namespace Content.Client.Communications; + +/// +/// Does nothing special, only exists to provide a client implementation. +/// +public sealed class CommsHackerSystem : SharedCommsHackerSystem +{ +} diff --git a/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs new file mode 100644 index 0000000000..7758c3d7e2 --- /dev/null +++ b/Content.Client/Ninja/Systems/NinjaGlovesSystem.cs @@ -0,0 +1,10 @@ +using Content.Shared.Ninja.Systems; + +namespace Content.Client.Ninja.Systems; + +/// +/// Does nothing special, only exists to provide a client implementation. +/// +public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem +{ +} diff --git a/Content.Client/Ninja/Systems/NinjaSuitSystem.cs b/Content.Client/Ninja/Systems/NinjaSuitSystem.cs new file mode 100644 index 0000000000..fde1801b37 --- /dev/null +++ b/Content.Client/Ninja/Systems/NinjaSuitSystem.cs @@ -0,0 +1,24 @@ +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; + +namespace Content.Client.Ninja.Systems; + +/// +/// Disables cloak prediction since client has no knowledge of battery power. +/// Cloak will still be enabled after server tells it. +/// +public sealed class NinjaSuitSystem : SharedNinjaSuitSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnAttemptStealth); + } + + private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args) + { + args.Cancel(); + } +} diff --git a/Content.Client/Ninja/Systems/NinjaSystem.cs b/Content.Client/Ninja/Systems/NinjaSystem.cs new file mode 100644 index 0000000000..aa2fa2047f --- /dev/null +++ b/Content.Client/Ninja/Systems/NinjaSystem.cs @@ -0,0 +1,12 @@ +using Content.Shared.Ninja.Systems; + +namespace Content.Client.Ninja.Systems; + +/// +/// Currently does nothing special clientside. +/// All functionality is in shared and server. +/// Only exists to prevent crashing. +/// +public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem +{ +} diff --git a/Content.Client/Research/ResearchStealerSystem.cs b/Content.Client/Research/ResearchStealerSystem.cs new file mode 100644 index 0000000000..31909044e4 --- /dev/null +++ b/Content.Client/Research/ResearchStealerSystem.cs @@ -0,0 +1,10 @@ +using Content.Shared.Research.Systems; + +namespace Content.Client.Research; + +/// +/// Does nothing special, only exists to provide a client implementation. +/// +public sealed class ResearchStealerSystem : EntitySystem +{ +} diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index ccd2778008..b6e8a0d300 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -1,4 +1,5 @@ using Content.Server.GameTicking.Rules; +using Content.Server.Ninja.Systems; using Content.Server.Zombies; using Content.Shared.Administration; using Content.Shared.Database; @@ -15,6 +16,7 @@ public sealed partial class AdminVerbSystem { [Dependency] private readonly ZombieSystem _zombie = default!; [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly SpaceNinjaSystem _ninja = default!; [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; [Dependency] private readonly PiratesRuleSystem _piratesRule = default!; [Dependency] private readonly SharedMindSystem _minds = default!; @@ -35,7 +37,7 @@ public sealed partial class AdminVerbSystem Verb traitor = new() { - Text = "Make Traitor", + Text = Loc.GetString("admin-verb-text-make-traitor"), Category = VerbCategory.Antag, Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"), Act = () => @@ -54,7 +56,7 @@ public sealed partial class AdminVerbSystem Verb zombie = new() { - Text = "Make Zombie", + Text = Loc.GetString("admin-verb-text-make-zombie"), Category = VerbCategory.Antag, Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")), Act = () => @@ -69,7 +71,7 @@ public sealed partial class AdminVerbSystem Verb nukeOp = new() { - Text = "Make nuclear operative", + Text = Loc.GetString("admin-verb-text-make-nuclear-operative"), Category = VerbCategory.Antag, Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"), Act = () => @@ -86,7 +88,7 @@ public sealed partial class AdminVerbSystem Verb pirate = new() { - Text = "Make Pirate", + Text = Loc.GetString("admin-verb-text-make-pirate"), Category = VerbCategory.Antag, Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"), Act = () => @@ -101,5 +103,21 @@ public sealed partial class AdminVerbSystem }; args.Verbs.Add(pirate); + Verb spaceNinja = new() + { + Text = Loc.GetString("admin-verb-text-make-space-ninja"), + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"), + Act = () => + { + if (!_minds.TryGetMind(args.Target, out var mindId, out var mind)) + return; + + _ninja.MakeNinja(mindId, mind); + }, + Impact = LogImpact.High, + Message = Loc.GetString("admin-verb-make-space-ninja"), + }; + args.Verbs.Add(spaceNinja); } } diff --git a/Content.Server/Communications/CommsHackerSystem.cs b/Content.Server/Communications/CommsHackerSystem.cs new file mode 100644 index 0000000000..6ef9e1b1bb --- /dev/null +++ b/Content.Server/Communications/CommsHackerSystem.cs @@ -0,0 +1,89 @@ +using Content.Server.Chat.Systems; +using Content.Server.GameTicking; +using Content.Server.Ninja.Systems; +using Content.Shared.Communications; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Robust.Shared.Random; +using Robust.Shared.Serialization; + +namespace Content.Server.Communications; + +public sealed class CommsHackerSystem : SharedCommsHackerSystem +{ + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly IRobustRandom _random = default!; + // TODO: remove when generic check event is used + [Dependency] private readonly NinjaGlovesSystem _gloves = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBeforeInteractHand); + SubscribeLocalEvent(OnDoAfter); + } + + /// + /// Start the doafter to hack a comms console + /// + private void OnBeforeInteractHand(EntityUid uid, CommsHackerComponent comp, BeforeInteractHandEvent args) + { + if (args.Handled || !HasComp(args.Target)) + return; + + // TODO: generic check event + if (!_gloves.AbilityCheck(uid, args, out var target)) + return; + + var doAfterArgs = new DoAfterArgs(uid, comp.Delay, new TerrorDoAfterEvent(), target: target, used: uid, eventTarget: uid) + { + BreakOnDamage = true, + BreakOnUserMove = true, + MovementThreshold = 0.5f, + CancelDuplicate = false + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + args.Handled = true; + } + + /// + /// Call in a random threat and do cleanup. + /// + private void OnDoAfter(EntityUid uid, CommsHackerComponent comp, TerrorDoAfterEvent args) + { + if (args.Cancelled || args.Handled || comp.Threats.Count == 0 || args.Target == null) + return; + + var threat = _random.Pick(comp.Threats); + CallInThreat(threat); + + // prevent calling in multiple threats + RemComp(uid); + + var ev = new ThreatCalledInEvent(uid, args.Target.Value); + RaiseLocalEvent(args.User, ref ev); + } + + /// + /// Makes announcement and adds game rule of the threat. + /// + public void CallInThreat(Threat threat) + { + _gameTicker.StartGameRule(threat.Rule, out _); + _chat.DispatchGlobalAnnouncement(Loc.GetString(threat.Announcement), playSound: true, colorOverride: Color.Red); + } +} + +/// +/// Raised on the user when a threat is called in on the communications console. +/// +/// +/// If you add , make sure to use this event to prevent adding it twice. +/// For example, you could add a marker component after a threat is called in then check if the user doesn't have that marker before adding CommsHackerComponent. +/// +[ByRefEvent] +public record struct ThreatCalledInEvent(EntityUid Used, EntityUid Target); diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index 1bb8a0505f..f9918dfb0a 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -276,4 +276,3 @@ public sealed class DoorSystem : SharedDoorSystem } } } - diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index a703086ed2..c6ce90692f 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -303,16 +303,8 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem } } - /// Entity being electrocuted. - /// Source entity of the electrocution. - /// How much shock damage the entity takes. - /// How long the entity will be stunned. - /// Should time be refreshed (instead of accumilated) if the entity is already electrocuted? - /// How insulated the entity is from the shock. 0 means completely insulated, and 1 means no insulation. - /// Status effects to apply to the entity. - /// Should the electrocution bypass the Insulated component? - /// Whether the entity was stunned by the shock. - public bool TryDoElectrocution( + /// + public override bool TryDoElectrocution( EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f, StatusEffectsComponent? statusEffects = null, bool ignoreInsulation = false) { diff --git a/Content.Server/Emp/EmpSystem.cs b/Content.Server/Emp/EmpSystem.cs index b327f086db..c95be2501e 100644 --- a/Content.Server/Emp/EmpSystem.cs +++ b/Content.Server/Emp/EmpSystem.cs @@ -31,6 +31,11 @@ public sealed class EmpSystem : SharedEmpSystem { foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range)) { + var attemptEv = new EmpAttemptEvent(); + RaiseLocalEvent(uid, attemptEv); + if (attemptEv.Cancelled) + continue; + var ev = new EmpPulseEvent(energyConsumption, false, false); RaiseLocalEvent(uid, ref ev); if (ev.Affected) @@ -100,6 +105,13 @@ public sealed class EmpSystem : SharedEmpSystem } } +/// +/// Raised on an entity before . Cancel this to prevent the emp event being raised. +/// +public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs +{ +} + [ByRefEvent] public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled); diff --git a/Content.Server/Explosion/Components/AutomatedTimerComponent.cs b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs new file mode 100644 index 0000000000..7019c08d43 --- /dev/null +++ b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Explosion.Components; + +/// +/// Disallows starting the timer by hand, must be stuck or triggered by a system. +/// +[RegisterComponent] +public sealed partial class AutomatedTimerComponent : Component +{ +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index b2ef93b74d..1a3323b1ce 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -140,7 +140,7 @@ public sealed partial class TriggerSystem private void OnTimerUse(EntityUid uid, OnUseTimerTriggerComponent component, UseInHandEvent args) { - if (args.Handled) + if (args.Handled || HasComp(uid)) return; HandleTimerTrigger( diff --git a/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs new file mode 100644 index 0000000000..62eee90d50 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/NinjaRuleComponent.cs @@ -0,0 +1,37 @@ +using Content.Server.Ninja.Systems; +using Content.Shared.Communications; +using Content.Shared.Objectives; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.GameTicking.Rules.Components; + +[RegisterComponent, Access(typeof(SpaceNinjaSystem))] +public sealed partial class NinjaRuleComponent : Component +{ + /// + /// All ninja minds that are using this rule. + /// Their SpaceNinjaComponent Rule field should point back to this rule. + /// + [DataField("minds")] + public List Minds = new(); + + /// + /// List of objective prototype ids to add + /// + [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Objectives = new(); + + /// + /// List of threats that can be called in. Copied onto when gloves are enabled. + /// + [DataField("threats", required: true)] + public List Threats = new(); + + /// + /// Sound played when making the player a ninja via antag control or ghost role + /// + [DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))] + public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/ninja_greeting.ogg"); +} diff --git a/Content.Server/GameTicking/Rules/NinjaRuleSystem.cs b/Content.Server/GameTicking/Rules/NinjaRuleSystem.cs new file mode 100644 index 0000000000..b75241eaf4 --- /dev/null +++ b/Content.Server/GameTicking/Rules/NinjaRuleSystem.cs @@ -0,0 +1,23 @@ +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Objectives; + +namespace Content.Server.GameTicking.Rules; + +/// +/// Only handles round end text for ninja. +/// +public sealed class NinjaRuleSystem : GameRuleSystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnObjectivesTextGetInfo); + } + + private void OnObjectivesTextGetInfo(EntityUid uid, NinjaRuleComponent comp, ref ObjectivesTextGetInfoEvent args) + { + args.Minds = comp.Minds; + args.AgentName = Loc.GetString("ninja-round-end-agent-name"); + } +} diff --git a/Content.Server/Implants/AutoImplantSystem.cs b/Content.Server/Implants/AutoImplantSystem.cs new file mode 100644 index 0000000000..b854e3ca3d --- /dev/null +++ b/Content.Server/Implants/AutoImplantSystem.cs @@ -0,0 +1,21 @@ +using Content.Server.Implants.Components; + +namespace Content.Server.Implants; + +public sealed class AutoImplantSystem : EntitySystem +{ + [Dependency] private readonly SubdermalImplantSystem _subdermalImplant = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + } + + private void OnMapInit(EntityUid uid, AutoImplantComponent comp, MapInitEvent args) + { + _subdermalImplant.AddImplants(uid, comp.Implants); + RemComp(uid); + } +} diff --git a/Content.Server/Implants/Components/AutoImplantComponent.cs b/Content.Server/Implants/Components/AutoImplantComponent.cs new file mode 100644 index 0000000000..e082354d8b --- /dev/null +++ b/Content.Server/Implants/Components/AutoImplantComponent.cs @@ -0,0 +1,17 @@ +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.Implants.Components; + +/// +/// Implants an entity automatically on MapInit. +/// +[RegisterComponent] +public sealed partial class AutoImplantComponent : Component +{ + /// + /// List of implants to inject. + /// + [DataField("implants", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + public List Implants = new(); +} diff --git a/Content.Server/Jobs/AddImplantSpecial.cs b/Content.Server/Jobs/AddImplantSpecial.cs index 8eb554cc7c..83193e7056 100644 --- a/Content.Server/Jobs/AddImplantSpecial.cs +++ b/Content.Server/Jobs/AddImplantSpecial.cs @@ -1,5 +1,4 @@ using Content.Shared.Implants; -using Content.Shared.Implants.Components; using Content.Shared.Roles; using JetBrains.Annotations; using Robust.Shared.Prototypes; @@ -13,7 +12,6 @@ namespace Content.Server.Jobs; [UsedImplicitly] public sealed partial class AddImplantSpecial : JobSpecial { - [DataField("implants", customTypeSerializer: typeof(PrototypeIdHashSetSerializer))] public HashSet Implants { get; private set; } = new(); @@ -21,19 +19,6 @@ public sealed partial class AddImplantSpecial : JobSpecial { var entMan = IoCManager.Resolve(); var implantSystem = entMan.System(); - var xformQuery = entMan.GetEntityQuery(); - - if (!xformQuery.TryGetComponent(mob, out var xform)) - return; - - foreach (var implantId in Implants) - { - var implant = entMan.SpawnEntity(implantId, xform.Coordinates); - - if (!entMan.TryGetComponent(implant, out var implantComp)) - return; - - implantSystem.ForceImplant(mob, implant, implantComp); - } + implantSystem.AddImplants(mob, Implants); } } diff --git a/Content.Server/Mind/MindSystem.cs b/Content.Server/Mind/MindSystem.cs index 797e8a273b..aca5a9d485 100644 --- a/Content.Server/Mind/MindSystem.cs +++ b/Content.Server/Mind/MindSystem.cs @@ -18,12 +18,12 @@ namespace Content.Server.Mind; public sealed class MindSystem : SharedMindSystem { [Dependency] private readonly ActorSystem _actor = default!; - [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly GameTicker _gameTicker = default!; - [Dependency] private readonly SharedGhostSystem _ghosts = default!; + [Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IMapManager _maps = default!; - [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly IPlayerManager _players = default!; + [Dependency] private readonly MetaDataSystem _metaData = default!; + [Dependency] private readonly SharedGhostSystem _ghosts = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; public override void Initialize() @@ -277,10 +277,13 @@ public sealed class MindSystem : SharedMindSystem } } + /// + /// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the + /// entity that any mind is connected to, except as a side effect of the fact that it may change a player's + /// attached entity. E.g., ghosts get deleted. + /// public override void SetUserId(EntityUid mindId, NetUserId? userId, MindComponent? mind = null) { - base.SetUserId(mindId, userId, mind); - if (!Resolve(mindId, ref mind)) return; diff --git a/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs new file mode 100644 index 0000000000..1910969f19 --- /dev/null +++ b/Content.Server/Ninja/Systems/BatteryDrainerSystem.cs @@ -0,0 +1,100 @@ +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Robust.Shared.Audio; + +namespace Content.Server.Ninja.Systems; + +/// +/// Handles the doafter and power transfer when draining. +/// +public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem +{ + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBeforeInteractHand); + } + + /// + /// Start do after for draining a power source. + /// Can't predict PNBC existing so only done on server. + /// + private void OnBeforeInteractHand(EntityUid uid, BatteryDrainerComponent comp, BeforeInteractHandEvent args) + { + var target = args.Target; + if (args.Handled || comp.BatteryUid == null || !HasComp(target)) + return; + + // handles even if battery is full so you can actually see the poup + args.Handled = true; + + if (_battery.IsFull(comp.BatteryUid.Value)) + { + _popup.PopupEntity(Loc.GetString("battery-drainer-full"), uid, uid, PopupType.Medium); + return; + } + + var doAfterArgs = new DoAfterArgs(uid, comp.DrainTime, new DrainDoAfterEvent(), target: target, eventTarget: uid) + { + BreakOnUserMove = true, + MovementThreshold = 0.5f, + CancelDuplicate = false, + AttemptFrequency = AttemptFrequency.StartAndEnd + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + /// + protected override void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent args) + { + base.OnDoAfterAttempt(uid, comp, args); + + if (comp.BatteryUid == null || _battery.IsFull(comp.BatteryUid.Value)) + args.Cancel(); + } + + /// + protected override bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target) + { + if (comp.BatteryUid == null || !TryComp(comp.BatteryUid.Value, out var battery)) + return false; + + if (!TryComp(target, out var targetBattery) || !TryComp(target, out var pnb)) + return false; + + if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0)) + { + _popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium); + return false; + } + + var available = targetBattery.CurrentCharge; + var required = battery.MaxCharge - battery.CurrentCharge; + // higher tier storages can charge more + var maxDrained = pnb.MaxSupply * comp.DrainTime; + var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained); + if (!_battery.TryUseCharge(target, input, targetBattery)) + return false; + + var output = input * comp.DrainEfficiency; + _battery.SetCharge(comp.BatteryUid.Value, battery.CurrentCharge + output, battery); + Spawn("EffectSparks", Transform(target).Coordinates); + _audio.PlayPvs(comp.SparkSound, target); + _popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid); + + // repeat the doafter until battery is full + return !battery.IsFullyCharged; + } +} diff --git a/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs new file mode 100644 index 0000000000..402650d6a6 --- /dev/null +++ b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs @@ -0,0 +1,102 @@ +using Content.Server.Communications; +using Content.Server.DoAfter; +using Content.Server.Mind; +using Content.Server.Ninja.Systems; +using Content.Server.Power.Components; +using Content.Server.Roles; +using Content.Shared.Communications; +using Content.Shared.DoAfter; +using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Content.Shared.Research.Components; +using Content.Shared.Toggleable; + +namespace Content.Server.Ninja.Systems; + +/// +/// Handles the toggle gloves action. +/// +public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem +{ + [Dependency] private readonly EmagProviderSystem _emagProvider = default!; + [Dependency] private readonly SharedBatteryDrainerSystem _drainer = default!; + [Dependency] private readonly SharedStunProviderSystem _stunProvider = default!; + [Dependency] private readonly SpaceNinjaSystem _ninja = default!; + [Dependency] private readonly CommsHackerSystem _commsHacker = default!; + [Dependency] private readonly MindSystem _mind = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnToggleAction); + } + + /// + /// Toggle gloves, if the user is a ninja wearing a ninja suit. + /// + private void OnToggleAction(EntityUid uid, NinjaGlovesComponent comp, ToggleActionEvent args) + { + if (args.Handled) + return; + + args.Handled = true; + + var user = args.Performer; + // need to wear suit to enable gloves + if (!TryComp(user, out var ninja) + || ninja.Suit == null + || !HasComp(ninja.Suit.Value)) + { + Popup.PopupEntity(Loc.GetString("ninja-gloves-not-wearing-suit"), user, user); + return; + } + + // show its state to the user + var enabling = comp.User == null; + Appearance.SetData(uid, ToggleVisuals.Toggled, enabling); + var message = Loc.GetString(enabling ? "ninja-gloves-on" : "ninja-gloves-off"); + Popup.PopupEntity(message, user, user); + + if (enabling) + { + EnableGloves(uid, comp, user, ninja); + } + else + { + DisableGloves(uid, comp); + } + } + + private void EnableGloves(EntityUid uid, NinjaGlovesComponent comp, EntityUid user, SpaceNinjaComponent ninja) + { + comp.User = user; + Dirty(uid, comp); + _ninja.AssignGloves(user, uid, ninja); + + var drainer = EnsureComp(user); + var stun = EnsureComp(user); + _stunProvider.SetNoPowerPopup(user, "ninja-no-power", stun); + if (_ninja.GetNinjaBattery(user, out var battery, out var _)) + { + _drainer.SetBattery(user, battery, drainer); + _stunProvider.SetBattery(user, battery, stun); + } + + var emag = EnsureComp(user); + _emagProvider.SetWhitelist(user, comp.DoorjackWhitelist, emag); + + EnsureComp(user); + // prevent calling in multiple threats by toggling gloves after + if (_mind.TryGetRole(user, out var role) && !role.CalledInThreat) + { + var hacker = EnsureComp(user); + var rule = _ninja.NinjaRule(user); + if (rule != null) + _commsHacker.SetThreats(user, rule.Threats, hacker); + } + } +} diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs new file mode 100644 index 0000000000..f6ad646d22 --- /dev/null +++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs @@ -0,0 +1,147 @@ +using Content.Server.Emp; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.PowerCell; +using Content.Shared.Actions; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Robust.Shared.Containers; + +namespace Content.Server.Ninja.Systems; + +/// +/// Handles power cell upgrading and actions. +/// +public sealed class NinjaSuitSystem : SharedNinjaSuitSystem +{ + [Dependency] private readonly EmpSystem _emp = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SpaceNinjaSystem _ninja = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSuitInsertAttempt); + SubscribeLocalEvent(OnEmpAttempt); + SubscribeLocalEvent(OnAttemptStealth); + SubscribeLocalEvent(OnCreateThrowingStar); + SubscribeLocalEvent(OnRecallKatana); + SubscribeLocalEvent(OnEmp); + } + + protected override void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja) + { + base.NinjaEquippedSuit(uid, comp, user, ninja); + + _ninja.SetSuitPowerAlert(user); + } + + // TODO: if/when battery is in shared, put this there too + // TODO: or put MaxCharge in shared along with powercellslot + private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args) + { + // no power cell for some reason??? allow it + if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery)) + return; + + // can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power + if (!TryComp(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge) + { + args.Cancel(); + } + + // TODO: raise event on ninja telling it to update battery + } + + private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args) + { + // ninja suit (battery) is immune to emp + // powercell relays the event to suit + args.Cancel(); + } + + protected override void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user) + { + base.UserUnequippedSuit(uid, comp, user); + + // remove power indicator + _ninja.SetSuitPowerAlert(user); + } + + private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args) + { + var user = args.User; + // need 1 second of charge to turn on stealth + var chargeNeeded = SuitWattage(uid, comp); + // being attacked while cloaked gives no power message since it overloads the power supply or something + if (!_ninja.GetNinjaBattery(user, out var _, out var battery) || battery.CurrentCharge < chargeNeeded || UseDelay.ActiveDelay(user)) + { + _popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + args.Cancel(); + return; + } + + StealthClothing.SetEnabled(uid, user, true); + } + + private void OnCreateThrowingStar(EntityUid uid, NinjaSuitComponent comp, CreateThrowingStarEvent args) + { + args.Handled = true; + var user = args.Performer; + if (!_ninja.TryUseCharge(user, comp.ThrowingStarCharge) || UseDelay.ActiveDelay(user)) + { + _popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + // try to put throwing star in hand, otherwise it goes on the ground + var star = Spawn(comp.ThrowingStarPrototype, Transform(user).Coordinates); + _hands.TryPickupAnyHand(user, star); + } + + private void OnRecallKatana(EntityUid uid, NinjaSuitComponent comp, RecallKatanaEvent args) + { + args.Handled = true; + var user = args.Performer; + if (!TryComp(user, out var ninja) || ninja.Katana == null) + return; + + var katana = ninja.Katana.Value; + var coords = _transform.GetWorldPosition(katana); + var distance = (_transform.GetWorldPosition(user) - coords).Length(); + var chargeNeeded = (float) distance * comp.RecallCharge; + if (!_ninja.TryUseCharge(user, chargeNeeded) || UseDelay.ActiveDelay(user)) + { + _popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + // TODO: teleporting into belt slot + var message = _hands.TryPickupAnyHand(user, katana) + ? "ninja-katana-recalled" + : "ninja-hands-full"; + _popup.PopupEntity(Loc.GetString(message), user, user); + } + + private void OnEmp(EntityUid uid, NinjaSuitComponent comp, NinjaEmpEvent args) + { + args.Handled = true; + var user = args.Performer; + if (!_ninja.TryUseCharge(user, comp.EmpCharge) || UseDelay.ActiveDelay(user)) + { + _popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + // I don't think this affects the suit battery, but if it ever does in the future add a blacklist for it + var coords = Transform(user).MapPosition; + _emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration); + } +} diff --git a/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs new file mode 100644 index 0000000000..4c8b20d46e --- /dev/null +++ b/Content.Server/Ninja/Systems/SpaceNinjaSystem.cs @@ -0,0 +1,301 @@ +using Content.Server.Administration.Commands; +using Content.Server.Communications; +using Content.Server.Chat.Managers; +using Content.Server.StationEvents.Components; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ghost.Roles.Events; +using Content.Server.Objectives; +using Content.Server.Power.Components; +using Content.Server.Power.EntitySystems; +using Content.Server.PowerCell; +using Content.Server.Research.Systems; +using Content.Server.Roles; +using Content.Server.Warps; +using Content.Shared.Alert; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Doors.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Mind; +using Content.Shared.Mind.Components; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Content.Shared.Roles; +using Content.Shared.PowerCell.Components; +using Content.Shared.Rounding; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Physics.Components; +using Robust.Shared.Player; +using Robust.Shared.Random; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Content.Server.Ninja.Systems; + +// TODO: when syndiborgs are a thing have a borg converter with 6 second doafter +// engi -> saboteur +// medi -> idk reskin it +// other -> assault +// TODO: when criminal records is merged, hack it to set everyone to arrest + +/// +/// Main ninja system that handles ninja setup and greentext, provides helper methods for the rest of the code to use. +/// +public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly IChatManager _chatMan = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly StealthClothingSystem _stealthClothing = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNinjaMindAdded); + SubscribeLocalEvent(OnDoorjack); + SubscribeLocalEvent(OnResearchStolen); + SubscribeLocalEvent(OnThreatCalledIn); + } + + public override void Update(float frameTime) + { + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var ninja)) + { + UpdateNinja(uid, ninja, frameTime); + } + } + + /// + /// Turns the player into a space ninja + /// + public void MakeNinja(EntityUid mindId, MindComponent mind) + { + if (mind.OwnedEntity == null) + return; + + // prevent double ninja'ing + var user = mind.OwnedEntity.Value; + if (HasComp(user)) + return; + + AddComp(user); + SetOutfitCommand.SetOutfit(user, "SpaceNinjaGear", EntityManager); + GreetNinja(mindId, mind); + } + + /// + /// Download the given set of nodes, returning how many new nodes were downloaded. + /// + private int Download(EntityUid uid, List ids) + { + if (!_mind.TryGetRole(uid, out var role)) + return 0; + + var oldCount = role.DownloadedNodes.Count; + role.DownloadedNodes.UnionWith(ids); + var newCount = role.DownloadedNodes.Count; + return newCount - oldCount; + } + + /// + /// Returns a ninja's gamerule config data. + /// If the gamerule was not started then it will be started automatically. + /// + public NinjaRuleComponent? NinjaRule(EntityUid uid, SpaceNinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return null; + + // already exists so just check it + if (comp.Rule != null) + return CompOrNull(comp.Rule); + + // start it + _gameTicker.StartGameRule("Ninja", out var rule); + comp.Rule = rule; + + if (!TryComp(rule, out var ninjaRule)) + return null; + + // add ninja mind to the rule's list for objective showing + if (TryComp(uid, out var mindContainer) && mindContainer.Mind != null) + { + ninjaRule.Minds.Add(mindContainer.Mind.Value); + } + + return ninjaRule; + } + + // TODO: can probably copy paste borg code here + /// + /// Update the alert for the ninja's suit power indicator. + /// + public void SetSuitPowerAlert(EntityUid uid, SpaceNinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp, false) || comp.Deleted || comp.Suit == null) + { + _alerts.ClearAlert(uid, AlertType.SuitPower); + return; + } + + if (GetNinjaBattery(uid, out var _, out var battery)) + { + var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8); + _alerts.ShowAlert(uid, AlertType.SuitPower, (short) severity); + } + else + { + _alerts.ClearAlert(uid, AlertType.SuitPower); + } + } + + /// + /// Get the battery component in a ninja's suit, if it's worn. + /// + public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery) + { + if (TryComp(user, out var ninja) + && ninja.Suit != null + && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery)) + { + return true; + } + + uid = null; + battery = null; + return false; + } + + /// + public override bool TryUseCharge(EntityUid user, float charge) + { + return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge(uid.Value, charge, battery); + } + + /// + /// Greets the ninja when a ghost takes over a ninja, if that happens. + /// + private void OnNinjaMindAdded(EntityUid uid, SpaceNinjaComponent comp, MindAddedMessage args) + { + if (TryComp(uid, out var mind) && mind.Mind != null) + GreetNinja(mind.Mind.Value); + } + + /// + /// Set up everything for ninja to work and send the greeting message/sound. + /// + private void GreetNinja(EntityUid mindId, MindComponent? mind = null) + { + if (!Resolve(mindId, ref mind) || mind.OwnedEntity == null || mind.Session == null) + return; + + var uid = mind.OwnedEntity.Value; + var config = NinjaRule(uid); + if (config == null) + return; + + var role = new NinjaRoleComponent + { + PrototypeId = "SpaceNinja" + }; + _role.MindAddRole(mindId, role, mind); + + // choose spider charge detonation point + // currently based on warp points, something better could be done (but would likely require mapping work) + var warps = new List(); + var query = EntityQueryEnumerator(); + var map = Transform(uid).MapID; + while (query.MoveNext(out var warpUid, out var warp, out var xform)) + { + // won't be asked to detonate the nuke disk or singularity or centcomm + if (warp.Location != null && !HasComp(warpUid) && xform.MapID == map) + warps.Add(warpUid); + } + + if (warps.Count > 0) + role.SpiderChargeTarget = _random.Pick(warps); + + // assign objectives - must happen after spider charge target so that the obj requirement works + foreach (var objective in config.Objectives) + { + if (!_mind.TryAddObjective(mindId, objective, mind)) + { + Log.Error($"Failed to add {objective} to ninja {mind.OwnedEntity.Value}"); + } + } + + var session = mind.Session; + _audio.PlayGlobal(config.GreetingSound, Filter.Empty().AddPlayer(session), false, AudioParams.Default); + _chatMan.DispatchServerMessage(session, Loc.GetString("ninja-role-greeting")); + } + + // TODO: PowerCellDraw, modify when cloak enabled + /// + /// Handle constant power drains from passive usage and cloak. + /// + private void UpdateNinja(EntityUid uid, SpaceNinjaComponent ninja, float frameTime) + { + if (ninja.Suit == null) + return; + + float wattage = _suit.SuitWattage(ninja.Suit.Value); + + SetSuitPowerAlert(uid, ninja); + if (!TryUseCharge(uid, wattage * frameTime)) + { + // ran out of power, uncloak ninja + _stealthClothing.SetEnabled(ninja.Suit.Value, uid, false); + } + } + + /// + /// Increment greentext when emagging a door. + /// + private void OnDoorjack(EntityUid uid, SpaceNinjaComponent comp, ref EmaggedSomethingEvent args) + { + // incase someone lets ninja emag non-doors double check it here + if (!HasComp(args.Target)) + return; + + // this popup is serverside since door emag logic is serverside (power funnies) + _popup.PopupEntity(Loc.GetString("ninja-doorjack-success", ("target", Identity.Entity(args.Target, EntityManager))), uid, uid, PopupType.Medium); + + // handle greentext + if (_mind.TryGetRole(uid, out var role)) + role.DoorsJacked++; + } + + /// + /// Add to greentext when stealing technologies. + /// + private void OnResearchStolen(EntityUid uid, SpaceNinjaComponent comp, ref ResearchStolenEvent args) + { + var gained = Download(uid, args.Techs); + var str = gained == 0 + ? Loc.GetString("ninja-research-steal-fail") + : Loc.GetString("ninja-research-steal-success", ("count", gained), ("server", args.Target)); + + _popup.PopupEntity(str, uid, uid, PopupType.Medium); + } + + private void OnThreatCalledIn(EntityUid uid, SpaceNinjaComponent comp, ref ThreatCalledInEvent args) + { + if (_mind.TryGetRole(uid, out var role)) + { + role.CalledInThreat = true; + } + } +} diff --git a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs new file mode 100644 index 0000000000..3ffc2a8ff3 --- /dev/null +++ b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs @@ -0,0 +1,78 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Server.Mind; +using Content.Server.Popups; +using Content.Server.Roles; +using Content.Server.Sticky.Events; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Robust.Shared.GameObjects; + +namespace Content.Server.Ninja.Systems; + +/// +/// Prevents planting a spider charge outside of its location and handles greentext. +/// +public sealed class SpiderChargeSystem : EntitySystem +{ + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(BeforePlant); + SubscribeLocalEvent(OnStuck); + SubscribeLocalEvent(OnExplode); + } + + /// + /// Require that the planter is a ninja and the charge is near the target warp point. + /// + private void BeforePlant(EntityUid uid, SpiderChargeComponent comp, BeforeRangedInteractEvent args) + { + var user = args.User; + + if (!_mind.TryGetRole(user, out var role)) + { + _popup.PopupEntity(Loc.GetString("spider-charge-not-ninja"), user, user); + args.Handled = true; + return; + } + + // allow planting anywhere if there is no target, which should never happen + if (role.SpiderChargeTarget == null) + return; + + // assumes warp point still exists + var target = Transform(role.SpiderChargeTarget.Value).MapPosition; + var coords = args.ClickLocation.ToMap(EntityManager, _transform); + if (!coords.InRange(target, comp.Range)) + { + _popup.PopupEntity(Loc.GetString("spider-charge-too-far"), user, user); + args.Handled = true; + } + } + + /// + /// Allows greentext to occur after exploding. + /// + private void OnStuck(EntityUid uid, SpiderChargeComponent comp, EntityStuckEvent args) + { + comp.Planter = args.User; + } + + /// + /// Handles greentext after exploding. + /// Assumes it didn't move and the target was destroyed so be nice. + /// + private void OnExplode(EntityUid uid, SpiderChargeComponent comp, TriggerEvent args) + { + if (comp.Planter == null || !_mind.TryGetRole(comp.Planter.Value, out var role)) + return; + + // assumes the target was destroyed, that the charge wasn't moved somehow + role.SpiderChargeDetonated = true; + } +} diff --git a/Content.Server/Ninja/Systems/StunProviderSystem.cs b/Content.Server/Ninja/Systems/StunProviderSystem.cs new file mode 100644 index 0000000000..21e8b2042f --- /dev/null +++ b/Content.Server/Ninja/Systems/StunProviderSystem.cs @@ -0,0 +1,60 @@ +using Content.Shared.Electrocution; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Content.Shared.Whitelist; +using Content.Server.Power.EntitySystems; +using Robust.Shared.Timing; + +namespace Content.Server.Ninja.Systems; + +/// +/// Shocks clicked mobs using battery charge. +/// +public sealed class StunProviderSystem : SharedStunProviderSystem +{ + [Dependency] private readonly BatterySystem _battery = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedElectrocutionSystem _electrocution = default!; + [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBeforeInteractHand); + } + + /// + /// Stun clicked mobs on the whitelist, if there is enough power. + /// + private void OnBeforeInteractHand(EntityUid uid, StunProviderComponent comp, BeforeInteractHandEvent args) + { + // TODO: generic check + if (args.Handled || comp.BatteryUid == null || !_gloves.AbilityCheck(uid, args, out var target)) + return; + + if (target == uid || !comp.Whitelist.IsValid(target, EntityManager)) + return; + + if (_timing.CurTime < comp.NextStun) + return; + + // take charge from battery + if (!_battery.TryUseCharge(comp.BatteryUid.Value, comp.StunCharge)) + { + _popup.PopupEntity(Loc.GetString(comp.NoPowerPopup), uid, uid); + return; + } + + // not holding hands with target so insuls don't matter + _electrocution.TryDoElectrocution(target, uid, comp.StunDamage, comp.StunTime, false, ignoreInsulation: true); + // short cooldown to prevent instant stunlocking + comp.NextStun = _timing.CurTime + comp.Cooldown; + Dirty(uid, comp); + + args.Handled = true; + } +} diff --git a/Content.Server/Objectives/Conditions/DoorjackCondition.cs b/Content.Server/Objectives/Conditions/DoorjackCondition.cs new file mode 100644 index 0000000000..0752048554 --- /dev/null +++ b/Content.Server/Objectives/Conditions/DoorjackCondition.cs @@ -0,0 +1,70 @@ +using Content.Server.Roles; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +/// +/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks. +/// +[DataDefinition] +public sealed partial class DoorjackCondition : IObjectiveCondition +{ + private EntityUid? _mind; + private int _target; + + public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind) + { + // TODO: clamp to number of doors on station incase its somehow a shittle or something + return new DoorjackCondition { + _mind = uid, + _target = IoCManager.Resolve().Next(15, 40) + }; + } + + public string Title => Loc.GetString("objective-condition-doorjack-title", ("count", _target)); + + public string Description => Loc.GetString("objective-condition-doorjack-description", ("count", _target)); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Tools/emag.rsi"), "icon"); + + public float Progress + { + get + { + // prevent divide-by-zero + if (_target == 0) + return 1f; + + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var role)) + return 0f; + + if (role.DoorsJacked >= _target) + return 1f; + + return (float) role.DoorsJacked / (float) _target; + } + } + + public float Difficulty => 1.5f; + + public bool Equals(IObjectiveCondition? other) + { + return other is DoorjackCondition cond && Equals(_mind, cond._mind) && _target == cond._target; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is DoorjackCondition cond && cond.Equals(this); + } + + public override int GetHashCode() + { + return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target); + } +} diff --git a/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs b/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs new file mode 100644 index 0000000000..5209296842 --- /dev/null +++ b/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs @@ -0,0 +1,75 @@ +using Content.Server.Roles; +using Content.Server.Warps; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +/// +/// Objective condition that requires the player to be a ninja and have detonated their spider charge. +/// +[DataDefinition] +public sealed partial class SpiderChargeCondition : IObjectiveCondition +{ + private EntityUid? _mind; + + public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind) + { + return new SpiderChargeCondition { + _mind = uid + }; + } + + public string Title + { + get + { + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var role) + || role.SpiderChargeTarget == null + || !entMan.TryGetComponent(role.SpiderChargeTarget, out var warp) + || warp.Location == null) + // this should never really happen but eh + return Loc.GetString("objective-condition-spider-charge-no-target"); + + return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location)); + } + } + + public string Description => Loc.GetString("objective-condition-spider-charge-description"); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Bombs/spidercharge.rsi"), "icon"); + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var role)) + return 0f; + + return role.SpiderChargeDetonated ? 1f : 0f; + } + } + + public float Difficulty => 2.5f; + + public bool Equals(IObjectiveCondition? other) + { + return other is SpiderChargeCondition cond && Equals(_mind, cond._mind); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is SpiderChargeCondition cond && cond.Equals(this); + } + + public override int GetHashCode() + { + return _mind?.GetHashCode() ?? 0; + } +} diff --git a/Content.Server/Objectives/Conditions/StealResearchCondition.cs b/Content.Server/Objectives/Conditions/StealResearchCondition.cs new file mode 100644 index 0000000000..4c32f3c3fc --- /dev/null +++ b/Content.Server/Objectives/Conditions/StealResearchCondition.cs @@ -0,0 +1,70 @@ +using Content.Server.Roles; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +/// +/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies. +/// +[DataDefinition] +public sealed partial class StealResearchCondition : IObjectiveCondition +{ + private EntityUid? _mind; + private int _target; + + public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind) + { + // TODO: clamp to number of research nodes in a single discipline maybe so easily maintainable + return new StealResearchCondition { + _mind = uid, + _target = IoCManager.Resolve().Next(5, 10) + }; + } + + public string Title => Loc.GetString("objective-condition-steal-research-title", ("count", _target)); + + public string Description => Loc.GetString("objective-condition-steal-research-description"); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Structures/Machines/server.rsi"), "server"); + + public float Progress + { + get + { + // prevent divide-by-zero + if (_target == 0) + return 1f; + + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var role)) + return 0f; + + if (role.DownloadedNodes.Count >= _target) + return 1f; + + return (float) role.DownloadedNodes.Count / (float) _target; + } + } + + public float Difficulty => 2.5f; + + public bool Equals(IObjectiveCondition? other) + { + return other is StealResearchCondition cond && Equals(_mind, cond._mind) && _target == cond._target; + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is StealResearchCondition cond && cond.Equals(this); + } + + public override int GetHashCode() + { + return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target); + } +} diff --git a/Content.Server/Objectives/Conditions/SurviveCondition.cs b/Content.Server/Objectives/Conditions/SurviveCondition.cs new file mode 100644 index 0000000000..98b5aa6c89 --- /dev/null +++ b/Content.Server/Objectives/Conditions/SurviveCondition.cs @@ -0,0 +1,58 @@ +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +/// +/// Just requires that the player is not dead, ignores evac and what not. +/// +[DataDefinition] +public sealed partial class SurviveCondition : IObjectiveCondition +{ + private EntityUid? _mind; + + public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind) + { + return new SurviveCondition {_mind = uid}; + } + + public string Title => Loc.GetString("objective-condition-survive-title"); + + public string Description => Loc.GetString("objective-condition-survive-description"); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Clothing/Mask/ninja.rsi"), "icon"); + + public float Difficulty => 0.5f; + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var mind)) + return 0f; + + var mindSystem = entMan.System(); + return mindSystem.IsCharacterDeadIc(mind) ? 0f : 1f; + } + } + + public bool Equals(IObjectiveCondition? other) + { + return other is SurviveCondition condition && Equals(_mind, condition._mind); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != GetType()) return false; + return Equals((SurviveCondition) obj); + } + + public override int GetHashCode() + { + return (_mind != null ? _mind.GetHashCode() : 0); + } +} diff --git a/Content.Server/Objectives/Conditions/TerrorCondition.cs b/Content.Server/Objectives/Conditions/TerrorCondition.cs new file mode 100644 index 0000000000..28cce20aa2 --- /dev/null +++ b/Content.Server/Objectives/Conditions/TerrorCondition.cs @@ -0,0 +1,57 @@ +using Content.Server.Roles; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +/// +/// Objective condition that requires the player to be a ninja and have called in a threat. +/// +[DataDefinition] +public sealed partial class TerrorCondition : IObjectiveCondition +{ + private EntityUid? _mind; + + public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind) + { + return new TerrorCondition {_mind = uid}; + } + + public string Title => Loc.GetString("objective-condition-terror-title"); + + public string Description => Loc.GetString("objective-condition-terror-description"); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Fun/Instruments/otherinstruments.rsi"), "red_phone"); + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (!entMan.TryGetComponent(_mind, out var role)) + return 0f; + + return role.CalledInThreat ? 1f : 0f; + } + } + + public float Difficulty => 2.75f; + + public bool Equals(IObjectiveCondition? other) + { + return other is TerrorCondition cond && Equals(_mind, cond._mind); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is TerrorCondition cond && cond.Equals(this); + } + + public override int GetHashCode() + { + return _mind?.GetHashCode() ?? 0; + } +} diff --git a/Content.Server/Objectives/Requirements/NinjaRequirement.cs b/Content.Server/Objectives/Requirements/NinjaRequirement.cs new file mode 100644 index 0000000000..8a0993bed8 --- /dev/null +++ b/Content.Server/Objectives/Requirements/NinjaRequirement.cs @@ -0,0 +1,18 @@ +using Content.Server.Roles; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; + +namespace Content.Server.Objectives.Requirements; + +/// +/// Requires the player's mind to have the ninja role component, aka be a ninja. +/// +[DataDefinition] +public sealed partial class NinjaRequirement : IObjectiveRequirement +{ + public bool CanBeAssigned(EntityUid mindId, MindComponent mind) + { + var entMan = IoCManager.Resolve(); + return entMan.HasComponent(mindId); + } +} diff --git a/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs b/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs new file mode 100644 index 0000000000..6bb6bbb7a8 --- /dev/null +++ b/Content.Server/Objectives/Requirements/SpiderChargeTargetRequirement.cs @@ -0,0 +1,19 @@ +using Content.Server.Roles; +using Content.Shared.Mind; +using Content.Shared.Objectives.Interfaces; + +namespace Content.Server.Objectives.Requirements; + +/// +/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case. +/// +[DataDefinition] +public sealed partial class SpiderChargeTargetRequirement : IObjectiveRequirement +{ + public bool CanBeAssigned(EntityUid mindId, MindComponent mind) + { + var entMan = IoCManager.Resolve(); + entMan.TryGetComponent(mindId, out var role); + return role?.SpiderChargeTarget != null; + } +} diff --git a/Content.Server/Power/EntitySystems/BatterySystem.cs b/Content.Server/Power/EntitySystems/BatterySystem.cs index 410aa30bba..c844988b06 100644 --- a/Content.Server/Power/EntitySystems/BatterySystem.cs +++ b/Content.Server/Power/EntitySystems/BatterySystem.cs @@ -156,5 +156,16 @@ namespace Content.Server.Power.EntitySystems UseCharge(uid, value, battery); return true; } + + /// + /// Returns whether the battery is at least 99% charged, basically full. + /// + public bool IsFull(EntityUid uid, BatteryComponent? battery = null) + { + if (!Resolve(uid, ref battery)) + return false; + + return battery.CurrentCharge / battery.MaxCharge >= 0.99f; + } } } diff --git a/Content.Server/PowerCell/PowerCellSystem.cs b/Content.Server/PowerCell/PowerCellSystem.cs index 6633e43d42..1cfb4d1d70 100644 --- a/Content.Server/PowerCell/PowerCellSystem.cs +++ b/Content.Server/PowerCell/PowerCellSystem.cs @@ -1,6 +1,7 @@ using Content.Server.Administration.Logs; using Content.Server.Chemistry.EntitySystems; using Content.Server.Explosion.EntitySystems; +using Content.Server.Emp; using Content.Server.Power.Components; using Content.Shared.Database; using Content.Shared.Examine; @@ -38,6 +39,7 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem SubscribeLocalEvent(OnChargeChanged); SubscribeLocalEvent(OnCellExamined); + SubscribeLocalEvent(OnCellEmpAttempt); SubscribeLocalEvent(OnUnpaused); SubscribeLocalEvent(OnDrawChargeChanged); @@ -233,6 +235,14 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem OnBatteryExamined(uid, battery, args); } + private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args) + { + var parent = Transform(uid).ParentUid; + // relay the attempt event to the slot so it can cancel it + if (HasComp(parent)) + RaiseLocalEvent(parent, args); + } + private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args) { TryGetBatteryFromSlot(uid, out var battery); diff --git a/Content.Server/Research/Systems/ResearchStealerSystem.cs b/Content.Server/Research/Systems/ResearchStealerSystem.cs new file mode 100644 index 0000000000..5bab6048de --- /dev/null +++ b/Content.Server/Research/Systems/ResearchStealerSystem.cs @@ -0,0 +1,39 @@ +using Content.Shared.Research.Components; +using Content.Shared.Research.Systems; + +namespace Content.Server.Research.Systems; + +public sealed class ResearchStealerSystem : SharedResearchStealerSystem +{ + [Dependency] private readonly SharedResearchSystem _research = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnDoAfter); + } + + private void OnDoAfter(EntityUid uid, ResearchStealerComponent comp, ResearchStealDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) + return; + + var target = args.Target.Value; + + if (!TryComp(target, out var database)) + return; + + var ev = new ResearchStolenEvent(uid, target, database.UnlockedTechnologies); + RaiseLocalEvent(uid, ref ev); + // oops, no more advanced lasers! + _research.ClearTechs(target, database); + } +} + +/// +/// Event raised on the user when research is stolen from a R&D server. +/// Techs contains every technology id researched. +/// +[ByRefEvent] +public record struct ResearchStolenEvent(EntityUid Used, EntityUid Target, List Techs); diff --git a/Content.Server/Roles/NinjaRoleComponent.cs b/Content.Server/Roles/NinjaRoleComponent.cs new file mode 100644 index 0000000000..aa9e1cfa32 --- /dev/null +++ b/Content.Server/Roles/NinjaRoleComponent.cs @@ -0,0 +1,40 @@ +using Content.Shared.Roles; + +namespace Content.Server.Roles; + +/// +/// Stores the ninja's objectives on the mind so if they die the rest of the greentext persists. +/// +[RegisterComponent] +public sealed partial class NinjaRoleComponent : AntagonistRoleComponent +{ + /// + /// Number of doors that have been doorjacked, used for objective + /// + [DataField("doorsJacked")] + public int DoorsJacked; + + /// + /// Research nodes that have been downloaded, used for objective + /// + [DataField("downloadedNodes")] + public HashSet DownloadedNodes = new(); + + /// + /// Warp point that the spider charge has to target + /// + [DataField("spiderChargeTarget")] + public EntityUid? SpiderChargeTarget; + + /// + /// Whether the spider charge has been detonated on the target, used for objective + /// + [DataField("spiderChargeDetonated")] + public bool SpiderChargeDetonated; + + /// + /// Whether the comms console has been hacked, used for objective + /// + [DataField("calledInThreat")] + public bool CalledInThreat; +} diff --git a/Content.Server/Roles/RoleSystem.cs b/Content.Server/Roles/RoleSystem.cs index 12962b2b50..4ca6c0ac80 100644 --- a/Content.Server/Roles/RoleSystem.cs +++ b/Content.Server/Roles/RoleSystem.cs @@ -10,6 +10,7 @@ public sealed class RoleSystem : SharedRoleSystem base.Initialize(); SubscribeAntagEvents(); + SubscribeAntagEvents(); SubscribeAntagEvents(); SubscribeAntagEvents(); SubscribeAntagEvents(); diff --git a/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs b/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs new file mode 100644 index 0000000000..d758247eca --- /dev/null +++ b/Content.Server/StationEvents/Components/NinjaSpawnRuleComponent.cs @@ -0,0 +1,16 @@ +using Content.Server.StationEvents.Events; + +namespace Content.Server.StationEvents.Components; + +/// +/// Configuration component for the Space Ninja antag. +/// +[RegisterComponent, Access(typeof(NinjaSpawnRule))] +public sealed partial class NinjaSpawnRuleComponent : Component +{ + /// + /// Distance that the ninja spawns from the station's half AABB radius + /// + [DataField("spawnDistance")] + public float SpawnDistance = 20f; +} diff --git a/Content.Server/StationEvents/Events/NinjaSpawnRule.cs b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs new file mode 100644 index 0000000000..c60f3298e7 --- /dev/null +++ b/Content.Server/StationEvents/Events/NinjaSpawnRule.cs @@ -0,0 +1,51 @@ +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Ninja.Systems; +using Content.Server.Station.Components; +using Content.Server.StationEvents.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Random; + +namespace Content.Server.StationEvents.Events; + +/// +/// Event for spawning a Space Ninja mid-game. +/// +public sealed class NinjaSpawnRule : StationEventSystem +{ + [Dependency] private readonly SpaceNinjaSystem _ninja = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + protected override void Started(EntityUid uid, NinjaSpawnRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, comp, gameRule, args); + + if (!TryGetRandomStation(out var station)) + return; + + var stationData = Comp(station.Value); + + // find a station grid + var gridUid = StationSystem.GetLargestGrid(stationData); + if (gridUid == null || !TryComp(gridUid, out var grid)) + { + Sawmill.Warning("Chosen station has no grids, cannot spawn space ninja!"); + return; + } + + // figure out its AABB size and use that as a guide to how far ninja should be + var size = grid.LocalAABB.Size.Length() / 2; + var distance = size + comp.SpawnDistance; + var angle = RobustRandom.NextAngle(); + // position relative to station center + var location = angle.ToVec() * distance; + + // create the spawner, the ninja will appear when a ghost has picked the role + var xform = Transform(gridUid.Value); + var position = _transform.GetWorldPosition(xform) + location; + var coords = new MapCoordinates(position, xform.MapID); + Sawmill.Info($"Creating ninja spawnpoint at {coords}"); + Spawn("SpawnPointGhostSpaceNinja", coords); + } +} diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index 7f74612010..8ba35ae282 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -47,7 +47,8 @@ namespace Content.Shared.Alert Debug3, Debug4, Debug5, - Debug6 + Debug6, + SuitPower } } diff --git a/Content.Shared/Communications/CommsHackerComponent.cs b/Content.Shared/Communications/CommsHackerComponent.cs new file mode 100644 index 0000000000..9116899cca --- /dev/null +++ b/Content.Shared/Communications/CommsHackerComponent.cs @@ -0,0 +1,47 @@ +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Communications; + +/// +/// Component for hacking a communications console to call in a threat. +/// Can only be done once, the component is remove afterwards. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedCommsHackerSystem))] +public sealed partial class CommsHackerComponent : Component +{ + /// + /// Time taken to hack the console + /// + [DataField("delay")] + public TimeSpan Delay = TimeSpan.FromSeconds(20); + + /// + /// Possible threats to choose from. + /// + [DataField("threats", required: true)] + public List Threats = new(); +} + +/// +/// A threat that can be called in to the station by a ninja hacking a communications console. +/// Generally some kind of mid-round minor antag, though you could make it call in scrubber backflow if you wanted to. +/// You wouldn't do that, right? +/// +[DataDefinition] +public sealed partial class Threat +{ + /// + /// Locale id for the announcement to be made from CentCom. + /// + [DataField("announcement")] + public string Announcement = default!; + + /// + /// The game rule for the threat to be added, it should be able to work when added mid-round otherwise this will do nothing. + /// + [DataField("rule", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string Rule = default!; +} diff --git a/Content.Shared/Communications/SharedCommsHackerSystem.cs b/Content.Shared/Communications/SharedCommsHackerSystem.cs new file mode 100644 index 0000000000..94c530878a --- /dev/null +++ b/Content.Shared/Communications/SharedCommsHackerSystem.cs @@ -0,0 +1,28 @@ +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Communications; + +/// +/// Only exists in shared to provide API and for access. +/// All logic is serverside. +/// +public abstract class SharedCommsHackerSystem : EntitySystem +{ + /// + /// Set the list of threats to choose from when hacking a comms console. + /// + public void SetThreats(EntityUid uid, List threats, CommsHackerComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.Threats = threats; + } +} + +/// +/// DoAfter event for comms console terror ability. +/// +[Serializable, NetSerializable] +public sealed partial class TerrorDoAfterEvent : SimpleDoAfterEvent { } diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs index a895311fb2..effba18210 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsComponent.cs @@ -112,6 +112,15 @@ namespace Content.Shared.Containers.ItemSlots [ViewVariables(VVAccess.ReadWrite)] public bool Locked = false; + /// + /// Prevents adding the eject alt-verb, but still lets you swap items. + /// + /// + /// This does not affect EjectOnInteract, since if you do that you probably want ejecting to work. + /// + [DataField("disableEject"), ViewVariables(VVAccess.ReadWrite)] + public bool DisableEject = false; + /// /// Whether the item slots system will attempt to insert item from the user's hands into this slot when interacted with. /// It doesn't block other insertion methods, like verbs. diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index f69e8d4d2f..594479d0df 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -477,7 +477,7 @@ namespace Content.Shared.Containers.ItemSlots // Add the eject-item verbs foreach (var slot in itemSlots.Slots.Values) { - if (slot.EjectOnInteract) + if (slot.EjectOnInteract || slot.DisableEject) // For this item slot, ejecting/inserting is a primary interaction. Instead of an eject category // alt-click verb, there will be a "Take item" primary interaction verb. continue; diff --git a/Content.Shared/Electrocution/SharedElectrocutionSystem.cs b/Content.Shared/Electrocution/SharedElectrocutionSystem.cs index d7848c073b..67a395bb81 100644 --- a/Content.Shared/Electrocution/SharedElectrocutionSystem.cs +++ b/Content.Shared/Electrocution/SharedElectrocutionSystem.cs @@ -1,4 +1,5 @@ using Content.Shared.Inventory; +using Content.Shared.StatusEffect; using Robust.Shared.GameStates; namespace Content.Shared.Electrocution @@ -25,6 +26,23 @@ namespace Content.Shared.Electrocution Dirty(insulated); } + /// Entity being electrocuted. + /// Source entity of the electrocution. + /// How much shock damage the entity takes. + /// How long the entity will be stunned. + /// Should time be refreshed (instead of accumilated) if the entity is already electrocuted? + /// How insulated the entity is from the shock. 0 means completely insulated, and 1 means no insulation. + /// Status effects to apply to the entity. + /// Should the electrocution bypass the Insulated component? + /// Whether the entity was stunned by the shock. + public virtual bool TryDoElectrocution( + EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f, + StatusEffectsComponent? statusEffects = null, bool ignoreInsulation = false) + { + // only done serverside + return false; + } + private void OnInsulatedElectrocutionAttempt(EntityUid uid, InsulatedComponent insulated, ElectrocutionAttemptEvent args) { args.SiemensCoefficient *= insulated.SiemensCoefficient; diff --git a/Content.Shared/Emag/Systems/EmagSystem.cs b/Content.Shared/Emag/Systems/EmagSystem.cs index 7d30438155..ebbd4c02ac 100644 --- a/Content.Shared/Emag/Systems/EmagSystem.cs +++ b/Content.Shared/Emag/Systems/EmagSystem.cs @@ -7,8 +7,6 @@ using Content.Shared.IdentityManagement; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Tag; -using Robust.Shared.Network; -using Robust.Shared.Timing; namespace Content.Shared.Emag.Systems; @@ -22,10 +20,8 @@ public sealed class EmagSystem : EntitySystem { [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedChargesSystem _charges = default!; - [Dependency] private readonly INetManager _net = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly TagSystem _tag = default!; - [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { @@ -56,8 +52,7 @@ public sealed class EmagSystem : EntitySystem TryComp(uid, out var charges); if (_charges.IsEmpty(uid, charges)) { - if (_net.IsClient && _timing.IsFirstTimePredicted) - _popup.PopupEntity(Loc.GetString("emag-no-charges"), user, user); + _popup.PopupClient(Loc.GetString("emag-no-charges"), user, user); return false; } @@ -65,12 +60,8 @@ public sealed class EmagSystem : EntitySystem if (!handled) return false; - // only do popup on client - if (_net.IsClient && _timing.IsFirstTimePredicted) - { - _popup.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user, - user, PopupType.Medium); - } + _popup.PopupClient(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user, + user, PopupType.Medium); _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}"); diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 15780138f9..6d43c3dea1 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -85,6 +85,28 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem } } + /// + /// Add a list of implants to a person. + /// Logs any implant ids that don't have . + /// + public void AddImplants(EntityUid uid, IEnumerable implants) + { + var coords = Transform(uid).Coordinates; + foreach (var id in implants) + { + var ent = Spawn(id, coords); + if (TryComp(ent, out var implant)) + { + ForceImplant(uid, ent, implant); + } + else + { + Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}"); + Del(ent); + } + } + } + /// /// Forces an implant into a person /// Good for on spawn related code or admin additions diff --git a/Content.Shared/Interaction/InteractHand.cs b/Content.Shared/Interaction/InteractHand.cs index 32e250b0f1..63ea3b6f30 100644 --- a/Content.Shared/Interaction/InteractHand.cs +++ b/Content.Shared/Interaction/InteractHand.cs @@ -38,6 +38,20 @@ namespace Content.Shared.Interaction } } + /// + /// Raised on the user before interacting on an entity with bare hand. + /// Interaction is cancelled if this event is handled, so set it to true if you do custom interaction logic. + /// + public sealed class BeforeInteractHandEvent : HandledEntityEventArgs + { + public EntityUid Target { get; } + + public BeforeInteractHandEvent(EntityUid target) + { + Target = target; + } + } + /// /// Low-level interaction event used for entities without hands. /// diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index f00d9c8c38..bf276dff0b 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -380,6 +380,15 @@ namespace Content.Shared.Interaction public void InteractHand(EntityUid user, EntityUid target) { + // allow for special logic before main interaction + var ev = new BeforeInteractHandEvent(target); + RaiseLocalEvent(user, ev); + if (ev.Handled) + { + _adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}, but it was handled by another system"); + return; + } + // all interactions should only happen when in range / unobstructed, so no range check is needed var message = new InteractHandEvent(user, target); RaiseLocalEvent(target, message, true); diff --git a/Content.Shared/Mind/SharedMindSystem.cs b/Content.Shared/Mind/SharedMindSystem.cs index 7bada2303f..fc6cb8d570 100644 --- a/Content.Shared/Mind/SharedMindSystem.cs +++ b/Content.Shared/Mind/SharedMindSystem.cs @@ -12,14 +12,16 @@ using Content.Shared.Players; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Players; +using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Shared.Mind; public abstract class SharedMindSystem : EntitySystem { - [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; [Dependency] private readonly SharedPlayerSystem _playerSystem = default!; // This is dictionary is required to track the minds of disconnected players that may have had their entity deleted. @@ -268,6 +270,23 @@ public abstract class SharedMindSystem : EntitySystem return true; } + /// + /// Adds an objective, by id, to this mind. + /// + public bool TryAddObjective(EntityUid mindId, string name, MindComponent? mind = null) + { + if (!Resolve(mindId, ref mind)) + return false; + + if (!_proto.TryIndex(name, out var objective)) + { + Log.Error($"Tried to add unknown objective prototype: {name}"); + return false; + } + + return TryAddObjective(mindId, mind, objective); + } + /// /// Removes an objective to this mind. /// @@ -340,6 +359,19 @@ public abstract class SharedMindSystem : EntitySystem return _playerSystem.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind); } + /// + /// Gets a role component from a player's mind. + /// + /// Whether a role was found + public bool TryGetRole(EntityUid user, [NotNullWhen(true)] out T? role) where T : Component + { + role = null; + if (!TryComp(user, out var mindContainer) || mindContainer.Mind == null) + return false; + + return TryComp(mindContainer.Mind, out role); + } + /// /// Sets the Mind's OwnedComponent and OwnedEntity /// diff --git a/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs b/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs new file mode 100644 index 0000000000..55bcdd0f0a --- /dev/null +++ b/Content.Shared/Ninja/Components/BatteryDrainerComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.Ninja.Systems; +using Robust.Shared.Audio; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for draining power from APCs/substations/SMESes, when ProviderUid is set to a battery cell. +/// Does not rely on relay, simply being on the user and having BatteryUid set is enough. +/// +[RegisterComponent, Access(typeof(SharedBatteryDrainerSystem))] +public sealed partial class BatteryDrainerComponent : Component +{ + /// + /// The powercell entity to drain power into. + /// Determines whether draining is possible. + /// + [DataField("batteryUid"), ViewVariables(VVAccess.ReadWrite)] + public EntityUid? BatteryUid; + + /// + /// Conversion rate between joules in a device and joules added to battery. + /// Should be very low since powercells store nothing compared to even an APC. + /// + [DataField("drainEfficiency"), ViewVariables(VVAccess.ReadWrite)] + public float DrainEfficiency = 0.001f; + + /// + /// Time that the do after takes to drain charge from a battery, in seconds + /// + [DataField("drainTime"), ViewVariables(VVAccess.ReadWrite)] + public float DrainTime = 1f; + + /// + /// Sound played after the doafter ends. + /// + [DataField("sparkSound")] + public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks"); +} diff --git a/Content.Shared/Ninja/Components/DashAbilityComponent.cs b/Content.Shared/Ninja/Components/DashAbilityComponent.cs new file mode 100644 index 0000000000..85e0963af1 --- /dev/null +++ b/Content.Shared/Ninja/Components/DashAbilityComponent.cs @@ -0,0 +1,33 @@ +using Content.Shared.Actions; +using Content.Shared.Ninja.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +/// +/// Adds an action to dash, teleport to clicked position, when this item is held. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(DashAbilitySystem))] +public sealed partial class DashAbilityComponent : Component +{ + /// + /// The action id for dashing. + /// + [DataField("dashAction", required: true, customTypeSerializer: typeof(PrototypeIdSerializer)), ViewVariables(VVAccess.ReadWrite)] + public string DashAction = string.Empty; + + [DataField("dashActionEntity")] + public EntityUid? DashActionEntity; + + /// + /// Sound played when using dash action. + /// + [DataField("blinkSound"), ViewVariables(VVAccess.ReadWrite)] + public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg") + { + Params = AudioParams.Default.WithVolume(5f) + }; +} + +public sealed partial class DashEvent : WorldTargetActionEvent { } diff --git a/Content.Shared/Ninja/Components/EmagProviderComponent.cs b/Content.Shared/Ninja/Components/EmagProviderComponent.cs new file mode 100644 index 0000000000..db7678f61d --- /dev/null +++ b/Content.Shared/Ninja/Components/EmagProviderComponent.cs @@ -0,0 +1,28 @@ +using Content.Shared.Ninja.Systems; +using Content.Shared.Tag; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for emagging things on click. +/// No charges but checks against a whitelist. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(EmagProviderSystem))] +public sealed partial class EmagProviderComponent : Component +{ + /// + /// The tag that marks an entity as immune to emagging. + /// + [DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EmagImmuneTag = "EmagImmune"; + + /// + /// Whitelist that entities must be on to work. + /// + [DataField("whitelist"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public EntityWhitelist? Whitelist = null; +} diff --git a/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs new file mode 100644 index 0000000000..33b8fc7893 --- /dev/null +++ b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs @@ -0,0 +1,12 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for a Space Ninja's katana, controls ninja related dash logic. +/// Requires a ninja with a suit for abilities to work. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class EnergyKatanaComponent : Component +{ +} diff --git a/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs new file mode 100644 index 0000000000..b104312b20 --- /dev/null +++ b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs @@ -0,0 +1,45 @@ +using Content.Shared.DoAfter; +using Content.Shared.Ninja.Systems; +using Content.Shared.Toggleable; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for toggling glove powers. +/// Powers being enabled is controlled by User not being null. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedNinjaGlovesSystem))] +public sealed partial class NinjaGlovesComponent : Component +{ + /// + /// Entity of the ninja using these gloves, usually means enabled + /// + [DataField("user"), AutoNetworkedField] + public EntityUid? User; + + /// + /// The action id for toggling ninja gloves abilities + /// + [DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ToggleAction = "ActionToggleNinjaGloves"; + + [DataField("toggleActionEntity")] + public EntityUid? ToggleActionEntity; + + /// + /// The whitelist used for the emag provider to emag airlocks only (not regular doors). + /// + [DataField("doorjackWhitelist")] + public EntityWhitelist DoorjackWhitelist = new() + { + Components = new[] {"Airlock"} + }; +} diff --git a/Content.Shared/Ninja/Components/NinjaSuitComponent.cs b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs new file mode 100644 index 0000000000..816cc9d731 --- /dev/null +++ b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs @@ -0,0 +1,125 @@ +using Content.Shared.Actions; +using Content.Shared.Ninja.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for ninja suit abilities and power consumption. +/// As an implementation detail, dashing with katana is a suit action which isn't ideal. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedNinjaSuitSystem))] +public sealed partial class NinjaSuitComponent : Component +{ + /// + /// Battery charge used passively, in watts. Will last 1000 seconds on a small-capacity power cell. + /// + [DataField("passiveWattage")] + public float PassiveWattage = 0.36f; + + /// + /// Battery charge used while cloaked, stacks with passive. Will last 200 seconds while cloaked on a small-capacity power cell. + /// + [DataField("cloakWattage")] + public float CloakWattage = 1.44f; + + /// + /// Sound played when a ninja is hit while cloaked. + /// + [DataField("revealSound")] + public SoundSpecifier RevealSound = new SoundPathSpecifier("/Audio/Effects/chime.ogg"); + + /// + /// How long to disable all abilities for when revealed. + /// This adds a UseDelay to the ninja so it should not be set by anything else. + /// + [DataField("disableTime")] + public TimeSpan DisableTime = TimeSpan.FromSeconds(5); + + /// + /// The action id for creating throwing stars. + /// + [DataField("createThrowingStarAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string CreateThrowingStarAction = "ActionCreateThrowingStar"; + + [DataField("createThrowingStarActionEntity")] + public EntityUid? CreateThrowingStarActionEntity; + + /// + /// Battery charge used to create a throwing star. Can do it 25 times on a small-capacity power cell. + /// + [DataField("throwingStarCharge")] + public float ThrowingStarCharge = 14.4f; + + /// + /// Throwing star item to create with the action + /// + [DataField("throwingStarPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string ThrowingStarPrototype = "ThrowingStarNinja"; + + /// + /// The action id for recalling a bound energy katana + /// + [DataField("recallKatanaAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string RecallKatanaAction = "ActionRecallKatana"; + + [DataField("recallKatanaActionEntity")] + public EntityUid? RecallKatanaActionEntity; + + /// + /// Battery charge used per tile the katana teleported. + /// Uses 1% of a default battery per tile. + /// + [DataField("recallCharge")] + public float RecallCharge = 3.6f; + + /// + /// The action id for creating an EMP burst + /// + [DataField("empAction", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EmpAction = "ActionNinjaEmp"; + + [DataField("empActionEntity")] + public EntityUid? EmpActionEntity; + + /// + /// Battery charge used to create an EMP burst. Can do it 2 times on a small-capacity power cell. + /// + [DataField("empCharge")] + public float EmpCharge = 180f; + + /// + /// Range of the EMP in tiles. + /// + [DataField("empRange")] + public float EmpRange = 6f; + + /// + /// Power consumed from batteries by the EMP + /// + [DataField("empConsumption")] + public float EmpConsumption = 100000f; + + /// + /// How long the EMP effects last for, in seconds + /// + [DataField("empDuration")] + public float EmpDuration = 60f; +} + +public sealed partial class CreateThrowingStarEvent : InstantActionEvent +{ +} + +public sealed partial class RecallKatanaEvent : InstantActionEvent +{ +} + +public sealed partial class NinjaEmpEvent : InstantActionEvent +{ +} diff --git a/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs new file mode 100644 index 0000000000..dff4b56aa4 --- /dev/null +++ b/Content.Shared/Ninja/Components/SpaceNinjaComponent.cs @@ -0,0 +1,38 @@ +using Content.Shared.Ninja.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component placed on a mob to make it a space ninja, able to use suit and glove powers. +/// Contains ids of all ninja equipment and the game rule. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedSpaceNinjaSystem))] +public sealed partial class SpaceNinjaComponent : Component +{ + /// + /// The ninja game rule that spawned this ninja. + /// + [DataField("rule")] + public EntityUid? Rule; + + /// + /// Currently worn suit + /// + [DataField("suit"), AutoNetworkedField] + public EntityUid? Suit; + + /// + /// Currently worn gloves + /// + [DataField("gloves"), AutoNetworkedField] + public EntityUid? Gloves; + + /// + /// Bound katana, set once picked up and never removed + /// + [DataField("katana"), AutoNetworkedField] + public EntityUid? Katana; +} diff --git a/Content.Shared/Ninja/Components/SpiderChargeComponent.cs b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs new file mode 100644 index 0000000000..dacf47bb23 --- /dev/null +++ b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs @@ -0,0 +1,19 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for the Space Ninja's unique Spider Charge. +/// Only this component detonating can trigger the ninja's objective. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class SpiderChargeComponent : Component +{ + /// Range for planting within the target area + [DataField("range")] + public float Range = 10f; + + /// The ninja that planted this charge + [DataField("planter")] + public EntityUid? Planter = null; +} diff --git a/Content.Shared/Ninja/Components/StunProviderComponent.cs b/Content.Shared/Ninja/Components/StunProviderComponent.cs new file mode 100644 index 0000000000..653b125478 --- /dev/null +++ b/Content.Shared/Ninja/Components/StunProviderComponent.cs @@ -0,0 +1,67 @@ +using Content.Shared.Ninja.Systems; +using Content.Shared.Whitelist; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for stunning mobs on click outside of harm mode. +/// Knocks them down for a bit and deals shock damage. +/// +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunProviderSystem))] +public sealed partial class StunProviderComponent : Component +{ + /// + /// The powercell entity to take power from. + /// Determines whether stunning is possible. + /// + [DataField("batteryUid"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public EntityUid? BatteryUid; + + /// + /// Joules required in the battery to stun someone. Defaults to 10 uses on a small battery. + /// + [DataField("stunCharge"), ViewVariables(VVAccess.ReadWrite)] + public float StunCharge = 36.0f; + + /// + /// Shock damage dealt when stunning someone + /// + [DataField("stunDamage"), ViewVariables(VVAccess.ReadWrite)] + public int StunDamage = 5; + + /// + /// Time that someone is stunned for, stacks if done multiple times. + /// + [DataField("stunTime"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan StunTime = TimeSpan.FromSeconds(3); + + /// + /// How long stunning is disabled after stunning something. + /// + [DataField("cooldown"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan Cooldown = TimeSpan.FromSeconds(1); + + /// + /// Locale string to popup when there is no power + /// + [DataField("noPowerPopup", required: true), ViewVariables(VVAccess.ReadWrite)] + public string NoPowerPopup = string.Empty; + + /// + /// Whitelist for what counts as a mob. + /// + [DataField("whitelist")] + public EntityWhitelist Whitelist = new() + { + Components = new[] {"Stamina"} + }; + + /// + /// When someone can next be stunned. + /// Essentially a UseDelay unique to this component. + /// + [DataField("nextStun", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan NextStun = TimeSpan.Zero; +} diff --git a/Content.Shared/Ninja/Systems/DashAbilitySystem.cs b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs new file mode 100644 index 0000000000..bd6320de68 --- /dev/null +++ b/Content.Shared/Ninja/Systems/DashAbilitySystem.cs @@ -0,0 +1,118 @@ +using Content.Shared.Actions; +using Content.Shared.Charges.Components; +using Content.Shared.Charges.Systems; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Content.Shared.Physics; +using Content.Shared.Popups; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Timing; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Handles dashing logic including charge consumption and checking attempt events. +/// +public sealed class DashAbilitySystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedChargesSystem _charges = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnDash); + } + + private void OnGetItemActions(EntityUid uid, DashAbilityComponent comp, GetItemActionsEvent args) + { + var ev = new AddDashActionEvent(args.User); + RaiseLocalEvent(uid, ev); + + if (ev.Cancelled) + return; + + args.AddAction(ref comp.DashActionEntity, comp.DashAction); + } + + /// + /// Handle charges and teleport to a visible location. + /// + private void OnDash(EntityUid uid, DashAbilityComponent comp, DashEvent args) + { + if (!_timing.IsFirstTimePredicted) + return; + + var user = args.Performer; + args.Handled = true; + + var ev = new DashAttemptEvent(user); + RaiseLocalEvent(uid, ev); + if (ev.Cancelled) + return; + + if (!_hands.IsHolding(user, uid, out var _)) + { + _popup.PopupClient(Loc.GetString("dash-ability-not-held", ("item", uid)), user, user); + return; + } + + TryComp(uid, out var charges); + if (_charges.IsEmpty(uid, charges)) + { + _popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user); + return; + } + + var origin = Transform(user).MapPosition; + var target = args.Target.ToMap(EntityManager, _transform); + // prevent collision with the user duh + if (!_interaction.InRangeUnobstructed(origin, target, 0f, CollisionGroup.Opaque, uid => uid == user)) + { + // can only dash if the destination is visible on screen + _popup.PopupClient(Loc.GetString("dash-ability-cant-see", ("item", uid)), user, user); + return; + } + + _transform.SetCoordinates(user, args.Target); + _transform.AttachToGridOrMap(user); + _audio.PlayPredicted(comp.BlinkSound, user, user); + if (charges != null) + _charges.UseCharge(uid, charges); + } +} + +/// +/// Raised on the item before adding the dash action +/// +public sealed class AddDashActionEvent : CancellableEntityEventArgs +{ + public EntityUid User; + + public AddDashActionEvent(EntityUid user) + { + User = user; + } +} + +/// +/// Raised on the item before dashing is done. +/// +public sealed class DashAttemptEvent : CancellableEntityEventArgs +{ + public EntityUid User; + + public DashAttemptEvent(EntityUid user) + { + User = user; + } +} diff --git a/Content.Shared/Ninja/Systems/EmagProviderSystem.cs b/Content.Shared/Ninja/Systems/EmagProviderSystem.cs new file mode 100644 index 0000000000..df9cf8ac82 --- /dev/null +++ b/Content.Shared/Ninja/Systems/EmagProviderSystem.cs @@ -0,0 +1,72 @@ +using Content.Shared.Administration.Logs; +using Content.Shared.Emag.Systems; +using Content.Shared.Database; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Content.Shared.Tag; +using Content.Shared.Whitelist; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Handles emagging whitelisted objects when clicked. +/// +public sealed class EmagProviderSystem : EntitySystem +{ + [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; + [Dependency] private readonly TagSystem _tags = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBeforeInteractHand); + } + + /// + /// Emag clicked entities that are on the whitelist. + /// + private void OnBeforeInteractHand(EntityUid uid, EmagProviderComponent comp, BeforeInteractHandEvent args) + { + // TODO: change this into a generic check event thing + if (args.Handled || !_gloves.AbilityCheck(uid, args, out var target)) + return; + + // only allowed to emag entities on the whitelist + if (comp.Whitelist != null && !comp.Whitelist.IsValid(target, EntityManager)) + return; + + // only allowed to emag non-immune entities + if (_tags.HasTag(target, comp.EmagImmuneTag)) + return; + + var handled = _emag.DoEmagEffect(uid, target); + if (!handled) + return; + + _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(uid):player} emagged {ToPrettyString(target):target}"); + var ev = new EmaggedSomethingEvent(target); + RaiseLocalEvent(uid, ref ev); + args.Handled = true; + } + + /// + /// Set the whitelist for emagging something outside of yaml. + /// + public void SetWhitelist(EntityUid uid, EntityWhitelist? whitelist, EmagProviderComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.Whitelist = whitelist; + Dirty(uid, comp); + } +} + +/// +/// Raised on the player when emagging something. +/// +[ByRefEvent] +public record struct EmaggedSomethingEvent(EntityUid Target); diff --git a/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs new file mode 100644 index 0000000000..d427ffa39b --- /dev/null +++ b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs @@ -0,0 +1,47 @@ +using Content.Shared.Inventory.Events; +using Content.Shared.Ninja.Components; + +namespace Content.Shared.Ninja.Systems; + +/// +/// System for katana binding and dash events. Recalling is handled by the suit. +/// +public sealed class EnergyKatanaSystem : EntitySystem +{ + [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnAddDashAction); + SubscribeLocalEvent(OnDashAttempt); + } + + /// + /// When equipped by a ninja, try to bind it. + /// + private void OnEquipped(EntityUid uid, EnergyKatanaComponent comp, GotEquippedEvent args) + { + // check if user isnt a ninja or already has a katana bound + var user = args.Equipee; + if (!TryComp(user, out var ninja) || ninja.Katana != null) + return; + + // bind it since its unbound + _ninja.BindKatana(user, uid, ninja); + } + + private void OnAddDashAction(EntityUid uid, EnergyKatanaComponent comp, AddDashActionEvent args) + { + if (!HasComp(args.User)) + args.Cancel(); + } + + private void OnDashAttempt(EntityUid uid, EnergyKatanaComponent comp, DashAttemptEvent args) + { + if (!TryComp(args.User, out var ninja) || ninja.Katana != uid) + args.Cancel(); + } +} diff --git a/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs b/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs new file mode 100644 index 0000000000..ac11063eb7 --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedBatteryDrainerSystem.cs @@ -0,0 +1,69 @@ +using Content.Shared.Ninja.Components; +using Content.Shared.DoAfter; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Basic draining prediction and API, all real logic is handled serverside. +/// +public abstract class SharedBatteryDrainerSystem : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnDoAfterAttempt); + SubscribeLocalEvent(OnDoAfter); + } + + /// + /// Cancel any drain doafters if the battery is removed or gets filled. + /// + protected virtual void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent args) + { + if (comp.BatteryUid == null) + { + args.Cancel(); + } + } + + /// + /// Drain power from a power source (on server) and repeat if it succeeded. + /// Client will predict always succeeding since power is serverside. + /// + private void OnDoAfter(EntityUid uid, BatteryDrainerComponent comp, DrainDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) + return; + + // repeat if there is still power to drain + args.Repeat = TryDrainPower(uid, comp, args.Target.Value); + } + + /// + /// Attempt to drain as much power as possible into the powercell. + /// Client always predicts this as succeeding since power is serverside and it can only fail once, when the powercell is filled or the target is emptied. + /// + protected virtual bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target) + { + return true; + } + + /// + /// Sets the battery field on the drainer. + /// + public void SetBattery(EntityUid uid, EntityUid? battery, BatteryDrainerComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.BatteryUid = battery; + } +} + +/// +/// DoAfter event for . +/// +[Serializable, NetSerializable] +public sealed partial class DrainDoAfterEvent : SimpleDoAfterEvent { } diff --git a/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs b/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs new file mode 100644 index 0000000000..45c97fd1aa --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedNinjaGlovesSystem.cs @@ -0,0 +1,116 @@ +using Content.Shared.Actions; +using Content.Shared.CombatMode; +using Content.Shared.Communications; +using Content.Shared.Doors.Components; +using Content.Shared.DoAfter; +using Content.Shared.Examine; +using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Inventory.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Popups; +using Content.Shared.Research.Components; +using Content.Shared.Timing; +using Content.Shared.Toggleable; +using Robust.Shared.Timing; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Provides the toggle action and handles examining and unequipping. +/// +public abstract class SharedNinjaGlovesSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] protected readonly SharedAppearanceSystem Appearance = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] protected readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] protected readonly SharedInteractionSystem Interaction = default!; + [Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!; + [Dependency] protected readonly SharedPopupSystem Popup = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnUnequipped); + } + + /// + /// Disable glove abilities and show the popup if they were enabled previously. + /// + public void DisableGloves(EntityUid uid, NinjaGlovesComponent? comp = null) + { + // already disabled? + if (!Resolve(uid, ref comp) || comp.User == null) + return; + + var user = comp.User.Value; + comp.User = null; + Dirty(uid, comp); + + Appearance.SetData(uid, ToggleVisuals.Toggled, false); + Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user); + + RemComp(user); + RemComp(user); + RemComp(user); + RemComp(user); + RemComp(user); + } + + /// + /// Adds the toggle action when equipped. + /// + private void OnGetItemActions(EntityUid uid, NinjaGlovesComponent comp, GetItemActionsEvent args) + { + if (HasComp(args.User)) + args.AddAction(ref comp.ToggleActionEntity, comp.ToggleAction); + } + + /// + /// Show if the gloves are enabled when examining. + /// + private void OnExamined(EntityUid uid, NinjaGlovesComponent comp, ExaminedEvent args) + { + if (!args.IsInDetailsRange) + return; + + args.PushText(Loc.GetString(comp.User != null ? "ninja-gloves-examine-on" : "ninja-gloves-examine-off")); + } + + /// + /// Disable gloves when unequipped and clean up ninja's gloves reference + /// + private void OnUnequipped(EntityUid uid, NinjaGlovesComponent comp, GotUnequippedEvent args) + { + if (comp.User != null) + { + var user = comp.User.Value; + Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user); + DisableGloves(uid, comp); + } + } + + + // TODO: generic event thing + /// + /// GloveCheck but for abilities stored on the player, skips some checks. + /// Intended to be more generic, doesn't require the user to be a ninja or have any ninja equipment. + /// + public bool AbilityCheck(EntityUid uid, BeforeInteractHandEvent args, out EntityUid target) + { + target = args.Target; + return _timing.IsFirstTimePredicted + && !_combatMode.IsInCombatMode(uid) + && !_useDelay.ActiveDelay(uid) + && TryComp(uid, out var hands) + && hands.ActiveHandEntity == null + && Interaction.InRangeUnobstructed(uid, target); + } +} diff --git a/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs b/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs new file mode 100644 index 0000000000..83fcba4ac6 --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedNinjaSuitSystem.cs @@ -0,0 +1,139 @@ +using Content.Shared.Actions; +using Content.Shared.Clothing.Components; +using Content.Shared.Clothing.EntitySystems; +using Content.Shared.Inventory.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Timing; +using Robust.Shared.Audio; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Handles (un)equipping and provides some API functions. +/// +public abstract class SharedNinjaSuitSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; + [Dependency] protected readonly SharedSpaceNinjaSystem _ninja = default!; + [Dependency] protected readonly StealthClothingSystem StealthClothing = default!; + [Dependency] protected readonly UseDelaySystem UseDelay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnAddStealthAction); + SubscribeLocalEvent(OnUnequipped); + } + + /// + /// Call the shared and serverside code for when a ninja equips the suit. + /// + private void OnEquipped(EntityUid uid, NinjaSuitComponent comp, GotEquippedEvent args) + { + var user = args.Equipee; + if (!TryComp(user, out var ninja)) + return; + + NinjaEquippedSuit(uid, comp, user, ninja); + } + + /// + /// Add all the actions when a suit is equipped by a ninja. + /// + private void OnGetItemActions(EntityUid uid, NinjaSuitComponent comp, GetItemActionsEvent args) + { + if (!HasComp(args.User)) + return; + + args.AddAction(ref comp.RecallKatanaActionEntity, comp.RecallKatanaAction); + args.AddAction(ref comp.CreateThrowingStarActionEntity, comp.CreateThrowingStarAction); + args.AddAction(ref comp.EmpActionEntity, comp.EmpAction); + } + + /// + /// Only add stealth clothing's toggle action when equipped by a ninja. + /// + private void OnAddStealthAction(EntityUid uid, NinjaSuitComponent comp, AddStealthActionEvent args) + { + if (!HasComp(args.User)) + args.Cancel(); + } + + /// + /// Call the shared and serverside code for when anyone unequips a suit. + /// + private void OnUnequipped(EntityUid uid, NinjaSuitComponent comp, GotUnequippedEvent args) + { + UserUnequippedSuit(uid, comp, args.Equipee); + } + + /// + /// Called when a suit is equipped by a space ninja. + /// In the future it might be changed to an explicit activation toggle/verb like gloves are. + /// + protected virtual void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja) + { + // mark the user as wearing this suit, used when being attacked among other things + _ninja.AssignSuit(user, uid, ninja); + + // initialize phase cloak, but keep it off + StealthClothing.SetEnabled(uid, user, false); + } + + /// + /// Force uncloaks the user and disables suit abilities. + /// + public void RevealNinja(EntityUid uid, EntityUid user, NinjaSuitComponent? comp = null, StealthClothingComponent? stealthClothing = null) + { + if (!Resolve(uid, ref comp, ref stealthClothing)) + return; + + if (!StealthClothing.SetEnabled(uid, user, false, stealthClothing)) + return; + + // previously cloaked, disable abilities for a short time + _audio.PlayPredicted(comp.RevealSound, uid, user); + // all abilities check for a usedelay on the ninja + var useDelay = EnsureComp(user); + useDelay.Delay = comp.DisableTime; + UseDelay.BeginDelay(user, useDelay); + } + + // TODO: modify PowerCellDrain + /// + /// Returns the power used by a suit + /// + public float SuitWattage(EntityUid uid, NinjaSuitComponent? suit = null) + { + if (!Resolve(uid, ref suit)) + return 0f; + + float wattage = suit.PassiveWattage; + if (TryComp(uid, out var stealthClothing) && stealthClothing.Enabled) + wattage += suit.CloakWattage; + return wattage; + } + + /// + /// Called when a suit is unequipped, not necessarily by a space ninja. + /// In the future it might be changed to also have explicit deactivation via toggle. + /// + protected virtual void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user) + { + if (!TryComp(user, out var ninja)) + return; + + // mark the user as not wearing a suit + _ninja.AssignSuit(user, null, ninja); + // disable glove abilities + if (ninja.Gloves != null && TryComp(ninja.Gloves.Value, out var gloves)) + _gloves.DisableGloves(ninja.Gloves.Value, gloves); + } +} diff --git a/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs b/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs new file mode 100644 index 0000000000..d8ff07c27a --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedSpaceNinjaSystem.cs @@ -0,0 +1,89 @@ +using Content.Shared.Clothing.Components; +using Content.Shared.Ninja.Components; +using Content.Shared.Weapons.Melee.Events; +using Content.Shared.Weapons.Ranged.Events; +using Content.Shared.Popups; + +namespace Content.Shared.Ninja.Systems; + +/// +/// Provides shared ninja API, handles being attacked revealing ninja and stops guns from shooting. +/// +public abstract class SharedSpaceNinjaSystem : EntitySystem +{ + [Dependency] protected readonly SharedNinjaSuitSystem _suit = default!; + [Dependency] protected readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNinjaAttacked); + SubscribeLocalEvent(OnShotAttempted); + } + + /// + /// Set the ninja's worn suit entity + /// + public void AssignSuit(EntityUid uid, EntityUid? suit, SpaceNinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp) || comp.Suit == suit) + return; + + comp.Suit = suit; + Dirty(uid, comp); + } + + /// + /// Set the ninja's worn gloves entity + /// + public void AssignGloves(EntityUid uid, EntityUid? gloves, SpaceNinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp) || comp.Gloves == gloves) + return; + + comp.Gloves = gloves; + Dirty(uid, comp); + } + + /// + /// Bind a katana entity to a ninja, letting it be recalled and dash. + /// + public void BindKatana(EntityUid uid, EntityUid? katana, SpaceNinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp) || comp.Katana == katana) + return; + + comp.Katana = katana; + Dirty(uid, comp); + } + + /// + /// Gets the user's battery and tries to use some charge from it, returning true if successful. + /// Serverside only. + /// + public virtual bool TryUseCharge(EntityUid user, float charge) + { + return false; + } + + /// + /// Handle revealing ninja if cloaked when attacked. + /// + private void OnNinjaAttacked(EntityUid uid, SpaceNinjaComponent comp, AttackedEvent args) + { + if (comp.Suit != null && TryComp(comp.Suit, out var stealthClothing) && stealthClothing.Enabled) + { + _suit.RevealNinja(comp.Suit.Value, uid, null, stealthClothing); + } + } + + /// + /// Require ninja to fight with HONOR, no guns! + /// + private void OnShotAttempted(EntityUid uid, SpaceNinjaComponent comp, ref ShotAttemptedEvent args) + { + _popup.PopupClient(Loc.GetString("gun-disabled"), uid, uid); + args.Cancel(); + } +} diff --git a/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs b/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs new file mode 100644 index 0000000000..61b6e4313e --- /dev/null +++ b/Content.Shared/Ninja/Systems/SharedStunProviderSystem.cs @@ -0,0 +1,32 @@ +using Content.Shared.Ninja.Components; + +namespace Content.Shared.Ninja.Systems; + +/// +/// All interaction logic is implemented serverside. +/// This is in shared for API and access. +/// +public abstract class SharedStunProviderSystem : EntitySystem +{ + /// + /// Set the battery field on the stun provider. + /// + public void SetBattery(EntityUid uid, EntityUid? battery, StunProviderComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.BatteryUid = battery; + } + + /// + /// Set the no power popup field on the stun provider. + /// + public void SetNoPowerPopup(EntityUid uid, string popup, StunProviderComponent? comp = null) + { + if (!Resolve(uid, ref comp)) + return; + + comp.NoPowerPopup = popup; + } +} diff --git a/Content.Shared/Research/Components/ResearchStealerComponent.cs b/Content.Shared/Research/Components/ResearchStealerComponent.cs new file mode 100644 index 0000000000..e0331fad1b --- /dev/null +++ b/Content.Shared/Research/Components/ResearchStealerComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Research.Systems; +using Robust.Shared.GameStates; + +namespace Content.Shared.Research.Components; + +/// +/// Component for stealing technologies from a R&D server, when gloves are enabled. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(SharedResearchStealerSystem))] +public sealed partial class ResearchStealerComponent : Component +{ + /// + /// Time taken to steal research from a server + /// + [DataField("delay"), ViewVariables(VVAccess.ReadWrite)] + public TimeSpan Delay = TimeSpan.FromSeconds(20); +} diff --git a/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs b/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs new file mode 100644 index 0000000000..64f596023d --- /dev/null +++ b/Content.Shared/Research/Systems/SharedResearchStealerSystem.cs @@ -0,0 +1,63 @@ +using Content.Shared.DoAfter; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Systems; +using Content.Shared.Popups; +using Content.Shared.Research.Components; +using Robust.Shared.Serialization; + +namespace Content.Shared.Research.Systems; + +public abstract class SharedResearchStealerSystem : EntitySystem +{ + [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnBeforeInteractHand); + } + + /// + /// Start do after for downloading techs from a r&d server. + /// Will only try if there is at least 1 tech researched. + /// + private void OnBeforeInteractHand(EntityUid uid, ResearchStealerComponent comp, BeforeInteractHandEvent args) + { + // TODO: generic event + if (args.Handled || !_gloves.AbilityCheck(uid, args, out var target)) + return; + + // can only hack the server, not a random console + if (!TryComp(target, out var database) || HasComp(target)) + return; + + args.Handled = true; + + // fail fast if theres no techs to steal right now + if (database.UnlockedTechnologies.Count == 0) + { + _popup.PopupClient(Loc.GetString("ninja-download-fail"), uid, uid); + return; + } + + var doAfterArgs = new DoAfterArgs(uid, comp.Delay, new ResearchStealDoAfterEvent(), target: target, used: uid, eventTarget: uid) + { + BreakOnDamage = true, + BreakOnUserMove = true, + MovementThreshold = 0.5f + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } +} + +/// +/// Raised on the research stealer when the doafter completes. +/// +[Serializable, NetSerializable] +public sealed partial class ResearchStealDoAfterEvent : SimpleDoAfterEvent +{ +} diff --git a/Content.Shared/Research/Systems/SharedResearchSystem.cs b/Content.Shared/Research/Systems/SharedResearchSystem.cs index fffbad847e..e0cc937b00 100644 --- a/Content.Shared/Research/Systems/SharedResearchSystem.cs +++ b/Content.Shared/Research/Systems/SharedResearchSystem.cs @@ -169,6 +169,18 @@ public abstract class SharedResearchSystem : EntitySystem if (prototype.Tier < discipline.LockoutTier) return; component.MainDiscipline = prototype.Discipline; - Dirty(component); + Dirty(uid, component); + } + + /// + /// Clear all unlocked technologies from the database. + /// + public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null) + { + if (!Resolve(uid, ref comp) || comp.UnlockedTechnologies.Count == 0) + return; + + comp.UnlockedTechnologies.Clear(); + Dirty(uid, comp); } } diff --git a/Resources/Audio/Misc/attributions.yml b/Resources/Audio/Misc/attributions.yml index 773d1e6e4e..fc70e1a9ca 100644 --- a/Resources/Audio/Misc/attributions.yml +++ b/Resources/Audio/Misc/attributions.yml @@ -2,3 +2,8 @@ license: "CC-BY-3.0" copyright: "Created by qwertyquerty" source: "https://www.youtube.com/@qwertyquerty" + +- files: ["ninja_greeting.ogg"] + license: "CC-BY-SA-3.0" + copyright: "Taken from TG station." + source: "https://github.com/tgstation/tgstation/blob/b02b93ce2ab891164511a973493cdf951b4120f7/sound/effects/ninja_greeting.ogg" diff --git a/Resources/Audio/Misc/ninja_greeting.ogg b/Resources/Audio/Misc/ninja_greeting.ogg new file mode 100644 index 0000000000..e8f17bdea6 Binary files /dev/null and b/Resources/Audio/Misc/ninja_greeting.ogg differ diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl index 529e960fb3..68f33f8c4d 100644 --- a/Resources/Locale/en-US/administration/antag.ftl +++ b/Resources/Locale/en-US/administration/antag.ftl @@ -2,4 +2,11 @@ verb-categories-antag = Antag ctrl admin-verb-make-traitor = Make the target into a traitor. admin-verb-make-zombie = Zombifies the target immediately. admin-verb-make-nuclear-operative = Make target a into lone Nuclear Operative. -admin-verb-make-pirate = Make the target into a pirate. Note this doesn't configure the game rule. +admin-verb-make-pirate = Make the target into a pirate. Note that this doesn't configure the game rule. +admin-verb-make-space-ninja = Make the target into a space ninja. + +admin-verb-text-make-traitor = Make Traitor +admin-verb-text-make-zombie = Make Zombie +admin-verb-text-make-nuclear-operative = Make Nuclear Operative +admin-verb-text-make-pirate = Make Pirate +admin-verb-text-make-space-ninja = Make Space Ninja diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index d5f14c5fc8..33642b44b5 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -95,3 +95,6 @@ alerts-bleed-desc = You're [color=red]bleeding[/color]. alerts-pacified-name = [color=green]Pacified[/color] alerts-pacified-desc = You're pacified; you won't be able to attack anyone directly. + +alerts-suit-power-name = Suit Power +alerts-suit-power-desc = How much power your space ninja suit has. diff --git a/Resources/Locale/en-US/battery/components/battery-drainer-component.ftl b/Resources/Locale/en-US/battery/components/battery-drainer-component.ftl new file mode 100644 index 0000000000..2f9b06ef58 --- /dev/null +++ b/Resources/Locale/en-US/battery/components/battery-drainer-component.ftl @@ -0,0 +1,3 @@ +battery-drainer-full = Your battery is already full +battery-drainer-empty = {CAPITALIZE(THE($battery))} does not have enough power to drain +battery-drainer-success = You drain power from {THE($battery)}! diff --git a/Resources/Locale/en-US/communications/terror.ftl b/Resources/Locale/en-US/communications/terror.ftl new file mode 100644 index 0000000000..4bc80c0bd6 --- /dev/null +++ b/Resources/Locale/en-US/communications/terror.ftl @@ -0,0 +1,2 @@ +terror-dragon = Attention crew, it appears that someone on your station has made an unexpected communication with a strange fish in nearby space. +terror-revenant = Attention crew, it appears that someone on your station has made an unexpected communication with an otherworldly energy in nearby space. diff --git a/Resources/Locale/en-US/guidebook/guides.ftl b/Resources/Locale/en-US/guidebook/guides.ftl index 1ece42a5b1..4fa42ecede 100644 --- a/Resources/Locale/en-US/guidebook/guides.ftl +++ b/Resources/Locale/en-US/guidebook/guides.ftl @@ -49,5 +49,6 @@ guide-entry-nuclear-operatives = Nuclear Operatives guide-entry-traitors = Traitors guide-entry-zombies = Zombies guide-entry-minor-antagonists = Minor Antagonists +guide-entry-space-ninja = Space Ninja guide-entry-writing = Writing diff --git a/Resources/Locale/en-US/ninja/gloves.ftl b/Resources/Locale/en-US/ninja/gloves.ftl new file mode 100644 index 0000000000..cb43208675 --- /dev/null +++ b/Resources/Locale/en-US/ninja/gloves.ftl @@ -0,0 +1,6 @@ +ninja-gloves-on = The gloves surge with power! +ninja-gloves-off = The gloves power down... +ninja-gloves-not-wearing-suit = You aren't wearing a ninja suit +ninja-gloves-examine-on = All abilities are enabled. +ninja-gloves-examine-off = Boring old gloves... +ninja-doorjack-success = The gloves zap something in {THE($target)}. diff --git a/Resources/Locale/en-US/ninja/katana.ftl b/Resources/Locale/en-US/ninja/katana.ftl new file mode 100644 index 0000000000..678d8779ec --- /dev/null +++ b/Resources/Locale/en-US/ninja/katana.ftl @@ -0,0 +1,6 @@ +ninja-katana-recalled = Your Energy Katana teleports into your hand! +ninja-hands-full = Your hands are full! + +dash-ability-not-held = You aren't holding your katana! +dash-ability-no-charges = No charges left! +dash-ability-cant-see = You can't see that! diff --git a/Resources/Locale/en-US/ninja/ninja-actions.ftl b/Resources/Locale/en-US/ninja/ninja-actions.ftl new file mode 100644 index 0000000000..28d2237c5a --- /dev/null +++ b/Resources/Locale/en-US/ninja/ninja-actions.ftl @@ -0,0 +1,21 @@ +action-name-toggle-ninja-gloves = Toggle ninja gloves +action-desc-toggle-ninja-gloves = Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies, downloading research and calling in a threat. + +action-name-toggle-phase-cloak = Phase cloak +action-desc-toggle-phase-cloak = Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak! +ninja-no-power = Not enough charge in suit battery! + +action-name-create-throwing-star = Create throwing star +action-desc-create-throwing-star = Channels suit power into creating a throwing star that deals extra stamina damage. + +action-name-recall-katana = Recall katana +action-desc-recall-katana = Teleports the Energy Katana linked to this suit to its wearer, cost based on distance. + +action-name-katana-dash = Katana dash +action-desc-katana-dash = Teleport to anywhere you can see, if your Energy Katana is in your hand. + +action-name-em-burst = EM Burst +action-desc-em-burst = Disable any nearby technology with an electro-magnetic pulse. + +ninja-research-steal-fail = No new research nodes were stolen... +ninja-research-steal-success = Stole {$count} new nodes from {THE($server)}. diff --git a/Resources/Locale/en-US/ninja/role.ftl b/Resources/Locale/en-US/ninja/role.ftl new file mode 100644 index 0000000000..24de27fa84 --- /dev/null +++ b/Resources/Locale/en-US/ninja/role.ftl @@ -0,0 +1,8 @@ +ninja-round-end-agent-name = ninja + +objective-issuer-spiderclan = [color=#33cc00]Spider Clan[/color] + +ninja-role-greeting = + I am an elite mercenary of the mighty Spider Clan! + Surprise is my weapon. Shadows are my armor. Without them, I am nothing. + Use your pinpointer to find the station. Good luck! diff --git a/Resources/Locale/en-US/ninja/spider-charge.ftl b/Resources/Locale/en-US/ninja/spider-charge.ftl new file mode 100644 index 0000000000..78a7b8688d --- /dev/null +++ b/Resources/Locale/en-US/ninja/spider-charge.ftl @@ -0,0 +1,2 @@ +spider-charge-not-ninja = While it appears normal, you can't seem to detonate the charge. +spider-charge-too-far = This isn't the location you're supposed to use this! diff --git a/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl b/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl new file mode 100644 index 0000000000..e2bed1cef8 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl @@ -0,0 +1,2 @@ +objective-condition-doorjack-title = Doorjack {$count} doors on the station. +objective-condition-doorjack-description = Your gloves can emag airlocks. Do this {$count} doors on the station. diff --git a/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl b/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl new file mode 100644 index 0000000000..3ce7a983b2 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl @@ -0,0 +1,3 @@ +objective-condition-spider-charge-title = Detonate the spider clan charge in {$location} +objective-condition-spider-charge-no-target = Detonate the spider clan charge... somewhere? +objective-condition-spider-charge-description = This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else! diff --git a/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl b/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl new file mode 100644 index 0000000000..a9a820c3c5 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/steal-research-condition.ftl @@ -0,0 +1,2 @@ +objective-condition-steal-research-title = Steal {$count} technologies. +objective-condition-steal-research-description = Your gloves can be used to hack a research server and steal its precious data. If science has been slacking you'll have to get to work. diff --git a/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl b/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl new file mode 100644 index 0000000000..5c9115a79f --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/survive-condition.ftl @@ -0,0 +1,2 @@ +objective-condition-survive-title = Survive +objective-condition-survive-description = You wouldn't be a very good ninja if you died, now would you? diff --git a/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl b/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl new file mode 100644 index 0000000000..104f5782dd --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/terror-condition.ftl @@ -0,0 +1,2 @@ +objective-condition-terror-title = Call in a threat +objective-condition-terror-description = Use your gloves on a communication console in order to bring another threat to the station. diff --git a/Resources/Locale/en-US/prototypes/roles/antags.ftl b/Resources/Locale/en-US/prototypes/roles/antags.ftl index 1c6d6cef26..f771b5d911 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -24,3 +24,6 @@ roles-antag-nuclear-operative-objective = Find the nuke disk and blow up the sta roles-antag-subverted-silicon-name = Subverted silicon roles-antag-subverted-silicon-objective = Follow your new laws and do bad unto the station. + +roles-antag-space-ninja-name = Space Ninja +roles-antag-space-ninja-objective = Use your stealth to sabotage the station, nom on electrical wires. diff --git a/Resources/Prototypes/Actions/ninja.yml b/Resources/Prototypes/Actions/ninja.yml new file mode 100644 index 0000000000..cd1b5afde4 --- /dev/null +++ b/Resources/Prototypes/Actions/ninja.yml @@ -0,0 +1,84 @@ +# gloves +- type: entity + id: ActionToggleNinjaGloves + name: action-name-toggle-ninja-gloves + description: action-desc-toggle-ninja-gloves + noSpawn: true + components: + - type: InstantAction + priority: -13 + event: !type:ToggleActionEvent {} + +# suit +- type: entity + id: ActionCreateThrowingStar + name: action-name-create-throwing-star + description: action-desc-create-throwing-star + noSpawn: true + components: + - type: InstantAction + useDelay: 0.5 + icon: + sprite: Objects/Weapons/Throwable/throwing_star.rsi + state: icon + itemIconStyle: NoItem + priority: -10 + event: !type:CreateThrowingStarEvent {} + +- type: entity + id: ActionRecallKatana + name: action-name-recall-katana + description: action-desc-recall-katana + noSpawn: true + components: + - type: InstantAction + useDelay: 1 + icon: + sprite: Objects/Weapons/Melee/energykatana.rsi + state: icon + itemIconStyle: NoItem + priority: -11 + event: !type:RecallKatanaEvent {} + +- type: entity + id: ActionNinjaEmp + name: action-name-em-burst + description: action-desc-em-burst + noSpawn: true + components: + - type: InstantAction + icon: + sprite: Objects/Weapons/Grenades/empgrenade.rsi + state: icon + itemIconStyle: BigAction + priority: -13 + event: !type:NinjaEmpEvent {} + +- type: entity + id: ActionTogglePhaseCloak + name: action-name-toggle-phase-cloak + description: action-desc-toggle-phase-cloak + noSpawn: true + components: + - type: InstantAction + # have to plan (un)cloaking ahead of time + useDelay: 5 + priority: -9 + event: !type:ToggleStealthEvent + +# katana +- type: entity + id: ActionEnergyKatanaDash + name: action-name-katana-dash + description: action-desc-katana-dash + noSpawn: true + components: + - type: WorldTargetAction + icon: + sprite: Objects/Magic/magicactions.rsi + state: blink + itemIconStyle: NoItem + priority: -12 + event: !type:DashEvent + checkCanAccess: false + range: 0 diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 0b7148b4d5..ce6b2d0510 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -6,6 +6,7 @@ order: - category: Health - category: Stamina + - alertType: SuitPower - category: Internals - alertType: Fire - alertType: Handcuffed diff --git a/Resources/Prototypes/Alerts/ninja.yml b/Resources/Prototypes/Alerts/ninja.yml new file mode 100644 index 0000000000..1190b753e4 --- /dev/null +++ b/Resources/Prototypes/Alerts/ninja.yml @@ -0,0 +1,23 @@ +- type: alert + id: SuitPower + icons: + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power0 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power1 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power2 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power3 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power4 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power5 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power6 + - sprite: /Textures/Interface/Alerts/ninja_power.rsi + state: power7 + name: alerts-suit-power-name + description: alerts-suit-power-desc + minSeverity: 0 + maxSeverity: 7 diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml index e6ef82570d..bdedcd69a1 100644 --- a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml +++ b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml @@ -9,6 +9,20 @@ - type: entity noSpawn: true + parent: ClothingBackpackSatchel + id: ClothingBackpackSatchelTools + components: + - type: StorageFill + contents: + - id: BoxSurvival + - id: Crowbar + - id: Wrench + - id: Screwdriver + - id: Wirecutter + - id: Welder + - id: Multitool + +- type: entity parent: ClothingBackpackSatchelClown id: ClothingBackpackSatchelClownFilled components: diff --git a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml index f9e8a27ad6..c97290fdaa 100644 --- a/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml +++ b/Resources/Prototypes/Entities/Clothing/Eyes/glasses.yml @@ -174,3 +174,15 @@ - type: Clothing sprite: Clothing/Eyes/Glasses/science.rsi - type: SolutionScanner + +- type: entity + parent: ClothingEyesBase + id: ClothingEyesVisorNinja + name: ninja visor + description: An advanced visor protecting a ninja's eyes from flashing lights. + components: + - type: Sprite + sprite: Clothing/Eyes/Glasses/ninjavisor.rsi + - type: Clothing + sprite: Clothing/Eyes/Glasses/ninjavisor.rsi + - type: FlashImmunity diff --git a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index 40f144e0d2..8b76554f92 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -191,8 +191,18 @@ components: - type: Sprite sprite: Clothing/Hands/Gloves/spaceninja.rsi + layers: + - state: icon + map: [ "enum.ToggleVisuals.Layer" ] - type: Clothing sprite: Clothing/Hands/Gloves/spaceninja.rsi + - type: Appearance + - type: GenericVisualizer + visuals: + enum.ToggleVisuals.Toggled: + enum.ToggleVisuals.Layer: + True: {state: icon-green} + False: {state: icon} - type: GloveHeatResistance heatResistance: 1400 - type: Insulated @@ -202,6 +212,9 @@ - type: Thieving stripTimeReduction: 1 stealthy: true + - type: NinjaGloves + # not actually electrified, just used to make stun ability work + - type: Electrified - type: entity parent: ClothingHandsBase diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index 97b8742b71..ddebd4b0fd 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -156,7 +156,7 @@ #Space Ninja Helmet - type: entity - parent: ClothingHeadBase + parent: ClothingHeadEVAHelmetBase id: ClothingHeadHelmetSpaceNinja name: space ninja helmet description: What may appear to be a simple black garment is in fact a highly sophisticated nano-weave helmet. Standard issue ninja gear. @@ -165,11 +165,11 @@ sprite: Clothing/Head/Helmets/spaceninja.rsi - type: Clothing sprite: Clothing/Head/Helmets/spaceninja.rsi - - type: IngestionBlocker - type: Tag tags: - HidesHair - WhitelistChameleon + - type: IngestionBlocker - type: IdentityBlocker #Templar Helmet diff --git a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml index 61acf0d6e1..1d2f1fb2ce 100644 --- a/Resources/Prototypes/Entities/Clothing/Masks/masks.yml +++ b/Resources/Prototypes/Entities/Clothing/Masks/masks.yml @@ -491,3 +491,17 @@ - type: AddAccentClothing accent: ReplacementAccent replacement: italian + +- type: entity + parent: ClothingMaskBase + id: ClothingMaskNinja + name: ninja mask + description: A close-fitting nano-enhanced mask that acts both as an air filter and a post-modern fashion statement. + components: + - type: Sprite + sprite: Clothing/Mask/ninja.rsi + - type: Clothing + sprite: Clothing/Mask/ninja.rsi + - type: EyeProtection + - type: BreathMask + - type: IdentityBlocker diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml index 280d05697f..aee07bfdb4 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml @@ -93,7 +93,7 @@ - type: entity parent: ClothingOuterBase - id: ClothingOuterSuitSpaceninja + id: ClothingOuterSuitSpaceNinja name: space ninja suit description: This black technologically advanced, cybernetically-enhanced suit provides good protection and many abilities like invisibility or teleportation. components: @@ -102,7 +102,8 @@ - type: Clothing sprite: Clothing/OuterClothing/Suits/spaceninja.rsi - type: StealthClothing - visibility: 0.3 + visibility: 1.1 + toggleAction: ActionTogglePhaseCloak - type: PressureProtection highPressureMultiplier: 0.6 lowPressureMultiplier: 1000 @@ -115,18 +116,20 @@ Slash: 0.6 Piercing: 0.6 Heat: 0.6 - -- type: entity - id: ActionTogglePhaseCloak - name: action-name-toggle-phase-cloak - description: action-desc-toggle-phase-cloak - noSpawn: true - components: - - type: InstantAction - # have to plan (un)cloaking ahead of time - useDelay: 5 - priority: -9 - event: !type:ToggleStealthEvent + - type: NinjaSuit + - type: PowerCellSlot + cellSlotId: cell_slot + # throwing in a recharger would bypass glove charging mechanic + fitsInCharger: false + - type: ContainerContainer + containers: + cell_slot: !type:ContainerSlot + - type: ItemSlots + slots: + cell_slot: + name: power-cell-slot-component-slot-name-default + startingItem: PowerCellSmall + disableEject: true - type: entity parent: ClothingOuterBase diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index c341f2ae22..7b864e806e 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -81,6 +81,10 @@ - type: Clothing sprite: Clothing/Shoes/Specific/spaceninja.rsi - type: NoSlip + - type: ClothingSpeedModifier + # ninja are masters of sneaking around relatively quickly, won't break cloak + walkModifier: 1.1 + sprintModifier: 1.3 - type: entity parent: ClothingShoesBaseButcherable diff --git a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml index e7f8f1b511..d7625bbde1 100644 --- a/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml +++ b/Resources/Prototypes/Entities/Clothing/Uniforms/jumpsuits.yml @@ -932,6 +932,17 @@ - type: Clothing sprite: Clothing/Uniforms/Jumpsuit/mercenary.rsi +- type: entity + parent: UnsensoredClothingUniformBase + id: ClothingUniformJumpsuitNinja + name: ninja jumpsuit + description: A nano-enhanced jumpsuit designed for maximum comfort and tacticality. + components: + - type: Sprite + sprite: Clothing/Uniforms/Jumpsuit/ninja.rsi + - type: Clothing + sprite: Clothing/Uniforms/Jumpsuit/ninja.rsi + - type: entity parent: ClothingUniformBase id: ClothingUniformJumpsuitAtmos diff --git a/Resources/Prototypes/Entities/Effects/sparks.yml b/Resources/Prototypes/Entities/Effects/sparks.yml new file mode 100644 index 0000000000..53542a9b7b --- /dev/null +++ b/Resources/Prototypes/Entities/Effects/sparks.yml @@ -0,0 +1,19 @@ +- type: entity + id: EffectSparks + noSpawn: true + components: + - type: TimedDespawn + lifetime: 0.5 + - type: Sprite + drawdepth: Effects + noRot: true + layers: + - shader: unshaded + map: ["enum.EffectLayers.Unshaded"] + sprite: Effects/sparks.rsi + state: sparks + - type: EffectVisuals + - type: Tag + tags: + - HideContextMenu + - type: AnimationPlayer diff --git a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index 18c86443b5..75f7b59f14 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -111,3 +111,22 @@ - state: green - sprite: Mobs/Aliens/Carps/dragon.rsi state: alive + +- type: entity + id: SpawnPointGhostSpaceNinja + name: ghost role spawn point + suffix: space ninja + parent: MarkerBase + components: + - type: GhostRole + name: Space Ninja + description: Use stealth and deception to sabotage the station. + rules: You are an elite mercenary of the Spider Clan. You aren't required to follow your objectives, yet your NINJA HONOR demands you try. + - type: GhostRoleMobSpawner + prototype: MobHumanSpaceNinja + - type: Sprite + sprite: Markers/jobs.rsi + layers: + - state: green + - sprite: Objects/Weapons/Melee/energykatana.rsi + state: icon diff --git a/Resources/Prototypes/Entities/Mobs/Player/human.yml b/Resources/Prototypes/Entities/Mobs/Player/human.yml index ba1d475b34..5d26e155e9 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -53,3 +53,24 @@ - type: NpcFactionMember factions: - Syndicate + +# Space Ninja +- type: entity + noSpawn: true + name: Space Ninja + parent: MobHuman + id: MobHumanSpaceNinja + components: + - type: Loadout + prototypes: [SpaceNinjaGear] + - type: NpcFactionMember + factions: + - Syndicate + - type: SpaceNinja + - type: AutoImplant + implants: + - MicroBombImplant + - type: RandomMetadata + nameSegments: + - names_ninja_title + - names_ninja diff --git a/Resources/Prototypes/Entities/Objects/Devices/pinpointer.yml b/Resources/Prototypes/Entities/Objects/Devices/pinpointer.yml index 2490c3a3d6..e00ba1ed08 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pinpointer.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pinpointer.yml @@ -54,3 +54,11 @@ - type: Pinpointer updateTargetName: true canRetarget: true + +- type: entity + parent: PinpointerBase + id: PinpointerStation + components: + - type: Pinpointer + component: BecomesStation + targetName: the station diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml new file mode 100644 index 0000000000..50a7ea8687 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml @@ -0,0 +1,47 @@ +- type: entity + name: spider clan charge + description: A modified C-4 charge supplied to you by the Spider Clan. Its explosive power has been juiced up, but only works in one specific area. + # not actually modified C-4! oh the horror! + parent: BaseItem + id: SpiderCharge + components: + - type: Sprite + sprite: Objects/Weapons/Bombs/spidercharge.rsi + state: icon + - type: Item + sprite: Objects/Weapons/Bombs/spidercharge.rsi + size: 10 + - type: SpiderCharge + - type: OnUseTimerTrigger + delay: 10 + delayOptions: [5, 10, 30, 60] + initialBeepDelay: 0 + beepSound: /Audio/Machines/Nuke/general_beep.ogg + startOnStick: true + - type: AutomatedTimer + - type: Sticky + stickDelay: 5 + stickPopupStart: comp-sticky-start-stick-bomb + stickPopupSuccess: comp-sticky-success-stick-bomb + # can only stick it in target area, no reason to unstick + canUnstick: false + blacklist: # can't stick it to movable things, even if they are in the target area + components: + - Anchorable + - Item + - Body + - type: Explosive # Powerful explosion in a large radius. Will break underplating. + explosionType: DemolitionCharge + totalIntensity: 360 + intensitySlope: 10 + maxIntensity: 120 + canCreateVacuum: true + - type: ExplodeOnTrigger + - type: StickyVisualizer + - type: Appearance + - type: GenericVisualizer + visuals: + enum.Trigger.TriggerVisuals.VisualState: + base: + Primed: { state: primed } + Unprimed: { state: complete } diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index 3ced854c5a..dbbe0febfd 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -51,6 +51,37 @@ sprite: Objects/Weapons/Melee/katana.rsi - type: DisarmMalus +- type: entity + name: energy katana + parent: Katana + id: EnergyKatana + description: A katana infused with strong energy. + components: + - type: Sprite + sprite: Objects/Weapons/Melee/energykatana.rsi + state: icon + - type: MeleeWeapon + damage: + types: + Slash: 30 + - type: Item + size: 15 + sprite: Objects/Weapons/Melee/energykatana.rsi + - type: EnergyKatana + - type: DashAbility + dashAction: ActionEnergyKatanaDash + - type: LimitedCharges + maxCharges: 3 + charges: 3 + - type: AutoRecharge + rechargeDuration: 20 + - type: Clothing + sprite: Objects/Weapons/Melee/energykatana.rsi + slots: + - Back + - Belt + - type: Reflect + - type: entity name: machete parent: BaseItem diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml index 198f634c16..b0fa94d440 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Throwable/throwing_stars.yml @@ -24,3 +24,13 @@ types: Slash: 8 Piercing: 10 + +- type: entity + parent: ThrowingStar + id: ThrowingStarNinja + name: ninja throwing star + components: + # prevent ninja crashing server + filling bag with 20 stars to instakill a single person + # also limits the crew's use + - type: TimedDespawn + lifetime: 30 diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index 8668a0193c..c8ef28395d 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -68,6 +68,18 @@ - type: RandomSpawnRule prototype: SpawnPointGhostDragon +- type: entity + parent: BaseGameRule + id: NinjaSpawn + noSpawn: true + components: + - type: StationEvent + weight: 10 + duration: 1 + earliestStart: 45 + minimumPlayers: 15 + - type: NinjaSpawnRule + - type: entity parent: BaseGameRule id: RevenantSpawn diff --git a/Resources/Prototypes/GameRules/midround.yml b/Resources/Prototypes/GameRules/midround.yml new file mode 100644 index 0000000000..0d5983b178 --- /dev/null +++ b/Resources/Prototypes/GameRules/midround.yml @@ -0,0 +1,19 @@ +# doesnt spawn a ninja or anything, just stores configuration for it +# see NinjaSpawn event for spawning +- type: entity + id: Ninja + parent: BaseGameRule + noSpawn: true + components: + - type: NinjaRule + objectives: + - StealResearchObjective + - DoorjackObjective + - SpiderChargeObjective + - TerrorObjective + - SurviveObjective + threats: + - announcement: terror-dragon + rule: Dragon + - announcement: terror-revenant + rule: RevenantSpawn diff --git a/Resources/Prototypes/Guidebook/antagonist.yml b/Resources/Prototypes/Guidebook/antagonist.yml index 32dda63590..e6e9afa017 100644 --- a/Resources/Prototypes/Guidebook/antagonist.yml +++ b/Resources/Prototypes/Guidebook/antagonist.yml @@ -7,6 +7,7 @@ - NuclearOperatives - Zombies - MinorAntagonists + - SpaceNinja - type: guideEntry id: Traitors @@ -27,3 +28,8 @@ id: MinorAntagonists name: guide-entry-minor-antagonists text: "/ServerInfo/Guidebook/Antagonist/MinorAntagonists.xml" + +- type: guideEntry + id: SpaceNinja + name: guide-entry-space-ninja + text: "/ServerInfo/Guidebook/Antagonist/SpaceNinja.xml" diff --git a/Resources/Prototypes/Objectives/ninjaObjectives.yml b/Resources/Prototypes/Objectives/ninjaObjectives.yml new file mode 100644 index 0000000000..f3df853d28 --- /dev/null +++ b/Resources/Prototypes/Objectives/ninjaObjectives.yml @@ -0,0 +1,40 @@ +- type: objective + id: StealResearchObjective + issuer: spiderclan + requirements: + - !type:NinjaRequirement {} + conditions: + - !type:StealResearchCondition {} + +- type: objective + id: DoorjackObjective + issuer: spiderclan + requirements: + - !type:NinjaRequirement {} + conditions: + - !type:DoorjackCondition {} + +- type: objective + id: SpiderChargeObjective + issuer: spiderclan + requirements: + - !type:NinjaRequirement {} + - !type:SpiderChargeTargetRequirement {} + conditions: + - !type:SpiderChargeCondition {} + +- type: objective + id: TerrorObjective + issuer: spiderclan + requirements: + - !type:NinjaRequirement {} + conditions: + - !type:TerrorCondition {} + +- type: objective + id: SurviveObjective + issuer: spiderclan + requirements: + - !type:NinjaRequirement {} + conditions: + - !type:SurviveCondition {} diff --git a/Resources/Prototypes/Roles/Antags/ninja.yml b/Resources/Prototypes/Roles/Antags/ninja.yml new file mode 100644 index 0000000000..a7492bd1b1 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/ninja.yml @@ -0,0 +1,6 @@ +- type: antag + id: SpaceNinja + name: roles-antag-space-ninja-name + antagonist: true + setPreference: false + objective: roles-antag-space-ninja-objective diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index e09f7e362f..367ffb9dea 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -23,16 +23,23 @@ - type: startingGear id: SpaceNinjaGear equipment: - jumpsuit: ClothingUniformJumpsuitColorBlack - back: ClothingBackpackFilled + jumpsuit: ClothingUniformJumpsuitNinja + # belt holds katana so satchel has the tools for sabotaging things + back: ClothingBackpackSatchelTools + mask: ClothingMaskNinja head: ClothingHeadHelmetSpaceNinja + eyes: ClothingEyesVisorNinja gloves: ClothingHandsGlovesSpaceNinja - outerClothing: ClothingOuterSuitSpaceninja + outerClothing: ClothingOuterSuitSpaceNinja shoes: ClothingShoesSpaceNinja - id: PassengerPDA - innerclothingskirt: ClothingUniformJumpskirtColorBlack - satchel: ClothingBackpackSatchelFilled - duffelbag: ClothingBackpackDuffelFilled + id: AgentIDCard + ears: ClothingHeadsetGrey + pocket1: SpiderCharge + pocket2: PinpointerStation + belt: EnergyKatana + suitstorage: YellowOxygenTankFilled + inhand: + left hand: JetpackBlackFilled #Deathsquad Outfit - type: startingGear diff --git a/Resources/ServerInfo/Guidebook/Antagonist/SpaceNinja.xml b/Resources/ServerInfo/Guidebook/Antagonist/SpaceNinja.xml new file mode 100644 index 0000000000..7fed84da73 --- /dev/null +++ b/Resources/ServerInfo/Guidebook/Antagonist/SpaceNinja.xml @@ -0,0 +1,74 @@ + +# Space Ninja + +The Space Ninja is a ghost role randomly available mid-late game. If you pick it you will be given your gear, your objectives and the greeting. + +You are a ninja, but in space. The Spider Clan has sent you to the station to wreak all kinds of havoc, from bolting the armory open and killing the entire station to engaging in a slipping war with the clown and fighting in rage cages. + +# Equipment + +You start with a microbomb implant, so if you get KIA or seppuku you will leave behind a nice crater and all your precious equipment is kept out of enemy hands. + +Your bag is full of tools for more subtle sabotage, along with a survival box if you need a snack. + +You have a jetpack and pinpointer that will let you find the station. + + + +## Ninja Suit + + + +Your single most important item is your suit, without it none of your abilities would work. +Your suit requires power to function, its internal battery can be replaced by clicking on it **with a better one**. +You can see the current charge by examining the suit or in a sweet battery alert at the top right of your screen. + +If you run out of power and need to recharge your battery, just use your gloves to drain an APC, substation or a SMES. + +## Ninja Gloves + + + +These bad boys are your bread and butter. + +They are insulated so you can nom on wires in peace. Obviously they block your fingerprints from being left on things you touch. + +You have an action to toggle gloves. When the gloves are turned on, they allow you to use special abilities, which are triggered by interacting with things with an empty hand and with combat mode disabled. + +Your glove abilities include: +- Emagging an unlimited number of doors. +- Draining power from transformers such as APCs, substations or SMESes. The higher the voltage, the more efficient the draining is. +- You can shock any mob, stunning and slightly damaging them. +- You can download technologies from a R&D server for one of your objectives. +- You can hack a communications console to call in a threat. + +## Energy Katana + + + +Deals a lot of damage and can be recalled at will, costing suit power proportional to the distance teleported. +While in hand you can teleport to anywhere that you can see, meaning most doors and windows, but not past solid walls. +This has a limited number of charges which regenerate slowly, so keep a charge or two spare incase you need a quick getaway. + +## Spider Clan Charge + + + +A modified C-4 explosive, you start with this in your pocket. Creates a large explosion but must be armed in your target area. +A random area on the map is selected for you to blow up, which is one of your objectives. It can't be activated manually, simply plant it on a wall or something. +Can't be unstuck once planted. + +## Ninja Shoes + +Special noslips that make you go really fast. +Energy not required. + +# Objectives + +- Download X research nodes: Use your gloves on an R&D server with a number of unlocked technologies +- Doorjack X doors on the station: Use your gloves to emag a number of doors. +- Detonate the spider clan charge: Plant your spider clan charge at a random location and watch it go boom. +- Call in a threat: Use your gloves on a communications console. +- Survive: Don't die. + + diff --git a/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/equipped-EYES.png b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/equipped-EYES.png new file mode 100644 index 0000000000..483d40449f Binary files /dev/null and b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/equipped-EYES.png differ diff --git a/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/icon.png b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/icon.png new file mode 100644 index 0000000000..0911957a8d Binary files /dev/null and b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-left.png b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-left.png new file mode 100644 index 0000000000..ebd7858dab Binary files /dev/null and b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-right.png b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-right.png new file mode 100644 index 0000000000..53535a1de7 Binary files /dev/null and b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/meta.json b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/meta.json new file mode 100644 index 0000000000..f803400908 --- /dev/null +++ b/Resources/Textures/Clothing/Eyes/Glasses/ninjavisor.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from paradise station https://github.com/ParadiseSS13/Paradise/commit/ede55cc8f5b50abcd4f51d37779b56853a440a9a", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-EYES", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/equipped-HELMET.png b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/equipped-HELMET.png index 123fa579d5..f5f0257ff7 100644 Binary files a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/equipped-HELMET.png and b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/equipped-HELMET.png differ diff --git a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/icon.png b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/icon.png index b814dfe5a5..78bbbcfd04 100644 Binary files a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/icon.png and b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-left.png b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-left.png index ab59e3cee8..4585c26849 100644 Binary files a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-left.png and b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-right.png b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-right.png index 6e43c65614..7b005ddacb 100644 Binary files a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-right.png and b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/meta.json b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/meta.json index a470e00944..922cbddb20 100644 --- a/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/meta.json +++ b/Resources/Textures/Clothing/Head/Helmets/spaceninja.rsi/meta.json @@ -1,7 +1,7 @@ { "version": 1, "license": "CC-BY-SA-3.0", - "copyright": "Taken from tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e", + "copyright": "Taken from tgstation https://github.com/tgstation/tgstation (unknown commit)", "size": { "x": 32, "y": 32 diff --git a/Resources/Textures/Clothing/Mask/ninja.rsi/equipped-MASK.png b/Resources/Textures/Clothing/Mask/ninja.rsi/equipped-MASK.png new file mode 100644 index 0000000000..920dd4912e Binary files /dev/null and b/Resources/Textures/Clothing/Mask/ninja.rsi/equipped-MASK.png differ diff --git a/Resources/Textures/Clothing/Mask/ninja.rsi/icon.png b/Resources/Textures/Clothing/Mask/ninja.rsi/icon.png new file mode 100644 index 0000000000..43dca0a730 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/ninja.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-left.png b/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-left.png new file mode 100644 index 0000000000..900114c31e Binary files /dev/null and b/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-right.png b/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-right.png new file mode 100644 index 0000000000..def44f1b59 Binary files /dev/null and b/Resources/Textures/Clothing/Mask/ninja.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Mask/ninja.rsi/meta.json b/Resources/Textures/Clothing/Mask/ninja.rsi/meta.json new file mode 100644 index 0000000000..25dfc5f68c --- /dev/null +++ b/Resources/Textures/Clothing/Mask/ninja.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "By Jackal298 (github), based off of tgstation at commit https://github.com/tgstation/tgstation/commit/4f6190e2895e09116663ef282d3ce1d8b35c032e and paradise at commit https://github.com/ParadiseSS13/Paradise/commit/33f7c1ef477fa67db5dda48078b469ab59aa7997", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-MASK", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/equipped-INNERCLOTHING.png b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/equipped-INNERCLOTHING.png new file mode 100644 index 0000000000..87282ec842 Binary files /dev/null and b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/equipped-INNERCLOTHING.png differ diff --git a/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/icon.png b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/icon.png new file mode 100644 index 0000000000..447174a09e Binary files /dev/null and b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-left.png b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-left.png new file mode 100644 index 0000000000..558124e55c Binary files /dev/null and b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-left.png differ diff --git a/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-right.png b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-right.png new file mode 100644 index 0000000000..e1db9b0bda Binary files /dev/null and b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/inhand-right.png differ diff --git a/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/meta.json b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/meta.json new file mode 100644 index 0000000000..b503d4acde --- /dev/null +++ b/Resources/Textures/Clothing/Uniforms/Jumpsuit/ninja.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Modified by Jackal298 (github), based on paradise station at https://github.com/ParadiseSS13/Paradise/blob/ede55cc8f5b50abcd4f51d37779b56853a440a9a/icons/obj/clothing/uniforms.dmi", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-INNERCLOTHING", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Effects/sparks.rsi/meta.json b/Resources/Textures/Effects/sparks.rsi/meta.json new file mode 100644 index 0000000000..43ce6cd1cc --- /dev/null +++ b/Resources/Textures/Effects/sparks.rsi/meta.json @@ -0,0 +1,28 @@ +{ + "version": 1, + "license": "CC0-1.0", + "copyright": "Drawn by deltanedas (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "sparks", + "delays": [ + [ + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05, + 0.05 + ] + ] + } + ] +} diff --git a/Resources/Textures/Effects/sparks.rsi/sparks.png b/Resources/Textures/Effects/sparks.rsi/sparks.png new file mode 100644 index 0000000000..24a124a70b Binary files /dev/null and b/Resources/Textures/Effects/sparks.rsi/sparks.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/meta.json b/Resources/Textures/Interface/Alerts/ninja_power.rsi/meta.json new file mode 100644 index 0000000000..1985bc0236 --- /dev/null +++ b/Resources/Textures/Interface/Alerts/ninja_power.rsi/meta.json @@ -0,0 +1,131 @@ +{ + "version": 1, + "license": "CC0-1.0", + "copyright": "Created by @deltanedas (github) for SS14", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "power7", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power6", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power5", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power4", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power3", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power2", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power1", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + }, + { + "name": "power0", + "delays": [ + [ + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25, + 0.25 + ] + ] + } + ] +} diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power0.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power0.png new file mode 100644 index 0000000000..f9adec28ce Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power0.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power1.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power1.png new file mode 100644 index 0000000000..1418859198 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power1.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power2.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power2.png new file mode 100644 index 0000000000..9a46893f7d Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power2.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power3.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power3.png new file mode 100644 index 0000000000..1013fee8e4 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power3.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power4.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power4.png new file mode 100644 index 0000000000..39007c5bb9 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power4.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power5.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power5.png new file mode 100644 index 0000000000..b167d03611 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power5.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power6.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power6.png new file mode 100644 index 0000000000..be02bf0fde Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power6.png differ diff --git a/Resources/Textures/Interface/Alerts/ninja_power.rsi/power7.png b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power7.png new file mode 100644 index 0000000000..2443849585 Binary files /dev/null and b/Resources/Textures/Interface/Alerts/ninja_power.rsi/power7.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/icon.png b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/icon.png new file mode 100644 index 0000000000..19eeac4947 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-left.png new file mode 100644 index 0000000000..0b7ddbf8ff Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-right.png new file mode 100644 index 0000000000..9147eb7598 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/meta.json b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/meta.json new file mode 100644 index 0000000000..16c2ce2aff --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/meta.json @@ -0,0 +1,31 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/db2efd4f0df2b630a8bb9851f53f4922b669a5b3", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "primed", + "delays": [ + [ + 0.1, + 0.1 + ] + ] + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/primed.png b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/primed.png new file mode 100644 index 0000000000..a3a5df1863 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/primed.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/equipped-BELT.png b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/equipped-BELT.png new file mode 100644 index 0000000000..f31e92edce Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/equipped-BELT.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/icon.png b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/icon.png new file mode 100644 index 0000000000..e185890fbf Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-left.png new file mode 100644 index 0000000000..5f35b502da Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-right.png new file mode 100644 index 0000000000..5926122d27 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/meta.json new file mode 100644 index 0000000000..1dfa76c4e3 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/meta.json @@ -0,0 +1,26 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/a9451f4d22f233d328b63490c2bcf64a640e42ff", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-BELT", + "directions": 4 + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + } + ] +}