From c1cda0dbf8b0f1e4bb69cd8cf54ef19a19f979bf Mon Sep 17 00:00:00 2001 From: deltanedas <39013340+deltanedas@users.noreply.github.com> Date: Mon, 17 Apr 2023 07:33:27 +0000 Subject: [PATCH] [Antag] add space ninja as midround antag (#14069) * start of space ninja midround antag * suit has powercell, can be upgraded only (not replaced with equal or worse battery) * add doorjacking to ninja gloves, power cell, doorjack objective (broken), tweaks * :skull: * add basic suit power display that uses stamina rsi * add draining apc/sub/smes - no wires yet * add research downloading * ninja starts implanted, move some stuff to yaml * add Automated field to OnUseTimerTrigger * implement spider charge and objective * fix client crash when taking suit off, some refactor * add survive condition and tweak locale * add comms console icon for objective * add calling in a threat - currently revenant and dragon * combine all glove abilities * locale * spark sounds when draining, refactoring * toggle is actually toggle now * prevent crash if disabling stealth with outline * add antag ctrl for ninja, hopefully show greentext * fix greentext and some other things * disabling gloves if taken off or suit taken off * basic energy katana, change ninja loadout * recallable katana, refactoring * start of dash - not done yet * katana dashing ability * merge upstream + compiling, make AutomatedTimer its own component * docs and stuff * partial refactor of glove abilities, still need to move handling * make dooremaggedevent by ref * move bunch of stuff to shared - broken * clean ninja antag verb * doc * mark rule config fields as required * fix client crash * wip systems refactor * big refactor of systems * fuck * make TryDoElectrocution callable from shared * finish refactoring? * no guns * start with internals on * clean up glove abilities, add range check * create soap, in place of ninja throwing stars * add emp suit ability * able to eat chefs stolen food in space * stuff, tell client when un/cloaked but there is bug with gloves * fix prediction breaking gloves on client * ninja soap despawns after a minute * ninja spawns outside the station now, with gps + station coords to navigate * add cooldown to stun ability * cant use glove abilities in combat mode * require empty hand to use glove abilities * use ghost role spawner * Update Content.Server/Ninja/Systems/NinjaSuitSystem.cs Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com> * some review changes * show powercell charge on examine * new is needed * address some reviews * ninja starts with jetpack, i hope * partial feedback * uhh * pro * remove pirate from threats list * use doafter refactor * pro i gave skeleton jetpack * some stuff * use auto gen state * mr handy * use EntityQueryEnumerator * cleanup * spider charge target anti-troll * mmmmmm --------- Co-authored-by: deltanedas Co-authored-by: deltanedas Co-authored-by: deltanedas <@deltanedas:kde.org> Co-authored-by: keronshb <54602815+keronshb@users.noreply.github.com> --- .../Ninja/Systems/NinjaGlovesSystem.cs | 10 + .../Ninja/Systems/NinjaSuitSystem.cs | 10 + Content.Client/Ninja/Systems/NinjaSystem.cs | 12 + Content.Client/Stealth/StealthSystem.cs | 2 +- .../Systems/AdminVerbSystem.Antags.cs | 18 + Content.Server/Doors/Systems/DoorSystem.cs | 11 + .../Electrocution/ElectrocutionSystem.cs | 12 +- .../Components/AutomatedTimerComponent.cs | 9 + .../EntitySystems/TriggerSystem.OnUse.cs | 2 +- .../Configurations/NinjaRuleConfiguration.cs | 66 ++++ .../Components/NinjaStationGridComponent.cs | 13 + .../Ninja/Systems/NinjaGlovesSystem.cs | 36 ++ .../Ninja/Systems/NinjaSuitSystem.cs | 148 +++++++++ Content.Server/Ninja/Systems/NinjaSystem.cs | 314 ++++++++++++++++++ .../Ninja/Systems/SpiderChargeSystem.cs | 64 ++++ .../Conditions/DoorjackCondition.cs | 64 ++++ .../Conditions/DownloadCondition.cs | 64 ++++ .../Conditions/SpiderChargeCondition.cs | 73 ++++ .../Objectives/Conditions/SurviveCondition.cs | 46 +++ .../Objectives/Conditions/TerrorCondition.cs | 54 +++ .../StationEvents/Events/SpaceNinjaSpawn.cs | 81 +++++ Content.Shared/Alert/AlertType.cs | 3 +- .../SharedElectrocutionSystem.cs | 18 + .../Interaction/SharedInteractionSystem.cs | 1 - .../Ninja/Components/EnergyKatanaComponent.cs | 66 ++++ .../Ninja/Components/NinjaComponent.cs | 69 ++++ .../Ninja/Components/NinjaGlovesComponent.cs | 151 +++++++++ .../Ninja/Components/NinjaSuitComponent.cs | 148 +++++++++ .../Ninja/Components/SpiderChargeComponent.cs | 17 + .../Ninja/Systems/EnergyKatanaSystem.cs | 147 ++++++++ .../Ninja/Systems/NinjaGlovesSystem.cs | 314 ++++++++++++++++++ .../Ninja/Systems/NinjaSuitSystem.cs | 151 +++++++++ Content.Shared/Ninja/Systems/NinjaSystem.cs | 101 ++++++ Resources/Audio/Misc/attributions.yml | 5 + Resources/Audio/Misc/ninja_greeting.ogg | Bin 0 -> 61293 bytes .../Locale/en-US/administration/antag.ftl | 3 +- Resources/Locale/en-US/alerts/alerts.ftl | 3 + .../game-presets/preset-traitor.ftl | 1 + Resources/Locale/en-US/ninja/gloves.ftl | 7 + Resources/Locale/en-US/ninja/katana.ftl | 4 + .../Locale/en-US/ninja/ninja-actions.ftl | 27 ++ Resources/Locale/en-US/ninja/role.ftl | 5 + .../Locale/en-US/ninja/spider-charge.ftl | 2 + Resources/Locale/en-US/ninja/terror.ftl | 2 + .../conditions/doorjack-condition.ftl | 2 + .../conditions/download-condition.ftl | 2 + .../conditions/spider-charge-condition.ftl | 3 + .../conditions/survive-condition.ftl | 2 + .../conditions/terror-condition.ftl | 2 + .../Locale/en-US/prototypes/roles/antags.ftl | 3 + Resources/Prototypes/Alerts/alerts.yml | 1 + Resources/Prototypes/Alerts/ninja.yml | 21 ++ .../Fills/Backpacks/StarterGear/satchel.yml | 14 + .../Entities/Clothing/Hands/gloves.yml | 11 + .../Entities/Clothing/Head/helmets.yml | 3 +- .../Entities/Clothing/OuterClothing/suits.yml | 23 ++ .../Entities/Clothing/Shoes/specific.yml | 4 + .../Entities/Markers/Spawners/ghost_roles.yml | 18 + .../Prototypes/Entities/Mobs/Player/human.yml | 23 ++ .../Objects/Specific/Janitorial/soap.yml | 15 + .../Entities/Objects/Weapons/Bombs/spider.yml | 47 +++ .../Entities/Objects/Weapons/Melee/sword.yml | 23 ++ Resources/Prototypes/GameRules/events.yml | 22 ++ .../Prototypes/Objectives/ninjaObjectives.yml | 39 +++ Resources/Prototypes/Roles/Antags/ninja.yml | 9 + .../Roles/Jobs/Fun/misc_startinggear.yml | 15 +- .../Weapons/Bombs/spidercharge.rsi/icon.png | Bin 0 -> 790 bytes .../Bombs/spidercharge.rsi/inhand-left.png | Bin 0 -> 752 bytes .../Bombs/spidercharge.rsi/inhand-right.png | Bin 0 -> 765 bytes .../Weapons/Bombs/spidercharge.rsi/meta.json | 31 ++ .../Weapons/Bombs/spidercharge.rsi/primed.png | Bin 0 -> 920 bytes .../Melee/energykatana.rsi/equipped-BELT.png | Bin 0 -> 879 bytes .../Weapons/Melee/energykatana.rsi/icon.png | Bin 0 -> 740 bytes .../Melee/energykatana.rsi/inhand-left.png | Bin 0 -> 875 bytes .../Melee/energykatana.rsi/inhand-right.png | Bin 0 -> 993 bytes .../Weapons/Melee/energykatana.rsi/meta.json | 26 ++ .../Machines/computers.rsi/comm_icon.png | Bin 0 -> 1151 bytes .../Machines/computers.rsi/meta.json | 3 + 78 files changed, 2697 insertions(+), 19 deletions(-) create mode 100644 Content.Client/Ninja/Systems/NinjaGlovesSystem.cs create mode 100644 Content.Client/Ninja/Systems/NinjaSuitSystem.cs create mode 100644 Content.Client/Ninja/Systems/NinjaSystem.cs create mode 100644 Content.Server/Explosion/Components/AutomatedTimerComponent.cs create mode 100644 Content.Server/GameTicking/Rules/Configurations/NinjaRuleConfiguration.cs create mode 100644 Content.Server/Ninja/Components/NinjaStationGridComponent.cs create mode 100644 Content.Server/Ninja/Systems/NinjaGlovesSystem.cs create mode 100644 Content.Server/Ninja/Systems/NinjaSuitSystem.cs create mode 100644 Content.Server/Ninja/Systems/NinjaSystem.cs create mode 100644 Content.Server/Ninja/Systems/SpiderChargeSystem.cs create mode 100644 Content.Server/Objectives/Conditions/DoorjackCondition.cs create mode 100644 Content.Server/Objectives/Conditions/DownloadCondition.cs create mode 100644 Content.Server/Objectives/Conditions/SpiderChargeCondition.cs create mode 100644 Content.Server/Objectives/Conditions/SurviveCondition.cs create mode 100644 Content.Server/Objectives/Conditions/TerrorCondition.cs create mode 100644 Content.Server/StationEvents/Events/SpaceNinjaSpawn.cs create mode 100644 Content.Shared/Ninja/Components/EnergyKatanaComponent.cs create mode 100644 Content.Shared/Ninja/Components/NinjaComponent.cs create mode 100644 Content.Shared/Ninja/Components/NinjaGlovesComponent.cs create mode 100644 Content.Shared/Ninja/Components/NinjaSuitComponent.cs create mode 100644 Content.Shared/Ninja/Components/SpiderChargeComponent.cs create mode 100644 Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs create mode 100644 Content.Shared/Ninja/Systems/NinjaGlovesSystem.cs create mode 100644 Content.Shared/Ninja/Systems/NinjaSuitSystem.cs create mode 100644 Content.Shared/Ninja/Systems/NinjaSystem.cs create mode 100644 Resources/Audio/Misc/ninja_greeting.ogg create mode 100644 Resources/Locale/en-US/ninja/gloves.ftl create mode 100644 Resources/Locale/en-US/ninja/katana.ftl create mode 100644 Resources/Locale/en-US/ninja/ninja-actions.ftl create mode 100644 Resources/Locale/en-US/ninja/role.ftl create mode 100644 Resources/Locale/en-US/ninja/spider-charge.ftl create mode 100644 Resources/Locale/en-US/ninja/terror.ftl create mode 100644 Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl create mode 100644 Resources/Locale/en-US/objectives/conditions/download-condition.ftl create mode 100644 Resources/Locale/en-US/objectives/conditions/spider-charge-condition.ftl create mode 100644 Resources/Locale/en-US/objectives/conditions/survive-condition.ftl create mode 100644 Resources/Locale/en-US/objectives/conditions/terror-condition.ftl create mode 100644 Resources/Prototypes/Alerts/ninja.yml create mode 100644 Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml create mode 100644 Resources/Prototypes/Objectives/ninjaObjectives.yml create mode 100644 Resources/Prototypes/Roles/Antags/ninja.yml create mode 100644 Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/icon.png create mode 100644 Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/meta.json create mode 100644 Resources/Textures/Objects/Weapons/Bombs/spidercharge.rsi/primed.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/equipped-BELT.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/icon.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-left.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/inhand-right.png create mode 100644 Resources/Textures/Objects/Weapons/Melee/energykatana.rsi/meta.json create mode 100644 Resources/Textures/Structures/Machines/computers.rsi/comm_icon.png 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..eabcb21ab4 --- /dev/null +++ b/Content.Client/Ninja/Systems/NinjaSuitSystem.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 NinjaSuitSystem : SharedNinjaSuitSystem +{ +} diff --git a/Content.Client/Ninja/Systems/NinjaSystem.cs b/Content.Client/Ninja/Systems/NinjaSystem.cs new file mode 100644 index 0000000000..bf9df3745b --- /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 NinjaSystem : SharedNinjaSystem +{ +} diff --git a/Content.Client/Stealth/StealthSystem.cs b/Content.Client/Stealth/StealthSystem.cs index 6ceff19576..c09f3fe2e4 100644 --- a/Content.Client/Stealth/StealthSystem.cs +++ b/Content.Client/Stealth/StealthSystem.cs @@ -44,7 +44,7 @@ public sealed class StealthSystem : SharedStealthSystem if (!enabled) { if (component.HadOutline) - AddComp(uid); + EnsureComp(uid); return; } diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index b134d15fb3..1023b1c515 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -1,5 +1,6 @@ using Content.Server.GameTicking.Rules; using Content.Server.Mind.Components; +using Content.Server.Ninja.Systems; using Content.Server.Zombies; using Content.Shared.Administration; using Content.Shared.Database; @@ -14,6 +15,7 @@ public sealed partial class AdminVerbSystem { [Dependency] private readonly ZombifyOnDeathSystem _zombify = default!; [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly NinjaSystem _ninja = default!; [Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!; [Dependency] private readonly PiratesRuleSystem _piratesRule = default!; @@ -102,5 +104,21 @@ public sealed partial class AdminVerbSystem }; args.Verbs.Add(pirate); + Verb spaceNinja = new() + { + Text = "Make space ninja", + Category = VerbCategory.Antag, + Icon = new SpriteSpecifier.Rsi(new ResourcePath("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"), + Act = () => + { + if (targetMindComp.Mind == null || targetMindComp.Mind.Session == null) + return; + + _ninja.MakeNinja(targetMindComp.Mind); + }, + Impact = LogImpact.High, + Message = Loc.GetString("admin-verb-make-space-ninja"), + }; + args.Verbs.Add(spaceNinja); } } diff --git a/Content.Server/Doors/Systems/DoorSystem.cs b/Content.Server/Doors/Systems/DoorSystem.cs index d6ecd5d65b..24f1914f66 100644 --- a/Content.Server/Doors/Systems/DoorSystem.cs +++ b/Content.Server/Doors/Systems/DoorSystem.cs @@ -266,6 +266,8 @@ public sealed class DoorSystem : SharedDoorSystem { SetState(uid, DoorState.Emagging, door); PlaySound(uid, door.SparkSound, AudioParams.Default.WithVolume(8), args.UserUid, false); + var emagged = new DoorEmaggedEvent(args.UserUid); + RaiseLocalEvent(uid, ref emagged); args.Handled = true; } } @@ -300,3 +302,12 @@ public sealed class DoorSystem : SharedDoorSystem } } +public sealed class PryFinishedEvent : EntityEventArgs { } +public sealed class PryCancelledEvent : EntityEventArgs { } + +/// +/// Event raised when a door is emagged, either with an emag or a Space Ninja's doorjack ability. +/// Used to track doors for ninja's objective. +/// +[ByRefEvent] +public readonly record struct DoorEmaggedEvent(EntityUid UserUid); diff --git a/Content.Server/Electrocution/ElectrocutionSystem.cs b/Content.Server/Electrocution/ElectrocutionSystem.cs index 811232a86a..33f38c648b 100644 --- a/Content.Server/Electrocution/ElectrocutionSystem.cs +++ b/Content.Server/Electrocution/ElectrocutionSystem.cs @@ -262,16 +262,8 @@ namespace Content.Server.Electrocution } } - /// 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/Explosion/Components/AutomatedTimerComponent.cs b/Content.Server/Explosion/Components/AutomatedTimerComponent.cs new file mode 100644 index 0000000000..aac1beebb5 --- /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 class AutomatedTimerComponent : Component +{ +} diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs index 826ed29e12..79c5455f3d 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.OnUse.cs @@ -141,7 +141,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/Configurations/NinjaRuleConfiguration.cs b/Content.Server/GameTicking/Rules/Configurations/NinjaRuleConfiguration.cs new file mode 100644 index 0000000000..5cba8542f2 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Configurations/NinjaRuleConfiguration.cs @@ -0,0 +1,66 @@ +using Content.Server.Objectives; +using Robust.Shared.Audio; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List; + +namespace Content.Server.GameTicking.Rules.Configurations; + +/// +/// Configuration for the Space Ninja antag. +/// +public sealed class NinjaRuleConfiguration : StationEventRuleConfiguration +{ + /// + /// List of objective prototype ids to add + /// + [DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + public readonly List Objectives = new(); + + // TODO: move to job and use job??? + /// + /// List of implants to inject on spawn + /// + [DataField("implants", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer))] + public readonly List Implants = new(); + + /// + /// List of threats that can be called in + /// + [DataField("threats", required: true)] + public readonly 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"); + + /// + /// Distance that the ninja spawns from the station's half AABB radius + /// + [DataField("spawnDistance")] + public float SpawnDistance = 20f; +} + +/// +/// A threat that can be called in to the station by a ninja hacking a communications console. +/// Generally some kind of mid-round antag, though you could make it call in scrubber backflow if you wanted to. +/// You wouldn't do that, right? +/// +[DataDefinition] +public sealed class Threat +{ + /// + /// Locale id for the announcement to be made from CentCom. + /// + [DataField("announcement")] + public readonly 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 readonly string Rule = default!; +} diff --git a/Content.Server/Ninja/Components/NinjaStationGridComponent.cs b/Content.Server/Ninja/Components/NinjaStationGridComponent.cs new file mode 100644 index 0000000000..7fe4004dd1 --- /dev/null +++ b/Content.Server/Ninja/Components/NinjaStationGridComponent.cs @@ -0,0 +1,13 @@ +namespace Content.Server.Ninja.Components; + +/// +/// Used by space ninja to indicate what station grid to head towards. +/// +[RegisterComponent] +public sealed class NinjaStationGridComponent : Component +{ + /// + /// The grid uid being targeted. + /// + public EntityUid Grid; +} diff --git a/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs new file mode 100644 index 0000000000..0a7fe722ec --- /dev/null +++ b/Content.Server/Ninja/Systems/NinjaGlovesSystem.cs @@ -0,0 +1,36 @@ +using Content.Server.Communications; +using Content.Server.DoAfter; +using Content.Server.Power.Components; +using Content.Shared.DoAfter; +using Content.Shared.Interaction.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; + +namespace Content.Server.Ninja.Systems; + +public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem +{ + protected override void OnDrain(EntityUid uid, NinjaDrainComponent comp, InteractionAttemptEvent args) + { + if (!GloveCheck(uid, args, out var gloves, out var user, out var target) + || !HasComp(target)) + return; + + // nicer for spam-clicking to not open apc ui, and when draining starts, so cancel the ui action + args.Cancel(); + + var doAfterArgs = new DoAfterArgs(user, comp.DrainTime, new DrainDoAfterEvent(), target: target, used: uid, eventTarget: uid) + { + BreakOnUserMove = true, + MovementThreshold = 0.5f, + CancelDuplicate = false + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + } + + protected override bool IsCommsConsole(EntityUid uid) + { + return HasComp(uid); + } +} diff --git a/Content.Server/Ninja/Systems/NinjaSuitSystem.cs b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs new file mode 100644 index 0000000000..bca1e8b6ea --- /dev/null +++ b/Content.Server/Ninja/Systems/NinjaSuitSystem.cs @@ -0,0 +1,148 @@ +using Content.Server.Emp; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.PowerCell; +using Content.Shared.Actions; +using Content.Shared.Examine; +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; + +public sealed class NinjaSuitSystem : SharedNinjaSuitSystem +{ + [Dependency] private readonly EmpSystem _emp = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly new NinjaSystem _ninja = default!; + [Dependency] private readonly PopupSystem _popups = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + // TODO: maybe have suit activation stuff + SubscribeLocalEvent(OnSuitInsertAttempt); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnTogglePhaseCloak); + SubscribeLocalEvent(OnCreateSoap); + SubscribeLocalEvent(OnRecallKatana); + SubscribeLocalEvent(OnEmp); + } + + protected override void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, NinjaComponent ninja) + { + base.NinjaEquippedSuit(uid, comp, user, ninja); + + _ninja.SetSuitPowerAlert(user); + } + + // TODO: if/when battery is in shared, put this there too + 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(); + } + } + + private void OnExamined(EntityUid uid, NinjaSuitComponent comp, ExaminedEvent args) + { + // TODO: make this also return the uid of the battery + if (_powerCell.TryGetBatteryFromSlot(uid, out var battery)) + RaiseLocalEvent(battery.Owner, args); + } + + protected override void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user) + { + base.UserUnequippedSuit(uid, comp, user); + + // remove power indicator + _ninja.SetSuitPowerAlert(user); + } + + private void OnTogglePhaseCloak(EntityUid uid, NinjaSuitComponent comp, TogglePhaseCloakEvent args) + { + args.Handled = true; + var user = args.Performer; + // need 1 second of charge to turn on stealth + var chargeNeeded = SuitWattage(comp); + if (!comp.Cloaked && (!_ninja.GetNinjaBattery(user, out var battery) || battery.CurrentCharge < chargeNeeded || _useDelay.ActiveDelay(uid))) + { + _popups.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + comp.Cloaked = !comp.Cloaked; + SetCloaked(args.Performer, comp.Cloaked); + RaiseNetworkEvent(new SetCloakedMessage() + { + User = user, + Cloaked = comp.Cloaked + }); + } + + private void OnCreateSoap(EntityUid uid, NinjaSuitComponent comp, CreateSoapEvent args) + { + args.Handled = true; + var user = args.Performer; + if (!_ninja.TryUseCharge(user, comp.SoapCharge) || _useDelay.ActiveDelay(uid)) + { + _popups.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + // try to put soap in hand, otherwise it goes on the ground + var soap = Spawn(comp.SoapPrototype, Transform(user).Coordinates); + _hands.TryPickupAnyHand(user, soap); + } + + 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; + + // 1% charge per tile + var katana = ninja.Katana.Value; + var coords = _transform.GetWorldPosition(katana); + var distance = (_transform.GetWorldPosition(user) - coords).Length; + var chargeNeeded = (float) distance * 3.6f; + if (!_ninja.TryUseCharge(user, chargeNeeded) || _useDelay.ActiveDelay(uid)) + { + _popups.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"; + _popups.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(uid)) + { + _popups.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); + } +} diff --git a/Content.Server/Ninja/Systems/NinjaSystem.cs b/Content.Server/Ninja/Systems/NinjaSystem.cs new file mode 100644 index 0000000000..318d1115bd --- /dev/null +++ b/Content.Server/Ninja/Systems/NinjaSystem.cs @@ -0,0 +1,314 @@ +using Content.Server.Administration.Commands; +using Content.Server.Body.Systems; +using Content.Server.Chat.Managers; +using Content.Server.Chat.Systems; +using Content.Server.Doors.Systems; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Configurations; +using Content.Server.Ghost.Roles.Events; +using Content.Server.Mind.Components; +using Content.Server.Ninja.Components; +using Content.Server.Objectives; +using Content.Server.Popups; +using Content.Server.Power.Components; +using Content.Server.PowerCell; +using Content.Server.Traitor; +using Content.Server.Warps; +using Content.Shared.Alert; +using Content.Shared.Doors.Components; +using Content.Shared.Implants; +using Content.Shared.Implants.Components; +using Content.Shared.Ninja.Components; +using Content.Shared.Ninja.Systems; +using Content.Shared.Roles; +using Content.Shared.Popups; +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.Prototypes; +using Robust.Shared.Random; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Server.Ninja.Systems; + +public sealed class NinjaSystem : SharedNinjaSystem +{ + [Dependency] private readonly AlertsSystem _alerts = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly IChatManager _chatMan = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly SharedSubdermalImplantSystem _implants = default!; + [Dependency] private readonly InternalsSystem _internals = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly PopupSystem _popups = default!; + [Dependency] private readonly PowerCellSystem _powerCell = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly TraitorRuleSystem _traitorRule = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNinjaStartup); + SubscribeLocalEvent(OnNinjaSpawned); + SubscribeLocalEvent(OnNinjaMindAdded); + + SubscribeLocalEvent(OnDoorEmagged); + } + + 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(Mind.Mind 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(mind); + } + + /// + /// Returns the space ninja spawn gamerule's config + /// + public NinjaRuleConfiguration RuleConfig() + { + return (NinjaRuleConfiguration) _proto.Index("SpaceNinjaSpawn").Configuration; + } + + /// + /// Update the alert for the ninja's suit power indicator. + /// + public void SetSuitPowerAlert(EntityUid uid, NinjaComponent? comp = null) + { + if (!Resolve(uid, ref comp, false) || comp.Deleted || comp.Suit == null) + { + _alerts.ClearAlert(uid, AlertType.SuitPower); + return; + } + + if (GetNinjaBattery(uid, out var battery)) + { + var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 7); + _alerts.ShowAlert(uid, AlertType.SuitPower, (short) severity); + } + else + { + _alerts.ClearAlert(uid, AlertType.SuitPower); + } + } + + /// + /// Set the station grid on an entity, either ninja spawner or the ninja itself. + /// Used to tell a ghost that takes ninja role where the station is. + /// + public void SetNinjaStationGrid(EntityUid uid, EntityUid grid) + { + var station = EnsureComp(uid); + station.Grid = grid; + } + + /// + /// Get the battery component in a ninja's suit, if it's worn. + /// + public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out BatteryComponent? battery) + { + if (TryComp(user, out var ninja) + && ninja.Suit != null + && _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out battery)) + { + return true; + } + + battery = null; + return false; + } + + public override bool TryUseCharge(EntityUid user, float charge) + { + return GetNinjaBattery(user, out var battery) && battery.TryUseCharge(charge); + } + + public override void CallInThreat(NinjaComponent comp) + { + base.CallInThreat(comp); + + var config = RuleConfig(); + if (config.Threats.Count == 0) + return; + + var threat = _random.Pick(config.Threats); + if (_proto.TryIndex(threat.Rule, out var rule)) + { + _gameTicker.AddGameRule(rule); + _chat.DispatchGlobalAnnouncement(Loc.GetString(threat.Announcement), playSound: false, colorOverride: Color.Red); + } + else + { + Logger.Error($"Threat gamerule does not exist: {threat.Rule}"); + } + } + + public override void TryDrainPower(EntityUid user, NinjaDrainComponent drain, EntityUid target) + { + if (!GetNinjaBattery(user, out var suitBattery)) + // took suit off or something, ignore draining + return; + + if (!TryComp(target, out var battery) || !TryComp(target, out var pnb)) + return; + + if (suitBattery.IsFullyCharged) + { + _popups.PopupEntity(Loc.GetString("ninja-drain-full"), user, user, PopupType.Medium); + return; + } + + if (MathHelper.CloseToPercent(battery.CurrentCharge, 0)) + { + _popups.PopupEntity(Loc.GetString("ninja-drain-empty", ("battery", target)), user, user, PopupType.Medium); + return; + } + + var available = battery.CurrentCharge; + var required = suitBattery.MaxCharge - suitBattery.CurrentCharge; + // higher tier storages can charge more + var maxDrained = pnb.MaxSupply * drain.DrainTime; + var input = Math.Min(Math.Min(available, required / drain.DrainEfficiency), maxDrained); + if (battery.TryUseCharge(input)) + { + var output = input * drain.DrainEfficiency; + suitBattery.CurrentCharge += output; + _popups.PopupEntity(Loc.GetString("ninja-drain-success", ("battery", target)), user, user); + // TODO: spark effects + _audio.PlayPvs(drain.SparkSound, target); + } + } + + private void OnNinjaStartup(EntityUid uid, NinjaComponent comp, ComponentStartup args) + { + var config = RuleConfig(); + + // start with internals on, only when spawned by event. antag control ninja won't do this due to component add order. + _internals.ToggleInternals(uid, uid, true); + + // inject starting implants + var coords = Transform(uid).Coordinates; + foreach (var id in config.Implants) + { + var implant = Spawn(id, coords); + + if (!TryComp(implant, out var implantComp)) + return; + + _implants.ForceImplant(uid, implant, implantComp); + } + + // 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(); + while (query.MoveNext(out var warpUid, out var warp)) + { + // won't be asked to detonate the nuke disk or singularity + if (warp.Location != null && !HasComp(warpUid)) + warps.Add(warpUid); + } + + if (warps.Count > 0) + comp.SpiderChargeTarget = _random.Pick(warps); + } + + private void OnNinjaSpawned(EntityUid uid, NinjaComponent comp, GhostRoleSpawnerUsedEvent args) + { + // inherit spawner's station grid + if (TryComp(args.Spawner, out var station)) + SetNinjaStationGrid(uid, station.Grid); + } + + private void OnNinjaMindAdded(EntityUid uid, NinjaComponent comp, MindAddedMessage args) + { + if (TryComp(uid, out var mind) && mind.Mind != null) + GreetNinja(mind.Mind); + } + + private void GreetNinja(Mind.Mind mind) + { + if (!mind.TryGetSession(out var session)) + return; + + var config = RuleConfig(); + var role = new TraitorRole(mind, _proto.Index("SpaceNinja")); + mind.AddRole(role); + _traitorRule.Traitors.Add(role); + foreach (var objective in config.Objectives) + { + AddObjective(mind, objective); + } + + _audio.PlayGlobal(config.GreetingSound, Filter.Empty().AddPlayer(session), false, AudioParams.Default); + _chatMan.DispatchServerMessage(session, Loc.GetString("ninja-role-greeting")); + + if (TryComp(mind.OwnedEntity, out var station)) + { + var gridPos = _transform.GetWorldPosition(station.Grid); + var ninjaPos = _transform.GetWorldPosition(mind.OwnedEntity.Value); + var vector = gridPos - ninjaPos; + var direction = vector.GetDir(); + var position = $"({(int) gridPos.X}, {(int) gridPos.Y})"; + var msg = Loc.GetString("ninja-role-greeting-direction", ("direction", direction), ("position", position)); + _chatMan.DispatchServerMessage(session, msg); + } + } + + private void OnDoorEmagged(EntityUid uid, DoorComponent door, ref DoorEmaggedEvent args) + { + // make sure it's a ninja doorjacking it + if (TryComp(args.UserUid, out var ninja)) + ninja.DoorsJacked++; + } + + private void UpdateNinja(EntityUid uid, NinjaComponent ninja, float frameTime) + { + if (ninja.Suit == null || !TryComp(ninja.Suit, out var suit)) + return; + + float wattage = _suit.SuitWattage(suit); + + SetSuitPowerAlert(uid, ninja); + if (!TryUseCharge(uid, wattage * frameTime)) + { + // ran out of power, reveal ninja + _suit.RevealNinja(ninja.Suit.Value, suit, uid); + } + } + + private void AddObjective(Mind.Mind mind, string name) + { + if (_proto.TryIndex(name, out var objective)) + mind.TryAddObjective(objective); + else + Logger.Error($"Ninja has unknown objective prototype: {name}"); + } +} diff --git a/Content.Server/Ninja/Systems/SpiderChargeSystem.cs b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs new file mode 100644 index 0000000000..94d3fb7911 --- /dev/null +++ b/Content.Server/Ninja/Systems/SpiderChargeSystem.cs @@ -0,0 +1,64 @@ +using Content.Server.Explosion.EntitySystems; +using Content.Server.Sticky.Events; +using Content.Server.Popups; +using Content.Shared.Interaction; +using Content.Shared.Ninja.Components; +using Robust.Shared.GameObjects; + +namespace Content.Server.Ninja.Systems; + +public sealed class SpiderChargeSystem : EntitySystem +{ + [Dependency] private readonly NinjaSystem _ninja = default!; + [Dependency] private readonly PopupSystem _popups = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(BeforePlant); + SubscribeLocalEvent(OnStuck); + SubscribeLocalEvent(OnExplode); + } + + private void BeforePlant(EntityUid uid, SpiderChargeComponent comp, BeforeRangedInteractEvent args) + { + var user = args.User; + + if (!TryComp(user, out var ninja)) + { + _popups.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 (ninja.SpiderChargeTarget != null) + { + // assumes warp point still exists + var target = Transform(ninja.SpiderChargeTarget.Value).MapPosition; + var coords = args.ClickLocation.ToMap(EntityManager, _transform); + if (!coords.InRange(target, comp.Range)) + { + _popups.PopupEntity(Loc.GetString("spider-charge-too-far"), user, user); + args.Handled = true; + return; + } + } + } + + private void OnStuck(EntityUid uid, SpiderChargeComponent comp, EntityStuckEvent args) + { + comp.Planter = args.User; + } + + private void OnExplode(EntityUid uid, SpiderChargeComponent comp, TriggerEvent args) + { + if (comp.Planter == null || !TryComp(comp.Planter, out var ninja)) + return; + + // assumes the target was destroyed, that the charge wasn't moved somehow + _ninja.DetonateSpiderCharge(ninja); + } +} diff --git a/Content.Server/Objectives/Conditions/DoorjackCondition.cs b/Content.Server/Objectives/Conditions/DoorjackCondition.cs new file mode 100644 index 0000000000..335b18f198 --- /dev/null +++ b/Content.Server/Objectives/Conditions/DoorjackCondition.cs @@ -0,0 +1,64 @@ +using Content.Server.Objectives.Interfaces; +using Content.Shared.Ninja.Components; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +[DataDefinition] +public sealed class DoorjackCondition : IObjectiveCondition +{ + private Mind.Mind? _mind; + private int _target; + + public IObjectiveCondition GetAssigned(Mind.Mind mind) + { + // TODO: clamp to number of doors on station incase its somehow a shittle or something + return new DoorjackCondition { + _mind = mind, + _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 ResourcePath("Objects/Tools/emag.rsi"), "icon"); + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (_mind?.OwnedEntity == null + || !entMan.TryGetComponent(_mind.OwnedEntity, out var ninja)) + return 0f; + + // prevent divide-by-zero + if (_target == 0) + return 1f; + + return (float) ninja.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/DownloadCondition.cs b/Content.Server/Objectives/Conditions/DownloadCondition.cs new file mode 100644 index 0000000000..18948b1955 --- /dev/null +++ b/Content.Server/Objectives/Conditions/DownloadCondition.cs @@ -0,0 +1,64 @@ +using Content.Server.Objectives.Interfaces; +using Content.Shared.Ninja.Components; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +[DataDefinition] +public sealed class DownloadCondition : IObjectiveCondition +{ + private Mind.Mind? _mind; + private int _target; + + public IObjectiveCondition GetAssigned(Mind.Mind mind) + { + // TODO: clamp to number of research nodes in tree so easily maintainable + return new DownloadCondition { + _mind = mind, + _target = IoCManager.Resolve().Next(5, 10) + }; + } + + public string Title => Loc.GetString("objective-condition-download-title", ("count", _target)); + + public string Description => Loc.GetString("objective-condition-download-description"); + + public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResourcePath("Structures/Machines/server.rsi"), "server"); + + public float Progress + { + get + { + // prevent divide-by-zero + if (_target == 0) + return 1f; + + var entMan = IoCManager.Resolve(); + if (_mind?.OwnedEntity == null + || !entMan.TryGetComponent(_mind.OwnedEntity, out var ninja)) + return 0f; + + return (float) ninja.DownloadedNodes.Count / (float) _target; + } + } + + public float Difficulty => 2.5f; + + public bool Equals(IObjectiveCondition? other) + { + return other is DownloadCondition 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 DownloadCondition 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..c8ea4897df --- /dev/null +++ b/Content.Server/Objectives/Conditions/SpiderChargeCondition.cs @@ -0,0 +1,73 @@ +using Content.Server.Objectives.Interfaces; +using Content.Server.Warps; +using Content.Shared.Ninja.Components; +using Robust.Shared.Random; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +[DataDefinition] +public sealed class SpiderChargeCondition : IObjectiveCondition +{ + private Mind.Mind? _mind; + + public IObjectiveCondition GetAssigned(Mind.Mind mind) + { + return new SpiderChargeCondition { + _mind = mind + }; + } + + public string Title + { + get + { + var entMan = IoCManager.Resolve(); + if (_mind?.OwnedEntity == null + || !entMan.TryGetComponent(_mind.OwnedEntity, out var ninja) + || ninja.SpiderChargeTarget == null + || !entMan.TryGetComponent(ninja.SpiderChargeTarget, out var warp) + || warp.Location == null) + // if you are funny and microbomb then press c, you get this + 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 ResourcePath("Objects/Weapons/Bombs/spidercharge.rsi"), "icon"); + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (_mind?.OwnedEntity == null + || !entMan.TryGetComponent(_mind.OwnedEntity, out var ninja)) + return 0f; + + return ninja.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/SurviveCondition.cs b/Content.Server/Objectives/Conditions/SurviveCondition.cs new file mode 100644 index 0000000000..b4bfe4426d --- /dev/null +++ b/Content.Server/Objectives/Conditions/SurviveCondition.cs @@ -0,0 +1,46 @@ +using Content.Server.Objectives.Interfaces; +using JetBrains.Annotations; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions +{ + [UsedImplicitly] + [DataDefinition] + public sealed class SurviveCondition : IObjectiveCondition + { + private Mind.Mind? _mind; + + public IObjectiveCondition GetAssigned(Mind.Mind mind) + { + return new SurviveCondition {_mind = mind}; + } + + 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 ResourcePath("Clothing/Head/Helmets/spaceninja.rsi"), "icon"); + + public float Difficulty => 0.5f; + + public float Progress => (_mind?.CharacterDeadIC ?? true) ? 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..10f76b6a6e --- /dev/null +++ b/Content.Server/Objectives/Conditions/TerrorCondition.cs @@ -0,0 +1,54 @@ +using Content.Server.Objectives.Interfaces; +using Content.Shared.Ninja.Components; +using Robust.Shared.Utility; + +namespace Content.Server.Objectives.Conditions; + +[DataDefinition] +public sealed class TerrorCondition : IObjectiveCondition +{ + private Mind.Mind? _mind; + + public IObjectiveCondition GetAssigned(Mind.Mind mind) + { + return new TerrorCondition {_mind = mind}; + } + + 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 ResourcePath("Structures/Machines/computers.rsi"), "comm_icon"); + + public float Progress + { + get + { + var entMan = IoCManager.Resolve(); + if (_mind?.OwnedEntity == null + || !entMan.TryGetComponent(_mind.OwnedEntity, out var ninja)) + return 0f; + + return ninja.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/StationEvents/Events/SpaceNinjaSpawn.cs b/Content.Server/StationEvents/Events/SpaceNinjaSpawn.cs new file mode 100644 index 0000000000..7deab89c2d --- /dev/null +++ b/Content.Server/StationEvents/Events/SpaceNinjaSpawn.cs @@ -0,0 +1,81 @@ +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules; +using Content.Server.GameTicking.Rules.Configurations; +using Content.Server.Ninja.Systems; +using Content.Server.StationEvents.Components; +using Content.Server.Station.Components; +using Robust.Server.GameObjects; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; +using Robust.Shared.Prototypes; +using Robust.Shared.Random; +using System.Linq; + +namespace Content.Server.StationEvents.Events; + +/// +/// Event for spawning a Space Ninja mid-game. +/// +public sealed class SpaceNinjaSpawn : StationEventSystem +{ + [Dependency] private readonly NinjaSystem _ninja = default!; + [Dependency] private readonly IPrototypeManager _proto = default!; + [Dependency] private readonly IRobustRandom _random = default!; + [Dependency] private readonly GameTicker _ticker = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override string Prototype => "SpaceNinjaSpawn"; + + public override void Started() + { + base.Started(); + + if (StationSystem.Stations.Count == 0) + { + Sawmill.Error("No stations exist, cannot spawn space ninja!"); + return; + } + + var station = _random.Pick(StationSystem.Stations); + if (!TryComp(station, out var stationData)) + { + Sawmill.Error("Chosen station isn't a station, cannot spawn space ninja!"); + return; + } + + // find a station grid + var gridUid = StationSystem.GetLargestGrid(stationData); + if (gridUid == null || !TryComp(gridUid, out var grid)) + { + Sawmill.Error("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 config = (NinjaRuleConfiguration) Configuration; + var size = grid.LocalAABB.Size.Length / 2; + var distance = size + config.SpawnDistance; + var angle = _random.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}"); + var spawner = Spawn("SpawnPointGhostSpaceNinja", coords); + + // tell the player where the station is when they pick the role + _ninja.SetNinjaStationGrid(spawner, gridUid.Value); + + // start traitor rule incase it isn't, for the sweet greentext + var rule = _proto.Index("Traitor"); + _ticker.StartGameRule(rule); + } + + public override void Added() + { + Sawmill.Info("Added space ninja spawn rule"); + } +} diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index 4d80a8acfd..47f3f0f241 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -43,7 +43,8 @@ namespace Content.Shared.Alert Debug3, Debug4, Debug5, - Debug6 + Debug6, + SuitPower } } 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/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 7d8de660a2..2caaa23a2b 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -449,7 +449,6 @@ namespace Content.Shared.Interaction // Have to be on same map regardless. if (other.MapId != origin.MapId) return false; - var dir = other.Position - origin.Position; var length = dir.Length; diff --git a/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs new file mode 100644 index 0000000000..77ba537741 --- /dev/null +++ b/Content.Shared/Ninja/Components/EnergyKatanaComponent.cs @@ -0,0 +1,66 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Ninja.Systems; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for a Space Ninja's katana, controls its dash sound. +/// Requires a ninja with a suit for abilities to work. +/// +// basically emag but without immune tag, TODO: make the charge thing its own component and have emag use it too +[Access(typeof(EnergyKatanaSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class EnergyKatanaComponent : Component +{ + public EntityUid? Ninja = null; + + /// + /// Sound played when using dash action. + /// + [DataField("blinkSound")] + public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg"); + + /// + /// Volume control for katana dash action. + /// + [DataField("blinkVolume")] + public float BlinkVolume = 5f; + + /// + /// The maximum number of dash charges the katana can have + /// + [DataField("maxCharges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public int MaxCharges = 3; + + /// + /// The current number of dash charges on the katana + /// + [DataField("charges"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public int Charges = 3; + + /// + /// Whether or not the katana automatically recharges over time. + /// + [DataField("autoRecharge"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public bool AutoRecharge = true; + + /// + /// The time it takes to regain a single dash charge + /// + [DataField("rechargeDuration"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public TimeSpan RechargeDuration = TimeSpan.FromSeconds(20); + + /// + /// The time when the next dash charge will be added + /// + [DataField("nextChargeTime", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField] + public TimeSpan NextChargeTime = TimeSpan.MaxValue; +} + +public sealed class KatanaDashEvent : WorldTargetActionEvent { } diff --git a/Content.Shared/Ninja/Components/NinjaComponent.cs b/Content.Shared/Ninja/Components/NinjaComponent.cs new file mode 100644 index 0000000000..851be9a5a9 --- /dev/null +++ b/Content.Shared/Ninja/Components/NinjaComponent.cs @@ -0,0 +1,69 @@ +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. +/// +// TODO: Contains objective related stuff, might want to move it out somehow +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +[Access(typeof(SharedNinjaSystem))] +public sealed partial class NinjaComponent : Component +{ + /// + /// Grid entity of the station the ninja was spawned around. Set if spawned naturally by the event. + /// + public EntityUid? StationGrid; + + /// + /// Currently worn suit + /// + [ViewVariables] + public EntityUid? Suit = null; + + /// + /// Currently worn gloves + /// + [ViewVariables] + public EntityUid? Gloves = null; + + /// + /// Bound katana, set once picked up and never removed + /// + [ViewVariables] + public EntityUid? Katana = null; + + /// + /// Number of doors that have been doorjacked, used for objective + /// + [ViewVariables, AutoNetworkedField] + public int DoorsJacked = 0; + + /// + /// Research nodes that have been downloaded, used for objective + /// + // TODO: client doesn't need to know what nodes are downloaded, just how many + [ViewVariables, AutoNetworkedField] + public HashSet DownloadedNodes = new(); + + /// + /// Warp point that the spider charge has to target + /// + [ViewVariables, AutoNetworkedField] + public EntityUid? SpiderChargeTarget = null; + + /// + /// Whether the spider charge has been detonated on the target, used for objective + /// + [ViewVariables, AutoNetworkedField] + public bool SpiderChargeDetonated; + + /// + /// Whether the comms console has been hacked, used for objective + /// + [ViewVariables, AutoNetworkedField] + public bool CalledInThreat; +} diff --git a/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs new file mode 100644 index 0000000000..58b760a086 --- /dev/null +++ b/Content.Shared/Ninja/Components/NinjaGlovesComponent.cs @@ -0,0 +1,151 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.DoAfter; +using Content.Shared.Ninja.Systems; +using Content.Shared.Tag; +using Content.Shared.Toggleable; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Serialization; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; +using Robust.Shared.Utility; +using System.Threading; + +namespace Content.Shared.Ninja.Components; + +/// +/// Component for toggling glove powers. +/// Powers being enabled is controlled by GlovesEnabledComponent +/// +[Access(typeof(SharedNinjaGlovesSystem))] +[RegisterComponent, NetworkedComponent] +public sealed class NinjaGlovesComponent : Component +{ + /// + /// Entity of the ninja using these gloves, usually means enabled + /// + [ViewVariables] + public EntityUid? User; + + /// + /// The action for toggling ninja gloves abilities + /// + [DataField("toggleAction")] + public InstantAction ToggleAction = new() + { + DisplayName = "action-name-toggle-ninja-gloves", + Description = "action-desc-toggle-ninja-gloves", + Priority = -13, + Event = new ToggleActionEvent() + }; +} + +/// +/// Component for emagging doors on click, when gloves are enabled. +/// Only works on entities with DoorComponent. +/// +[RegisterComponent] +public sealed class NinjaDoorjackComponent : Component +{ + /// + /// The tag that marks an entity as immune to doorjacking + /// + [DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string EmagImmuneTag = "EmagImmune"; +} + +/// +/// Component for stunning mobs on click, when gloves are enabled. +/// Knocks them down for a bit and deals shock damage. +/// +[RegisterComponent] +public sealed class NinjaStunComponent : Component +{ + /// + /// Joules required in the suit to stun someone. Defaults to 10 uses on a small battery. + /// + [DataField("stunCharge")] + public float StunCharge = 36.0f; + + /// + /// Shock damage dealt when stunning someone + /// + [DataField("stunDamage")] + public int StunDamage = 5; + + /// + /// Time that someone is stunned for, stacks if done multiple times. + /// + [DataField("stunTime")] + public TimeSpan StunTime = TimeSpan.FromSeconds(3); +} + +/// +/// Component for draining power from APCs/substations/SMESes, when gloves are enabled. +/// +[RegisterComponent] +public sealed class NinjaDrainComponent : Component +{ + /// + /// Conversion rate between joules in a device and joules added to suit. + /// Should be very low since powercells store nothing compared to even an APC. + /// + [DataField("drainEfficiency")] + public float DrainEfficiency = 0.001f; + + /// + /// Time that the do after takes to drain charge from a battery, in seconds + /// + [DataField("drainTime")] + public float DrainTime = 1f; + + [DataField("sparkSound")] + public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks"); +} + +/// +/// Component for downloading research nodes from a R&D server, when gloves are enabled. +/// Requirement for greentext. +/// +[RegisterComponent] +public sealed class NinjaDownloadComponent : Component +{ + /// + /// Time taken to download research from a server + /// + [DataField("downloadTime")] + public float DownloadTime = 20f; +} + + +/// +/// Component for hacking a communications console to call in a threat. +/// Called threat is rolled from the ninja gamerule config. +/// +[RegisterComponent] +public sealed class NinjaTerrorComponent : Component +{ + /// + /// Time taken to hack the console + /// + [DataField("terrorTime")] + public float TerrorTime = 20f; +} + +/// +/// DoAfter event for drain ability. +/// +[Serializable, NetSerializable] +public sealed class DrainDoAfterEvent : SimpleDoAfterEvent { } + +/// +/// DoAfter event for research download ability. +/// +[Serializable, NetSerializable] +public sealed class DownloadDoAfterEvent : SimpleDoAfterEvent { } + +/// +/// DoAfter event for comms console terror ability. +/// +[Serializable, NetSerializable] +public sealed class TerrorDoAfterEvent : SimpleDoAfterEvent { } diff --git a/Content.Shared/Ninja/Components/NinjaSuitComponent.cs b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs new file mode 100644 index 0000000000..575940da11 --- /dev/null +++ b/Content.Shared/Ninja/Components/NinjaSuitComponent.cs @@ -0,0 +1,148 @@ +using Content.Shared.Actions; +using Content.Shared.Actions.ActionTypes; +using Content.Shared.Ninja.Systems; +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; + +// TODO: ResourcePath -> ResPath when thing gets merged + +/// +/// Component for ninja suit abilities and power consumption. +/// As an implementation detail, dashing with katana is a suit action which isn't ideal. +/// +[Access(typeof(SharedNinjaSuitSystem))] +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] +public sealed partial class NinjaSuitComponent : Component +{ + [ViewVariables, AutoNetworkedField] + public bool Cloaked = false; + + /// + /// The action for toggling suit phase cloak ability + /// + [DataField("togglePhaseCloakAction")] + public InstantAction TogglePhaseCloakAction = new() + { + UseDelay = TimeSpan.FromSeconds(5), // have to plan un/cloaking ahead of time + DisplayName = "action-name-toggle-phase-cloak", + Description = "action-desc-toggle-phase-cloak", + Priority = -9, + Event = new TogglePhaseCloakEvent() + }; + + /// + /// 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; + + /// + /// The action for creating throwing soap, in place of ninja throwing stars since embedding doesn't exist. + /// + [DataField("createSoapAction")] + public InstantAction CreateSoapAction = new() + { + UseDelay = TimeSpan.FromSeconds(10), + Icon = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Specific/Janitorial/soap.rsi"), "soap"), + ItemIconStyle = ItemActionIconStyle.NoItem, + DisplayName = "action-name-create-soap", + Description = "action-desc-create-soap", + Priority = -10, + Event = new CreateSoapEvent() + }; + + /// + /// Battery charge used to create a throwing soap. Can do it 25 times on a small-capacity power cell. + /// + [DataField("soapCharge")] + public float SoapCharge = 14.4f; + + /// + /// Soap item to create with the action + /// + [DataField("soapPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] + public string SoapPrototype = "SoapNinja"; + + /// + /// The action for recalling a bound energy katana + /// + [DataField("recallkatanaAction")] + public InstantAction RecallKatanaAction = new() + { + UseDelay = TimeSpan.FromSeconds(1), + Icon = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Weapons/Melee/energykatana.rsi"), "icon"), + ItemIconStyle = ItemActionIconStyle.NoItem, + DisplayName = "action-name-recall-katana", + Description = "action-desc-recall-katana", + Priority = -11, + Event = new RecallKatanaEvent() + }; + + /// + /// The action for dashing somewhere using katana + /// + [DataField("katanaDashAction")] + public WorldTargetAction KatanaDashAction = new() + { + Icon = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Magic/magicactions.rsi"), "blink"), + ItemIconStyle = ItemActionIconStyle.NoItem, + DisplayName = "action-name-katana-dash", + Description = "action-desc-katana-dash", + Priority = -12, + Event = new KatanaDashEvent(), + // doing checks manually + CheckCanAccess = false, + Range = 0f + }; + + /// + /// The action for creating an EMP burst + /// + [DataField("empAction")] + public InstantAction EmpAction = new() + { + Icon = new SpriteSpecifier.Rsi(new ResourcePath("Objects/Weapons/Grenades/empgrenade.rsi"), "icon"), + ItemIconStyle = ItemActionIconStyle.BigAction, + DisplayName = "action-name-em-burst", + Description = "action-desc-em-burst", + Priority = -13, + Event = new NinjaEmpEvent() + }; + + /// + /// 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; +} + +public sealed class TogglePhaseCloakEvent : InstantActionEvent { } + +public sealed class CreateSoapEvent : InstantActionEvent { } + +public sealed class RecallKatanaEvent : InstantActionEvent { } + +public sealed class NinjaEmpEvent : InstantActionEvent { } diff --git a/Content.Shared/Ninja/Components/SpiderChargeComponent.cs b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs new file mode 100644 index 0000000000..221716c599 --- /dev/null +++ b/Content.Shared/Ninja/Components/SpiderChargeComponent.cs @@ -0,0 +1,17 @@ +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] +public sealed class SpiderChargeComponent : Component +{ + /// Range for planting within the target area + [DataField("range")] + public float Range = 10f; + + /// The ninja that planted this charge + [ViewVariables] + public EntityUid? Planter = null; +} diff --git a/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs new file mode 100644 index 0000000000..b7adb5415a --- /dev/null +++ b/Content.Shared/Ninja/Systems/EnergyKatanaSystem.cs @@ -0,0 +1,147 @@ +using Content.Shared.Examine; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Interaction; +using Content.Shared.Inventory.Events; +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; + +/// +/// System for katana dashing, recharging and what not. +/// +// TODO: move all recharging stuff into its own system and use for emag too +public sealed class EnergyKatanaSystem : EntitySystem +{ + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly SharedNinjaSystem _ninja = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnExamine); + SubscribeLocalEvent(OnDash); + SubscribeLocalEvent(OnUnpaused); + } + + private void OnEquipped(EntityUid uid, EnergyKatanaComponent comp, GotEquippedEvent args) + { + // check if already bound + if (comp.Ninja != null) + return; + + // check if ninja already has a katana bound + var user = args.Equipee; + if (!TryComp(user, out var ninja) || ninja.Katana != null) + return; + + // bind it + comp.Ninja = user; + _ninja.BindKatana(ninja, uid); + } + + private void OnUnpaused(EntityUid uid, EnergyKatanaComponent component, ref EntityUnpausedEvent args) + { + component.NextChargeTime += args.PausedTime; + } + + private void OnExamine(EntityUid uid, EnergyKatanaComponent component, ExaminedEvent args) + { + args.PushMarkup(Loc.GetString("emag-charges-remaining", ("charges", component.Charges))); + if (component.Charges == component.MaxCharges) + { + args.PushMarkup(Loc.GetString("emag-max-charges")); + return; + } + var timeRemaining = Math.Round((component.NextChargeTime - _timing.CurTime).TotalSeconds); + args.PushMarkup(Loc.GetString("emag-recharging", ("seconds", timeRemaining))); + } + + // TODO: remove and use LimitedCharges+AutoRecharge + public override void Update(float frameTime) + { + base.Update(frameTime); + + foreach (var comp in EntityQuery()) + { + if (!comp.AutoRecharge) + continue; + + if (comp.Charges == comp.MaxCharges) + continue; + + if (_timing.CurTime < comp.NextChargeTime) + continue; + + ChangeCharge(comp.Owner, 1, true, comp); + } + } + + public void OnDash(EntityUid suit, NinjaSuitComponent comp, KatanaDashEvent args) + { + var user = args.Performer; + args.Handled = true; + if (!TryComp(user, out var ninja) || ninja.Katana == null) + return; + + var uid = ninja.Katana.Value; + if (!TryComp(uid, out var katana) || !_hands.IsHolding(user, uid, out var _)) + { + _popups.PopupEntity(Loc.GetString("ninja-katana-not-held"), user, user); + return; + } + + if (katana.Charges <= 0) + { + _popups.PopupEntity(Loc.GetString("emag-no-charges"), user, user); + return; + } + + // TODO: check that target is not dense + 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 + _popups.PopupEntity(Loc.GetString("ninja-katana-cant-see"), user, user); + return; + } + + _transform.SetCoordinates(user, args.Target); + _transform.AttachToGridOrMap(user); + _audio.PlayPvs(katana.BlinkSound, user, AudioParams.Default.WithVolume(katana.BlinkVolume)); + // TODO: show the funny green man thing + ChangeCharge(uid, -1, false, katana); + } + + /// + /// Changes the charge on an energy katana. + /// + public bool ChangeCharge(EntityUid uid, int change, bool resetTimer, EnergyKatanaComponent? katana = null) + { + if (!Resolve(uid, ref katana)) + return false; + + if (katana.Charges + change < 0 || katana.Charges + change > katana.MaxCharges) + return false; + + if (resetTimer || katana.Charges == katana.MaxCharges) + katana.NextChargeTime = _timing.CurTime + katana.RechargeDuration; + + katana.Charges += change; + Dirty(katana); + return true; + } +} diff --git a/Content.Shared/Ninja/Systems/NinjaGlovesSystem.cs b/Content.Shared/Ninja/Systems/NinjaGlovesSystem.cs new file mode 100644 index 0000000000..bccb3d9521 --- /dev/null +++ b/Content.Shared/Ninja/Systems/NinjaGlovesSystem.cs @@ -0,0 +1,314 @@ +using Content.Shared.Actions; +using Content.Shared.Administration.Logs; +using Content.Shared.CombatMode; +using Content.Shared.Damage.Components; +using Content.Shared.Database; +using Content.Shared.Doors.Components; +using Content.Shared.DoAfter; +using Content.Shared.Electrocution; +using Content.Shared.Emag.Systems; +using Content.Shared.Examine; +using Content.Shared.Hands.Components; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Components; +using Content.Shared.Interaction.Events; +using Content.Shared.Inventory.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Popups; +using Content.Shared.Research.Components; +using Content.Shared.Tag; +using Content.Shared.Timing; +using Content.Shared.Toggleable; +using Robust.Shared.Network; +using Robust.Shared.Timing; +using System.Diagnostics.CodeAnalysis; + +namespace Content.Shared.Ninja.Systems; + +public abstract class SharedNinjaGlovesSystem : EntitySystem +{ + [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; + [Dependency] private readonly SharedCombatModeSystem _combatMode = default!; + [Dependency] protected readonly SharedDoAfterSystem _doAfter = default!; + [Dependency] private readonly SharedElectrocutionSystem _electrocution = default!; + [Dependency] private readonly EmagSystem _emag = default!; + [Dependency] private readonly SharedInteractionSystem _interaction = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedNinjaSystem _ninja = default!; + [Dependency] private readonly SharedPopupSystem _popups = default!; + [Dependency] private readonly TagSystem _tags = default!; + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly UseDelaySystem _useDelay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnExamined); + SubscribeLocalEvent(OnToggleAction); + SubscribeLocalEvent(OnUnequipped); + + SubscribeLocalEvent(OnDoorjack); + + SubscribeLocalEvent(OnStun); + + SubscribeLocalEvent(OnDrain); + SubscribeLocalEvent(OnDrainDoAfter); + + SubscribeLocalEvent(OnDownload); + SubscribeLocalEvent(OnDownloadDoAfter); + + SubscribeLocalEvent(OnTerror); + SubscribeLocalEvent(OnTerrorDoAfter); + } + + /// + /// Disable glove abilities and show the popup if they were enabled previously. + /// + public void DisableGloves(NinjaGlovesComponent comp, EntityUid user) + { + if (comp.User != null) + { + comp.User = null; + _popups.PopupEntity(Loc.GetString("ninja-gloves-off"), user, user); + } + } + + private void OnGetItemActions(EntityUid uid, NinjaGlovesComponent comp, GetItemActionsEvent args) + { + args.Actions.Add(comp.ToggleAction); + } + + private void OnToggleAction(EntityUid uid, NinjaGlovesComponent comp, ToggleActionEvent args) + { + // client prediction desyncs it hard + if (args.Handled || !_timing.IsFirstTimePredicted) + 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)) + { + ClientPopup(Loc.GetString("ninja-gloves-not-wearing-suit"), user); + return; + } + + var enabling = comp.User == null; + var message = Loc.GetString(enabling ? "ninja-gloves-on" : "ninja-gloves-off"); + ClientPopup(message, user); + + if (enabling) + { + comp.User = user; + _ninja.AssignGloves(ninja, uid); + // set up interaction relay for handling glove abilities, comp.User is used to see the actual user of the events + _interaction.SetRelay(user, uid, EnsureComp(user)); + } + else + { + comp.User = null; + _ninja.AssignGloves(ninja, null); + RemComp(user); + } + } + + 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")); + } + + private void OnUnequipped(EntityUid uid, NinjaGlovesComponent comp, GotUnequippedEvent args) + { + comp.User = null; + if (TryComp(args.Equipee, out var ninja)) + _ninja.AssignGloves(ninja, null); + } + + /// + /// Helper for glove ability handlers, checks gloves, range, combat mode and stuff. + /// + protected bool GloveCheck(EntityUid uid, InteractionAttemptEvent args, [NotNullWhen(true)] out NinjaGlovesComponent? gloves, + out EntityUid user, out EntityUid target) + { + if (args.Target != null && TryComp(uid, out gloves) + && gloves.User != null + && !_combatMode.IsInCombatMode(gloves.User) + && _timing.IsFirstTimePredicted + && TryComp(gloves.User, out var hands) + && hands.ActiveHandEntity == null) + { + user = gloves.User.Value; + target = args.Target.Value; + + if (_interaction.InRangeUnobstructed(user, target)) + return true; + } + + gloves = null; + user = target = EntityUid.Invalid; + return false; + } + + private void OnDoorjack(EntityUid uid, NinjaDoorjackComponent comp, InteractionAttemptEvent args) + { + if (!GloveCheck(uid, args, out var gloves, out var user, out var target)) + return; + + // only allowed to emag non-immune doors + if (!HasComp(target) || _tags.HasTag(target, comp.EmagImmuneTag)) + return; + + var handled = _emag.DoEmagEffect(user, target); + if (!handled) + return; + + ClientPopup(Loc.GetString("ninja-doorjack-success", ("target", Identity.Entity(target, EntityManager))), user, PopupType.Medium); + _adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} doorjacked {ToPrettyString(target):target}"); + } + + private void OnStun(EntityUid uid, NinjaStunComponent comp, InteractionAttemptEvent args) + { + if (!GloveCheck(uid, args, out var gloves, out var user, out var target)) + return; + + // short cooldown to prevent instant stunlocking + if (_useDelay.ActiveDelay(uid)) + return; + + // battery can't be predicted since it's serverside + if (user == target || _net.IsClient || !HasComp(target)) + return; + + // take charge from battery + if (!_ninja.TryUseCharge(user, comp.StunCharge)) + { + _popups.PopupEntity(Loc.GetString("ninja-no-power"), user, user); + return; + } + + // not holding hands with target so insuls don't matter + _electrocution.TryDoElectrocution(target, uid, comp.StunDamage, comp.StunTime, false, ignoreInsulation: true); + _useDelay.BeginDelay(uid); + } + + // can't predict PNBC existing so only done on server. + protected virtual void OnDrain(EntityUid uid, NinjaDrainComponent comp, InteractionAttemptEvent args) { } + + private void OnDrainDoAfter(EntityUid uid, NinjaDrainComponent comp, DrainDoAfterEvent args) + { + if (args.Cancelled || args.Handled || args.Target == null) + return; + + _ninja.TryDrainPower(args.User, comp, args.Target.Value); + } + + private void OnDownload(EntityUid uid, NinjaDownloadComponent comp, InteractionAttemptEvent args) + { + if (!GloveCheck(uid, args, out var gloves, out var user, out var target)) + return; + + // can only hack the server, not a random console + if (!TryComp(target, out var database) || HasComp(target)) + return; + + // fail fast if theres no tech right now + if (database.TechnologyIds.Count == 0) + { + ClientPopup(Loc.GetString("ninja-download-fail"), user); + return; + } + + var doAfterArgs = new DoAfterArgs(user, comp.DownloadTime, new DownloadDoAfterEvent(), target: target, used: uid, eventTarget: uid) + { + BreakOnDamage = true, + BreakOnUserMove = true, + MovementThreshold = 0.5f, + CancelDuplicate = false + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + args.Cancel(); + } + + private void OnDownloadDoAfter(EntityUid uid, NinjaDownloadComponent comp, DownloadDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + var user = args.User; + var target = args.Target; + + if (!TryComp(user, out var ninja) + || !TryComp(target, out var database)) + return; + + var gained = _ninja.Download(ninja, database.TechnologyIds); + var str = gained == 0 + ? Loc.GetString("ninja-download-fail") + : Loc.GetString("ninja-download-success", ("count", gained), ("server", target)); + + _popups.PopupEntity(str, user, user, PopupType.Medium); + } + + private void OnTerror(EntityUid uid, NinjaTerrorComponent comp, InteractionAttemptEvent args) + { + if (!GloveCheck(uid, args, out var gloves, out var user, out var target) + || !TryComp(user, out var ninja)) + return; + + if (!IsCommsConsole(target)) + return; + + // can only do it once + if (ninja.CalledInThreat) + { + _popups.PopupEntity(Loc.GetString("ninja-terror-already-called"), user, user); + return; + } + + var doAfterArgs = new DoAfterArgs(user, comp.TerrorTime, new TerrorDoAfterEvent(), target: target, used: uid, eventTarget: uid) + { + BreakOnDamage = true, + BreakOnUserMove = true, + MovementThreshold = 0.5f, + CancelDuplicate = false + }; + + _doAfter.TryStartDoAfter(doAfterArgs); + // FIXME: doesnt work, don't show the console popup + args.Cancel(); + } + + //for some reason shared comms console component isn't a component, so this has to be done server-side + protected virtual bool IsCommsConsole(EntityUid uid) + { + return false; + } + + private void OnTerrorDoAfter(EntityUid uid, NinjaTerrorComponent comp, TerrorDoAfterEvent args) + { + if (args.Cancelled || args.Handled) + return; + + var user = args.User; + if (!TryComp(user, out var ninja) || ninja.CalledInThreat) + return; + + _ninja.CallInThreat(ninja); + } + + private void ClientPopup(string msg, EntityUid user, PopupType type = PopupType.Small) + { + if (_net.IsClient) + _popups.PopupEntity(msg, user, user, type); + } +} diff --git a/Content.Shared/Ninja/Systems/NinjaSuitSystem.cs b/Content.Shared/Ninja/Systems/NinjaSuitSystem.cs new file mode 100644 index 0000000000..3090218865 --- /dev/null +++ b/Content.Shared/Ninja/Systems/NinjaSuitSystem.cs @@ -0,0 +1,151 @@ +using Content.Shared.Actions; +using Content.Shared.Inventory.Events; +using Content.Shared.Ninja.Components; +using Content.Shared.Stealth; +using Content.Shared.Stealth.Components; +using Content.Shared.Timing; +using Robust.Shared.Network; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Ninja.Systems; + +public abstract class SharedNinjaSuitSystem : EntitySystem +{ + [Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!; + [Dependency] protected readonly SharedNinjaSystem _ninja = default!; + [Dependency] private readonly SharedStealthSystem _stealth = default!; + [Dependency] protected readonly UseDelaySystem _useDelay = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnEquipped); + SubscribeLocalEvent(OnGetItemActions); + SubscribeLocalEvent(OnUnequipped); + + SubscribeNetworkEvent(OnSetCloakedMessage); + } + + 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); + } + + private void OnGetItemActions(EntityUid uid, NinjaSuitComponent comp, GetItemActionsEvent args) + { + args.Actions.Add(comp.TogglePhaseCloakAction); + args.Actions.Add(comp.RecallKatanaAction); + // TODO: ninja stars instead of soap, when embedding is a thing + // The cooldown should also be reduced from 10 to 1 or so + args.Actions.Add(comp.CreateSoapAction); + args.Actions.Add(comp.KatanaDashAction); + args.Actions.Add(comp.EmpAction); + } + + 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, NinjaComponent ninja) + { + // mark the user as wearing this suit, used when being attacked among other things + _ninja.AssignSuit(ninja, uid); + + // initialize phase cloak + EnsureComp(user); + SetCloaked(user, comp.Cloaked); + } + + /// + /// Force uncloak the user, disables suit abilities if the bool is set. + /// + public void RevealNinja(EntityUid uid, NinjaSuitComponent comp, EntityUid user, bool disableAbilities = false) + { + if (comp.Cloaked) + { + comp.Cloaked = false; + SetCloaked(user, false); + // TODO: add the box open thing its funny + + if (disableAbilities) + _useDelay.BeginDelay(uid); + } + } + + /// + /// Returns the power used by a suit + /// + public float SuitWattage(NinjaSuitComponent suit) + { + float wattage = suit.PassiveWattage; + if (suit.Cloaked) + wattage += suit.CloakWattage; + return wattage; + } + + /// + /// Sets the stealth effect for a ninja cloaking. + /// Does not update suit Cloaked field, has to be done yourself. + /// + protected void SetCloaked(EntityUid user, bool cloaked) + { + if (!TryComp(user, out var stealth) || stealth.Deleted) + return; + + // slightly visible, but doesn't change when moving so it's ok + var visibility = cloaked ? stealth.MinVisibility + 0.25f : stealth.MaxVisibility; + _stealth.SetVisibility(user, visibility, stealth); + _stealth.SetEnabled(user, cloaked, stealth); + } + + /// + /// 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) + { + // mark the user as not wearing a suit + if (TryComp(user, out var ninja)) + { + _ninja.AssignSuit(ninja, null); + // disable glove abilities + if (ninja.Gloves != null && TryComp(ninja.Gloves.Value, out var gloves)) + _gloves.DisableGloves(gloves, user); + } + + // force uncloak the user + comp.Cloaked = false; + SetCloaked(user, false); + RemComp(user); + } + + private void OnSetCloakedMessage(SetCloakedMessage msg) + { + if (TryComp(msg.User, out var ninja) && TryComp(ninja.Suit, out var suit)) + { + suit.Cloaked = msg.Cloaked; + SetCloaked(msg.User, msg.Cloaked); + } + } +} + +/// +/// Calls SetCloaked on the client from the server, along with updating the suit Cloaked bool. +/// +[Serializable, NetSerializable] +public sealed class SetCloakedMessage : EntityEventArgs +{ + public EntityUid User; + public bool Cloaked; +} diff --git a/Content.Shared/Ninja/Systems/NinjaSystem.cs b/Content.Shared/Ninja/Systems/NinjaSystem.cs new file mode 100644 index 0000000000..d1b51ad77c --- /dev/null +++ b/Content.Shared/Ninja/Systems/NinjaSystem.cs @@ -0,0 +1,101 @@ +using Content.Shared.Ninja.Components; +using Content.Shared.Weapons.Melee.Events; +using Robust.Shared.GameStates; +using Robust.Shared.Network; + +namespace Content.Shared.Ninja.Systems; + +public abstract class SharedNinjaSystem : EntitySystem +{ + [Dependency] protected readonly SharedNinjaSuitSystem _suit = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnNinjaAttacked); + } + + /// + /// Sets the station grid entity that the ninja was spawned near. + /// + public void SetStationGrid(NinjaComponent comp, EntityUid? grid) + { + comp.StationGrid = grid; + } + + /// + /// Set the ninja's worn suit entity + /// + public void AssignSuit(NinjaComponent comp, EntityUid? suit) + { + comp.Suit = suit; + } + + /// + /// Set the ninja's worn gloves entity + /// + public void AssignGloves(NinjaComponent comp, EntityUid? gloves) + { + comp.Gloves = gloves; + } + + /// + /// Bind a katana entity to a ninja, letting it be recalled and dash. + /// + public void BindKatana(NinjaComponent comp, EntityUid? katana) + { + comp.Katana = katana; + } + + // TODO: remove when objective stuff moved into objectives somehow + public void DetonateSpiderCharge(NinjaComponent comp) + { + comp.SpiderChargeDetonated = true; + } + + /// + /// Marks the objective as complete. + /// On server, makes announcement and adds rule of random threat. + /// + public virtual void CallInThreat(NinjaComponent comp) + { + comp.CalledInThreat = true; + } + + /// + /// Drain power from a target battery into the ninja's suit battery. + /// Serverside only. + /// + public virtual void TryDrainPower(EntityUid user, NinjaDrainComponent drain, EntityUid target) + { + } + + /// + /// Download the given set of nodes, returning how many new nodes were downloaded.' + /// + public int Download(NinjaComponent ninja, List ids) + { + var oldCount = ninja.DownloadedNodes.Count; + ninja.DownloadedNodes.UnionWith(ids); + var newCount = ninja.DownloadedNodes.Count; + return newCount - oldCount; + } + + /// + /// 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; + } + + private void OnNinjaAttacked(EntityUid uid, NinjaComponent comp, AttackedEvent args) + { + if (comp.Suit != null && TryComp(comp.Suit, out var suit) && suit.Cloaked) + { + _suit.RevealNinja(comp.Suit.Value, suit, uid, true); + } + } +} diff --git a/Resources/Audio/Misc/attributions.yml b/Resources/Audio/Misc/attributions.yml index 1ab305412c..0d65aab7be 100644 --- a/Resources/Audio/Misc/attributions.yml +++ b/Resources/Audio/Misc/attributions.yml @@ -2,3 +2,8 @@ license: "CC-BY-NC-SA-3.0" copyright: "Taken from TG station." source: "https://github.com/tgstation/tgstation/commit/97945e7d08d1457ffc27e46526a48c0453cc95e4" + +- files: ["ninja_greeting.ogg"] + license: "CC-BY-NC-SA-3.0" + copyright: "Taken from TG station." + source: "https://github.com/tgstation/tgstation/commit/b02b93ce2ab891164511a973493cdf951b4120f7" diff --git a/Resources/Audio/Misc/ninja_greeting.ogg b/Resources/Audio/Misc/ninja_greeting.ogg new file mode 100644 index 0000000000000000000000000000000000000000..e8f17bdea6cd3c93cdbd377cd8ed2fa0da16860a GIT binary patch literal 61293 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 F{{bx@H)#L> literal 0 HcmV?d00001 diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl index 529e960fb3..a906eecc56 100644 --- a/Resources/Locale/en-US/administration/antag.ftl +++ b/Resources/Locale/en-US/administration/antag.ftl @@ -2,4 +2,5 @@ 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. Note that you must enable the Traitor game rule for the end round summary, as space ninja uses this. diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 4b5980696d..de78b8341c 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -83,3 +83,6 @@ alerts-pulled-desc = You're being pulled. Move to break free. alerts-pulling-name = Pulling alerts-pulling-desc = You're pulling something. Click the alert to stop. + +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/game-ticking/game-presets/preset-traitor.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl index 44c4a3cc32..35d29eda74 100644 --- a/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-traitor.ftl @@ -19,6 +19,7 @@ traitor-user-was-a-traitor-with-objectives-named = [color=White]{$name}[/color] traitor-was-a-traitor-with-objectives-named = [color=White]{$name}[/color] was a traitor who had the following objectives: preset-traitor-objective-issuer-syndicate = [color=#87cefa]The Syndicate[/color] +preset-traitor-objective-issuer-spiderclan = [color=#33cc00]Spider Clan[/color] # Shown at the end of a round of Traitor traitor-objective-condition-success = {$condition} | [color={$markupColor}]Success![/color] diff --git a/Resources/Locale/en-US/ninja/gloves.ftl b/Resources/Locale/en-US/ninja/gloves.ftl new file mode 100644 index 0000000000..af3b207a0d --- /dev/null +++ b/Resources/Locale/en-US/ninja/gloves.ftl @@ -0,0 +1,7 @@ +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..30fba96c48 --- /dev/null +++ b/Resources/Locale/en-US/ninja/katana.ftl @@ -0,0 +1,4 @@ +ninja-katana-recalled = Your Energy Katana teleports into your hand! +ninja-katana-not-held = You aren't holding your katana! +ninja-katana-cant-see = You can't see that! +ninja-hands-full = Your hands are full! 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..04e63fc6d1 --- /dev/null +++ b/Resources/Locale/en-US/ninja/ninja-actions.ftl @@ -0,0 +1,27 @@ +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-soap = Create soap +action-desc-create-soap = Channels suit power into creating a bar of ninja soap. The future is now, old man! + +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-full-power = Suit battery is already full +ninja-drain-empty = {CAPITALIZE(THE($battery))} does not have enough power to drain +ninja-drain-success = You drain power from {THE($battery)}! + +ninja-download-fail = No new research nodes were copied... +ninja-download-success = Copied {$count} new nodes from {THE($server)}. + +ninja-terror-already-called = You already called in a threat! diff --git a/Resources/Locale/en-US/ninja/role.ftl b/Resources/Locale/en-US/ninja/role.ftl new file mode 100644 index 0000000000..45dd7e7b93 --- /dev/null +++ b/Resources/Locale/en-US/ninja/role.ftl @@ -0,0 +1,5 @@ +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. + +ninja-role-greeting-direction = The station is located to your {$direction} at {$position}. 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/ninja/terror.ftl b/Resources/Locale/en-US/ninja/terror.ftl new file mode 100644 index 0000000000..4bc80c0bd6 --- /dev/null +++ b/Resources/Locale/en-US/ninja/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/objectives/conditions/doorjack-condition.ftl b/Resources/Locale/en-US/objectives/conditions/doorjack-condition.ftl new file mode 100644 index 0000000000..cc8c2fc002 --- /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 = Use your gloves to doorjack {$count} airlocks on the station. diff --git a/Resources/Locale/en-US/objectives/conditions/download-condition.ftl b/Resources/Locale/en-US/objectives/conditions/download-condition.ftl new file mode 100644 index 0000000000..cecd7c07c6 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/download-condition.ftl @@ -0,0 +1,2 @@ +objective-condition-download-title = Download {$count} research nodes. +objective-condition-download-description = Use your gloves on a research server to download its data. 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..f24abb670e --- /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 charge in {$location} +objective-condition-spider-charge-no-target = Detonate the spider charge... somewhere? +objective-condition-spider-charge-description = Detonate your starter bomb in a specific location. Note that the bomb will not work anywhere else! 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 fb3e14d677..390b635884 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -18,3 +18,6 @@ roles-antag-nuclear-operative-commander-objective = Lead your team to the destru roles-antag-nuclear-operative-name = Nuclear operative roles-antag-nuclear-operative-objective = Find the nuke disk and blow up the station. + +roles-antag-space-ninja-name = Space Ninja +roles-antag-space-ninja-objective = Energy sword everything, nom on electrical wires. diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 090256de7a..402fbc3e03 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..7a0c4a7d2f --- /dev/null +++ b/Resources/Prototypes/Alerts/ninja.yml @@ -0,0 +1,21 @@ +- type: alert + id: SuitPower + icons: + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina0 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina1 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina2 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina3 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina4 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina5 + - sprite: /Textures/Interface/Alerts/stamina.rsi + state: stamina6 + name: alerts-suit-power-name + description: alerts-suit-power-desc + minSeverity: 0 + maxSeverity: 6 diff --git a/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml b/Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/satchel.yml index cb9f4e9d92..93454dc206 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/Hands/gloves.yml b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml index 9edab0d4d1..80dcd1ae01 100644 --- a/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml +++ b/Resources/Prototypes/Entities/Clothing/Hands/gloves.yml @@ -203,6 +203,17 @@ - type: Thieving stripTimeReduction: 1 stealthy: true + - type: NinjaGloves + - type: NinjaDoorjack + - type: NinjaDrain + - type: NinjaStun + - type: NinjaDownload + - type: NinjaTerror + # not actually electrified, just used to make stun ability work + - type: Electrified + # delay for stunning to prevent instant stunlocking + - type: UseDelay + delay: 1 - type: entity parent: ClothingHandsBase diff --git a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml index 92a42e8636..19612e133c 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/helmets.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/helmets.yml @@ -137,7 +137,7 @@ - HidesHair - 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. @@ -146,7 +146,6 @@ sprite: Clothing/Head/Helmets/spaceninja.rsi - type: Clothing sprite: Clothing/Head/Helmets/spaceninja.rsi - - type: IngestionBlocker - type: Tag tags: - HidesHair diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml index 8254b3e91d..22e7f69970 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/suits.yml @@ -92,6 +92,13 @@ sprite: Clothing/OuterClothing/Suits/spaceninja.rsi - type: Clothing sprite: Clothing/OuterClothing/Suits/spaceninja.rsi + - type: PressureProtection + highPressureMultiplier: 0.6 + lowPressureMultiplier: 1000 + - type: DiseaseProtection + protection: 0.05 + - type: TemperatureProtection + coefficient: 0.01 - type: Armor modifiers: coefficients: @@ -99,6 +106,22 @@ Slash: 0.6 Piercing: 0.6 Heat: 0.6 + - 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 + # delay for when attacked while cloaked + - type: UseDelay + delay: 5 - type: entity parent: ClothingOuterBase diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index e690d890d6..25a81900b6 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -77,6 +77,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/Markers/Spawners/ghost_roles.yml b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml index 5ef59790ac..558f7fb6f4 100644 --- a/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml +++ b/Resources/Prototypes/Entities/Markers/Spawners/ghost_roles.yml @@ -93,3 +93,21 @@ - state: green - sprite: Structures/Wallmounts/signs.rsi state: radiation + +- type: entity + id: SpawnPointGhostSpaceNinja + name: ghost role spawn point + suffix: space ninja + parent: MarkerBase + components: + - type: GhostRoleMobSpawner + prototype: MobHumanSpaceNinja + 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: 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 c373aee051..ca73570c50 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/human.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/human.yml @@ -77,3 +77,26 @@ - type: Faction factions: - Syndicate + +# Space Ninja +- type: entity + noSpawn: true + name: Space Ninja + parent: MobHuman + id: MobHumanSpaceNinja + components: + - type: Loadout + prototype: SpaceNinjaGear + prototypes: [SpaceNinjaGear] + - type: Faction + factions: + - Syndicate + - type: Ninja + - type: RandomMetadata + nameSegments: + - names_ninja_title + - names_ninja + - type: Tag + tags: + # fight with honor! + - GunsDisabled diff --git a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml index 58f484a1a0..a8c3d51c40 100644 --- a/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml +++ b/Resources/Prototypes/Entities/Objects/Specific/Janitorial/soap.yml @@ -102,3 +102,18 @@ - type: StepTrigger - type: Item heldPrefix: omega + +- type: entity + name: ninja soap + id: SoapNinja + parent: Soap + description: The most important soap in the entire universe, as without it we would all cease to exist. Smells of honor. + components: + - type: Item + heldPrefix: ninja + # despawn to prevent ninja killing server + - type: TimedDespawn + lifetime: 60 + # no holding ninja hostage and forcing him to make infinite money for cargo + - type: StaticPrice + price: 0 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..28d7a62047 --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Weapons/Bombs/spider.yml @@ -0,0 +1,47 @@ +- type: entity + name: spider 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 medium radius. Will break underplating. + explosionType: DemolitionCharge + totalIntensity: 60 + intensitySlope: 10 + maxIntensity: 60 + canCreateVacuum: true + - type: ExplodeOnTrigger + - type: StickyVisualizer + - type: Appearance + visuals: + - type: GenericEnumVisualizer + key: enum.Trigger.TriggerVisuals.VisualState + states: + enum.Trigger.TriggerVisualState.Primed: primed + enum.Trigger.TriggerVisualState.Unprimed: complete diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml index c2e4adaf10..633034f6b8 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/sword.yml @@ -43,6 +43,29 @@ 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: Clothing + sprite: Objects/Weapons/Melee/energykatana.rsi + slots: + - Back + - Belt + - type: entity name: machete parent: BaseItem diff --git a/Resources/Prototypes/GameRules/events.yml b/Resources/Prototypes/GameRules/events.yml index f1736e8a3b..0b6ef2fee6 100644 --- a/Resources/Prototypes/GameRules/events.yml +++ b/Resources/Prototypes/GameRules/events.yml @@ -69,6 +69,28 @@ earliestStart: 15 minimumPlayers: 15 +- type: gameRule + id: SpaceNinjaSpawn + config: + !type:NinjaRuleConfiguration + id: SpaceNinjaSpawn + weight: 10 + endAfter: 1 + earliestStart: 60 + minimumPlayers: 15 + objectives: + - DownloadObjective + - DoorjackObjective + - SpiderChargeObjective + - TerrorObjective + - SurviveObjective + implants: [ MicroBombImplant ] + threats: + - announcement: terror-dragon + rule: Dragon + - announcement: terror-revenant + rule: RevenantSpawn + - type: gameRule id: RevenantSpawn config: diff --git a/Resources/Prototypes/Objectives/ninjaObjectives.yml b/Resources/Prototypes/Objectives/ninjaObjectives.yml new file mode 100644 index 0000000000..d2e1fa4068 --- /dev/null +++ b/Resources/Prototypes/Objectives/ninjaObjectives.yml @@ -0,0 +1,39 @@ +- type: objective + id: DownloadObjective + issuer: spiderclan + requirements: + - !type:TraitorRequirement {} + conditions: + - !type:DownloadCondition {} + +- type: objective + id: DoorjackObjective + issuer: spiderclan + requirements: + - !type:TraitorRequirement {} + conditions: + - !type:DoorjackCondition {} + +- type: objective + id: SpiderChargeObjective + issuer: spiderclan + requirements: + - !type:TraitorRequirement {} + conditions: + - !type:SpiderChargeCondition {} + +- type: objective + id: TerrorObjective + issuer: spiderclan + requirements: + - !type:TraitorRequirement {} + conditions: + - !type:TerrorCondition {} + +- type: objective + id: SurviveObjective + issuer: spiderclan + requirements: + - !type:TraitorRequirement {} + 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..cde1235256 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/ninja.yml @@ -0,0 +1,9 @@ +- type: antag + id: SpaceNinja + name: roles-antag-space-ninja-name + antagonist: true + setPreference: false + objective: roles-antag-space-ninja-objective +# special: +# - !type:AddImplantSpecial +# implants: [ MicroBombImplant ] diff --git a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml index 71e001d640..b9055b0958 100644 --- a/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml +++ b/Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml @@ -40,12 +40,23 @@ id: SpaceNinjaGear equipment: jumpsuit: ClothingUniformJumpsuitColorBlack - back: ClothingBackpackFilled + # belt holds katana so satchel has the tools for sabotaging things + back: ClothingBackpackSatchelTools + mask: ClothingMaskGasSyndicate head: ClothingHeadHelmetSpaceNinja + # TODO: space ninja mask + eyes: ClothingEyesGlassesMeson gloves: ClothingHandsGlovesSpaceNinja outerClothing: ClothingOuterSuitSpaceninja shoes: ClothingShoesSpaceNinja - id: PassengerPDA + id: AgentIDCard + ears: ClothingHeadsetGrey + pocket1: SpiderCharge + pocket2: HandheldGPSBasic + belt: EnergyKatana + suitstorage: YellowOxygenTankFilled + inhand: + left hand: JetpackBlackFilled innerclothingskirt: ClothingUniformJumpskirtColorBlack satchel: ClothingBackpackSatchelFilled duffelbag: ClothingBackpackDuffelFilled 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 0000000000000000000000000000000000000000..19eeac4947d73bab0aa9918d7ab980fe9326606f GIT binary patch literal 790 zcmV+x1L^#UP)1yN8(twWb0r7HMZ(%7^@+J>Y>imTw_P;d}k zd=35wx(coef}jW@ZtlJok$7%fsEEDc=KT2X{SGHzz!Nj|tkXOR*<8U+L?eNP#ic;& z3m&@g;b&aa9cwBckI`^_-qED$ZCLeceDB}YzM67$Xz~e<>6Tp(mT8Es7c9lw!aaIM zTNOSO4%srG@Rib&Ren~SuJXHLdoD2z9@R6D{>8~(tY_@3@Q`pYYZmp|JnBn2Qn~qA zseQ8haYzs)LVzSi)-Wj$mYS2y{`U=6d^6<5qs!Z(ge{IV8S#chElSsvR73QXJSHWT z``t2Ja(W(x%q?S-rk(3C3{rm-{-@JVdrbUEB;cOT{ z76Y+idZ?NUKnBAc!qFqj@OXzHgRIAUFwL;%7F?cYvCug8+I6pv%*wD2Dlthlh{h zOa3zkL561xU$!%VX|Nbfo&-k_6#^SKZ(@A+_6-9w6AOyrJQJnB7&(MMYCvj1dZ=C( z`1|_(=j7ss^Z)++4W^lyS>a;ezkMaza*`bYa|EH#qFHS?+9Dctz^DU89Y6~J02QIO U`Pg=jUH||907*qoM6N<$f@dpY)&Kwi literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..0b7ddbf8ff4a94050b716ffccf2e1d2542ba29b5 GIT binary patch literal 752 zcmV)=vEY6V|Q6O&d*+mNJ4aTQz~1P9T@ z*WiDktKh022#O%$=I(0|iRaovi|7qE=f`*NcR2Y1p}1{iy}B{T=6p9X8Pn$G7qsv* zAzFzrNN>vUobl=DIRCDXTWVCj=~un}zxU^ATsA!eY9hiThU5CeG7a%n-%-3N+-_u2 z%fbi3eXa~Be5rK3#7~OTC4N)vCKD47Qa!bF73)=OWZbN9uW&SL7mV^e>Pwo;-0Y0h zPT5^}B$y6>xNLN#;CAuk(O;P3k zJ5SFV9x8Qjib4Hj5WIFncpJX9g5dKo2)>S>_5sdw6@OlG8hsaitD;5sp=BLTFDjb8 z17{o1aqpzu)Q?mvJ7ih#astis&~^0i}=N#Wba!r2G73}UokKAur`UWhXXddy& z8`=N>010qNS#tmY3labT3lag+-G2N400AgTL_t(|+U?r0YQj(y#_`i4y2MDqj+v{2 z_yW57PM!Ns-F=9SK|K3m;XrR zo!wqnWZ%#u;L8hTFTkS6@ZY^7 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9147eb7598d9f16cba99b5c7f4f3fc480e79712a GIT binary patch literal 765 zcmV)=vEY6V|Q6O&d*+mNJ4aTQz~1P9T@ z*WiDktKh022#O%$=I(0|iRaovi|7qE=f`*NcR2Y1p}1{iy}B{T=6p9X8Pn$G7qsv* zAzFzrNN>vUobl=DIRCDXTWVCj=~un}zxU^ATsA!eY9hiThU5CeG7a%n-%-3N+-_u2 z%fbi3eXa~Be5rK3#7~OTC4N)vCKD47Qa!bF73)=OWZbN9uW&SL7mV^e>Pwo;-0Y0h zPT5^}B$y6>xNLN#;CAuk(O;P3k zJ5SFV9x8Qjib4Hj5WIFncpJX9g5dKo2)>S>_5sdw6@OlG8hsaitD;5sp=BLTFDjb8 z17{o1aqpzu)Q?mvJ7ih#astis&~^0i}=N#Wba!r2G73}UokKAur`UWhXXddy& z8`=N>010qNS#tmY3labT3lag+-G2N400A{gL_t(|+U?rEPQx$|$MGYI$`Xo@+A%XQ zpuRx6_nnw|C-y!>*)viBl`KVc30x>0Sg@_jA6MUxWGNY9UrtF>)jtsc00000008)7 zKASZ<&avLyXR_P7`=67SZS%U@U5x$aL+Z_aCf$GRlX_x9>ixF?htGpJ=T66C7g}%T zHS1F$)v8S9321BmuGxoI1FGj6DcA4GG(c1yN8(twWb0r7HMZ(%7^@+J>Y>imTw_P;d}k zd=35wx(coef}jW@ZtlJok$7%fsEEDc=KT2X{SGHzz!Nj|tkXOR*<8U+L?eNP#ic;& z3m&@g;b&aa9cwBckI`^_-qED$ZCLeceDB}YzM67$Xz~e<>6Tp(mT8Es7c9lw!aaIM zTNOSO4%srG@Rib&Ren~SuJXHLdoD2z9@R6D{>8~(tY_@3@Q`pYYZmp|JnBn2Qn~qA zseQ8haYzs)LVzSi)-Wj$mYS2y{`U=6d^6<5qs!Z(ge{IV8S#chElSsvR73QXJSHWT z``t2Ja(WU;KfH#4|*_$XYhg&ZYDh_sKlZXB*;u`v#nCPb?4Omlg*Uf+5NhI zhf-?P>vf2;5)gA~1GE9!0BwLaAkIq3PT1c&7zK?V9UmH;C#;%p$UF!kywz$3=G1B{ zY$kBL!TrN~Jm#j=~(womQ$c z;^c?A3HCqx6x*FAilxP}d+bQ;!((TT3UTtKeg$x0=R_>C0C3X)*f1$pg>f#4{Pal~ z9AuA7ngQqKVhcAXbCPtiZlAaQ+h^BAM@LmOb*Hl$}W)V>A-@8-u5``R~+l^?1C&MwZ4=S~}viIivY$MrHb@d{xM z=HeVx8=u6_omQ#+hcESCU}tN4l$p!ApNGQ%#bk2E?fZD|Dcw9b-;jMPj3+VeYH>ab usOB5`wYYvGqz%vpXalqX+5l}pT=flNT~f@2$W0ai0000i(!DMA=km}4(wGHG;OKaU8A{E!f}x}WQ~1`3+PfIuJdEVH>wOx+;8<=TSxh=*87 zDG;9!$2m1fd@FR-=QqJcpT7k2Ty_zVL`}Ka#%de05;ut_iDRZwVar&2LYX{_{x+)MhTds+DKr zZLBC500009a7bBm000XT000XT0n*)m`~Uy}rAb6VRCwC$nmulVFc^kylmYb$T%mh6 z;*oYj9osWh;sgwAY+Qo5jBF6D&@-@hZ8sOABB3^MFh4D}-)Bk`k$ms-eFkJg>$)a3 z;}FY%00@8p2!H?xfB*=900@8p2$&3GjAH_96<|{Ll?AkI%U#zcd7j%l0Uw_?_AY^^ z=NCfBJMlb^S|{Mh9{I!l9T7{w$_WtRtEwVpS?<33K@jlG&H=iRrYT=lfG*t^h&n~R zs|lEDQ^zU5o!}IUI*tF&Wzg0Cwn)I#wX|2d`fp{QQ#>2QIRF9x000000002k#unRL zysqnu-^&=29xU6S`F=3FYRw1L111z`_5@rKFbc2L=IJUMB5@q|;eFrN?i{dKK$c~^ zX_`KKk|fkB0V4csQS`5M48K@F6h%~|*7-j(ie2uJ002ovPDHLk FV1mfmhF|~y literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..e185890fbf360ec5e9d1430d45e43b02e2574214 GIT binary patch literal 740 zcmVY}yv(*JWnR?5dqL&FrSlRk=7Q2XXQ1 z;6LE1Tq&hUNp9|bU8M26W@|U`PVYRQzVG{edgeP2P3BC;uNeg=U+~fs32k<6UaNa1 zN(<1&fNuKkSSpp|-}P}zMAe&q)$9Lzf3C(Q%QqoXFFb6zUO`xkc+npu5G z_&~VNlL3VQ&@=}v*U+#KW>r4t_ztGm24FsiIRpe=y?#wIN3fs^AX^S%!yH8`hp=E11X&KUj5W>yY_aU& z$24;YBQdohxPy! W(5qCqTG2}Y0000Y}yv(*JWnR?5dqL&FrSlRk=7Q2XXQ1 z;6LE1Tq&hUNp9|bU8M26W@|U`PVYRQzVG{edgeP2P3BC;uNeg=U+~fs32k<6UaNa1 zN(<1&fNuKkSSpp|-}P}zMAe&q)$9Lzf3C(Q%QqoXFFb6zUO`xkc+npu5G z_&~VNlL3VQ&@=}v*U+#KW>r4t_ztGmj`5 zBUo8m$4+c~0P_S^!3WR>un{}0wY3s##6T(w3qcSA;&GKk*38Mqmcb(fm)foJ=*}kvKv*yh3w-4g!TB-T{ z&%t}6Y9A@YA*i(v;*(ZC000000000000000057mT(2#Xr-t&zQNbo>I8KW&qme13r z^A1!5ri%&>7rquPN{Zr14;%6vGRwYh)`>2aS*#}A9S?-Tqp=#oH5mzF0E&#dUYxYX774Fvu|fhyz~IlK`_6`D{&6V z<}8A9T~l2=E2N1CfNwu9$>dJLv!b002ovPDHLkV1oB~ Blm`F+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5926122d27bfe0b4623bcf94a44ba3a8ac46fa4f GIT binary patch literal 993 zcmV<710MW|P)Y}yv(*JWnR?5dqL&FrSlRk=7Q2XXQ1 z;6LE1Tq&hUNp9|bU8M26W@|U`PVYRQzVG{edgeP2P3BC;uNeg=U+~fs32k<6UaNa1 zN(<1&fNuKkSSpp|-}P}zMAe&q)$9Lzf3C(Q%QqoXFFb6zUO`xkc+npu5G z_&~VNlL3VQ&@=}v*U+#KW>r4t_ztGmM2JN#2)S`p9UUz^n1d^sBIK2LyY%^c-lI$* zQrtiA5nvY)ZE`}qzwZL@X9xly00000000000001hVZbd_?$lPX!|e8a)${eWC&*Lf z0-h@8?XXhjykJ9qzAFA?hA4H+K~Px+uNxpYRbz0jl^8utoc|`yW{h@Yx2d#$+D(Yc zgX0o--GE-Kn*(e%X|JEae-p3mRH%m+)O!T@L4NBR`AL@-3F|!PBf_kSAC#xb#qF3i z@t{!NdFC>2Oh810+o!>Efahy(R+~?pW0wz99RbP(h!7Ta17dPcBIHEq@nSX9`hs{+ z9b2JW&b8I+*{l(LE2;=A0_@&$XSjEUy6O-QU4R=Wk6FjOf7k*50MX_ffJW7s7@3p5 P00000NkvXXu0mjfm{iK| literal 0 HcmV?d00001 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 + } + ] +} diff --git a/Resources/Textures/Structures/Machines/computers.rsi/comm_icon.png b/Resources/Textures/Structures/Machines/computers.rsi/comm_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce1a3cb33351dc9ebeb5226e6b00b7aa80a4fc18 GIT binary patch literal 1151 zcmV-_1c3XAP)zX zb?_f>Rj!m$q$D?Yzb?{vUaMB(o!)sqec$){^vri4oXVR8uYME?Mc+*)6WVNMPHTK5 zOdC;R4Ctokj7?6a_;1s)}MYrV1Q&zeE z&eOAp2CLm8323+vg6Cdn+=P!x5WMdP!N&pA-N9+G=3kbaMqWj))U=*mXkCNjvzlgX z!O1#w-a5Lge@S(+!?q1iM-a_G`z18bSF`#)=kNxmSLGL7!1fNr0K z1=(^h{{#kEX$b})Awim0{O;X*hR07|09gzE!yLhaOCh}gh65ljWEeOHAV&cxB+#P+ zn;1ShWHCe(;BWxQ@?E>O)6~aqZl1Uu0J8kS-Agoez^+{cq5uRg%V#n$D0@-Y=DXV% zbRSM9oCd%EWVyNwgF?F^R_OehduJn}~D)&`hEo00L=1iEP#nvi($UM zdSxMo+?jMuI7a5f*d&z$a4-w#(e_kGkT1ZI2B5%X=5?HWbUBiu0No;70Om`WW$3=f z=Li;DS%yJ94Uv!tBr1F%fMO;g2WV;=A_}5yz&Mc7$7I9!ARy?2U=s;IcHF;n5wkrY ztDx|opP!!rR3%@(dY&l5e*OH;z{bu=lI7brE@1fh@gu|I2MW$r!+IDsHvl9 zymRX&oGm8!4NOM_U`IU0mj-0z zQA%Q+MD1!zdUm003Qa7mnVl RMaTdE002ovPDHLkV1lVi3a9`8 literal 0 HcmV?d00001 diff --git a/Resources/Textures/Structures/Machines/computers.rsi/meta.json b/Resources/Textures/Structures/Machines/computers.rsi/meta.json index 2fb8dcee28..8f5f5ac62d 100644 --- a/Resources/Textures/Structures/Machines/computers.rsi/meta.json +++ b/Resources/Textures/Structures/Machines/computers.rsi/meta.json @@ -275,6 +275,9 @@ ] ] }, + { + "name": "comm_icon" + }, { "name": "comm_logs", "directions": 4,