Traitor Deathmatch (#2719)

* Traitor Deathmatch: Add the preset [Rebase onto factors line]

* Traitor Deathmatch: Being a human head or other not-quite-fully-animate object counts as dead. [ROFL]

* Traitor Deathmatch: Add redemption machine (but it doesn't work) [ROFL]

* Traitor Deathmatch: Map in redemption machines [ROFL]

* Traitor Deathmatch: Make the rounds end properly [ROFL]

* Traitor Deathmatch: PDA redemption works [ROFL]

* Traitor Deathmatch: New redemption machine sprite ( @Tomeno )

* Traitor Deathmatch: Get rid of redundant using

* Traitor Deathmatch: Change scoring, prevent redeeming your own PDA, fix bug where PDAs are not tagged with their owner names

* Traitor Deathmatch: Better messages, change cvar name, don't count ghosts for spawnpoint selection
This commit is contained in:
20kdc
2020-12-13 15:00:49 +00:00
committed by GitHub
parent 4ea18f2449
commit fd0df9a00a
15 changed files with 514 additions and 4 deletions

View File

@@ -1,6 +1,7 @@
#nullable enable #nullable enable
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Server.GameObjects.EntitySystems; using Content.Server.GameObjects.EntitySystems;
using Content.Shared.Atmos;
using Robust.Shared.GameObjects.Systems; using Robust.Shared.GameObjects.Systems;
using Robust.Shared.Interfaces.GameObjects; using Robust.Shared.Interfaces.GameObjects;
using Robust.Shared.Interfaces.GameObjects.Components; using Robust.Shared.Interfaces.GameObjects.Components;
@@ -38,6 +39,23 @@ namespace Content.Server.Atmos
return !Equals(air = coordinates.GetTileAir(entityManager)!, default); 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) public static TileAtmosphere GetTileAtmosphere(this Vector2i indices, GridId gridId)
{ {
var gridAtmos = EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(gridId); var gridAtmos = EntitySystem.Get<AtmosphereSystem>().GetGridAtmosphere(gridId);

View File

@@ -54,7 +54,9 @@ namespace Content.Server.GameObjects.Components.PDA
[ViewVariables] public bool IdSlotEmpty => _idSlot.ContainedEntity == null; [ViewVariables] public bool IdSlotEmpty => _idSlot.ContainedEntity == null;
[ViewVariables] public bool PenSlotEmpty => _penSlot.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; [ViewVariables] private readonly PdaAccessSet _accessSet;

View File

@@ -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
{
/// <inheritdoc />
public override string Name => "TraitorDeathMatchRedemption";
public async Task<bool> InteractUsing(InteractUsingEventArgs eventArgs)
{
if (!eventArgs.User.TryGetComponent<InventoryComponent>(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<MindComponent>(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<PDAComponent>(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<TraitorDeathMatchReliableOwnerTagComponent>(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<PDAComponent>(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<DamagableComponent>(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;
}
}
}

View File

@@ -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
{
/// <inheritdoc />
public override string Name => "TraitorDeathMatchReliableOwnerTag";
[ViewVariables]
public NetUserId? UserId { get; set; }
}
}

View File

@@ -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<UplinkAccount, string> _allOriginalNames = new();
public override bool Start(IReadOnlyList<IPlayerSession> readyPlayers, bool force = false)
{
_gameTicker.AddGameRule<RuleTraitorDeathMatch>();
_restarter = _gameTicker.AddGameRule<RuleMaxTimeRestart>();
_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<InventoryComponent>();
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<ItemComponent>());
// belt
var newTmp = _entityManager.SpawnEntity(BeltPrototypeName, mind.OwnedEntity.Transform.Coordinates);
inventory.Equip(EquipmentSlotDefines.Slots.BELT, newTmp.GetComponent<ItemComponent>());
// backpack
newTmp = _entityManager.SpawnEntity(BackpackPrototypeName, mind.OwnedEntity.Transform.Coordinates);
inventory.Equip(EquipmentSlotDefines.Slots.BACKPACK, newTmp.GetComponent<ItemComponent>());
// Like normal traitors, they need access to a traitor account.
var uplinkAccount = new UplinkAccount(mind.OwnedEntity.Uid, startingBalance);
var pdaComponent = newPDA.GetComponent<PDAComponent>();
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<TraitorDeathMatchReliableOwnerTagComponent>().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<EntityCoordinates>();
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<IEntity>(_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<HandsComponent>())
{
return false;
}
}
}
var session = mind.Session;
if (session == null)
return false;
_gameTicker.Respawn(session);
return true;
}
public override string GetRoundEndDescription()
{
var lines = new List<string>();
lines.Add("The PDAs recovered afterwards...");
foreach (var entity in _entityManager.GetEntities(new TypeEntityQuery(typeof(PDAComponent))))
{
var pda = entity.GetComponent<PDAComponent>();
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.");
}
}

View File

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

View File

@@ -491,6 +491,8 @@ namespace Content.Server.GameTicking
"deathmatch" => typeof(PresetDeathMatch), "deathmatch" => typeof(PresetDeathMatch),
"suspicion" => typeof(PresetSuspicion), "suspicion" => typeof(PresetSuspicion),
"traitor" => typeof(PresetTraitor), "traitor" => typeof(PresetTraitor),
"traitordm" => typeof(PresetTraitorDeathMatch),
"traitordeathmatch" => typeof(PresetTraitorDeathMatch),
_ => default _ => default
}; };

View File

@@ -102,6 +102,13 @@ namespace Content.Shared
public static readonly CVarDef<int> TraitorMaxPicks = public static readonly CVarDef<int> TraitorMaxPicks =
CVarDef.Create("traitor.max_picks", 20); CVarDef.Create("traitor.max_picks", 20);
/*
* TraitorDeathMatch
*/
public static readonly CVarDef<int> TraitorDeathMatchStartingBalance =
CVarDef.Create("traitordm.starting_balance", 20);
/* /*
* Console * Console
*/ */

View File

@@ -2,6 +2,7 @@
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.GameObjects.Components.UserInterface; using Robust.Shared.GameObjects.Components.UserInterface;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.ViewVariables;
namespace Content.Shared.GameObjects.Components.PDA namespace Content.Shared.GameObjects.Components.PDA
{ {
@@ -129,12 +130,14 @@ namespace Content.Shared.GameObjects.Components.PDA
{ {
public event Action<UplinkAccount> BalanceChanged; public event Action<UplinkAccount> BalanceChanged;
public EntityUid AccountHolder; public EntityUid AccountHolder;
public int Balance { get; private set; } private int _balance;
[ViewVariables]
public int Balance => _balance;
public UplinkAccount(EntityUid uid, int startingBalance) public UplinkAccount(EntityUid uid, int startingBalance)
{ {
AccountHolder = uid; AccountHolder = uid;
Balance = startingBalance; _balance = startingBalance;
} }
public bool ModifyAccountBalance(int newBalance) public bool ModifyAccountBalance(int newBalance)
@@ -143,7 +146,7 @@ namespace Content.Shared.GameObjects.Components.PDA
{ {
return false; return false;
} }
Balance = newBalance; _balance = newBalance;
BalanceChanged?.Invoke(this); BalanceChanged?.Invoke(this);
return true; return true;

View File

@@ -44662,4 +44662,32 @@ entities:
components: components:
- parent: 1045 - parent: 1045
type: Transform 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
... ...

View File

@@ -344,3 +344,22 @@
chance: 0.95 chance: 0.95
gameRules: gameRules:
- RuleSuspicion - 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

View File

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

View File

@@ -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)"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 766 B