ninja 2 electric boogaloo (#15534)

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas
2023-09-10 07:20:27 +01:00
committed by GitHub
parent 25c8a03276
commit 24810d916b
153 changed files with 3892 additions and 78 deletions

View File

@@ -0,0 +1,10 @@
using Content.Shared.Communications;
namespace Content.Client.Communications;
/// <summary>
/// Does nothing special, only exists to provide a client implementation.
/// </summary>
public sealed class CommsHackerSystem : SharedCommsHackerSystem
{
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.Ninja.Systems;
namespace Content.Client.Ninja.Systems;
/// <summary>
/// Does nothing special, only exists to provide a client implementation.
/// </summary>
public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem
{
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
namespace Content.Client.Ninja.Systems;
/// <summary>
/// Disables cloak prediction since client has no knowledge of battery power.
/// Cloak will still be enabled after server tells it.
/// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaSuitComponent, AttemptStealthEvent>(OnAttemptStealth);
}
private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args)
{
args.Cancel();
}
}

View File

@@ -0,0 +1,12 @@
using Content.Shared.Ninja.Systems;
namespace Content.Client.Ninja.Systems;
/// <summary>
/// Currently does nothing special clientside.
/// All functionality is in shared and server.
/// Only exists to prevent crashing.
/// </summary>
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{
}

View File

@@ -0,0 +1,10 @@
using Content.Shared.Research.Systems;
namespace Content.Client.Research;
/// <summary>
/// Does nothing special, only exists to provide a client implementation.
/// </summary>
public sealed class ResearchStealerSystem : EntitySystem
{
}

View File

@@ -1,4 +1,5 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Ninja.Systems;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Shared.Database;
@@ -15,6 +16,7 @@ public sealed partial class AdminVerbSystem
{
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
[Dependency] private readonly SharedMindSystem _minds = default!;
@@ -35,7 +37,7 @@ public sealed partial class AdminVerbSystem
Verb traitor = new()
{
Text = "Make Traitor",
Text = Loc.GetString("admin-verb-text-make-traitor"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Structures/Wallmounts/posters.rsi"), "poster5_contraband"),
Act = () =>
@@ -54,7 +56,7 @@ public sealed partial class AdminVerbSystem
Verb zombie = new()
{
Text = "Make Zombie",
Text = Loc.GetString("admin-verb-text-make-zombie"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/Actions/zombie-turn.png")),
Act = () =>
@@ -69,7 +71,7 @@ public sealed partial class AdminVerbSystem
Verb nukeOp = new()
{
Text = "Make nuclear operative",
Text = Loc.GetString("admin-verb-text-make-nuclear-operative"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Structures/Wallmounts/signs.rsi"), "radiation"),
Act = () =>
@@ -86,7 +88,7 @@ public sealed partial class AdminVerbSystem
Verb pirate = new()
{
Text = "Make Pirate",
Text = Loc.GetString("admin-verb-text-make-pirate"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Clothing/Head/Hats/pirate.rsi"), "icon"),
Act = () =>
@@ -101,5 +103,21 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(pirate);
Verb spaceNinja = new()
{
Text = Loc.GetString("admin-verb-text-make-space-ninja"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Objects/Weapons/Melee/energykatana.rsi"), "icon"),
Act = () =>
{
if (!_minds.TryGetMind(args.Target, out var mindId, out var mind))
return;
_ninja.MakeNinja(mindId, mind);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-space-ninja"),
};
args.Verbs.Add(spaceNinja);
}
}

View File

@@ -0,0 +1,89 @@
using Content.Server.Chat.Systems;
using Content.Server.GameTicking;
using Content.Server.Ninja.Systems;
using Content.Shared.Communications;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
namespace Content.Server.Communications;
public sealed class CommsHackerSystem : SharedCommsHackerSystem
{
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IRobustRandom _random = default!;
// TODO: remove when generic check event is used
[Dependency] private readonly NinjaGlovesSystem _gloves = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<CommsHackerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
SubscribeLocalEvent<CommsHackerComponent, TerrorDoAfterEvent>(OnDoAfter);
}
/// <summary>
/// Start the doafter to hack a comms console
/// </summary>
private void OnBeforeInteractHand(EntityUid uid, CommsHackerComponent comp, BeforeInteractHandEvent args)
{
if (args.Handled || !HasComp<CommunicationsConsoleComponent>(args.Target))
return;
// TODO: generic check event
if (!_gloves.AbilityCheck(uid, args, out var target))
return;
var doAfterArgs = new DoAfterArgs(uid, comp.Delay, new TerrorDoAfterEvent(), target: target, used: uid, eventTarget: uid)
{
BreakOnDamage = true,
BreakOnUserMove = true,
MovementThreshold = 0.5f,
CancelDuplicate = false
};
_doAfter.TryStartDoAfter(doAfterArgs);
args.Handled = true;
}
/// <summary>
/// Call in a random threat and do cleanup.
/// </summary>
private void OnDoAfter(EntityUid uid, CommsHackerComponent comp, TerrorDoAfterEvent args)
{
if (args.Cancelled || args.Handled || comp.Threats.Count == 0 || args.Target == null)
return;
var threat = _random.Pick(comp.Threats);
CallInThreat(threat);
// prevent calling in multiple threats
RemComp<CommsHackerComponent>(uid);
var ev = new ThreatCalledInEvent(uid, args.Target.Value);
RaiseLocalEvent(args.User, ref ev);
}
/// <summary>
/// Makes announcement and adds game rule of the threat.
/// </summary>
public void CallInThreat(Threat threat)
{
_gameTicker.StartGameRule(threat.Rule, out _);
_chat.DispatchGlobalAnnouncement(Loc.GetString(threat.Announcement), playSound: true, colorOverride: Color.Red);
}
}
/// <summary>
/// Raised on the user when a threat is called in on the communications console.
/// </summary>
/// <remarks>
/// If you add <see cref="CommsHackerComponent"/>, make sure to use this event to prevent adding it twice.
/// For example, you could add a marker component after a threat is called in then check if the user doesn't have that marker before adding CommsHackerComponent.
/// </remarks>
[ByRefEvent]
public record struct ThreatCalledInEvent(EntityUid Used, EntityUid Target);

View File

@@ -276,4 +276,3 @@ public sealed class DoorSystem : SharedDoorSystem
}
}
}

View File

@@ -303,16 +303,8 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
}
}
/// <param name="uid">Entity being electrocuted.</param>
/// <param name="sourceUid">Source entity of the electrocution.</param>
/// <param name="shockDamage">How much shock damage the entity takes.</param>
/// <param name="time">How long the entity will be stunned.</param>
/// <param name="refresh">Should <paramref>time</paramref> be refreshed (instead of accumilated) if the entity is already electrocuted?</param>
/// <param name="siemensCoefficient">How insulated the entity is from the shock. 0 means completely insulated, and 1 means no insulation.</param>
/// <param name="statusEffects">Status effects to apply to the entity.</param>
/// <param name="ignoreInsulation">Should the electrocution bypass the Insulated component?</param>
/// <returns>Whether the entity <see cref="uid"/> was stunned by the shock.</returns>
public bool TryDoElectrocution(
/// <inheritdoc/>
public override bool TryDoElectrocution(
EntityUid uid, EntityUid? sourceUid, int shockDamage, TimeSpan time, bool refresh, float siemensCoefficient = 1f,
StatusEffectsComponent? statusEffects = null, bool ignoreInsulation = false)
{

View File

@@ -31,6 +31,11 @@ public sealed class EmpSystem : SharedEmpSystem
{
foreach (var uid in _lookup.GetEntitiesInRange(coordinates, range))
{
var attemptEv = new EmpAttemptEvent();
RaiseLocalEvent(uid, attemptEv);
if (attemptEv.Cancelled)
continue;
var ev = new EmpPulseEvent(energyConsumption, false, false);
RaiseLocalEvent(uid, ref ev);
if (ev.Affected)
@@ -100,6 +105,13 @@ public sealed class EmpSystem : SharedEmpSystem
}
}
/// <summary>
/// Raised on an entity before <see cref="EmpPulseEvent"/>. Cancel this to prevent the emp event being raised.
/// </summary>
public sealed partial class EmpAttemptEvent : CancellableEntityEventArgs
{
}
[ByRefEvent]
public record struct EmpPulseEvent(float EnergyConsumption, bool Affected, bool Disabled);

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Explosion.Components;
/// <summary>
/// Disallows starting the timer by hand, must be stuck or triggered by a system.
/// </summary>
[RegisterComponent]
public sealed partial class AutomatedTimerComponent : Component
{
}

View File

@@ -140,7 +140,7 @@ public sealed partial class TriggerSystem
private void OnTimerUse(EntityUid uid, OnUseTimerTriggerComponent component, UseInHandEvent args)
{
if (args.Handled)
if (args.Handled || HasComp<AutomatedTimerComponent>(uid))
return;
HandleTimerTrigger(

View File

@@ -0,0 +1,37 @@
using Content.Server.Ninja.Systems;
using Content.Shared.Communications;
using Content.Shared.Objectives;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.GameTicking.Rules.Components;
[RegisterComponent, Access(typeof(SpaceNinjaSystem))]
public sealed partial class NinjaRuleComponent : Component
{
/// <summary>
/// All ninja minds that are using this rule.
/// Their SpaceNinjaComponent Rule field should point back to this rule.
/// </summary>
[DataField("minds")]
public List<EntityUid> Minds = new();
/// <summary>
/// List of objective prototype ids to add
/// </summary>
[DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<ObjectivePrototype>))]
public List<string> Objectives = new();
/// <summary>
/// List of threats that can be called in. Copied onto <see cref="CommsHackerComponent"/> when gloves are enabled.
/// </summary>
[DataField("threats", required: true)]
public List<Threat> Threats = new();
/// <summary>
/// Sound played when making the player a ninja via antag control or ghost role
/// </summary>
[DataField("greetingSound", customTypeSerializer: typeof(SoundSpecifierTypeSerializer))]
public SoundSpecifier? GreetingSound = new SoundPathSpecifier("/Audio/Misc/ninja_greeting.ogg");
}

View File

@@ -0,0 +1,23 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
namespace Content.Server.GameTicking.Rules;
/// <summary>
/// Only handles round end text for ninja.
/// </summary>
public sealed class NinjaRuleSystem : GameRuleSystem<NinjaRuleComponent>
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
private void OnObjectivesTextGetInfo(EntityUid uid, NinjaRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = comp.Minds;
args.AgentName = Loc.GetString("ninja-round-end-agent-name");
}
}

View File

@@ -0,0 +1,21 @@
using Content.Server.Implants.Components;
namespace Content.Server.Implants;
public sealed class AutoImplantSystem : EntitySystem
{
[Dependency] private readonly SubdermalImplantSystem _subdermalImplant = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AutoImplantComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, AutoImplantComponent comp, MapInitEvent args)
{
_subdermalImplant.AddImplants(uid, comp.Implants);
RemComp<AutoImplantComponent>(uid);
}
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
namespace Content.Server.Implants.Components;
/// <summary>
/// Implants an entity automatically on MapInit.
/// </summary>
[RegisterComponent]
public sealed partial class AutoImplantComponent : Component
{
/// <summary>
/// List of implants to inject.
/// </summary>
[DataField("implants", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> Implants = new();
}

View File

@@ -1,5 +1,4 @@
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
using Content.Shared.Roles;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
@@ -13,7 +12,6 @@ namespace Content.Server.Jobs;
[UsedImplicitly]
public sealed partial class AddImplantSpecial : JobSpecial
{
[DataField("implants", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<EntityPrototype>))]
public HashSet<String> Implants { get; private set; } = new();
@@ -21,19 +19,6 @@ public sealed partial class AddImplantSpecial : JobSpecial
{
var entMan = IoCManager.Resolve<IEntityManager>();
var implantSystem = entMan.System<SharedSubdermalImplantSystem>();
var xformQuery = entMan.GetEntityQuery<TransformComponent>();
if (!xformQuery.TryGetComponent(mob, out var xform))
return;
foreach (var implantId in Implants)
{
var implant = entMan.SpawnEntity(implantId, xform.Coordinates);
if (!entMan.TryGetComponent<SubdermalImplantComponent>(implant, out var implantComp))
return;
implantSystem.ForceImplant(mob, implant, implantComp);
}
implantSystem.AddImplants(mob, Implants);
}
}

View File

@@ -18,12 +18,12 @@ namespace Content.Server.Mind;
public sealed class MindSystem : SharedMindSystem
{
[Dependency] private readonly ActorSystem _actor = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly SharedGhostSystem _ghosts = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly IMapManager _maps = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedGhostSystem _ghosts = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
@@ -277,10 +277,13 @@ public sealed class MindSystem : SharedMindSystem
}
}
/// <summary>
/// Sets the Mind's UserId, Session, and updates the player's PlayerData. This should have no direct effect on the
/// entity that any mind is connected to, except as a side effect of the fact that it may change a player's
/// attached entity. E.g., ghosts get deleted.
/// </summary>
public override void SetUserId(EntityUid mindId, NetUserId? userId, MindComponent? mind = null)
{
base.SetUserId(mindId, userId, mind);
if (!Resolve(mindId, ref mind))
return;

View File

@@ -0,0 +1,100 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Robust.Shared.Audio;
namespace Content.Server.Ninja.Systems;
/// <summary>
/// Handles the doafter and power transfer when draining.
/// </summary>
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BatteryDrainerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
}
/// <summary>
/// Start do after for draining a power source.
/// Can't predict PNBC existing so only done on server.
/// </summary>
private void OnBeforeInteractHand(EntityUid uid, BatteryDrainerComponent comp, BeforeInteractHandEvent args)
{
var target = args.Target;
if (args.Handled || comp.BatteryUid == null || !HasComp<PowerNetworkBatteryComponent>(target))
return;
// handles even if battery is full so you can actually see the poup
args.Handled = true;
if (_battery.IsFull(comp.BatteryUid.Value))
{
_popup.PopupEntity(Loc.GetString("battery-drainer-full"), uid, uid, PopupType.Medium);
return;
}
var doAfterArgs = new DoAfterArgs(uid, comp.DrainTime, new DrainDoAfterEvent(), target: target, eventTarget: uid)
{
BreakOnUserMove = true,
MovementThreshold = 0.5f,
CancelDuplicate = false,
AttemptFrequency = AttemptFrequency.StartAndEnd
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
/// <inheritdoc/>
protected override void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent<DrainDoAfterEvent> args)
{
base.OnDoAfterAttempt(uid, comp, args);
if (comp.BatteryUid == null || _battery.IsFull(comp.BatteryUid.Value))
args.Cancel();
}
/// <inheritdoc/>
protected override bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target)
{
if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery))
return false;
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
return false;
if (MathHelper.CloseToPercent(targetBattery.CurrentCharge, 0))
{
_popup.PopupEntity(Loc.GetString("battery-drainer-empty", ("battery", target)), uid, uid, PopupType.Medium);
return false;
}
var available = targetBattery.CurrentCharge;
var required = battery.MaxCharge - battery.CurrentCharge;
// higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
if (!_battery.TryUseCharge(target, input, targetBattery))
return false;
var output = input * comp.DrainEfficiency;
_battery.SetCharge(comp.BatteryUid.Value, battery.CurrentCharge + output, battery);
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full
return !battery.IsFullyCharged;
}
}

View File

@@ -0,0 +1,102 @@
using Content.Server.Communications;
using Content.Server.DoAfter;
using Content.Server.Mind;
using Content.Server.Ninja.Systems;
using Content.Server.Power.Components;
using Content.Server.Roles;
using Content.Shared.Communications;
using Content.Shared.DoAfter;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Research.Components;
using Content.Shared.Toggleable;
namespace Content.Server.Ninja.Systems;
/// <summary>
/// Handles the toggle gloves action.
/// </summary>
public sealed class NinjaGlovesSystem : SharedNinjaGlovesSystem
{
[Dependency] private readonly EmagProviderSystem _emagProvider = default!;
[Dependency] private readonly SharedBatteryDrainerSystem _drainer = default!;
[Dependency] private readonly SharedStunProviderSystem _stunProvider = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
[Dependency] private readonly CommsHackerSystem _commsHacker = default!;
[Dependency] private readonly MindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaGlovesComponent, ToggleActionEvent>(OnToggleAction);
}
/// <summary>
/// Toggle gloves, if the user is a ninja wearing a ninja suit.
/// </summary>
private void OnToggleAction(EntityUid uid, NinjaGlovesComponent comp, ToggleActionEvent args)
{
if (args.Handled)
return;
args.Handled = true;
var user = args.Performer;
// need to wear suit to enable gloves
if (!TryComp<SpaceNinjaComponent>(user, out var ninja)
|| ninja.Suit == null
|| !HasComp<NinjaSuitComponent>(ninja.Suit.Value))
{
Popup.PopupEntity(Loc.GetString("ninja-gloves-not-wearing-suit"), user, user);
return;
}
// show its state to the user
var enabling = comp.User == null;
Appearance.SetData(uid, ToggleVisuals.Toggled, enabling);
var message = Loc.GetString(enabling ? "ninja-gloves-on" : "ninja-gloves-off");
Popup.PopupEntity(message, user, user);
if (enabling)
{
EnableGloves(uid, comp, user, ninja);
}
else
{
DisableGloves(uid, comp);
}
}
private void EnableGloves(EntityUid uid, NinjaGlovesComponent comp, EntityUid user, SpaceNinjaComponent ninja)
{
comp.User = user;
Dirty(uid, comp);
_ninja.AssignGloves(user, uid, ninja);
var drainer = EnsureComp<BatteryDrainerComponent>(user);
var stun = EnsureComp<StunProviderComponent>(user);
_stunProvider.SetNoPowerPopup(user, "ninja-no-power", stun);
if (_ninja.GetNinjaBattery(user, out var battery, out var _))
{
_drainer.SetBattery(user, battery, drainer);
_stunProvider.SetBattery(user, battery, stun);
}
var emag = EnsureComp<EmagProviderComponent>(user);
_emagProvider.SetWhitelist(user, comp.DoorjackWhitelist, emag);
EnsureComp<ResearchStealerComponent>(user);
// prevent calling in multiple threats by toggling gloves after
if (_mind.TryGetRole<NinjaRoleComponent>(user, out var role) && !role.CalledInThreat)
{
var hacker = EnsureComp<CommsHackerComponent>(user);
var rule = _ninja.NinjaRule(user);
if (rule != null)
_commsHacker.SetThreats(user, rule.Threats, hacker);
}
}
}

View File

@@ -0,0 +1,147 @@
using Content.Server.Emp;
using Content.Server.Popups;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Actions;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Robust.Shared.Containers;
namespace Content.Server.Ninja.Systems;
/// <summary>
/// Handles power cell upgrading and actions.
/// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
[Dependency] private readonly EmpSystem _emp = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaSuitComponent, ContainerIsInsertingAttemptEvent>(OnSuitInsertAttempt);
SubscribeLocalEvent<NinjaSuitComponent, EmpAttemptEvent>(OnEmpAttempt);
SubscribeLocalEvent<NinjaSuitComponent, AttemptStealthEvent>(OnAttemptStealth);
SubscribeLocalEvent<NinjaSuitComponent, CreateThrowingStarEvent>(OnCreateThrowingStar);
SubscribeLocalEvent<NinjaSuitComponent, RecallKatanaEvent>(OnRecallKatana);
SubscribeLocalEvent<NinjaSuitComponent, NinjaEmpEvent>(OnEmp);
}
protected override void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja)
{
base.NinjaEquippedSuit(uid, comp, user, ninja);
_ninja.SetSuitPowerAlert(user);
}
// TODO: if/when battery is in shared, put this there too
// TODO: or put MaxCharge in shared along with powercellslot
private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args)
{
// no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting) || inserting.MaxCharge <= battery.MaxCharge)
{
args.Cancel();
}
// TODO: raise event on ninja telling it to update battery
}
private void OnEmpAttempt(EntityUid uid, NinjaSuitComponent comp, EmpAttemptEvent args)
{
// ninja suit (battery) is immune to emp
// powercell relays the event to suit
args.Cancel();
}
protected override void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user)
{
base.UserUnequippedSuit(uid, comp, user);
// remove power indicator
_ninja.SetSuitPowerAlert(user);
}
private void OnAttemptStealth(EntityUid uid, NinjaSuitComponent comp, AttemptStealthEvent args)
{
var user = args.User;
// need 1 second of charge to turn on stealth
var chargeNeeded = SuitWattage(uid, comp);
// being attacked while cloaked gives no power message since it overloads the power supply or something
if (!_ninja.GetNinjaBattery(user, out var _, out var battery) || battery.CurrentCharge < chargeNeeded || UseDelay.ActiveDelay(user))
{
_popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user);
args.Cancel();
return;
}
StealthClothing.SetEnabled(uid, user, true);
}
private void OnCreateThrowingStar(EntityUid uid, NinjaSuitComponent comp, CreateThrowingStarEvent args)
{
args.Handled = true;
var user = args.Performer;
if (!_ninja.TryUseCharge(user, comp.ThrowingStarCharge) || UseDelay.ActiveDelay(user))
{
_popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user);
return;
}
// try to put throwing star in hand, otherwise it goes on the ground
var star = Spawn(comp.ThrowingStarPrototype, Transform(user).Coordinates);
_hands.TryPickupAnyHand(user, star);
}
private void OnRecallKatana(EntityUid uid, NinjaSuitComponent comp, RecallKatanaEvent args)
{
args.Handled = true;
var user = args.Performer;
if (!TryComp<SpaceNinjaComponent>(user, out var ninja) || ninja.Katana == null)
return;
var katana = ninja.Katana.Value;
var coords = _transform.GetWorldPosition(katana);
var distance = (_transform.GetWorldPosition(user) - coords).Length();
var chargeNeeded = (float) distance * comp.RecallCharge;
if (!_ninja.TryUseCharge(user, chargeNeeded) || UseDelay.ActiveDelay(user))
{
_popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user);
return;
}
// TODO: teleporting into belt slot
var message = _hands.TryPickupAnyHand(user, katana)
? "ninja-katana-recalled"
: "ninja-hands-full";
_popup.PopupEntity(Loc.GetString(message), user, user);
}
private void OnEmp(EntityUid uid, NinjaSuitComponent comp, NinjaEmpEvent args)
{
args.Handled = true;
var user = args.Performer;
if (!_ninja.TryUseCharge(user, comp.EmpCharge) || UseDelay.ActiveDelay(user))
{
_popup.PopupEntity(Loc.GetString("ninja-no-power"), user, user);
return;
}
// I don't think this affects the suit battery, but if it ever does in the future add a blacklist for it
var coords = Transform(user).MapPosition;
_emp.EmpPulse(coords, comp.EmpRange, comp.EmpConsumption, comp.EmpDuration);
}
}

View File

@@ -0,0 +1,301 @@
using Content.Server.Administration.Commands;
using Content.Server.Communications;
using Content.Server.Chat.Managers;
using Content.Server.StationEvents.Components;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Ghost.Roles.Events;
using Content.Server.Objectives;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems;
using Content.Server.Roles;
using Content.Server.Warps;
using Content.Shared.Alert;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Doors.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Roles;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rounding;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.Ninja.Systems;
// TODO: when syndiborgs are a thing have a borg converter with 6 second doafter
// engi -> saboteur
// medi -> idk reskin it
// other -> assault
// TODO: when criminal records is merged, hack it to set everyone to arrest
/// <summary>
/// Main ninja system that handles ninja setup and greentext, provides helper methods for the rest of the code to use.
/// </summary>
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IChatManager _chatMan = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly RoleSystem _role = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly StealthClothingSystem _stealthClothing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpaceNinjaComponent, MindAddedMessage>(OnNinjaMindAdded);
SubscribeLocalEvent<SpaceNinjaComponent, EmaggedSomethingEvent>(OnDoorjack);
SubscribeLocalEvent<SpaceNinjaComponent, ResearchStolenEvent>(OnResearchStolen);
SubscribeLocalEvent<SpaceNinjaComponent, ThreatCalledInEvent>(OnThreatCalledIn);
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<SpaceNinjaComponent>();
while (query.MoveNext(out var uid, out var ninja))
{
UpdateNinja(uid, ninja, frameTime);
}
}
/// <summary>
/// Turns the player into a space ninja
/// </summary>
public void MakeNinja(EntityUid mindId, MindComponent mind)
{
if (mind.OwnedEntity == null)
return;
// prevent double ninja'ing
var user = mind.OwnedEntity.Value;
if (HasComp<SpaceNinjaComponent>(user))
return;
AddComp<SpaceNinjaComponent>(user);
SetOutfitCommand.SetOutfit(user, "SpaceNinjaGear", EntityManager);
GreetNinja(mindId, mind);
}
/// <summary>
/// Download the given set of nodes, returning how many new nodes were downloaded.
/// </summary>
private int Download(EntityUid uid, List<string> ids)
{
if (!_mind.TryGetRole<NinjaRoleComponent>(uid, out var role))
return 0;
var oldCount = role.DownloadedNodes.Count;
role.DownloadedNodes.UnionWith(ids);
var newCount = role.DownloadedNodes.Count;
return newCount - oldCount;
}
/// <summary>
/// Returns a ninja's gamerule config data.
/// If the gamerule was not started then it will be started automatically.
/// </summary>
public NinjaRuleComponent? NinjaRule(EntityUid uid, SpaceNinjaComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return null;
// already exists so just check it
if (comp.Rule != null)
return CompOrNull<NinjaRuleComponent>(comp.Rule);
// start it
_gameTicker.StartGameRule("Ninja", out var rule);
comp.Rule = rule;
if (!TryComp<NinjaRuleComponent>(rule, out var ninjaRule))
return null;
// add ninja mind to the rule's list for objective showing
if (TryComp<MindContainerComponent>(uid, out var mindContainer) && mindContainer.Mind != null)
{
ninjaRule.Minds.Add(mindContainer.Mind.Value);
}
return ninjaRule;
}
// TODO: can probably copy paste borg code here
/// <summary>
/// Update the alert for the ninja's suit power indicator.
/// </summary>
public void SetSuitPowerAlert(EntityUid uid, SpaceNinjaComponent? comp = null)
{
if (!Resolve(uid, ref comp, false) || comp.Deleted || comp.Suit == null)
{
_alerts.ClearAlert(uid, AlertType.SuitPower);
return;
}
if (GetNinjaBattery(uid, out var _, out var battery))
{
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8);
_alerts.ShowAlert(uid, AlertType.SuitPower, (short) severity);
}
else
{
_alerts.ClearAlert(uid, AlertType.SuitPower);
}
}
/// <summary>
/// Get the battery component in a ninja's suit, if it's worn.
/// </summary>
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery)
{
if (TryComp<SpaceNinjaComponent>(user, out var ninja)
&& ninja.Suit != null
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery))
{
return true;
}
uid = null;
battery = null;
return false;
}
/// <inheritdoc/>
public override bool TryUseCharge(EntityUid user, float charge)
{
return GetNinjaBattery(user, out var uid, out var battery) && _battery.TryUseCharge(uid.Value, charge, battery);
}
/// <summary>
/// Greets the ninja when a ghost takes over a ninja, if that happens.
/// </summary>
private void OnNinjaMindAdded(EntityUid uid, SpaceNinjaComponent comp, MindAddedMessage args)
{
if (TryComp<MindContainerComponent>(uid, out var mind) && mind.Mind != null)
GreetNinja(mind.Mind.Value);
}
/// <summary>
/// Set up everything for ninja to work and send the greeting message/sound.
/// </summary>
private void GreetNinja(EntityUid mindId, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind) || mind.OwnedEntity == null || mind.Session == null)
return;
var uid = mind.OwnedEntity.Value;
var config = NinjaRule(uid);
if (config == null)
return;
var role = new NinjaRoleComponent
{
PrototypeId = "SpaceNinja"
};
_role.MindAddRole(mindId, role, mind);
// choose spider charge detonation point
// currently based on warp points, something better could be done (but would likely require mapping work)
var warps = new List<EntityUid>();
var query = EntityQueryEnumerator<WarpPointComponent, TransformComponent>();
var map = Transform(uid).MapID;
while (query.MoveNext(out var warpUid, out var warp, out var xform))
{
// won't be asked to detonate the nuke disk or singularity or centcomm
if (warp.Location != null && !HasComp<PhysicsComponent>(warpUid) && xform.MapID == map)
warps.Add(warpUid);
}
if (warps.Count > 0)
role.SpiderChargeTarget = _random.Pick(warps);
// assign objectives - must happen after spider charge target so that the obj requirement works
foreach (var objective in config.Objectives)
{
if (!_mind.TryAddObjective(mindId, objective, mind))
{
Log.Error($"Failed to add {objective} to ninja {mind.OwnedEntity.Value}");
}
}
var session = mind.Session;
_audio.PlayGlobal(config.GreetingSound, Filter.Empty().AddPlayer(session), false, AudioParams.Default);
_chatMan.DispatchServerMessage(session, Loc.GetString("ninja-role-greeting"));
}
// TODO: PowerCellDraw, modify when cloak enabled
/// <summary>
/// Handle constant power drains from passive usage and cloak.
/// </summary>
private void UpdateNinja(EntityUid uid, SpaceNinjaComponent ninja, float frameTime)
{
if (ninja.Suit == null)
return;
float wattage = _suit.SuitWattage(ninja.Suit.Value);
SetSuitPowerAlert(uid, ninja);
if (!TryUseCharge(uid, wattage * frameTime))
{
// ran out of power, uncloak ninja
_stealthClothing.SetEnabled(ninja.Suit.Value, uid, false);
}
}
/// <summary>
/// Increment greentext when emagging a door.
/// </summary>
private void OnDoorjack(EntityUid uid, SpaceNinjaComponent comp, ref EmaggedSomethingEvent args)
{
// incase someone lets ninja emag non-doors double check it here
if (!HasComp<DoorComponent>(args.Target))
return;
// this popup is serverside since door emag logic is serverside (power funnies)
_popup.PopupEntity(Loc.GetString("ninja-doorjack-success", ("target", Identity.Entity(args.Target, EntityManager))), uid, uid, PopupType.Medium);
// handle greentext
if (_mind.TryGetRole<NinjaRoleComponent>(uid, out var role))
role.DoorsJacked++;
}
/// <summary>
/// Add to greentext when stealing technologies.
/// </summary>
private void OnResearchStolen(EntityUid uid, SpaceNinjaComponent comp, ref ResearchStolenEvent args)
{
var gained = Download(uid, args.Techs);
var str = gained == 0
? Loc.GetString("ninja-research-steal-fail")
: Loc.GetString("ninja-research-steal-success", ("count", gained), ("server", args.Target));
_popup.PopupEntity(str, uid, uid, PopupType.Medium);
}
private void OnThreatCalledIn(EntityUid uid, SpaceNinjaComponent comp, ref ThreatCalledInEvent args)
{
if (_mind.TryGetRole<NinjaRoleComponent>(uid, out var role))
{
role.CalledInThreat = true;
}
}
}

View File

@@ -0,0 +1,78 @@
using Content.Server.Explosion.EntitySystems;
using Content.Server.Mind;
using Content.Server.Popups;
using Content.Server.Roles;
using Content.Server.Sticky.Events;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Ninja.Systems;
/// <summary>
/// Prevents planting a spider charge outside of its location and handles greentext.
/// </summary>
public sealed class SpiderChargeSystem : EntitySystem
{
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpiderChargeComponent, BeforeRangedInteractEvent>(BeforePlant);
SubscribeLocalEvent<SpiderChargeComponent, EntityStuckEvent>(OnStuck);
SubscribeLocalEvent<SpiderChargeComponent, TriggerEvent>(OnExplode);
}
/// <summary>
/// Require that the planter is a ninja and the charge is near the target warp point.
/// </summary>
private void BeforePlant(EntityUid uid, SpiderChargeComponent comp, BeforeRangedInteractEvent args)
{
var user = args.User;
if (!_mind.TryGetRole<NinjaRoleComponent>(user, out var role))
{
_popup.PopupEntity(Loc.GetString("spider-charge-not-ninja"), user, user);
args.Handled = true;
return;
}
// allow planting anywhere if there is no target, which should never happen
if (role.SpiderChargeTarget == null)
return;
// assumes warp point still exists
var target = Transform(role.SpiderChargeTarget.Value).MapPosition;
var coords = args.ClickLocation.ToMap(EntityManager, _transform);
if (!coords.InRange(target, comp.Range))
{
_popup.PopupEntity(Loc.GetString("spider-charge-too-far"), user, user);
args.Handled = true;
}
}
/// <summary>
/// Allows greentext to occur after exploding.
/// </summary>
private void OnStuck(EntityUid uid, SpiderChargeComponent comp, EntityStuckEvent args)
{
comp.Planter = args.User;
}
/// <summary>
/// Handles greentext after exploding.
/// Assumes it didn't move and the target was destroyed so be nice.
/// </summary>
private void OnExplode(EntityUid uid, SpiderChargeComponent comp, TriggerEvent args)
{
if (comp.Planter == null || !_mind.TryGetRole<NinjaRoleComponent>(comp.Planter.Value, out var role))
return;
// assumes the target was destroyed, that the charge wasn't moved somehow
role.SpiderChargeDetonated = true;
}
}

View File

@@ -0,0 +1,60 @@
using Content.Shared.Electrocution;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Whitelist;
using Content.Server.Power.EntitySystems;
using Robust.Shared.Timing;
namespace Content.Server.Ninja.Systems;
/// <summary>
/// Shocks clicked mobs using battery charge.
/// </summary>
public sealed class StunProviderSystem : SharedStunProviderSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedElectrocutionSystem _electrocution = default!;
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StunProviderComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
}
/// <summary>
/// Stun clicked mobs on the whitelist, if there is enough power.
/// </summary>
private void OnBeforeInteractHand(EntityUid uid, StunProviderComponent comp, BeforeInteractHandEvent args)
{
// TODO: generic check
if (args.Handled || comp.BatteryUid == null || !_gloves.AbilityCheck(uid, args, out var target))
return;
if (target == uid || !comp.Whitelist.IsValid(target, EntityManager))
return;
if (_timing.CurTime < comp.NextStun)
return;
// take charge from battery
if (!_battery.TryUseCharge(comp.BatteryUid.Value, comp.StunCharge))
{
_popup.PopupEntity(Loc.GetString(comp.NoPowerPopup), uid, uid);
return;
}
// not holding hands with target so insuls don't matter
_electrocution.TryDoElectrocution(target, uid, comp.StunDamage, comp.StunTime, false, ignoreInsulation: true);
// short cooldown to prevent instant stunlocking
comp.NextStun = _timing.CurTime + comp.Cooldown;
Dirty(uid, comp);
args.Handled = true;
}
}

View File

@@ -0,0 +1,70 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks.
/// </summary>
[DataDefinition]
public sealed partial class DoorjackCondition : IObjectiveCondition
{
private EntityUid? _mind;
private int _target;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
// TODO: clamp to number of doors on station incase its somehow a shittle or something
return new DoorjackCondition {
_mind = uid,
_target = IoCManager.Resolve<IRobustRandom>().Next(15, 40)
};
}
public string Title => Loc.GetString("objective-condition-doorjack-title", ("count", _target));
public string Description => Loc.GetString("objective-condition-doorjack-description", ("count", _target));
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Tools/emag.rsi"), "icon");
public float Progress
{
get
{
// prevent divide-by-zero
if (_target == 0)
return 1f;
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
if (role.DoorsJacked >= _target)
return 1f;
return (float) role.DoorsJacked / (float) _target;
}
}
public float Difficulty => 1.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is DoorjackCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is DoorjackCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
}
}

View File

@@ -0,0 +1,75 @@
using Content.Server.Roles;
using Content.Server.Warps;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have detonated their spider charge.
/// </summary>
[DataDefinition]
public sealed partial class SpiderChargeCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new SpiderChargeCondition {
_mind = uid
};
}
public string Title
{
get
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role)
|| role.SpiderChargeTarget == null
|| !entMan.TryGetComponent<WarpPointComponent>(role.SpiderChargeTarget, out var warp)
|| warp.Location == null)
// this should never really happen but eh
return Loc.GetString("objective-condition-spider-charge-no-target");
return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location));
}
}
public string Description => Loc.GetString("objective-condition-spider-charge-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Bombs/spidercharge.rsi"), "icon");
public float Progress
{
get
{
var entMan = IoCManager.Resolve<EntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
return role.SpiderChargeDetonated ? 1f : 0f;
}
}
public float Difficulty => 2.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is SpiderChargeCondition cond && Equals(_mind, cond._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is SpiderChargeCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return _mind?.GetHashCode() ?? 0;
}
}

View File

@@ -0,0 +1,70 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies.
/// </summary>
[DataDefinition]
public sealed partial class StealResearchCondition : IObjectiveCondition
{
private EntityUid? _mind;
private int _target;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
// TODO: clamp to number of research nodes in a single discipline maybe so easily maintainable
return new StealResearchCondition {
_mind = uid,
_target = IoCManager.Resolve<IRobustRandom>().Next(5, 10)
};
}
public string Title => Loc.GetString("objective-condition-steal-research-title", ("count", _target));
public string Description => Loc.GetString("objective-condition-steal-research-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Structures/Machines/server.rsi"), "server");
public float Progress
{
get
{
// prevent divide-by-zero
if (_target == 0)
return 1f;
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
if (role.DownloadedNodes.Count >= _target)
return 1f;
return (float) role.DownloadedNodes.Count / (float) _target;
}
}
public float Difficulty => 2.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is StealResearchCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is StealResearchCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
}
}

View File

@@ -0,0 +1,58 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Just requires that the player is not dead, ignores evac and what not.
/// </summary>
[DataDefinition]
public sealed partial class SurviveCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new SurviveCondition {_mind = uid};
}
public string Title => Loc.GetString("objective-condition-survive-title");
public string Description => Loc.GetString("objective-condition-survive-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Clothing/Mask/ninja.rsi"), "icon");
public float Difficulty => 0.5f;
public float Progress
{
get
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<MindComponent>(_mind, out var mind))
return 0f;
var mindSystem = entMan.System<SharedMindSystem>();
return mindSystem.IsCharacterDeadIc(mind) ? 0f : 1f;
}
}
public bool Equals(IObjectiveCondition? other)
{
return other is SurviveCondition condition && Equals(_mind, condition._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((SurviveCondition) obj);
}
public override int GetHashCode()
{
return (_mind != null ? _mind.GetHashCode() : 0);
}
}

View File

@@ -0,0 +1,57 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have called in a threat.
/// </summary>
[DataDefinition]
public sealed partial class TerrorCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new TerrorCondition {_mind = uid};
}
public string Title => Loc.GetString("objective-condition-terror-title");
public string Description => Loc.GetString("objective-condition-terror-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Fun/Instruments/otherinstruments.rsi"), "red_phone");
public float Progress
{
get
{
var entMan = IoCManager.Resolve<EntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
return role.CalledInThreat ? 1f : 0f;
}
}
public float Difficulty => 2.75f;
public bool Equals(IObjectiveCondition? other)
{
return other is TerrorCondition cond && Equals(_mind, cond._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is TerrorCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return _mind?.GetHashCode() ?? 0;
}
}

View File

@@ -0,0 +1,18 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements;
/// <summary>
/// Requires the player's mind to have the ninja role component, aka be a ninja.
/// </summary>
[DataDefinition]
public sealed partial class NinjaRequirement : IObjectiveRequirement
{
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
var entMan = IoCManager.Resolve<IEntityManager>();
return entMan.HasComponent<NinjaRoleComponent>(mindId);
}
}

View File

@@ -0,0 +1,19 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements;
/// <summary>
/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case.
/// </summary>
[DataDefinition]
public sealed partial class SpiderChargeTargetRequirement : IObjectiveRequirement
{
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.TryGetComponent<NinjaRoleComponent>(mindId, out var role);
return role?.SpiderChargeTarget != null;
}
}

View File

@@ -156,5 +156,16 @@ namespace Content.Server.Power.EntitySystems
UseCharge(uid, value, battery);
return true;
}
/// <summary>
/// Returns whether the battery is at least 99% charged, basically full.
/// </summary>
public bool IsFull(EntityUid uid, BatteryComponent? battery = null)
{
if (!Resolve(uid, ref battery))
return false;
return battery.CurrentCharge / battery.MaxCharge >= 0.99f;
}
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Chemistry.EntitySystems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Emp;
using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
@@ -38,6 +39,7 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
SubscribeLocalEvent<PowerCellDrawComponent, EntityUnpausedEvent>(OnUnpaused);
SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
@@ -233,6 +235,14 @@ public sealed partial class PowerCellSystem : SharedPowerCellSystem
OnBatteryExamined(uid, battery, args);
}
private void OnCellEmpAttempt(EntityUid uid, PowerCellComponent component, EmpAttemptEvent args)
{
var parent = Transform(uid).ParentUid;
// relay the attempt event to the slot so it can cancel it
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, args);
}
private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
{
TryGetBatteryFromSlot(uid, out var battery);

View File

@@ -0,0 +1,39 @@
using Content.Shared.Research.Components;
using Content.Shared.Research.Systems;
namespace Content.Server.Research.Systems;
public sealed class ResearchStealerSystem : SharedResearchStealerSystem
{
[Dependency] private readonly SharedResearchSystem _research = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ResearchStealerComponent, ResearchStealDoAfterEvent>(OnDoAfter);
}
private void OnDoAfter(EntityUid uid, ResearchStealerComponent comp, ResearchStealDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Target == null)
return;
var target = args.Target.Value;
if (!TryComp<TechnologyDatabaseComponent>(target, out var database))
return;
var ev = new ResearchStolenEvent(uid, target, database.UnlockedTechnologies);
RaiseLocalEvent(uid, ref ev);
// oops, no more advanced lasers!
_research.ClearTechs(target, database);
}
}
/// <summary>
/// Event raised on the user when research is stolen from a R&D server.
/// Techs contains every technology id researched.
/// </summary>
[ByRefEvent]
public record struct ResearchStolenEvent(EntityUid Used, EntityUid Target, List<String> Techs);

View File

@@ -0,0 +1,40 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Stores the ninja's objectives on the mind so if they die the rest of the greentext persists.
/// </summary>
[RegisterComponent]
public sealed partial class NinjaRoleComponent : AntagonistRoleComponent
{
/// <summary>
/// Number of doors that have been doorjacked, used for objective
/// </summary>
[DataField("doorsJacked")]
public int DoorsJacked;
/// <summary>
/// Research nodes that have been downloaded, used for objective
/// </summary>
[DataField("downloadedNodes")]
public HashSet<string> DownloadedNodes = new();
/// <summary>
/// Warp point that the spider charge has to target
/// </summary>
[DataField("spiderChargeTarget")]
public EntityUid? SpiderChargeTarget;
/// <summary>
/// Whether the spider charge has been detonated on the target, used for objective
/// </summary>
[DataField("spiderChargeDetonated")]
public bool SpiderChargeDetonated;
/// <summary>
/// Whether the comms console has been hacked, used for objective
/// </summary>
[DataField("calledInThreat")]
public bool CalledInThreat;
}

View File

@@ -10,6 +10,7 @@ public sealed class RoleSystem : SharedRoleSystem
base.Initialize();
SubscribeAntagEvents<InitialInfectedRoleComponent>();
SubscribeAntagEvents<NinjaRoleComponent>();
SubscribeAntagEvents<NukeopsRoleComponent>();
SubscribeAntagEvents<SubvertedSiliconRoleComponent>();
SubscribeAntagEvents<TraitorRoleComponent>();

View File

@@ -0,0 +1,16 @@
using Content.Server.StationEvents.Events;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// Configuration component for the Space Ninja antag.
/// </summary>
[RegisterComponent, Access(typeof(NinjaSpawnRule))]
public sealed partial class NinjaSpawnRuleComponent : Component
{
/// <summary>
/// Distance that the ninja spawns from the station's half AABB radius
/// </summary>
[DataField("spawnDistance")]
public float SpawnDistance = 20f;
}

View File

@@ -0,0 +1,51 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Ninja.Systems;
using Content.Server.Station.Components;
using Content.Server.StationEvents.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Random;
namespace Content.Server.StationEvents.Events;
/// <summary>
/// Event for spawning a Space Ninja mid-game.
/// </summary>
public sealed class NinjaSpawnRule : StationEventSystem<NinjaSpawnRuleComponent>
{
[Dependency] private readonly SpaceNinjaSystem _ninja = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
protected override void Started(EntityUid uid, NinjaSpawnRuleComponent comp, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, comp, gameRule, args);
if (!TryGetRandomStation(out var station))
return;
var stationData = Comp<StationDataComponent>(station.Value);
// find a station grid
var gridUid = StationSystem.GetLargestGrid(stationData);
if (gridUid == null || !TryComp<MapGridComponent>(gridUid, out var grid))
{
Sawmill.Warning("Chosen station has no grids, cannot spawn space ninja!");
return;
}
// figure out its AABB size and use that as a guide to how far ninja should be
var size = grid.LocalAABB.Size.Length() / 2;
var distance = size + comp.SpawnDistance;
var angle = RobustRandom.NextAngle();
// position relative to station center
var location = angle.ToVec() * distance;
// create the spawner, the ninja will appear when a ghost has picked the role
var xform = Transform(gridUid.Value);
var position = _transform.GetWorldPosition(xform) + location;
var coords = new MapCoordinates(position, xform.MapID);
Sawmill.Info($"Creating ninja spawnpoint at {coords}");
Spawn("SpawnPointGhostSpaceNinja", coords);
}
}

View File

@@ -47,7 +47,8 @@ namespace Content.Shared.Alert
Debug3,
Debug4,
Debug5,
Debug6
Debug6,
SuitPower
}
}

View File

@@ -0,0 +1,47 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Communications;
/// <summary>
/// Component for hacking a communications console to call in a threat.
/// Can only be done once, the component is remove afterwards.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedCommsHackerSystem))]
public sealed partial class CommsHackerComponent : Component
{
/// <summary>
/// Time taken to hack the console
/// </summary>
[DataField("delay")]
public TimeSpan Delay = TimeSpan.FromSeconds(20);
/// <summary>
/// Possible threats to choose from.
/// </summary>
[DataField("threats", required: true)]
public List<Threat> Threats = new();
}
/// <summary>
/// A threat that can be called in to the station by a ninja hacking a communications console.
/// Generally some kind of mid-round minor antag, though you could make it call in scrubber backflow if you wanted to.
/// You wouldn't do that, right?
/// </summary>
[DataDefinition]
public sealed partial class Threat
{
/// <summary>
/// Locale id for the announcement to be made from CentCom.
/// </summary>
[DataField("announcement")]
public string Announcement = default!;
/// <summary>
/// The game rule for the threat to be added, it should be able to work when added mid-round otherwise this will do nothing.
/// </summary>
[DataField("rule", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Rule = default!;
}

View File

@@ -0,0 +1,28 @@
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Communications;
/// <summary>
/// Only exists in shared to provide API and for access.
/// All logic is serverside.
/// </summary>
public abstract class SharedCommsHackerSystem : EntitySystem
{
/// <summary>
/// Set the list of threats to choose from when hacking a comms console.
/// </summary>
public void SetThreats(EntityUid uid, List<Threat> threats, CommsHackerComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.Threats = threats;
}
}
/// <summary>
/// DoAfter event for comms console terror ability.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class TerrorDoAfterEvent : SimpleDoAfterEvent { }

View File

@@ -112,6 +112,15 @@ namespace Content.Shared.Containers.ItemSlots
[ViewVariables(VVAccess.ReadWrite)]
public bool Locked = false;
/// <summary>
/// Prevents adding the eject alt-verb, but still lets you swap items.
/// </summary>
/// <remarks>
/// This does not affect EjectOnInteract, since if you do that you probably want ejecting to work.
/// </remarks>
[DataField("disableEject"), ViewVariables(VVAccess.ReadWrite)]
public bool DisableEject = false;
/// <summary>
/// Whether the item slots system will attempt to insert item from the user's hands into this slot when interacted with.
/// It doesn't block other insertion methods, like verbs.

View File

@@ -477,7 +477,7 @@ namespace Content.Shared.Containers.ItemSlots
// Add the eject-item verbs
foreach (var slot in itemSlots.Slots.Values)
{
if (slot.EjectOnInteract)
if (slot.EjectOnInteract || slot.DisableEject)
// For this item slot, ejecting/inserting is a primary interaction. Instead of an eject category
// alt-click verb, there will be a "Take item" primary interaction verb.
continue;

View File

@@ -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);
}
/// <param name="uid">Entity being electrocuted.</param>
/// <param name="sourceUid">Source entity of the electrocution.</param>
/// <param name="shockDamage">How much shock damage the entity takes.</param>
/// <param name="time">How long the entity will be stunned.</param>
/// <param name="refresh">Should <paramref>time</paramref> be refreshed (instead of accumilated) if the entity is already electrocuted?</param>
/// <param name="siemensCoefficient">How insulated the entity is from the shock. 0 means completely insulated, and 1 means no insulation.</param>
/// <param name="statusEffects">Status effects to apply to the entity.</param>
/// <param name="ignoreInsulation">Should the electrocution bypass the Insulated component?</param>
/// <returns>Whether the entity <see cref="uid"/> was stunned by the shock.</returns>
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;

View File

@@ -7,8 +7,6 @@ using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Tag;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Shared.Emag.Systems;
@@ -22,10 +20,8 @@ public sealed class EmagSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TagSystem _tag = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
@@ -56,8 +52,7 @@ public sealed class EmagSystem : EntitySystem
TryComp<LimitedChargesComponent>(uid, out var charges);
if (_charges.IsEmpty(uid, charges))
{
if (_net.IsClient && _timing.IsFirstTimePredicted)
_popup.PopupEntity(Loc.GetString("emag-no-charges"), user, user);
_popup.PopupClient(Loc.GetString("emag-no-charges"), user, user);
return false;
}
@@ -65,12 +60,8 @@ public sealed class EmagSystem : EntitySystem
if (!handled)
return false;
// only do popup on client
if (_net.IsClient && _timing.IsFirstTimePredicted)
{
_popup.PopupEntity(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
_popup.PopupClient(Loc.GetString("emag-success", ("target", Identity.Entity(target, EntityManager))), user,
user, PopupType.Medium);
}
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(user):player} emagged {ToPrettyString(target):target}");

View File

@@ -85,6 +85,28 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
}
}
/// <summary>
/// Add a list of implants to a person.
/// Logs any implant ids that don't have <see cref="SubdermalImplantComponent"/>.
/// </summary>
public void AddImplants(EntityUid uid, IEnumerable<String> implants)
{
var coords = Transform(uid).Coordinates;
foreach (var id in implants)
{
var ent = Spawn(id, coords);
if (TryComp<SubdermalImplantComponent>(ent, out var implant))
{
ForceImplant(uid, ent, implant);
}
else
{
Log.Warning($"Found invalid starting implant '{id}' on {uid} {ToPrettyString(uid):implanted}");
Del(ent);
}
}
}
/// <summary>
/// Forces an implant into a person
/// Good for on spawn related code or admin additions

View File

@@ -38,6 +38,20 @@ namespace Content.Shared.Interaction
}
}
/// <summary>
/// Raised on the user before interacting on an entity with bare hand.
/// Interaction is cancelled if this event is handled, so set it to true if you do custom interaction logic.
/// </summary>
public sealed class BeforeInteractHandEvent : HandledEntityEventArgs
{
public EntityUid Target { get; }
public BeforeInteractHandEvent(EntityUid target)
{
Target = target;
}
}
/// <summary>
/// Low-level interaction event used for entities without hands.
/// </summary>

View File

@@ -380,6 +380,15 @@ namespace Content.Shared.Interaction
public void InteractHand(EntityUid user, EntityUid target)
{
// allow for special logic before main interaction
var ev = new BeforeInteractHandEvent(target);
RaiseLocalEvent(user, ev);
if (ev.Handled)
{
_adminLogger.Add(LogType.InteractHand, LogImpact.Low, $"{ToPrettyString(user):user} interacted with {ToPrettyString(target):target}, but it was handled by another system");
return;
}
// all interactions should only happen when in range / unobstructed, so no range check is needed
var message = new InteractHandEvent(user, target);
RaiseLocalEvent(target, message, true);

View File

@@ -12,14 +12,16 @@ using Content.Shared.Players;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Shared.Mind;
public abstract class SharedMindSystem : EntitySystem
{
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPlayerSystem _playerSystem = default!;
// This is dictionary is required to track the minds of disconnected players that may have had their entity deleted.
@@ -268,6 +270,23 @@ public abstract class SharedMindSystem : EntitySystem
return true;
}
/// <summary>
/// Adds an objective, by id, to this mind.
/// </summary>
public bool TryAddObjective(EntityUid mindId, string name, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return false;
if (!_proto.TryIndex<ObjectivePrototype>(name, out var objective))
{
Log.Error($"Tried to add unknown objective prototype: {name}");
return false;
}
return TryAddObjective(mindId, mind, objective);
}
/// <summary>
/// Removes an objective to this mind.
/// </summary>
@@ -340,6 +359,19 @@ public abstract class SharedMindSystem : EntitySystem
return _playerSystem.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
}
/// <summary>
/// Gets a role component from a player's mind.
/// </summary>
/// <returns>Whether a role was found</returns>
public bool TryGetRole<T>(EntityUid user, [NotNullWhen(true)] out T? role) where T : Component
{
role = null;
if (!TryComp<MindContainerComponent>(user, out var mindContainer) || mindContainer.Mind == null)
return false;
return TryComp(mindContainer.Mind, out role);
}
/// <summary>
/// Sets the Mind's OwnedComponent and OwnedEntity
/// </summary>

View File

@@ -0,0 +1,38 @@
using Content.Shared.Ninja.Systems;
using Robust.Shared.Audio;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for draining power from APCs/substations/SMESes, when ProviderUid is set to a battery cell.
/// Does not rely on relay, simply being on the user and having BatteryUid set is enough.
/// </summary>
[RegisterComponent, Access(typeof(SharedBatteryDrainerSystem))]
public sealed partial class BatteryDrainerComponent : Component
{
/// <summary>
/// The powercell entity to drain power into.
/// Determines whether draining is possible.
/// </summary>
[DataField("batteryUid"), ViewVariables(VVAccess.ReadWrite)]
public EntityUid? BatteryUid;
/// <summary>
/// Conversion rate between joules in a device and joules added to battery.
/// Should be very low since powercells store nothing compared to even an APC.
/// </summary>
[DataField("drainEfficiency"), ViewVariables(VVAccess.ReadWrite)]
public float DrainEfficiency = 0.001f;
/// <summary>
/// Time that the do after takes to drain charge from a battery, in seconds
/// </summary>
[DataField("drainTime"), ViewVariables(VVAccess.ReadWrite)]
public float DrainTime = 1f;
/// <summary>
/// Sound played after the doafter ends.
/// </summary>
[DataField("sparkSound")]
public SoundSpecifier SparkSound = new SoundCollectionSpecifier("sparks");
}

View File

@@ -0,0 +1,33 @@
using Content.Shared.Actions;
using Content.Shared.Ninja.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
/// <summary>
/// Adds an action to dash, teleport to clicked position, when this item is held.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(DashAbilitySystem))]
public sealed partial class DashAbilityComponent : Component
{
/// <summary>
/// The action id for dashing.
/// </summary>
[DataField("dashAction", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string DashAction = string.Empty;
[DataField("dashActionEntity")]
public EntityUid? DashActionEntity;
/// <summary>
/// Sound played when using dash action.
/// </summary>
[DataField("blinkSound"), ViewVariables(VVAccess.ReadWrite)]
public SoundSpecifier BlinkSound = new SoundPathSpecifier("/Audio/Magic/blink.ogg")
{
Params = AudioParams.Default.WithVolume(5f)
};
}
public sealed partial class DashEvent : WorldTargetActionEvent { }

View File

@@ -0,0 +1,28 @@
using Content.Shared.Ninja.Systems;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for emagging things on click.
/// No charges but checks against a whitelist.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(EmagProviderSystem))]
public sealed partial class EmagProviderComponent : Component
{
/// <summary>
/// The tag that marks an entity as immune to emagging.
/// </summary>
[DataField("emagImmuneTag", customTypeSerializer: typeof(PrototypeIdSerializer<TagPrototype>))]
public string EmagImmuneTag = "EmagImmune";
/// <summary>
/// Whitelist that entities must be on to work.
/// </summary>
[DataField("whitelist"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public EntityWhitelist? Whitelist = null;
}

View File

@@ -0,0 +1,12 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for a Space Ninja's katana, controls ninja related dash logic.
/// Requires a ninja with a suit for abilities to work.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EnergyKatanaComponent : Component
{
}

View File

@@ -0,0 +1,45 @@
using Content.Shared.DoAfter;
using Content.Shared.Ninja.Systems;
using Content.Shared.Toggleable;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for toggling glove powers.
/// Powers being enabled is controlled by User not being null.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedNinjaGlovesSystem))]
public sealed partial class NinjaGlovesComponent : Component
{
/// <summary>
/// Entity of the ninja using these gloves, usually means enabled
/// </summary>
[DataField("user"), AutoNetworkedField]
public EntityUid? User;
/// <summary>
/// The action id for toggling ninja gloves abilities
/// </summary>
[DataField("toggleAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ToggleAction = "ActionToggleNinjaGloves";
[DataField("toggleActionEntity")]
public EntityUid? ToggleActionEntity;
/// <summary>
/// The whitelist used for the emag provider to emag airlocks only (not regular doors).
/// </summary>
[DataField("doorjackWhitelist")]
public EntityWhitelist DoorjackWhitelist = new()
{
Components = new[] {"Airlock"}
};
}

View File

@@ -0,0 +1,125 @@
using Content.Shared.Actions;
using Content.Shared.Ninja.Systems;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.Utility;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for ninja suit abilities and power consumption.
/// As an implementation detail, dashing with katana is a suit action which isn't ideal.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedNinjaSuitSystem))]
public sealed partial class NinjaSuitComponent : Component
{
/// <summary>
/// Battery charge used passively, in watts. Will last 1000 seconds on a small-capacity power cell.
/// </summary>
[DataField("passiveWattage")]
public float PassiveWattage = 0.36f;
/// <summary>
/// Battery charge used while cloaked, stacks with passive. Will last 200 seconds while cloaked on a small-capacity power cell.
/// </summary>
[DataField("cloakWattage")]
public float CloakWattage = 1.44f;
/// <summary>
/// Sound played when a ninja is hit while cloaked.
/// </summary>
[DataField("revealSound")]
public SoundSpecifier RevealSound = new SoundPathSpecifier("/Audio/Effects/chime.ogg");
/// <summary>
/// How long to disable all abilities for when revealed.
/// This adds a UseDelay to the ninja so it should not be set by anything else.
/// </summary>
[DataField("disableTime")]
public TimeSpan DisableTime = TimeSpan.FromSeconds(5);
/// <summary>
/// The action id for creating throwing stars.
/// </summary>
[DataField("createThrowingStarAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string CreateThrowingStarAction = "ActionCreateThrowingStar";
[DataField("createThrowingStarActionEntity")]
public EntityUid? CreateThrowingStarActionEntity;
/// <summary>
/// Battery charge used to create a throwing star. Can do it 25 times on a small-capacity power cell.
/// </summary>
[DataField("throwingStarCharge")]
public float ThrowingStarCharge = 14.4f;
/// <summary>
/// Throwing star item to create with the action
/// </summary>
[DataField("throwingStarPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string ThrowingStarPrototype = "ThrowingStarNinja";
/// <summary>
/// The action id for recalling a bound energy katana
/// </summary>
[DataField("recallKatanaAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string RecallKatanaAction = "ActionRecallKatana";
[DataField("recallKatanaActionEntity")]
public EntityUid? RecallKatanaActionEntity;
/// <summary>
/// Battery charge used per tile the katana teleported.
/// Uses 1% of a default battery per tile.
/// </summary>
[DataField("recallCharge")]
public float RecallCharge = 3.6f;
/// <summary>
/// The action id for creating an EMP burst
/// </summary>
[DataField("empAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string EmpAction = "ActionNinjaEmp";
[DataField("empActionEntity")]
public EntityUid? EmpActionEntity;
/// <summary>
/// Battery charge used to create an EMP burst. Can do it 2 times on a small-capacity power cell.
/// </summary>
[DataField("empCharge")]
public float EmpCharge = 180f;
/// <summary>
/// Range of the EMP in tiles.
/// </summary>
[DataField("empRange")]
public float EmpRange = 6f;
/// <summary>
/// Power consumed from batteries by the EMP
/// </summary>
[DataField("empConsumption")]
public float EmpConsumption = 100000f;
/// <summary>
/// How long the EMP effects last for, in seconds
/// </summary>
[DataField("empDuration")]
public float EmpDuration = 60f;
}
public sealed partial class CreateThrowingStarEvent : InstantActionEvent
{
}
public sealed partial class RecallKatanaEvent : InstantActionEvent
{
}
public sealed partial class NinjaEmpEvent : InstantActionEvent
{
}

View File

@@ -0,0 +1,38 @@
using Content.Shared.Ninja.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component placed on a mob to make it a space ninja, able to use suit and glove powers.
/// Contains ids of all ninja equipment and the game rule.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(SharedSpaceNinjaSystem))]
public sealed partial class SpaceNinjaComponent : Component
{
/// <summary>
/// The ninja game rule that spawned this ninja.
/// </summary>
[DataField("rule")]
public EntityUid? Rule;
/// <summary>
/// Currently worn suit
/// </summary>
[DataField("suit"), AutoNetworkedField]
public EntityUid? Suit;
/// <summary>
/// Currently worn gloves
/// </summary>
[DataField("gloves"), AutoNetworkedField]
public EntityUid? Gloves;
/// <summary>
/// Bound katana, set once picked up and never removed
/// </summary>
[DataField("katana"), AutoNetworkedField]
public EntityUid? Katana;
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for the Space Ninja's unique Spider Charge.
/// Only this component detonating can trigger the ninja's objective.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class SpiderChargeComponent : Component
{
/// Range for planting within the target area
[DataField("range")]
public float Range = 10f;
/// The ninja that planted this charge
[DataField("planter")]
public EntityUid? Planter = null;
}

View File

@@ -0,0 +1,67 @@
using Content.Shared.Ninja.Systems;
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Ninja.Components;
/// <summary>
/// Component for stunning mobs on click outside of harm mode.
/// Knocks them down for a bit and deals shock damage.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, Access(typeof(SharedStunProviderSystem))]
public sealed partial class StunProviderComponent : Component
{
/// <summary>
/// The powercell entity to take power from.
/// Determines whether stunning is possible.
/// </summary>
[DataField("batteryUid"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public EntityUid? BatteryUid;
/// <summary>
/// Joules required in the battery to stun someone. Defaults to 10 uses on a small battery.
/// </summary>
[DataField("stunCharge"), ViewVariables(VVAccess.ReadWrite)]
public float StunCharge = 36.0f;
/// <summary>
/// Shock damage dealt when stunning someone
/// </summary>
[DataField("stunDamage"), ViewVariables(VVAccess.ReadWrite)]
public int StunDamage = 5;
/// <summary>
/// Time that someone is stunned for, stacks if done multiple times.
/// </summary>
[DataField("stunTime"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan StunTime = TimeSpan.FromSeconds(3);
/// <summary>
/// How long stunning is disabled after stunning something.
/// </summary>
[DataField("cooldown"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Cooldown = TimeSpan.FromSeconds(1);
/// <summary>
/// Locale string to popup when there is no power
/// </summary>
[DataField("noPowerPopup", required: true), ViewVariables(VVAccess.ReadWrite)]
public string NoPowerPopup = string.Empty;
/// <summary>
/// Whitelist for what counts as a mob.
/// </summary>
[DataField("whitelist")]
public EntityWhitelist Whitelist = new()
{
Components = new[] {"Stamina"}
};
/// <summary>
/// When someone can next be stunned.
/// Essentially a UseDelay unique to this component.
/// </summary>
[DataField("nextStun", customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan NextStun = TimeSpan.Zero;
}

View File

@@ -0,0 +1,118 @@
using Content.Shared.Actions;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Physics;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Robust.Shared.GameObjects;
using Robust.Shared.Timing;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Handles dashing logic including charge consumption and checking attempt events.
/// </summary>
public sealed class DashAbilitySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedChargesSystem _charges = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DashAbilityComponent, GetItemActionsEvent>(OnGetItemActions);
SubscribeLocalEvent<DashAbilityComponent, DashEvent>(OnDash);
}
private void OnGetItemActions(EntityUid uid, DashAbilityComponent comp, GetItemActionsEvent args)
{
var ev = new AddDashActionEvent(args.User);
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
return;
args.AddAction(ref comp.DashActionEntity, comp.DashAction);
}
/// <summary>
/// Handle charges and teleport to a visible location.
/// </summary>
private void OnDash(EntityUid uid, DashAbilityComponent comp, DashEvent args)
{
if (!_timing.IsFirstTimePredicted)
return;
var user = args.Performer;
args.Handled = true;
var ev = new DashAttemptEvent(user);
RaiseLocalEvent(uid, ev);
if (ev.Cancelled)
return;
if (!_hands.IsHolding(user, uid, out var _))
{
_popup.PopupClient(Loc.GetString("dash-ability-not-held", ("item", uid)), user, user);
return;
}
TryComp<LimitedChargesComponent>(uid, out var charges);
if (_charges.IsEmpty(uid, charges))
{
_popup.PopupClient(Loc.GetString("dash-ability-no-charges", ("item", uid)), user, user);
return;
}
var origin = Transform(user).MapPosition;
var target = args.Target.ToMap(EntityManager, _transform);
// prevent collision with the user duh
if (!_interaction.InRangeUnobstructed(origin, target, 0f, CollisionGroup.Opaque, uid => uid == user))
{
// can only dash if the destination is visible on screen
_popup.PopupClient(Loc.GetString("dash-ability-cant-see", ("item", uid)), user, user);
return;
}
_transform.SetCoordinates(user, args.Target);
_transform.AttachToGridOrMap(user);
_audio.PlayPredicted(comp.BlinkSound, user, user);
if (charges != null)
_charges.UseCharge(uid, charges);
}
}
/// <summary>
/// Raised on the item before adding the dash action
/// </summary>
public sealed class AddDashActionEvent : CancellableEntityEventArgs
{
public EntityUid User;
public AddDashActionEvent(EntityUid user)
{
User = user;
}
}
/// <summary>
/// Raised on the item before dashing is done.
/// </summary>
public sealed class DashAttemptEvent : CancellableEntityEventArgs
{
public EntityUid User;
public DashAttemptEvent(EntityUid user)
{
User = user;
}
}

View File

@@ -0,0 +1,72 @@
using Content.Shared.Administration.Logs;
using Content.Shared.Emag.Systems;
using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Tag;
using Content.Shared.Whitelist;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Handles emagging whitelisted objects when clicked.
/// </summary>
public sealed class EmagProviderSystem : EntitySystem
{
[Dependency] private readonly EmagSystem _emag = default!;
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
[Dependency] private readonly TagSystem _tags = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EmagProviderComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
}
/// <summary>
/// Emag clicked entities that are on the whitelist.
/// </summary>
private void OnBeforeInteractHand(EntityUid uid, EmagProviderComponent comp, BeforeInteractHandEvent args)
{
// TODO: change this into a generic check event thing
if (args.Handled || !_gloves.AbilityCheck(uid, args, out var target))
return;
// only allowed to emag entities on the whitelist
if (comp.Whitelist != null && !comp.Whitelist.IsValid(target, EntityManager))
return;
// only allowed to emag non-immune entities
if (_tags.HasTag(target, comp.EmagImmuneTag))
return;
var handled = _emag.DoEmagEffect(uid, target);
if (!handled)
return;
_adminLogger.Add(LogType.Emag, LogImpact.High, $"{ToPrettyString(uid):player} emagged {ToPrettyString(target):target}");
var ev = new EmaggedSomethingEvent(target);
RaiseLocalEvent(uid, ref ev);
args.Handled = true;
}
/// <summary>
/// Set the whitelist for emagging something outside of yaml.
/// </summary>
public void SetWhitelist(EntityUid uid, EntityWhitelist? whitelist, EmagProviderComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.Whitelist = whitelist;
Dirty(uid, comp);
}
}
/// <summary>
/// Raised on the player when emagging something.
/// </summary>
[ByRefEvent]
public record struct EmaggedSomethingEvent(EntityUid Target);

View File

@@ -0,0 +1,47 @@
using Content.Shared.Inventory.Events;
using Content.Shared.Ninja.Components;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// System for katana binding and dash events. Recalling is handled by the suit.
/// </summary>
public sealed class EnergyKatanaSystem : EntitySystem
{
[Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EnergyKatanaComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<EnergyKatanaComponent, AddDashActionEvent>(OnAddDashAction);
SubscribeLocalEvent<EnergyKatanaComponent, DashAttemptEvent>(OnDashAttempt);
}
/// <summary>
/// When equipped by a ninja, try to bind it.
/// </summary>
private void OnEquipped(EntityUid uid, EnergyKatanaComponent comp, GotEquippedEvent args)
{
// check if user isnt a ninja or already has a katana bound
var user = args.Equipee;
if (!TryComp<SpaceNinjaComponent>(user, out var ninja) || ninja.Katana != null)
return;
// bind it since its unbound
_ninja.BindKatana(user, uid, ninja);
}
private void OnAddDashAction(EntityUid uid, EnergyKatanaComponent comp, AddDashActionEvent args)
{
if (!HasComp<SpaceNinjaComponent>(args.User))
args.Cancel();
}
private void OnDashAttempt(EntityUid uid, EnergyKatanaComponent comp, DashAttemptEvent args)
{
if (!TryComp<SpaceNinjaComponent>(args.User, out var ninja) || ninja.Katana != uid)
args.Cancel();
}
}

View File

@@ -0,0 +1,69 @@
using Content.Shared.Ninja.Components;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Basic draining prediction and API, all real logic is handled serverside.
/// </summary>
public abstract class SharedBatteryDrainerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BatteryDrainerComponent, DoAfterAttemptEvent<DrainDoAfterEvent>>(OnDoAfterAttempt);
SubscribeLocalEvent<BatteryDrainerComponent, DrainDoAfterEvent>(OnDoAfter);
}
/// <summary>
/// Cancel any drain doafters if the battery is removed or gets filled.
/// </summary>
protected virtual void OnDoAfterAttempt(EntityUid uid, BatteryDrainerComponent comp, DoAfterAttemptEvent<DrainDoAfterEvent> args)
{
if (comp.BatteryUid == null)
{
args.Cancel();
}
}
/// <summary>
/// Drain power from a power source (on server) and repeat if it succeeded.
/// Client will predict always succeeding since power is serverside.
/// </summary>
private void OnDoAfter(EntityUid uid, BatteryDrainerComponent comp, DrainDoAfterEvent args)
{
if (args.Cancelled || args.Handled || args.Target == null)
return;
// repeat if there is still power to drain
args.Repeat = TryDrainPower(uid, comp, args.Target.Value);
}
/// <summary>
/// Attempt to drain as much power as possible into the powercell.
/// Client always predicts this as succeeding since power is serverside and it can only fail once, when the powercell is filled or the target is emptied.
/// </summary>
protected virtual bool TryDrainPower(EntityUid uid, BatteryDrainerComponent comp, EntityUid target)
{
return true;
}
/// <summary>
/// Sets the battery field on the drainer.
/// </summary>
public void SetBattery(EntityUid uid, EntityUid? battery, BatteryDrainerComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.BatteryUid = battery;
}
}
/// <summary>
/// DoAfter event for <see cref="BatteryDrainerComponent"/>.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class DrainDoAfterEvent : SimpleDoAfterEvent { }

View File

@@ -0,0 +1,116 @@
using Content.Shared.Actions;
using Content.Shared.CombatMode;
using Content.Shared.Communications;
using Content.Shared.Doors.Components;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Hands.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Inventory.Events;
using Content.Shared.Ninja.Components;
using Content.Shared.Popups;
using Content.Shared.Research.Components;
using Content.Shared.Timing;
using Content.Shared.Toggleable;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Provides the toggle action and handles examining and unequipping.
/// </summary>
public abstract class SharedNinjaGlovesSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
[Dependency] protected readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] protected readonly SharedInteractionSystem Interaction = default!;
[Dependency] private readonly SharedSpaceNinjaSystem _ninja = default!;
[Dependency] protected readonly SharedPopupSystem Popup = default!;
[Dependency] private readonly UseDelaySystem _useDelay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaGlovesComponent, GetItemActionsEvent>(OnGetItemActions);
SubscribeLocalEvent<NinjaGlovesComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<NinjaGlovesComponent, GotUnequippedEvent>(OnUnequipped);
}
/// <summary>
/// Disable glove abilities and show the popup if they were enabled previously.
/// </summary>
public void DisableGloves(EntityUid uid, NinjaGlovesComponent? comp = null)
{
// already disabled?
if (!Resolve(uid, ref comp) || comp.User == null)
return;
var user = comp.User.Value;
comp.User = null;
Dirty(uid, comp);
Appearance.SetData(uid, ToggleVisuals.Toggled, false);
Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user);
RemComp<BatteryDrainerComponent>(user);
RemComp<EmagProviderComponent>(user);
RemComp<StunProviderComponent>(user);
RemComp<ResearchStealerComponent>(user);
RemComp<CommsHackerComponent>(user);
}
/// <summary>
/// Adds the toggle action when equipped.
/// </summary>
private void OnGetItemActions(EntityUid uid, NinjaGlovesComponent comp, GetItemActionsEvent args)
{
if (HasComp<SpaceNinjaComponent>(args.User))
args.AddAction(ref comp.ToggleActionEntity, comp.ToggleAction);
}
/// <summary>
/// Show if the gloves are enabled when examining.
/// </summary>
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"));
}
/// <summary>
/// Disable gloves when unequipped and clean up ninja's gloves reference
/// </summary>
private void OnUnequipped(EntityUid uid, NinjaGlovesComponent comp, GotUnequippedEvent args)
{
if (comp.User != null)
{
var user = comp.User.Value;
Popup.PopupClient(Loc.GetString("ninja-gloves-off"), user, user);
DisableGloves(uid, comp);
}
}
// TODO: generic event thing
/// <summary>
/// GloveCheck but for abilities stored on the player, skips some checks.
/// Intended to be more generic, doesn't require the user to be a ninja or have any ninja equipment.
/// </summary>
public bool AbilityCheck(EntityUid uid, BeforeInteractHandEvent args, out EntityUid target)
{
target = args.Target;
return _timing.IsFirstTimePredicted
&& !_combatMode.IsInCombatMode(uid)
&& !_useDelay.ActiveDelay(uid)
&& TryComp<HandsComponent>(uid, out var hands)
&& hands.ActiveHandEntity == null
&& Interaction.InRangeUnobstructed(uid, target);
}
}

View File

@@ -0,0 +1,139 @@
using Content.Shared.Actions;
using Content.Shared.Clothing.Components;
using Content.Shared.Clothing.EntitySystems;
using Content.Shared.Inventory.Events;
using Content.Shared.Ninja.Components;
using Content.Shared.Timing;
using Robust.Shared.Audio;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Handles (un)equipping and provides some API functions.
/// </summary>
public abstract class SharedNinjaSuitSystem : EntitySystem
{
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
[Dependency] protected readonly SharedSpaceNinjaSystem _ninja = default!;
[Dependency] protected readonly StealthClothingSystem StealthClothing = default!;
[Dependency] protected readonly UseDelaySystem UseDelay = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NinjaSuitComponent, GotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<NinjaSuitComponent, GetItemActionsEvent>(OnGetItemActions);
SubscribeLocalEvent<NinjaSuitComponent, AddStealthActionEvent>(OnAddStealthAction);
SubscribeLocalEvent<NinjaSuitComponent, GotUnequippedEvent>(OnUnequipped);
}
/// <summary>
/// Call the shared and serverside code for when a ninja equips the suit.
/// </summary>
private void OnEquipped(EntityUid uid, NinjaSuitComponent comp, GotEquippedEvent args)
{
var user = args.Equipee;
if (!TryComp<SpaceNinjaComponent>(user, out var ninja))
return;
NinjaEquippedSuit(uid, comp, user, ninja);
}
/// <summary>
/// Add all the actions when a suit is equipped by a ninja.
/// </summary>
private void OnGetItemActions(EntityUid uid, NinjaSuitComponent comp, GetItemActionsEvent args)
{
if (!HasComp<SpaceNinjaComponent>(args.User))
return;
args.AddAction(ref comp.RecallKatanaActionEntity, comp.RecallKatanaAction);
args.AddAction(ref comp.CreateThrowingStarActionEntity, comp.CreateThrowingStarAction);
args.AddAction(ref comp.EmpActionEntity, comp.EmpAction);
}
/// <summary>
/// Only add stealth clothing's toggle action when equipped by a ninja.
/// </summary>
private void OnAddStealthAction(EntityUid uid, NinjaSuitComponent comp, AddStealthActionEvent args)
{
if (!HasComp<SpaceNinjaComponent>(args.User))
args.Cancel();
}
/// <summary>
/// Call the shared and serverside code for when anyone unequips a suit.
/// </summary>
private void OnUnequipped(EntityUid uid, NinjaSuitComponent comp, GotUnequippedEvent args)
{
UserUnequippedSuit(uid, comp, args.Equipee);
}
/// <summary>
/// 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.
/// </summary>
protected virtual void NinjaEquippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user, SpaceNinjaComponent ninja)
{
// mark the user as wearing this suit, used when being attacked among other things
_ninja.AssignSuit(user, uid, ninja);
// initialize phase cloak, but keep it off
StealthClothing.SetEnabled(uid, user, false);
}
/// <summary>
/// Force uncloaks the user and disables suit abilities.
/// </summary>
public void RevealNinja(EntityUid uid, EntityUid user, NinjaSuitComponent? comp = null, StealthClothingComponent? stealthClothing = null)
{
if (!Resolve(uid, ref comp, ref stealthClothing))
return;
if (!StealthClothing.SetEnabled(uid, user, false, stealthClothing))
return;
// previously cloaked, disable abilities for a short time
_audio.PlayPredicted(comp.RevealSound, uid, user);
// all abilities check for a usedelay on the ninja
var useDelay = EnsureComp<UseDelayComponent>(user);
useDelay.Delay = comp.DisableTime;
UseDelay.BeginDelay(user, useDelay);
}
// TODO: modify PowerCellDrain
/// <summary>
/// Returns the power used by a suit
/// </summary>
public float SuitWattage(EntityUid uid, NinjaSuitComponent? suit = null)
{
if (!Resolve(uid, ref suit))
return 0f;
float wattage = suit.PassiveWattage;
if (TryComp<StealthClothingComponent>(uid, out var stealthClothing) && stealthClothing.Enabled)
wattage += suit.CloakWattage;
return wattage;
}
/// <summary>
/// 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.
/// </summary>
protected virtual void UserUnequippedSuit(EntityUid uid, NinjaSuitComponent comp, EntityUid user)
{
if (!TryComp<SpaceNinjaComponent>(user, out var ninja))
return;
// mark the user as not wearing a suit
_ninja.AssignSuit(user, null, ninja);
// disable glove abilities
if (ninja.Gloves != null && TryComp<NinjaGlovesComponent>(ninja.Gloves.Value, out var gloves))
_gloves.DisableGloves(ninja.Gloves.Value, gloves);
}
}

View File

@@ -0,0 +1,89 @@
using Content.Shared.Clothing.Components;
using Content.Shared.Ninja.Components;
using Content.Shared.Weapons.Melee.Events;
using Content.Shared.Weapons.Ranged.Events;
using Content.Shared.Popups;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// Provides shared ninja API, handles being attacked revealing ninja and stops guns from shooting.
/// </summary>
public abstract class SharedSpaceNinjaSystem : EntitySystem
{
[Dependency] protected readonly SharedNinjaSuitSystem _suit = default!;
[Dependency] protected readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpaceNinjaComponent, AttackedEvent>(OnNinjaAttacked);
SubscribeLocalEvent<SpaceNinjaComponent, ShotAttemptedEvent>(OnShotAttempted);
}
/// <summary>
/// Set the ninja's worn suit entity
/// </summary>
public void AssignSuit(EntityUid uid, EntityUid? suit, SpaceNinjaComponent? comp = null)
{
if (!Resolve(uid, ref comp) || comp.Suit == suit)
return;
comp.Suit = suit;
Dirty(uid, comp);
}
/// <summary>
/// Set the ninja's worn gloves entity
/// </summary>
public void AssignGloves(EntityUid uid, EntityUid? gloves, SpaceNinjaComponent? comp = null)
{
if (!Resolve(uid, ref comp) || comp.Gloves == gloves)
return;
comp.Gloves = gloves;
Dirty(uid, comp);
}
/// <summary>
/// Bind a katana entity to a ninja, letting it be recalled and dash.
/// </summary>
public void BindKatana(EntityUid uid, EntityUid? katana, SpaceNinjaComponent? comp = null)
{
if (!Resolve(uid, ref comp) || comp.Katana == katana)
return;
comp.Katana = katana;
Dirty(uid, comp);
}
/// <summary>
/// Gets the user's battery and tries to use some charge from it, returning true if successful.
/// Serverside only.
/// </summary>
public virtual bool TryUseCharge(EntityUid user, float charge)
{
return false;
}
/// <summary>
/// Handle revealing ninja if cloaked when attacked.
/// </summary>
private void OnNinjaAttacked(EntityUid uid, SpaceNinjaComponent comp, AttackedEvent args)
{
if (comp.Suit != null && TryComp<StealthClothingComponent>(comp.Suit, out var stealthClothing) && stealthClothing.Enabled)
{
_suit.RevealNinja(comp.Suit.Value, uid, null, stealthClothing);
}
}
/// <summary>
/// Require ninja to fight with HONOR, no guns!
/// </summary>
private void OnShotAttempted(EntityUid uid, SpaceNinjaComponent comp, ref ShotAttemptedEvent args)
{
_popup.PopupClient(Loc.GetString("gun-disabled"), uid, uid);
args.Cancel();
}
}

View File

@@ -0,0 +1,32 @@
using Content.Shared.Ninja.Components;
namespace Content.Shared.Ninja.Systems;
/// <summary>
/// All interaction logic is implemented serverside.
/// This is in shared for API and access.
/// </summary>
public abstract class SharedStunProviderSystem : EntitySystem
{
/// <summary>
/// Set the battery field on the stun provider.
/// </summary>
public void SetBattery(EntityUid uid, EntityUid? battery, StunProviderComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.BatteryUid = battery;
}
/// <summary>
/// Set the no power popup field on the stun provider.
/// </summary>
public void SetNoPowerPopup(EntityUid uid, string popup, StunProviderComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.NoPowerPopup = popup;
}
}

View File

@@ -0,0 +1,17 @@
using Content.Shared.Research.Systems;
using Robust.Shared.GameStates;
namespace Content.Shared.Research.Components;
/// <summary>
/// Component for stealing technologies from a R&D server, when gloves are enabled.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedResearchStealerSystem))]
public sealed partial class ResearchStealerComponent : Component
{
/// <summary>
/// Time taken to steal research from a server
/// </summary>
[DataField("delay"), ViewVariables(VVAccess.ReadWrite)]
public TimeSpan Delay = TimeSpan.FromSeconds(20);
}

View File

@@ -0,0 +1,63 @@
using Content.Shared.DoAfter;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Research.Components;
using Robust.Shared.Serialization;
namespace Content.Shared.Research.Systems;
public abstract class SharedResearchStealerSystem : EntitySystem
{
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedNinjaGlovesSystem _gloves = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ResearchStealerComponent, BeforeInteractHandEvent>(OnBeforeInteractHand);
}
/// <summary>
/// Start do after for downloading techs from a r&d server.
/// Will only try if there is at least 1 tech researched.
/// </summary>
private void OnBeforeInteractHand(EntityUid uid, ResearchStealerComponent comp, BeforeInteractHandEvent args)
{
// TODO: generic event
if (args.Handled || !_gloves.AbilityCheck(uid, args, out var target))
return;
// can only hack the server, not a random console
if (!TryComp<TechnologyDatabaseComponent>(target, out var database) || HasComp<ResearchClientComponent>(target))
return;
args.Handled = true;
// fail fast if theres no techs to steal right now
if (database.UnlockedTechnologies.Count == 0)
{
_popup.PopupClient(Loc.GetString("ninja-download-fail"), uid, uid);
return;
}
var doAfterArgs = new DoAfterArgs(uid, comp.Delay, new ResearchStealDoAfterEvent(), target: target, used: uid, eventTarget: uid)
{
BreakOnDamage = true,
BreakOnUserMove = true,
MovementThreshold = 0.5f
};
_doAfter.TryStartDoAfter(doAfterArgs);
}
}
/// <summary>
/// Raised on the research stealer when the doafter completes.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class ResearchStealDoAfterEvent : SimpleDoAfterEvent
{
}

View File

@@ -169,6 +169,18 @@ public abstract class SharedResearchSystem : EntitySystem
if (prototype.Tier < discipline.LockoutTier)
return;
component.MainDiscipline = prototype.Discipline;
Dirty(component);
Dirty(uid, component);
}
/// <summary>
/// Clear all unlocked technologies from the database.
/// </summary>
public void ClearTechs(EntityUid uid, TechnologyDatabaseComponent? comp = null)
{
if (!Resolve(uid, ref comp) || comp.UnlockedTechnologies.Count == 0)
return;
comp.UnlockedTechnologies.Clear();
Dirty(uid, comp);
}
}

View File

@@ -2,3 +2,8 @@
license: "CC-BY-3.0"
copyright: "Created by qwertyquerty"
source: "https://www.youtube.com/@qwertyquerty"
- files: ["ninja_greeting.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from TG station."
source: "https://github.com/tgstation/tgstation/blob/b02b93ce2ab891164511a973493cdf951b4120f7/sound/effects/ninja_greeting.ogg"

Binary file not shown.

View File

@@ -2,4 +2,11 @@ verb-categories-antag = Antag ctrl
admin-verb-make-traitor = Make the target into a traitor.
admin-verb-make-zombie = Zombifies the target immediately.
admin-verb-make-nuclear-operative = Make target a into lone Nuclear Operative.
admin-verb-make-pirate = Make the target into a pirate. Note this doesn't configure the game rule.
admin-verb-make-pirate = Make the target into a pirate. Note that this doesn't configure the game rule.
admin-verb-make-space-ninja = Make the target into a space ninja.
admin-verb-text-make-traitor = Make Traitor
admin-verb-text-make-zombie = Make Zombie
admin-verb-text-make-nuclear-operative = Make Nuclear Operative
admin-verb-text-make-pirate = Make Pirate
admin-verb-text-make-space-ninja = Make Space Ninja

View File

@@ -95,3 +95,6 @@ alerts-bleed-desc = You're [color=red]bleeding[/color].
alerts-pacified-name = [color=green]Pacified[/color]
alerts-pacified-desc = You're pacified; you won't be able to attack anyone directly.
alerts-suit-power-name = Suit Power
alerts-suit-power-desc = How much power your space ninja suit has.

View File

@@ -0,0 +1,3 @@
battery-drainer-full = Your battery is already full
battery-drainer-empty = {CAPITALIZE(THE($battery))} does not have enough power to drain
battery-drainer-success = You drain power from {THE($battery)}!

View File

@@ -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.

View File

@@ -49,5 +49,6 @@ guide-entry-nuclear-operatives = Nuclear Operatives
guide-entry-traitors = Traitors
guide-entry-zombies = Zombies
guide-entry-minor-antagonists = Minor Antagonists
guide-entry-space-ninja = Space Ninja
guide-entry-writing = Writing

View File

@@ -0,0 +1,6 @@
ninja-gloves-on = The gloves surge with power!
ninja-gloves-off = The gloves power down...
ninja-gloves-not-wearing-suit = You aren't wearing a ninja suit
ninja-gloves-examine-on = All abilities are enabled.
ninja-gloves-examine-off = Boring old gloves...
ninja-doorjack-success = The gloves zap something in {THE($target)}.

View File

@@ -0,0 +1,6 @@
ninja-katana-recalled = Your Energy Katana teleports into your hand!
ninja-hands-full = Your hands are full!
dash-ability-not-held = You aren't holding your katana!
dash-ability-no-charges = No charges left!
dash-ability-cant-see = You can't see that!

View File

@@ -0,0 +1,21 @@
action-name-toggle-ninja-gloves = Toggle ninja gloves
action-desc-toggle-ninja-gloves = Toggles all glove actions on left click. Includes your doorjack, draining power, stunning enemies, downloading research and calling in a threat.
action-name-toggle-phase-cloak = Phase cloak
action-desc-toggle-phase-cloak = Toggles your suit's phase cloak. Beware that if you are hit, all abilities are disabled for 5 seconds, including your cloak!
ninja-no-power = Not enough charge in suit battery!
action-name-create-throwing-star = Create throwing star
action-desc-create-throwing-star = Channels suit power into creating a throwing star that deals extra stamina damage.
action-name-recall-katana = Recall katana
action-desc-recall-katana = Teleports the Energy Katana linked to this suit to its wearer, cost based on distance.
action-name-katana-dash = Katana dash
action-desc-katana-dash = Teleport to anywhere you can see, if your Energy Katana is in your hand.
action-name-em-burst = EM Burst
action-desc-em-burst = Disable any nearby technology with an electro-magnetic pulse.
ninja-research-steal-fail = No new research nodes were stolen...
ninja-research-steal-success = Stole {$count} new nodes from {THE($server)}.

View File

@@ -0,0 +1,8 @@
ninja-round-end-agent-name = ninja
objective-issuer-spiderclan = [color=#33cc00]Spider Clan[/color]
ninja-role-greeting =
I am an elite mercenary of the mighty Spider Clan!
Surprise is my weapon. Shadows are my armor. Without them, I am nothing.
Use your pinpointer to find the station. Good luck!

View File

@@ -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!

View File

@@ -0,0 +1,2 @@
objective-condition-doorjack-title = Doorjack {$count} doors on the station.
objective-condition-doorjack-description = Your gloves can emag airlocks. Do this {$count} doors on the station.

View File

@@ -0,0 +1,3 @@
objective-condition-spider-charge-title = Detonate the spider clan charge in {$location}
objective-condition-spider-charge-no-target = Detonate the spider clan charge... somewhere?
objective-condition-spider-charge-description = This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!

View File

@@ -0,0 +1,2 @@
objective-condition-steal-research-title = Steal {$count} technologies.
objective-condition-steal-research-description = Your gloves can be used to hack a research server and steal its precious data. If science has been slacking you'll have to get to work.

View File

@@ -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?

View File

@@ -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.

View File

@@ -24,3 +24,6 @@ roles-antag-nuclear-operative-objective = Find the nuke disk and blow up the sta
roles-antag-subverted-silicon-name = Subverted silicon
roles-antag-subverted-silicon-objective = Follow your new laws and do bad unto the station.
roles-antag-space-ninja-name = Space Ninja
roles-antag-space-ninja-objective = Use your stealth to sabotage the station, nom on electrical wires.

View File

@@ -0,0 +1,84 @@
# gloves
- type: entity
id: ActionToggleNinjaGloves
name: action-name-toggle-ninja-gloves
description: action-desc-toggle-ninja-gloves
noSpawn: true
components:
- type: InstantAction
priority: -13
event: !type:ToggleActionEvent {}
# suit
- type: entity
id: ActionCreateThrowingStar
name: action-name-create-throwing-star
description: action-desc-create-throwing-star
noSpawn: true
components:
- type: InstantAction
useDelay: 0.5
icon:
sprite: Objects/Weapons/Throwable/throwing_star.rsi
state: icon
itemIconStyle: NoItem
priority: -10
event: !type:CreateThrowingStarEvent {}
- type: entity
id: ActionRecallKatana
name: action-name-recall-katana
description: action-desc-recall-katana
noSpawn: true
components:
- type: InstantAction
useDelay: 1
icon:
sprite: Objects/Weapons/Melee/energykatana.rsi
state: icon
itemIconStyle: NoItem
priority: -11
event: !type:RecallKatanaEvent {}
- type: entity
id: ActionNinjaEmp
name: action-name-em-burst
description: action-desc-em-burst
noSpawn: true
components:
- type: InstantAction
icon:
sprite: Objects/Weapons/Grenades/empgrenade.rsi
state: icon
itemIconStyle: BigAction
priority: -13
event: !type:NinjaEmpEvent {}
- type: entity
id: ActionTogglePhaseCloak
name: action-name-toggle-phase-cloak
description: action-desc-toggle-phase-cloak
noSpawn: true
components:
- type: InstantAction
# have to plan (un)cloaking ahead of time
useDelay: 5
priority: -9
event: !type:ToggleStealthEvent
# katana
- type: entity
id: ActionEnergyKatanaDash
name: action-name-katana-dash
description: action-desc-katana-dash
noSpawn: true
components:
- type: WorldTargetAction
icon:
sprite: Objects/Magic/magicactions.rsi
state: blink
itemIconStyle: NoItem
priority: -12
event: !type:DashEvent
checkCanAccess: false
range: 0

View File

@@ -6,6 +6,7 @@
order:
- category: Health
- category: Stamina
- alertType: SuitPower
- category: Internals
- alertType: Fire
- alertType: Handcuffed

View File

@@ -0,0 +1,23 @@
- type: alert
id: SuitPower
icons:
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power0
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power1
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power2
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power3
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power4
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power5
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power6
- sprite: /Textures/Interface/Alerts/ninja_power.rsi
state: power7
name: alerts-suit-power-name
description: alerts-suit-power-desc
minSeverity: 0
maxSeverity: 7

View File

@@ -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:

View File

@@ -174,3 +174,15 @@
- type: Clothing
sprite: Clothing/Eyes/Glasses/science.rsi
- type: SolutionScanner
- type: entity
parent: ClothingEyesBase
id: ClothingEyesVisorNinja
name: ninja visor
description: An advanced visor protecting a ninja's eyes from flashing lights.
components:
- type: Sprite
sprite: Clothing/Eyes/Glasses/ninjavisor.rsi
- type: Clothing
sprite: Clothing/Eyes/Glasses/ninjavisor.rsi
- type: FlashImmunity

View File

@@ -191,8 +191,18 @@
components:
- type: Sprite
sprite: Clothing/Hands/Gloves/spaceninja.rsi
layers:
- state: icon
map: [ "enum.ToggleVisuals.Layer" ]
- type: Clothing
sprite: Clothing/Hands/Gloves/spaceninja.rsi
- type: Appearance
- type: GenericVisualizer
visuals:
enum.ToggleVisuals.Toggled:
enum.ToggleVisuals.Layer:
True: {state: icon-green}
False: {state: icon}
- type: GloveHeatResistance
heatResistance: 1400
- type: Insulated
@@ -202,6 +212,9 @@
- type: Thieving
stripTimeReduction: 1
stealthy: true
- type: NinjaGloves
# not actually electrified, just used to make stun ability work
- type: Electrified
- type: entity
parent: ClothingHandsBase

View File

@@ -156,7 +156,7 @@
#Space Ninja Helmet
- type: entity
parent: ClothingHeadBase
parent: ClothingHeadEVAHelmetBase
id: ClothingHeadHelmetSpaceNinja
name: space ninja helmet
description: What may appear to be a simple black garment is in fact a highly sophisticated nano-weave helmet. Standard issue ninja gear.
@@ -165,11 +165,11 @@
sprite: Clothing/Head/Helmets/spaceninja.rsi
- type: Clothing
sprite: Clothing/Head/Helmets/spaceninja.rsi
- type: IngestionBlocker
- type: Tag
tags:
- HidesHair
- WhitelistChameleon
- type: IngestionBlocker
- type: IdentityBlocker
#Templar Helmet

View File

@@ -491,3 +491,17 @@
- type: AddAccentClothing
accent: ReplacementAccent
replacement: italian
- type: entity
parent: ClothingMaskBase
id: ClothingMaskNinja
name: ninja mask
description: A close-fitting nano-enhanced mask that acts both as an air filter and a post-modern fashion statement.
components:
- type: Sprite
sprite: Clothing/Mask/ninja.rsi
- type: Clothing
sprite: Clothing/Mask/ninja.rsi
- type: EyeProtection
- type: BreathMask
- type: IdentityBlocker

View File

@@ -93,7 +93,7 @@
- type: entity
parent: ClothingOuterBase
id: ClothingOuterSuitSpaceninja
id: ClothingOuterSuitSpaceNinja
name: space ninja suit
description: This black technologically advanced, cybernetically-enhanced suit provides good protection and many abilities like invisibility or teleportation.
components:
@@ -102,7 +102,8 @@
- type: Clothing
sprite: Clothing/OuterClothing/Suits/spaceninja.rsi
- type: StealthClothing
visibility: 0.3
visibility: 1.1
toggleAction: ActionTogglePhaseCloak
- type: PressureProtection
highPressureMultiplier: 0.6
lowPressureMultiplier: 1000
@@ -115,18 +116,20 @@
Slash: 0.6
Piercing: 0.6
Heat: 0.6
- type: entity
id: ActionTogglePhaseCloak
name: action-name-toggle-phase-cloak
description: action-desc-toggle-phase-cloak
noSpawn: true
components:
- type: InstantAction
# have to plan (un)cloaking ahead of time
useDelay: 5
priority: -9
event: !type:ToggleStealthEvent
- type: NinjaSuit
- type: PowerCellSlot
cellSlotId: cell_slot
# throwing in a recharger would bypass glove charging mechanic
fitsInCharger: false
- type: ContainerContainer
containers:
cell_slot: !type:ContainerSlot
- type: ItemSlots
slots:
cell_slot:
name: power-cell-slot-component-slot-name-default
startingItem: PowerCellSmall
disableEject: true
- type: entity
parent: ClothingOuterBase

View File

@@ -81,6 +81,10 @@
- type: Clothing
sprite: Clothing/Shoes/Specific/spaceninja.rsi
- type: NoSlip
- type: ClothingSpeedModifier
# ninja are masters of sneaking around relatively quickly, won't break cloak
walkModifier: 1.1
sprintModifier: 1.3
- type: entity
parent: ClothingShoesBaseButcherable

View File

@@ -932,6 +932,17 @@
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/mercenary.rsi
- type: entity
parent: UnsensoredClothingUniformBase
id: ClothingUniformJumpsuitNinja
name: ninja jumpsuit
description: A nano-enhanced jumpsuit designed for maximum comfort and tacticality.
components:
- type: Sprite
sprite: Clothing/Uniforms/Jumpsuit/ninja.rsi
- type: Clothing
sprite: Clothing/Uniforms/Jumpsuit/ninja.rsi
- type: entity
parent: ClothingUniformBase
id: ClothingUniformJumpsuitAtmos

View File

@@ -0,0 +1,19 @@
- type: entity
id: EffectSparks
noSpawn: true
components:
- type: TimedDespawn
lifetime: 0.5
- type: Sprite
drawdepth: Effects
noRot: true
layers:
- shader: unshaded
map: ["enum.EffectLayers.Unshaded"]
sprite: Effects/sparks.rsi
state: sparks
- type: EffectVisuals
- type: Tag
tags:
- HideContextMenu
- type: AnimationPlayer

View File

@@ -111,3 +111,22 @@
- state: green
- sprite: Mobs/Aliens/Carps/dragon.rsi
state: alive
- type: entity
id: SpawnPointGhostSpaceNinja
name: ghost role spawn point
suffix: space ninja
parent: MarkerBase
components:
- type: GhostRole
name: Space Ninja
description: Use stealth and deception to sabotage the station.
rules: You are an elite mercenary of the Spider Clan. You aren't required to follow your objectives, yet your NINJA HONOR demands you try.
- type: GhostRoleMobSpawner
prototype: MobHumanSpaceNinja
- type: Sprite
sprite: Markers/jobs.rsi
layers:
- state: green
- sprite: Objects/Weapons/Melee/energykatana.rsi
state: icon

Some files were not shown because too many files have changed in this diff Show More