diff --git a/Content.Server/Atmos/AtmosHelpers.cs b/Content.Server/Atmos/AtmosHelpers.cs index b0e0baac24..f0bdeab213 100644 --- a/Content.Server/Atmos/AtmosHelpers.cs +++ b/Content.Server/Atmos/AtmosHelpers.cs @@ -1,6 +1,7 @@ #nullable enable using System.Diagnostics.CodeAnalysis; using Content.Server.GameObjects.EntitySystems; +using Content.Shared.Atmos; using Robust.Shared.GameObjects.Systems; using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects.Components; @@ -38,6 +39,23 @@ namespace Content.Server.Atmos return !Equals(air = coordinates.GetTileAir(entityManager)!, default); } + public static bool IsTileAirProbablySafe(this EntityCoordinates coordinates) + { + // Note that oxygen mix isn't checked, but survival boxes make that not necessary. + var air = coordinates.GetTileAir(); + if (air == null) + return false; + if (air.Pressure <= Atmospherics.WarningLowPressure) + return false; + if (air.Pressure >= Atmospherics.WarningHighPressure) + return false; + if (air.Temperature <= 260) + return false; + if (air.Temperature >= 360) + return false; + return true; + } + public static TileAtmosphere GetTileAtmosphere(this Vector2i indices, GridId gridId) { var gridAtmos = EntitySystem.Get().GetGridAtmosphere(gridId); diff --git a/Content.Server/GameObjects/Components/PDA/PDAComponent.cs b/Content.Server/GameObjects/Components/PDA/PDAComponent.cs index 406188572e..0a83101a6a 100644 --- a/Content.Server/GameObjects/Components/PDA/PDAComponent.cs +++ b/Content.Server/GameObjects/Components/PDA/PDAComponent.cs @@ -54,7 +54,9 @@ namespace Content.Server.GameObjects.Components.PDA [ViewVariables] public bool IdSlotEmpty => _idSlot.ContainedEntity == null; [ViewVariables] public bool PenSlotEmpty => _penSlot.ContainedEntity == null; - [ViewVariables] private UplinkAccount? _syndicateUplinkAccount; + private UplinkAccount? _syndicateUplinkAccount; + + [ViewVariables] public UplinkAccount? SyndicateUplinkAccount => _syndicateUplinkAccount; [ViewVariables] private readonly PdaAccessSet _accessSet; diff --git a/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchRedemptionComponent.cs b/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchRedemptionComponent.cs new file mode 100644 index 0000000000..4e35a50e49 --- /dev/null +++ b/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchRedemptionComponent.cs @@ -0,0 +1,123 @@ +#nullable enable +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.Components.PDA; +using Content.Server.GameObjects.EntitySystems; +using Content.Server.Mobs; +using Content.Server.Mobs.Roles; +using Content.Server.Mobs.Roles.Suspicion; +using Content.Server.Interfaces.GameObjects; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Inventory; +using Content.Shared.GameObjects.EntitySystems; +using Content.Shared.Interfaces; +using Content.Shared.Interfaces.GameObjects.Components; +using Robust.Server.GameObjects; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Localization; +using Robust.Shared.Utility; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.TraitorDeathMatch +{ + [RegisterComponent] + public class TraitorDeathMatchRedemptionComponent : Component, IInteractUsing + { + /// + public override string Name => "TraitorDeathMatchRedemption"; + + public async Task InteractUsing(InteractUsingEventArgs eventArgs) + { + if (!eventArgs.User.TryGetComponent(out var userInv)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"USER PDA OUT OF RANGE (0039)\"")); + return false; + } + + if (!eventArgs.User.TryGetComponent(out var userMindComponent)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"AUTHENTICATION FAILED (0045)\"")); + return false; + } + + var userMind = userMindComponent.Mind; + if (userMind == null) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"AUTHENTICATION FAILED (0052)\"")); + return false; + } + + if (!eventArgs.Using.TryGetComponent(out var victimPDA)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"GIVEN PDA IS NOT A PDA (0058)\"")); + return false; + } + + if (!eventArgs.Using.TryGetComponent(out var victimPDAOwner)) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"GIVEN PDA HAS NO OWNER (0064)\"")); + return false; + } + + if (victimPDAOwner.UserId == userMind.UserId) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"GIVEN PDA OWNED BY USER (0070)\"")); + return false; + } + + var userPDAEntity = userInv.GetSlotItem(EquipmentSlotDefines.Slots.IDCARD)?.Owner; + PDAComponent? userPDA = null; + + if (userPDAEntity != null) + if (userPDAEntity.TryGetComponent(out var userPDAComponent)) + userPDA = userPDAComponent; + + if (userPDA == null) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"NO USER PDA IN IDCARD POCKET (0083)\"")); + return false; + } + + // We have finally determined both PDA components. FINALLY. + + var userAccount = userPDA.SyndicateUplinkAccount; + var victimAccount = victimPDA.SyndicateUplinkAccount; + + if (userAccount == null) + { + // This shouldn't even BE POSSIBLE in the actual mode this is meant for. + // Advanced Syndicate anti-tampering technology. + // Owner.PopupMessage(eventArgs.User, Loc.GetString("Tampering detected.")); + // if (eventArgs.User.TryGetComponent(out var userDamagable)) + // userDamagable.ChangeDamage(DamageType.Shock, 9001, true, null); + // ...So apparently, "it probably shouldn't kill people for a mistake". + // :( + // Give boring error message instead. + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"USER PDA HAS NO UPLINK ACCOUNT (0102)\"")); + return false; + } + + if (victimAccount == null) + { + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine buzzes, and displays: \"GIVEN PDA HAS NO UPLINK ACCOUNT (0108)\"")); + return false; + } + + // 4 is the per-PDA bonus amount. + var transferAmount = victimAccount.Balance + 4; + victimAccount.ModifyAccountBalance(0); + userAccount.ModifyAccountBalance(userAccount.Balance + transferAmount); + + victimPDA.Owner.Delete(); + + Owner.PopupMessage(eventArgs.User, Loc.GetString("The machine plays a happy little tune, and displays: \"SUCCESS: {0} TC TRANSFERRED\"", transferAmount)); + return true; + } + } +} diff --git a/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchReliableOwnerTagComponent.cs b/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchReliableOwnerTagComponent.cs new file mode 100644 index 0000000000..31a43e4239 --- /dev/null +++ b/Content.Server/GameObjects/Components/TraitorDeathMatch/TraitorDeathMatchReliableOwnerTagComponent.cs @@ -0,0 +1,23 @@ +#nullable enable +using Robust.Server.GameObjects; +using Robust.Server.Interfaces.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Utility; +using Robust.Shared.Network; +using Robust.Shared.ViewVariables; + +namespace Content.Server.GameObjects.Components.TraitorDeathMatch +{ + [RegisterComponent] + public class TraitorDeathMatchReliableOwnerTagComponent : Component + { + /// + public override string Name => "TraitorDeathMatchReliableOwnerTag"; + + [ViewVariables] + public NetUserId? UserId { get; set; } + } +} + diff --git a/Content.Server/GameTicking/GamePresets/PresetTraitorDeathMatch.cs b/Content.Server/GameTicking/GamePresets/PresetTraitorDeathMatch.cs new file mode 100644 index 0000000000..b39b418650 --- /dev/null +++ b/Content.Server/GameTicking/GamePresets/PresetTraitorDeathMatch.cs @@ -0,0 +1,232 @@ +using System; +using System.Collections.Generic; +using Content.Server.GameTicking.GameRules; +using Content.Server.Interfaces.GameTicking; +using Content.Server.Interfaces.Chat; +using Content.Server.GameObjects.Components.GUI; +using Content.Server.GameObjects.Components.Items.Storage; +using Content.Server.GameObjects.Components.PDA; +using Content.Server.GameObjects.Components.Mobs; +using Content.Server.GameObjects.Components.Markers; +using Content.Server.GameObjects.Components.TraitorDeathMatch; +using Content.Server.Mobs; +using Content.Server.Mobs.Roles.Traitor; +using Content.Server.Players; +using Content.Server.Atmos; +using Content.Shared.Damage; +using Content.Shared.GameObjects.Components.Damage; +using Content.Shared.GameObjects.Components.Inventory; +using Content.Shared.GameObjects.Components.PDA; +using Content.Shared.GameObjects.Components.Mobs.State; +using Content.Shared; +using Robust.Shared.Map; +using Robust.Server.Player; +using Robust.Server.Interfaces.Player; +using Robust.Server.Interfaces.Console; +using Robust.Shared.IoC; +using Robust.Shared.Localization; +using Robust.Shared.GameObjects; +using Robust.Shared.Interfaces.GameObjects; +using Robust.Shared.Interfaces.Configuration; +using Robust.Shared.Interfaces.Random; +using Robust.Shared.Log; + +namespace Content.Server.GameTicking.GamePresets +{ + public sealed class PresetTraitorDeathMatch : GamePreset + { + [Dependency] private readonly IConfigurationManager _cfg = default!; + [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IGameTicker _gameTicker = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IChatManager _chatManager = default!; + [Dependency] private readonly IRobustRandom _robustRandom = default!; + + public string PDAPrototypeName => "CaptainPDA"; + public string BeltPrototypeName => "ClothingBeltJanitorFilled"; + public string BackpackPrototypeName => "ClothingBackpackFilled"; + + private RuleMaxTimeRestart _restarter; + private bool _safeToEndRound = false; + + private Dictionary _allOriginalNames = new(); + + public override bool Start(IReadOnlyList readyPlayers, bool force = false) + { + _gameTicker.AddGameRule(); + _restarter = _gameTicker.AddGameRule(); + _restarter.RoundMaxTime = TimeSpan.FromMinutes(30); + _restarter.RestartTimer(); + _safeToEndRound = true; + return true; + } + + public override void OnSpawnPlayerCompleted(IPlayerSession session, IEntity mob, bool lateJoin) + { + int startingBalance = _cfg.GetCVar(CCVars.TraitorDeathMatchStartingBalance); + + // Yup, they're a traitor + var mind = session.Data.ContentData()?.Mind; + var traitorRole = new TraitorRole(mind); + if (mind == null) + { + Logger.ErrorS("preset", "Failed getting mind for TDM player."); + return; + } + + mind.AddRole(traitorRole); + + // Delete anything that may contain "dangerous" role-specific items. + // (This includes the PDA, as everybody gets the captain PDA in this mode for true-all-access reasons.) + var inventory = mind.OwnedEntity.GetComponent(); + EquipmentSlotDefines.Slots[] victimSlots = new EquipmentSlotDefines.Slots[] {EquipmentSlotDefines.Slots.IDCARD, EquipmentSlotDefines.Slots.BELT, EquipmentSlotDefines.Slots.BACKPACK}; + foreach (var slot in victimSlots) + if (inventory.TryGetSlotItem(slot, out ItemComponent vItem)) + vItem.Owner.Delete(); + + // Replace their items: + + // pda + var newPDA = _entityManager.SpawnEntity(PDAPrototypeName, mind.OwnedEntity.Transform.Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.IDCARD, newPDA.GetComponent()); + + // belt + var newTmp = _entityManager.SpawnEntity(BeltPrototypeName, mind.OwnedEntity.Transform.Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.BELT, newTmp.GetComponent()); + + // backpack + newTmp = _entityManager.SpawnEntity(BackpackPrototypeName, mind.OwnedEntity.Transform.Coordinates); + inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, newTmp.GetComponent()); + + // Like normal traitors, they need access to a traitor account. + var uplinkAccount = new UplinkAccount(mind.OwnedEntity.Uid, startingBalance); + var pdaComponent = newPDA.GetComponent(); + pdaComponent.InitUplinkAccount(uplinkAccount); + _allOriginalNames[uplinkAccount] = mind.OwnedEntity.Name; + + // The PDA needs to be marked with the correct owner. + pdaComponent.SetPDAOwner(mind.OwnedEntity.Name); + newPDA.AddComponent().UserId = mind.UserId; + + // Finally, it would be preferrable if they spawned as far away from other players as reasonably possible. + if (FindAnyIsolatedSpawnLocation(mind, out var bestTarget)) + { + mind.OwnedEntity.Transform.Coordinates = bestTarget; + } + else + { + // The station is too drained of air to safely continue. + if (_safeToEndRound) + { + _chatManager.DispatchServerAnnouncement(Loc.GetString("The station is too unsafe to continue. You have one minute.")); + _restarter.RoundMaxTime = TimeSpan.FromMinutes(1); + _restarter.RestartTimer(); + _safeToEndRound = false; + } + } + } + + // It would be nice if this function were moved to some generic helpers class. + private bool FindAnyIsolatedSpawnLocation(Mind ignoreMe, out EntityCoordinates bestTarget) + { + // Collate people to avoid... + var existingPlayerPoints = new List(); + foreach (var player in _playerManager.GetAllPlayers()) + { + var avoidMeMind = player.Data.ContentData()?.Mind; + if ((avoidMeMind == null) || (avoidMeMind == ignoreMe)) + continue; + var avoidMeEntity = avoidMeMind.OwnedEntity; + if (avoidMeEntity == null) + continue; + if (avoidMeEntity.TryGetComponent(out IMobStateComponent mobState)) + { + // Does have mob state component; if critical or dead, they don't really matter for spawn checks + if (mobState.IsCritical() || mobState.IsDead()) + continue; + } + else + { + // Doesn't have mob state component. Assume something interesting is going on and don't count this as someone to avoid. + continue; + } + existingPlayerPoints.Add(avoidMeEntity.Transform.Coordinates); + } + + // Iterate over each possible spawn point, comparing to the existing player points. + // On failure, the returned target is the location that we're already at. + var bestTargetDistanceFromNearest = -1.0f; + // Need the random shuffle or it stuffs the first person into Atmospherics pretty reliably + var ents = new List(_entityManager.GetEntities(new TypeEntityQuery(typeof(SpawnPointComponent)))); + _robustRandom.Shuffle(ents); + var foundATarget = false; + bestTarget = EntityCoordinates.Invalid; + foreach (var entity in ents) + { + if (!entity.Transform.Coordinates.IsTileAirProbablySafe()) + continue; + var distanceFromNearest = float.PositiveInfinity; + foreach (var existing in existingPlayerPoints) + { + if (entity.Transform.Coordinates.TryDistance(_entityManager, existing, out var dist)) + distanceFromNearest = Math.Min(distanceFromNearest, dist); + } + if (bestTargetDistanceFromNearest < distanceFromNearest) + { + bestTarget = entity.Transform.Coordinates; + bestTargetDistanceFromNearest = distanceFromNearest; + foundATarget = true; + } + } + return foundATarget; + } + + public override bool OnGhostAttempt(Mind mind, bool canReturnGlobal) + { + var entity = mind.OwnedEntity; + if ((entity != null) && (entity.TryGetComponent(out IMobStateComponent mobState))) + { + if (mobState.IsCritical()) + { + // TODO: This is copy/pasted from ghost code. Really, IDamagableComponent needs a method to reliably kill the target. + if (entity.TryGetComponent(out IDamageableComponent damageable)) + { + //todo: what if they dont breathe lol + damageable.ChangeDamage(DamageType.Asphyxiation, 100, true); + } + } + else if (!mobState.IsDead()) + { + if (entity.HasComponent()) + { + return false; + } + } + } + var session = mind.Session; + if (session == null) + return false; + _gameTicker.Respawn(session); + return true; + } + + public override string GetRoundEndDescription() + { + var lines = new List(); + lines.Add("The PDAs recovered afterwards..."); + foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(PDAComponent)))) + { + var pda = entity.GetComponent(); + var uplink = pda.SyndicateUplinkAccount; + if ((uplink != null) && _allOriginalNames.ContainsKey(uplink)) + { + lines.Add(Loc.GetString("{0}'s PDA, with {1} TC", _allOriginalNames[uplink], uplink.Balance)); + } + } + return string.Join('\n', lines); + } + + public override string ModeTitle => "Traitor Deathmatch"; + public override string Description => Loc.GetString("Everyone's a traitor. Everyone wants each other dead."); + } +} diff --git a/Content.Server/GameTicking/GameRules/RuleTraitorDeathMatch.cs b/Content.Server/GameTicking/GameRules/RuleTraitorDeathMatch.cs new file mode 100644 index 0000000000..f7f3a76d7b --- /dev/null +++ b/Content.Server/GameTicking/GameRules/RuleTraitorDeathMatch.cs @@ -0,0 +1,17 @@ +using Content.Server.Interfaces.Chat; +using Content.Server.Mobs.Roles.Traitor; +using Content.Server.Players; +using Robust.Server.GameObjects.EntitySystems; +using Robust.Server.Interfaces.Player; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects.Systems; +using Robust.Shared.IoC; +using Robust.Shared.Localization; + +namespace Content.Server.GameTicking.GameRules +{ + public class RuleTraitorDeathMatch : GameRule + { + // This class only exists so that the game rule is available for the conditional spawner. + } +} diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index c49120c272..76730e5dca 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -491,6 +491,8 @@ namespace Content.Server.GameTicking "deathmatch" => typeof(PresetDeathMatch), "suspicion" => typeof(PresetSuspicion), "traitor" => typeof(PresetTraitor), + "traitordm" => typeof(PresetTraitorDeathMatch), + "traitordeathmatch" => typeof(PresetTraitorDeathMatch), _ => default }; diff --git a/Content.Shared/CCVars.cs b/Content.Shared/CCVars.cs index 0b70c44cd1..06a99baa82 100644 --- a/Content.Shared/CCVars.cs +++ b/Content.Shared/CCVars.cs @@ -102,6 +102,13 @@ namespace Content.Shared public static readonly CVarDef TraitorMaxPicks = CVarDef.Create("traitor.max_picks", 20); + /* + * TraitorDeathMatch + */ + + public static readonly CVarDef TraitorDeathMatchStartingBalance = + CVarDef.Create("traitordm.starting_balance", 20); + /* * Console */ diff --git a/Content.Shared/GameObjects/Components/PDA/SharedPDAComponent.cs b/Content.Shared/GameObjects/Components/PDA/SharedPDAComponent.cs index ceaabec258..d7dbdbea3c 100644 --- a/Content.Shared/GameObjects/Components/PDA/SharedPDAComponent.cs +++ b/Content.Shared/GameObjects/Components/PDA/SharedPDAComponent.cs @@ -2,6 +2,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.GameObjects.Components.UserInterface; using Robust.Shared.Serialization; +using Robust.Shared.ViewVariables; namespace Content.Shared.GameObjects.Components.PDA { @@ -129,12 +130,14 @@ namespace Content.Shared.GameObjects.Components.PDA { public event Action BalanceChanged; public EntityUid AccountHolder; - public int Balance { get; private set; } + private int _balance; + [ViewVariables] + public int Balance => _balance; public UplinkAccount(EntityUid uid, int startingBalance) { AccountHolder = uid; - Balance = startingBalance; + _balance = startingBalance; } public bool ModifyAccountBalance(int newBalance) @@ -143,7 +146,7 @@ namespace Content.Shared.GameObjects.Components.PDA { return false; } - Balance = newBalance; + _balance = newBalance; BalanceChanged?.Invoke(this); return true; diff --git a/Resources/Maps/saltern.yml b/Resources/Maps/saltern.yml index 22f03a0b0c..3d6bd1e8fe 100644 --- a/Resources/Maps/saltern.yml +++ b/Resources/Maps/saltern.yml @@ -44662,4 +44662,32 @@ entities: components: - parent: 1045 type: Transform +- uid: 4285 + type: TraitorDMRedemptionMachineSpawner + components: + - parent: 855 + pos: -18.5,25.5 + rot: -1.5707963267948966 rad + type: Transform +- uid: 4286 + type: TraitorDMRedemptionMachineSpawner + components: + - parent: 855 + pos: 10.5,13.5 + rot: -1.5707963267948966 rad + type: Transform +- uid: 4287 + type: TraitorDMRedemptionMachineSpawner + components: + - parent: 855 + pos: -9.5,-25.5 + rot: -1.5707963267948966 rad + type: Transform +- uid: 4288 + type: TraitorDMRedemptionMachineSpawner + components: + - parent: 855 + pos: 27.5,-1.5 + rot: -1.5707963267948966 rad + type: Transform ... diff --git a/Resources/Prototypes/Entities/Effects/Markers/gamemode_conditional_spawners.yml b/Resources/Prototypes/Entities/Effects/Markers/gamemode_conditional_spawners.yml index 43f8d68482..68d1485e5f 100644 --- a/Resources/Prototypes/Entities/Effects/Markers/gamemode_conditional_spawners.yml +++ b/Resources/Prototypes/Entities/Effects/Markers/gamemode_conditional_spawners.yml @@ -344,3 +344,22 @@ chance: 0.95 gameRules: - RuleSuspicion + +- type: entity + name: Traitor DeathMatch PDA Redemption Machine Spawner + id: TraitorDMRedemptionMachineSpawner + parent: BaseConditionalSpawner + components: + - type: Sprite + netsync: false + visible: false + sprite: Objects/Misc/traitordm.rsi + state: redemption + + - type: ConditionalSpawner + prototypes: + - TraitorDMRedemptionMachine + chance: 1.0 + gameRules: + - RuleTraitorDeathMatch + diff --git a/Resources/Prototypes/Entities/traitordm.yml b/Resources/Prototypes/Entities/traitordm.yml new file mode 100644 index 0000000000..bdd92fe792 --- /dev/null +++ b/Resources/Prototypes/Entities/traitordm.yml @@ -0,0 +1,26 @@ +- type: entity + id: TraitorDMRedemptionMachine + name: traitor deathmatch pda redemption machine + description: Put someone else's PDA into this to get telecrystals. + components: + - type: Sprite + layers: + - sprite: Objects/Misc/traitordm.rsi + state: redemption + - sprite: Objects/Misc/traitordm.rsi + state: redemption-unshaded + shader: unshaded + - type: Clickable + - type: InteractionOutline + - type: Physics + anchored: true + mass: 1 + shapes: + - !type:PhysShapeAabb + bounds: "-0.25,-0.25,0.25,0.25" + layer: + - Clickable + - type: TraitorDeathMatchRedemption + placement: + mode: AlignTileAny + diff --git a/Resources/Textures/Objects/Misc/traitordm.rsi/meta.json b/Resources/Textures/Objects/Misc/traitordm.rsi/meta.json new file mode 100644 index 0000000000..49ec5fba6f --- /dev/null +++ b/Resources/Textures/Objects/Misc/traitordm.rsi/meta.json @@ -0,0 +1,10 @@ +{ + "version":1, + "size":{"x":32,"y":32}, + "states":[ + {"name":"redemption","directions":1,"delays":[[1.0]]}, + {"name":"redemption-unshaded","directions":1,"delays":[[1.0]]} + ], + "license": "CC-BY-SA-3.0", + "copyright": "Edit by Tomeno using small parts of autolathe from https://github.com/tgstation/tgstation/blob/acb091f9744e9ab7d5a27fb32dd0c03bd019f58c/icons/obj/stationobjs.dmi (may be an earlier version) and tcboss from https://github.com/tgstation/tgstation/blob/e32357e6b0ec0f0a1821d2773f0d1e1d6ce7d494/icons/obj/computer.dmi (may be an earlier version)" +} diff --git a/Resources/Textures/Objects/Misc/traitordm.rsi/redemption-unshaded.png b/Resources/Textures/Objects/Misc/traitordm.rsi/redemption-unshaded.png new file mode 100644 index 0000000000..66fde4775b Binary files /dev/null and b/Resources/Textures/Objects/Misc/traitordm.rsi/redemption-unshaded.png differ diff --git a/Resources/Textures/Objects/Misc/traitordm.rsi/redemption.png b/Resources/Textures/Objects/Misc/traitordm.rsi/redemption.png new file mode 100644 index 0000000000..cc4c1cd92e Binary files /dev/null and b/Resources/Textures/Objects/Misc/traitordm.rsi/redemption.png differ