- add: Changeling antagonist (#2)

* Changeling WIP

* UI

* Pointers fix

* Moved out abilities

* Regenerate ability

* Fixed Regenerate ability
Prevent ghosting while regenerating

* Cleanup

* Base lesser form

* Finished Lesser Form && Transform

* Transform Sting

* Blind Sting

* Mute Sting
Added OnExamine on absorbed human

* Hallucination Sting
Changeling Absorb and transfer absorbed entities to absorber

* Cryogenic Sting

* Adrenaline Sacs

* Transform now uses Polymorph

* Armblade, Shield, Armor

* Tentacle Arm ability
Tentacle Gun system

* WIP with bugs

* WiP bugs

* fix implant transfer

* Fixed bugs with shop transfer and actions transfer

* Just in case

* Vi sitter i ventrilo och spelar DotA

* Fixes and proper LesserForm tracking

* !!!!!

* Fixed empty buttons

* WIP Gamerule
Ready - shop

* nerf stun time cause its sucks

* cleaning

* just in case

* Absorb DNA Objective.

* Partial objectives with bugs

* fix

* fix pointer

* Changeling objectives

* Changeling objectives №2

* Admin verb, game rule

* Fixed empty list check
Icons for objectives

* Changeling chat, changeling names etc.

* fix some merge errors

* - fix: Fixed all bugs with changeling

---------

Co-authored-by: Y-Parvus <yevhen.parvus@gmail.com>
Co-authored-by: Y-Parvus <61109031+Y-Parvus@users.noreply.github.com>
Co-authored-by: HitPanda <104197232+EnefFlow@users.noreply.github.com>
Co-authored-by: EnefFlow <regeto90@mail.ru>
This commit is contained in:
rhailrake
2024-01-31 14:01:35 +00:00
committed by GitHub
parent 7872502bf8
commit aa8e31fa7e
127 changed files with 3747 additions and 33 deletions

View File

@@ -1,8 +1,8 @@
using Content.Client.Administration.Managers;
using Content.Client.Ghost;
using Content.Shared.Administration;
using Content.Shared.Changeling;
using Content.Shared.Chat;
using Content.Shared._White.Cult;
using Robust.Client.Console;
using Robust.Client.Player;
using Robust.Shared.Utility;
@@ -15,7 +15,7 @@ namespace Content.Client.Chat.Managers
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
[Dependency] private readonly IClientAdminManager _adminMgr = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
@@ -29,7 +29,6 @@ namespace Content.Client.Chat.Managers
public void SendMessage(string text, ChatSelectChannel channel)
{
var str = text.ToString();
switch (channel)
{
case ChatSelectChannel.Console:
@@ -38,25 +37,25 @@ namespace Content.Client.Chat.Managers
break;
case ChatSelectChannel.LOOC:
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"looc \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.OOC:
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"ooc \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Admin:
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"asay \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Emotes:
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"me \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Cult:
var localEnt = _player.LocalPlayer != null ? _player.LocalPlayer.ControlledEntity : null;
if (_entities.TryGetComponent(localEnt, out CultistComponent? comp))
_consoleHost.ExecuteCommand($"csay \"{CommandParsing.Escape(str)}\"");
if (_entityManager.TryGetComponent(localEnt, out CultistComponent? comp))
_consoleHost.ExecuteCommand($"csay \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Dead:
@@ -64,7 +63,7 @@ namespace Content.Client.Chat.Managers
goto case ChatSelectChannel.Local;
if (_adminMgr.HasFlag(AdminFlags.Admin))
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"dsay \"{CommandParsing.Escape(text)}\"");
else
_sawmill.Warning("Tried to speak on deadchat without being ghost or admin.");
break;
@@ -72,13 +71,20 @@ namespace Content.Client.Chat.Managers
// TODO sepearate radio and say into separate commands.
case ChatSelectChannel.Radio:
case ChatSelectChannel.Local:
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"say \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Whisper:
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(str)}\"");
_consoleHost.ExecuteCommand($"whisper \"{CommandParsing.Escape(text)}\"");
break;
case ChatSelectChannel.Changeling:
var localEntity = _player.LocalPlayer != null ? _player.LocalPlayer.ControlledEntity : null;
if (_entityManager.HasComponent<ChangelingComponent>(localEntity))
_consoleHost.ExecuteCommand($"gsay \"{CommandParsing.Escape(text)}\"");
break;
default:
throw new ArgumentOutOfRangeException(nameof(channel), channel, null);
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Changeling;
namespace Content.Client.Miracle.Changeling;
public sealed class TentacleGun : SharedTentacleGun
{
}

View File

@@ -0,0 +1,9 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Changeling UI"
MinWidth="350"
MinHeight="400">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="ItemsContainer" Orientation="Vertical"></BoxContainer>
</ScrollContainer>
</DefaultWindow>

View File

@@ -0,0 +1,33 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Miracle.Changeling.UI.ListViewSelector;
[GenerateTypedNameReferences]
public sealed partial class ListViewChangelingSelectorWindow : DefaultWindow
{
public Action<string>? ItemSelected;
public ListViewChangelingSelectorWindow()
{
RobustXamlLoader.Load(this);
}
public void PopulateList(Dictionary<string, string> items)
{
ItemsContainer.RemoveAllChildren();
foreach (var item in items)
{
var button = new Button();
button.Text = item.Value;
button.OnPressed += _ => ItemSelected?.Invoke(item.Key);
ItemsContainer.AddChild(button);
}
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.Miracle.UI;
namespace Content.Client.Miracle.Changeling.UI.ListViewSelector;
public sealed class ListViewSelectorBui : BoundUserInterface
{
private ListViewChangelingSelectorWindow? _window;
public ListViewSelectorBui(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_window = new ListViewChangelingSelectorWindow();
_window.OpenCentered();
_window.OnClose += Close;
_window.ItemSelected += item =>
{
var msg = new ListViewItemSelectedMessage(item);
SendMessage(msg);
};
if(State != null)
UpdateState(State);
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is ListViewBuiState newState)
{
_window?.PopulateList(newState.Items);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_window?.Close();
}
}

View File

@@ -0,0 +1,9 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Changeling UI"
MinWidth="350"
MinHeight="400">
<ScrollContainer HorizontalExpand="True" VerticalExpand="True">
<BoxContainer Name="ItemsContainer" Orientation="Vertical"></BoxContainer>
</ScrollContainer>
</DefaultWindow>

View File

@@ -0,0 +1,33 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Miracle.Changeling.UI.TransformStingUI;
[GenerateTypedNameReferences]
public sealed partial class TransformStingSelectorWindow : DefaultWindow
{
public Action<string, NetEntity>? ItemSelected;
public TransformStingSelectorWindow()
{
RobustXamlLoader.Load(this);
}
public void PopulateList(Dictionary<string, string> items, NetEntity target)
{
ItemsContainer.RemoveAllChildren();
foreach (var item in items)
{
var button = new Button();
button.Text = item.Value;
button.OnPressed += _ => ItemSelected?.Invoke(item.Key, target);
ItemsContainer.AddChild(button);
}
}
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.Miracle.UI;
namespace Content.Client.Miracle.Changeling.UI.TransformStingUI;
public sealed class TransformStingSelectorBui : BoundUserInterface
{
private TransformStingSelectorWindow? _window;
public TransformStingSelectorBui(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
IoCManager.InjectDependencies(this);
}
protected override void Open()
{
base.Open();
_window = new TransformStingSelectorWindow();
_window.OpenCentered();
_window.OnClose += Close;
_window.ItemSelected += (item, target) =>
{
var msg = new TransformStingItemSelectedMessage(item, target);
SendMessage(msg);
};
if(State != null)
UpdateState(State);
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is TransformStingBuiState newState)
{
_window?.PopulateList(newState.Items, newState.Target);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
_window?.Close();
}
}

View File

@@ -13,6 +13,7 @@ using Content.Client.UserInterface.Systems.Chat.Widgets;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Changeling;
using Content.Shared.Chat;
using Content.Shared.Damage.ForceSay;
using Content.Shared.Examine;
@@ -75,7 +76,8 @@ public sealed class ChatUIController : UIController
{SharedChatSystem.AdminPrefix, ChatSelectChannel.Admin},
{SharedChatSystem.RadioCommonPrefix, ChatSelectChannel.Radio},
{SharedChatSystem.DeadPrefix, ChatSelectChannel.Dead},
{SharedChatSystem.CultPrefix, ChatSelectChannel.Cult}, //WD EDIT
{SharedChatSystem.CultPrefix, ChatSelectChannel.Cult},
{SharedChatSystem.ChangelingPrefix, ChatSelectChannel.Changeling}
};
public static readonly Dictionary<ChatSelectChannel, char> ChannelPrefixes = new()
@@ -89,7 +91,8 @@ public sealed class ChatUIController : UIController
{ChatSelectChannel.Admin, SharedChatSystem.AdminPrefix},
{ChatSelectChannel.Radio, SharedChatSystem.RadioCommonPrefix},
{ChatSelectChannel.Dead, SharedChatSystem.DeadPrefix},
{ChatSelectChannel.Cult, SharedChatSystem.CultPrefix} // WD EDIT
{ChatSelectChannel.Cult, SharedChatSystem.CultPrefix},
{ChatSelectChannel.Changeling, SharedChatSystem.ChangelingPrefix}
};
@@ -203,6 +206,9 @@ public sealed class ChatUIController : UIController
_input.SetInputCommand(ContentKeyFunctions.FocusAdminChat,
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Admin)));
_input.SetInputCommand(ContentKeyFunctions.FocusChangelingChat,
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Changeling)));
_input.SetInputCommand(ContentKeyFunctions.FocusCultChat,
InputCmdHandler.FromDelegate(_ => FocusChannel(ChatSelectChannel.Cult)));
@@ -221,6 +227,7 @@ public sealed class ChatUIController : UIController
_input.SetInputCommand(ContentKeyFunctions.CycleChatChannelBackward,
InputCmdHandler.FromDelegate(_ => CycleChatChannel(false)));
SubscribeLocalEvent<ChangelingUserStart>(OnUpdateChangelingChat);
// WD EDIT
SubscribeLocalEvent<EventCultistComponentState>(OnUpdateCultState);
// WD EDIT END
@@ -230,6 +237,11 @@ public sealed class ChatUIController : UIController
gameplayStateLoad.OnScreenUnload += OnScreenUnload;
}
private void OnUpdateChangelingChat(ChangelingUserStart ev)
{
UpdateChannelPermissions();
}
// WD EDIT
private void OnUpdateCultState(EventCultistComponentState ev)
{

View File

@@ -23,6 +23,7 @@ public sealed partial class ChannelFilterPopup : Popup
ChatChannel.AdminAlert,
ChatChannel.AdminChat,
ChatChannel.Server,
ChatChannel.Changeling,
ChatChannel.Cult // WD EDIT
};

View File

@@ -64,6 +64,7 @@ public sealed class ChannelSelectorButton : ChatPopupButton<ChannelSelectorPopup
ChatSelectChannel.OOC => Color.LightSkyBlue,
ChatSelectChannel.Dead => Color.MediumPurple,
ChatSelectChannel.Admin => Color.HotPink,
ChatSelectChannel.Changeling => Color.Purple,
ChatSelectChannel.Cult => Color.DarkRed,
_ => Color.DarkGray
};

View File

@@ -17,6 +17,7 @@ public sealed class ChannelSelectorPopup : Popup
ChatSelectChannel.OOC,
ChatSelectChannel.Dead,
ChatSelectChannel.Admin,
ChatSelectChannel.Changeling,
ChatSelectChannel.Cult // WD EDIT
// NOTE: Console is not in there and it can never be permanently selected.
// You can, however, still submit commands as console by prefixing with /.

View File

@@ -1,3 +1,4 @@
using Content.Server.Changeling;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.StationEvents.Events;
@@ -23,6 +24,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly ThiefRuleSystem _thief = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
[Dependency] private readonly ChangelingRuleSystem _changelingRule = default!;
[Dependency] private readonly NukeopsRuleSystem _nukeopsRule = default!;
[Dependency] private readonly PiratesRuleSystem _piratesRule = default!;
[Dependency] private readonly RevolutionaryRuleSystem _revolutionaryRule = default!;
@@ -63,6 +65,25 @@ public sealed partial class AdminVerbSystem
};
args.Verbs.Add(traitor);
Verb changeling = new()
{
Text = Loc.GetString("admin-verb-text-make-changeling"),
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/Actions/ling_absorb.png")),
Act = () =>
{
if (!_minds.TryGetSession(targetMindComp.Mind, out var session))
return;
var isHuman = HasComp<HumanoidAppearanceComponent>(args.Target);
_changelingRule.MakeChangeling(session);
},
Impact = LogImpact.High,
Message = Loc.GetString("admin-verb-make-changeling"),
};
args.Verbs.Add(changeling);
Verb zombie = new()
{
Text = Loc.GetString("admin-verb-text-make-zombie"),

View File

@@ -0,0 +1,8 @@
using Content.Shared.Roles;
namespace Content.Server.Changeling;
[RegisterComponent]
public sealed partial class ChangelingRoleComponent : AntagonistRoleComponent
{
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Preferences;
using Content.Shared.Roles;
using Robust.Shared.Audio;
using Robust.Shared.Player;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Changeling;
[RegisterComponent, Access(typeof(ChangelingRuleSystem))]
public sealed partial class ChangelingRuleComponent : Component
{
public readonly List<EntityUid> ChangelingMinds = new();
[DataField("changelingPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer<AntagPrototype>))]
public string ChangelingPrototypeId = "Changeling";
public int TotalChangelings => ChangelingMinds.Count;
public enum SelectionState
{
WaitingForSpawn = 0,
ReadyToSelect = 1,
SelectionMade = 2,
}
public SelectionState SelectionStatus = SelectionState.WaitingForSpawn;
public TimeSpan AnnounceAt = TimeSpan.Zero;
public Dictionary<ICommonSession, HumanoidCharacterProfile> StartCandidates = new();
/// <summary>
/// Path to antagonist alert sound.
/// </summary>
[DataField("greetSoundNotification")]
public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/changeling_start.ogg");
}

View File

@@ -0,0 +1,275 @@
using System.Linq;
using Content.Server.Antag;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Server.NPC.Systems;
using Content.Server.Objectives;
using Content.Server.Roles;
using Content.Shared.Changeling;
using Content.Shared.GameTicking;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Server.Changeling;
public sealed class ChangelingRuleSystem : GameRuleSystem<ChangelingRuleComponent>
{
[Dependency] private readonly AntagSelectionSystem _antagSelection = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly NpcFactionSystem _npcFaction = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
[Dependency] private readonly SharedRoleSystem _roleSystem = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
[Dependency] private readonly ChangelingNameGenerator _nameGenerator = default!;
private const int PlayersPerChangeling = 10;
private const int MaxChangelings = 5;
private const float ChangelingStartDelay = 3f * 60;
private const float ChangelingStartDelayVariance = 3f * 60;
private const int ChangelingMinPlayers = 10;
private const int ChangelingMaxDifficulty = 5;
private const int ChangelingMaxPicks = 20;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoundStartAttemptEvent>(OnStartAttempt);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnPlayersSpawned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(HandleLatejoin);
SubscribeLocalEvent<RoundRestartCleanupEvent>(ClearUsedNames);
SubscribeLocalEvent<ChangelingRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
protected override void ActiveTick(EntityUid uid, ChangelingRuleComponent component, GameRuleComponent gameRule, float frameTime)
{
base.ActiveTick(uid, component, gameRule, frameTime);
if (component.SelectionStatus == ChangelingRuleComponent.SelectionState.ReadyToSelect && _gameTiming.CurTime > component.AnnounceAt)
DoChangelingStart(component);
}
private void OnStartAttempt(RoundStartAttemptEvent ev)
{
var query = EntityQueryEnumerator<ChangelingRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out _, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var minPlayers = ChangelingMinPlayers;
if (!ev.Forced && ev.Players.Length < minPlayers)
{
_chatManager.SendAdminAnnouncement(Loc.GetString("changeling-not-enough-ready-players",
("readyPlayersCount", ev.Players.Length), ("minimumPlayers", minPlayers)));
ev.Cancel();
continue;
}
if (ev.Players.Length == 0)
{
_chatManager.DispatchServerAnnouncement(Loc.GetString("changeling-no-one-ready"));
ev.Cancel();
}
}
}
private void DoChangelingStart(ChangelingRuleComponent component)
{
if (!component.StartCandidates.Any())
{
Log.Error("Tried to start Changeling mode without any candidates.");
return;
}
var numChangelings = MathHelper.Clamp(component.StartCandidates.Count / PlayersPerChangeling, 1, MaxChangelings);
var changelingPool = _antagSelection.FindPotentialAntags(component.StartCandidates, component.ChangelingPrototypeId);
var selectedChangelings = _antagSelection.PickAntag(numChangelings, changelingPool);
foreach (var changeling in selectedChangelings)
{
MakeChangeling(changeling);
}
component.SelectionStatus = ChangelingRuleComponent.SelectionState.SelectionMade;
}
private void OnPlayersSpawned(RulePlayerJobsAssignedEvent ev)
{
var query = EntityQueryEnumerator<ChangelingRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var changeling, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
foreach (var player in ev.Players)
{
if (!ev.Profiles.ContainsKey(player.UserId))
continue;
changeling.StartCandidates[player] = ev.Profiles[player.UserId];
}
var delay = TimeSpan.FromSeconds(ChangelingStartDelay + _random.NextFloat(0f, ChangelingStartDelayVariance));
changeling.AnnounceAt = _gameTiming.CurTime + delay;
changeling.SelectionStatus = ChangelingRuleComponent.SelectionState.ReadyToSelect;
}
}
public bool MakeChangeling(ICommonSession changeling, bool giveObjectives = true)
{
var changelingRule = EntityQuery<ChangelingRuleComponent>().FirstOrDefault();
if (changelingRule == null)
{
GameTicker.StartGameRule("Changeling", out var ruleEntity);
changelingRule = Comp<ChangelingRuleComponent>(ruleEntity);
}
if (!_mindSystem.TryGetMind(changeling, out var mindId, out var mind))
{
Log.Info("Failed getting mind for picked changeling.");
return false;
}
if (HasComp<ChangelingRoleComponent>(mindId))
{
Log.Error($"Player {changeling.Name} is already a changeling.");
return false;
}
if (mind.OwnedEntity is not { } entity)
{
Log.Error("Mind picked for changeling did not have an attached entity.");
return false;
}
_roleSystem.MindAddRole(mindId, new ChangelingRoleComponent
{
PrototypeId = changelingRule.ChangelingPrototypeId
}, mind);
var briefing = Loc.GetString("changeling-role-briefing-short");
_roleSystem.MindAddRole(mindId, new RoleBriefingComponent
{
Briefing = briefing
}, mind, true);
_roleSystem.MindPlaySound(mindId, changelingRule.GreetSoundNotification, mind);
SendChangelingBriefing(mindId);
changelingRule.ChangelingMinds.Add(mindId);
// Change the faction
_npcFaction.RemoveFaction(entity, "NanoTrasen", false);
_npcFaction.AddFaction(entity, "Syndicate");
EnsureComp<ChangelingComponent>(entity, out var readyChangeling);
readyChangeling.HiveName = _nameGenerator.GetName();
Dirty(entity, readyChangeling);
if (giveObjectives)
{
var maxDifficulty = ChangelingMaxDifficulty;
var maxPicks = ChangelingMaxPicks;
var difficulty = 0f;
for (var pick = 0; pick < maxPicks && maxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, "ChangelingObjectiveGroups");
if (objective == null)
continue;
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
}
return true;
}
private void SendChangelingBriefing(EntityUid mind)
{
if (!_mindSystem.TryGetSession(mind, out var session))
return;
_chatManager.DispatchServerMessage(session, Loc.GetString("changeling-role-greeting"));
}
private void HandleLatejoin(PlayerSpawnCompleteEvent ev)
{
var query = EntityQueryEnumerator<ChangelingRuleComponent, GameRuleComponent>();
while (query.MoveNext(out var uid, out var changeling, out var gameRule))
{
if (!GameTicker.IsGameRuleAdded(uid, gameRule))
continue;
if (changeling.TotalChangelings >= MaxChangelings)
continue;
if (!ev.LateJoin)
continue;
if (!ev.Profile.AntagPreferences.Contains(changeling.ChangelingPrototypeId))
continue;
if (ev.JobId == null || !_prototypeManager.TryIndex<JobPrototype>(ev.JobId, out var job))
continue;
if (!job.CanBeAntag)
continue;
// Before the announcement is made, late-joiners are considered the same as players who readied.
if (changeling.SelectionStatus < ChangelingRuleComponent.SelectionState.SelectionMade)
{
changeling.StartCandidates[ev.Player] = ev.Profile;
continue;
}
var target = PlayersPerChangeling * changeling.TotalChangelings + 1;
var chance = 1f / PlayersPerChangeling;
if (ev.JoinOrder < target)
{
chance /= (target - ev.JoinOrder);
}
else
{
chance *= ((ev.JoinOrder + 1) - target);
}
if (chance > 1)
chance = 1;
if (_random.Prob(chance))
{
MakeChangeling(ev.Player);
}
}
}
private void OnObjectivesTextGetInfo(EntityUid uid, ChangelingRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = comp.ChangelingMinds;
args.AgentName = Loc.GetString("changeling-round-end-agent-name");
}
private void ClearUsedNames(RoundRestartCleanupEvent ev)
{
_nameGenerator.ClearUsed();
}
}

View File

@@ -0,0 +1,879 @@
using System.Linq;
using Content.Server.Administration.Systems;
using Content.Server.DoAfter;
using Content.Server.Forensics;
using Content.Server.Humanoid;
using Content.Server.IdentityManagement;
using Content.Server.Mind;
using Content.Server.Polymorph.Systems;
using Content.Server.Popups;
using Content.Server.Store.Components;
using Content.Server.Temperature.Systems;
using Content.Shared.Actions;
using Content.Shared.Changeling;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Eye.Blinding.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Implants.Components;
using Content.Shared.Inventory;
using Content.Shared.Miracle.UI;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Pulling;
using Content.Shared.Pulling.Components;
using Content.Shared.Standing;
using Content.Shared.StatusEffect;
using Robust.Server.GameObjects;
using Robust.Shared.GameObjects.Components.Localization;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Serialization.Manager;
namespace Content.Server.Changeling;
public sealed partial class ChangelingSystem
{
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly MobThresholdSystem _mobThresholdSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly DamageableSystem _damage = default!;
[Dependency] private readonly StandingStateSystem _stateSystem = default!;
[Dependency] private readonly HumanoidAppearanceSystem _humanoidAppearance = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly IdentitySystem _identity = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly ISerializationManager _serializationManager = default!;
[Dependency] private readonly RejuvenateSystem _rejuvenate = default!;
[Dependency] private readonly PolymorphSystem _polymorph = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffectsSystem = default!;
[Dependency] private readonly TemperatureSystem _temperatureSystem = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ActionContainerSystem _actionContainerSystem = default!;
[Dependency] private readonly SharedPullingSystem _pullingSystem = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
private void InitializeAbilities()
{
SubscribeLocalEvent<ChangelingComponent, AbsorbDnaActionEvent>(OnAbsorb);
SubscribeLocalEvent<ChangelingComponent, TransformActionEvent>(OnTransform);
SubscribeLocalEvent<ChangelingComponent, RegenerateActionEvent>(OnRegenerate);
SubscribeLocalEvent<ChangelingComponent, LesserFormActionEvent>(OnLesserForm);
SubscribeLocalEvent<ChangelingComponent, TransformStingActionEvent>(OnTransformSting);
SubscribeLocalEvent<ChangelingComponent, TransformStingItemSelectedMessage>(OnTransformStingMessage);
SubscribeLocalEvent<ChangelingComponent, BlindStingActionEvent>(OnBlindSting);
SubscribeLocalEvent<ChangelingComponent, MuteStingActionEvent>(OnMuteSting);
SubscribeLocalEvent<ChangelingComponent, HallucinationStingActionEvent>(OnHallucinationSting);
SubscribeLocalEvent<ChangelingComponent, CryoStingActionEvent>(OnCryoSting);
SubscribeLocalEvent<ChangelingComponent, AdrenalineSacsActionEvent>(OnAdrenalineSacs);
SubscribeLocalEvent<ChangelingComponent, FleshmendActionEvent>(OnFleshMend);
SubscribeLocalEvent<ChangelingComponent, ArmbladeActionEvent>(OnArmBlade);
SubscribeLocalEvent<ChangelingComponent, OrganicShieldActionEvent>(OnShield);
SubscribeLocalEvent<ChangelingComponent, ChitinousArmorActionEvent>(OnArmor);
SubscribeLocalEvent<ChangelingComponent, TentacleArmActionEvent>(OnTentacleArm);
SubscribeLocalEvent<ChangelingComponent, TransformDoAfterEvent>(OnTransformDoAfter);
SubscribeLocalEvent<ChangelingComponent, AbsorbDnaDoAfterEvent>(OnAbsorbDoAfter);
SubscribeLocalEvent<ChangelingComponent, RegenerateDoAfterEvent>(OnRegenerateDoAfter);
SubscribeLocalEvent<ChangelingComponent, LesserFormDoAfterEvent>(OnLesserFormDoAfter);
SubscribeLocalEvent<ChangelingComponent, ListViewItemSelectedMessage>(OnTransformUiMessage);
}
#region Data
private const string ChangelingAbsorb = "ActionChangelingAbsorb";
private const string ChangelingTransform = "ActionChangelingTransform";
private const string ChangelingRegenerate = "ActionChangelingRegenerate";
private const string ChangelingLesserForm = "ActionChangelingLesserForm";
private const string ChangelingTransformSting = "ActionTransformSting";
private const string ChangelingBlindSting = "ActionBlindSting";
private const string ChangelingMuteSting = "ActionMuteSting";
private const string ChangelingHallucinationSting = "ActionHallucinationSting";
private const string ChangelingCryoSting = "ActionCryoSting";
private const string ChangelingAdrenalineSacs = "ActionAdrenalineSacs";
private const string ChangelingFleshMend = "ActionFleshmend";
private const string ChangelingArmBlade = "ActionArmblade";
private const string ChangelingShield = "ActionShield";
private const string ChangelingArmor = "ActionArmor";
private const string ChangelingTentacleArm = "ActionTentacleArm";
#endregion
#region Handlers
private void OnAbsorb(EntityUid uid, ChangelingComponent component, AbsorbDnaActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
{
_popup.PopupEntity("You can't absorb not humans!", args.Performer);
return;
}
if (HasComp<AbsorbedComponent>(args.Target))
{
_popup.PopupEntity("This person already absorbed!", args.Performer);
return;
}
if (!TryComp<DnaComponent>(args.Target, out var dnaComponent))
{
_popup.PopupEntity("Unknown creature!", uid);
return;
}
if (component.AbsorbedEntities.ContainsKey(dnaComponent.DNA))
{
_popup.PopupEntity("This DNA already absorbed!", uid);
return;
}
if (!_stateSystem.IsDown(args.Target))
{
_popup.PopupEntity("Target must be down!", args.Performer);
return;
}
if (!TryComp<SharedPullableComponent>(args.Target, out var pulled))
{
_popup.PopupEntity("You must pull target!", args.Performer);
return;
}
if (!pulled.BeingPulled)
{
_popup.PopupEntity("You must pull target!", args.Performer);
return;
}
_doAfterSystem.TryStartDoAfter(
new DoAfterArgs(EntityManager, args.Performer, component.AbsorbDnaDelay, new AbsorbDnaDoAfterEvent(), uid,
args.Target, uid)
{
BreakOnTargetMove = true,
BreakOnUserMove = true
});
}
private void OnTransform(EntityUid uid, ChangelingComponent component, TransformActionEvent args)
{
if (!TryComp<ActorComponent>(uid, out var actorComponent))
return;
if (component.AbsorbedEntities.Count <= 1 && !component.IsLesserForm)
{
_popup.PopupEntity("You don't have any persons to transform!", uid);
return;
}
if (!_ui.TryGetUi(uid, ListViewSelectorUiKeyChangeling.Key, out var bui))
return;
Dictionary<string, string> state;
if (TryComp<DnaComponent>(uid, out var dnaComponent))
{
state = component.AbsorbedEntities.Where(key => key.Key != dnaComponent.DNA).ToDictionary(humanoidData
=> humanoidData.Key, humanoidData
=> humanoidData.Value.Name);
}
else
{
state = component.AbsorbedEntities.ToDictionary(humanoidData
=> humanoidData.Key, humanoidData
=> humanoidData.Value.Name);
}
_ui.SetUiState(bui, new ListViewBuiState(state));
_ui.OpenUi(bui, actorComponent.PlayerSession);
}
private void OnTransformUiMessage(EntityUid uid, ChangelingComponent component, ListViewItemSelectedMessage args)
{
var selectedDna = args.SelectedItem;
var user = GetEntity(args.Entity);
_doAfterSystem.TryStartDoAfter(
new DoAfterArgs(EntityManager, user, component.TransformDelay,
new TransformDoAfterEvent { SelectedDna = selectedDna }, user,
user, user)
{
BreakOnUserMove = true
});
if (!TryComp<ActorComponent>(uid, out var actorComponent))
return;
if (!_ui.TryGetUi(user, ListViewSelectorUiKeyChangeling.Key, out var bui))
return;
_ui.CloseUi(bui, actorComponent.PlayerSession);
}
private void OnRegenerate(EntityUid uid, ChangelingComponent component, RegenerateActionEvent args)
{
if (!TryComp<DamageableComponent>(uid, out var damageableComponent))
return;
if (damageableComponent.TotalDamage >= 0 && !_mobStateSystem.IsDead(uid))
{
KillUser(uid, "Cellular");
}
_popup.PopupEntity("We beginning our regeneration.", uid);
_doAfterSystem.TryStartDoAfter(
new DoAfterArgs(EntityManager, args.Performer, component.RegenerateDelay,
new RegenerateDoAfterEvent(), args.Performer,
args.Performer, args.Performer)
{
RequireCanInteract = false
});
component.IsRegenerating = true;
}
private void OnLesserForm(EntityUid uid, ChangelingComponent component, LesserFormActionEvent args)
{
if (_mobStateSystem.IsDead(uid) || component.IsRegenerating)
{
_popup.PopupEntity("We can do this right now!", uid);
return;
}
if (component.IsLesserForm)
{
_popup.PopupEntity("We're already in the lesser form!", uid);
return;
}
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, args.Performer, component.LesserFormDelay,
new LesserFormDoAfterEvent(), args.Performer, args.Performer)
{
BreakOnUserMove = true
});
}
private void OnTransformSting(EntityUid uid, ChangelingComponent component, TransformStingActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
{
_popup.PopupEntity("We can't transform that!", args.Performer);
return;
}
if (!TryComp<ActorComponent>(uid, out var actorComponent))
return;
if (component.AbsorbedEntities.Count < 1)
{
_popup.PopupEntity("You don't have any persons to transform!", uid);
return;
}
if (!_ui.TryGetUi(uid, TransformStingSelectorUiKey.Key, out var bui))
return;
var target = GetNetEntity(args.Target);
var state = component.AbsorbedEntities.ToDictionary(humanoidData
=> humanoidData.Key, humanoidData
=> humanoidData.Value.Name);
_ui.SetUiState(bui, new TransformStingBuiState(state, target));
_ui.OpenUi(bui, actorComponent.PlayerSession);
}
private void OnTransformStingMessage(EntityUid uid, ChangelingComponent component,
TransformStingItemSelectedMessage args)
{
var selectedDna = args.SelectedItem;
var humanData = component.AbsorbedEntities[selectedDna];
var target = GetEntity(args.Target);
var user = GetEntity(args.Entity);
if (!TryComp<ActorComponent>(uid, out var actorComponent))
return;
if (!_ui.TryGetUi(user, TransformStingSelectorUiKey.Key, out var bui))
return;
if (HasComp<ChangelingComponent>(target))
{
_popup.PopupEntity("Transform virus was ineffective!", user);
return;
}
if (!TakeChemicals(uid, component, 50))
return;
if (TryComp(target, out SharedPullerComponent? puller) && puller.Pulling is { } pulled &&
TryComp(pulled, out SharedPullableComponent? pullable))
_pullingSystem.TryStopPull(pullable);
TransformPerson(target, humanData);
_ui.CloseUi(bui, actorComponent.PlayerSession);
StartUseDelayById(uid, ChangelingTransformSting);
}
private void OnBlindSting(EntityUid uid, ChangelingComponent component, BlindStingActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target) ||
!HasComp<BlindableComponent>(args.Target))
{
_popup.PopupEntity("We cannot sting that!", uid);
return;
}
if (!TakeChemicals(uid, component, 25))
return;
var statusTimeSpan = TimeSpan.FromSeconds(25);
_statusEffectsSystem.TryAddStatusEffect(args.Target, TemporaryBlindnessSystem.BlindingStatusEffect,
statusTimeSpan, false, TemporaryBlindnessSystem.BlindingStatusEffect);
args.Handled = true;
}
private void OnMuteSting(EntityUid uid, ChangelingComponent component, MuteStingActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
{
_popup.PopupEntity("We cannot sting that!", uid);
return;
}
if (!TakeChemicals(uid, component, 20))
return;
var statusTimeSpan = TimeSpan.FromSeconds(30);
_statusEffectsSystem.TryAddStatusEffect(args.Target, "Muted",
statusTimeSpan, false, "Muted");
args.Handled = true;
}
private void OnHallucinationSting(EntityUid uid, ChangelingComponent component, HallucinationStingActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
{
_popup.PopupEntity("We cannot sting that!", uid);
return;
}
if (!TakeChemicals(uid, component, 5))
return;
var statusTimeSpan = TimeSpan.FromSeconds(30);
_statusEffectsSystem.TryAddStatusEffect(args.Target, "BlurryVision",
statusTimeSpan, false, "BlurryVision");
args.Handled = true;
}
private void OnCryoSting(EntityUid uid, ChangelingComponent component, CryoStingActionEvent args)
{
if (!HasComp<HumanoidAppearanceComponent>(args.Target))
{
_popup.PopupEntity("We cannot sting that!", uid);
return;
}
if (!TakeChemicals(uid, component, 15))
return;
var statusTimeSpan = TimeSpan.FromSeconds(30);
_statusEffectsSystem.TryAddStatusEffect(args.Target, "SlowedDown",
statusTimeSpan, false, "SlowedDown");
_temperatureSystem.ForceChangeTemperature(args.Target, 100);
args.Handled = true;
}
private void OnAdrenalineSacs(EntityUid uid, ChangelingComponent component, AdrenalineSacsActionEvent args)
{
if (_mobStateSystem.IsDead(uid))
return;
if (!_solutionContainer.TryGetInjectableSolution(uid, out var injectable, out _))
return;
if (!TakeChemicals(uid, component, 30))
return;
_solutionContainer.TryAddReagent(injectable.Value, "Stimulants", 50);
args.Handled = true;
}
private void OnFleshMend(EntityUid uid, ChangelingComponent component, FleshmendActionEvent args)
{
if (_mobStateSystem.IsDead(uid))
return;
if (!_solutionContainer.TryGetInjectableSolution(uid, out var injectable, out _))
return;
if (!TakeChemicals(uid, component, 20))
return;
_solutionContainer.TryAddReagent(injectable.Value, "Omnizine", 50);
_solutionContainer.TryAddReagent(injectable.Value, "TranexamicAcid", 10);
args.Handled = true;
}
private void OnArmBlade(EntityUid uid, ChangelingComponent component, ArmbladeActionEvent args)
{
SpawnOrDeleteItem(uid, "ArmBlade");
args.Handled = true;
}
private void OnShield(EntityUid uid, ChangelingComponent component, OrganicShieldActionEvent args)
{
SpawnOrDeleteItem(uid, "OrganicShield");
args.Handled = true;
}
private void OnArmor(EntityUid uid, ChangelingComponent component, ChitinousArmorActionEvent args)
{
const string outerName = "outerClothing";
const string protoName = "ClothingOuterChangeling";
if (!_inventorySystem.TryGetSlotEntity(uid, outerName, out var outerEnt))
{
_inventorySystem.SpawnItemInSlot(uid, outerName, protoName, silent: true);
return;
}
if (!TryComp<MetaDataComponent>(outerEnt, out var meta))
{
_inventorySystem.SpawnItemInSlot(uid, outerName, protoName, silent: true);
return;
}
if (meta.EntityPrototype == null)
{
_inventorySystem.SpawnItemInSlot(uid, outerName, protoName, silent: true);
return;
}
if (meta.EntityPrototype.ID == protoName)
{
_inventorySystem.TryUnequip(uid, outerName, out var removedItem);
QueueDel(removedItem);
return;
}
_inventorySystem.TryUnequip(uid, outerName, out _);
_inventorySystem.SpawnItemInSlot(uid, outerName, protoName, silent: true);
args.Handled = true;
}
private void OnTentacleArm(EntityUid uid, ChangelingComponent component, TentacleArmActionEvent args)
{
SpawnOrDeleteItem(uid, "TentacleArmGun");
args.Handled = true;
}
#endregion
#region DoAfters
private void OnTransformDoAfter(EntityUid uid, ChangelingComponent component, TransformDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
if (!TakeChemicals(uid, component, 5))
return;
if (TryComp(uid, out SharedPullerComponent? puller) && puller.Pulling is { } pulled &&
TryComp(pulled, out SharedPullableComponent? pullable))
_pullingSystem.TryStopPull(pullable);
TryTransformChangeling(args.User, args.SelectedDna, component);
args.Handled = true;
}
private void OnAbsorbDoAfter(EntityUid uid, ChangelingComponent component, AbsorbDnaDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Target == null)
{
return;
}
if(!_mindSystem.TryGetMind(uid, out var mindId, out _))
return;
if (TryComp(uid, out SharedPullerComponent? puller) && puller.Pulling is { } pulled &&
TryComp(pulled, out SharedPullableComponent? pullable))
_pullingSystem.TryStopPull(pullable);
if (TryComp<ChangelingComponent>(args.Target.Value, out var changelingComponent))
{
var total = component.AbsorbedEntities
.Concat(changelingComponent.AbsorbedEntities)
.ToDictionary(pair => pair.Key, pair => pair.Value);
component.AbsorbedEntities = total;
}
else
{
CopyHumanoidData(uid, args.Target.Value, component);
}
AddCurrency(uid, args.Target.Value);
KillUser(args.Target.Value, "Cellular");
EnsureComp<AbsorbedComponent>(args.Target.Value, out var absorbedComponent);
absorbedComponent.AbsorberMind = mindId;
EnsureComp<UncloneableComponent>(args.Target.Value);
StartUseDelayById(uid, ChangelingAbsorb);
args.Handled = true;
}
private void OnRegenerateDoAfter(EntityUid uid, ChangelingComponent component, RegenerateDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Target == null)
{
return;
}
if (HasComp<AbsorbedComponent>(args.Target))
{
_popup.PopupEntity("You're lost.", args.Target.Value);
component.IsRegenerating = false;
return;
}
if (!TakeChemicals(uid, component, 15))
return;
_rejuvenate.PerformRejuvenate(args.Target.Value);
_popup.PopupEntity("We're fully regenerated!", args.Target.Value);
component.IsRegenerating = false;
StartUseDelayById(uid, ChangelingRegenerate);
args.Handled = true;
}
private void OnLesserFormDoAfter(EntityUid uid, ChangelingComponent component, LesserFormDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
var polymorphEntity = _polymorph.PolymorphEntity(args.User, "MonkeyChangeling");
if (polymorphEntity == null)
return;
if (!TakeChemicals(uid, component, 5))
return;
var toAdd = new ChangelingComponent
{
HiveName = component.HiveName,
ChemicalsBalance = component.ChemicalsBalance,
AbsorbedEntities = component.AbsorbedEntities,
IsInited = component.IsInited,
IsLesserForm = true
};
EntityManager.AddComponent(polymorphEntity.Value, toAdd);
_implantSystem.TransferImplants(uid, polymorphEntity.Value);
_actionContainerSystem.TransferAllActionsFiltered(uid, polymorphEntity.Value);
_action.GrantContainedActions(polymorphEntity.Value, polymorphEntity.Value);
RemoveLesserFormActions(polymorphEntity.Value);
_chemicalsSystem.UpdateAlert(polymorphEntity.Value, component);
args.Handled = true;
}
#endregion
#region Helpers
private void RemoveLesserFormActions(EntityUid uid)
{
if (!TryComp<ActionsComponent>(uid, out var actionsComponent))
return;
foreach (var action in actionsComponent.Actions.ToArray())
{
if (!HasComp<LesserFormRestrictedComponent>(action))
continue;
_action.RemoveAction(uid, action);
}
}
private void StartUseDelayById(EntityUid performer, string actionProto)
{
if (!TryComp<ActionsComponent>(performer, out var actionsComponent))
return;
foreach (var action in actionsComponent.Actions.ToArray())
{
var id = MetaData(action).EntityPrototype?.ID;
if (id != actionProto)
continue;
_action.StartUseDelay(action);
}
}
private void KillUser(EntityUid target, string damageType)
{
if (!_mobThresholdSystem.TryGetThresholdForState(target, MobState.Dead, out var damage))
return;
DamageSpecifier dmg = new();
dmg.DamageDict.Add(damageType, damage.Value);
_damage.TryChangeDamage(target, dmg, true);
}
private void CopyHumanoidData(EntityUid uid, EntityUid target, ChangelingComponent component)
{
if (!TryComp<MetaDataComponent>(target, out var targetMeta))
return;
if (!TryComp<HumanoidAppearanceComponent>(target, out var targetAppearance))
return;
if (!TryComp<DnaComponent>(target, out var targetDna))
return;
if (!TryPrototype(target, out var prototype, targetMeta))
return;
if (component.AbsorbedEntities.ContainsKey(targetDna.DNA))
return;
var appearance = _serializationManager.CreateCopy(targetAppearance, notNullableOverride: true);
var meta = _serializationManager.CreateCopy(targetMeta, notNullableOverride: true);
var name = string.IsNullOrEmpty(meta.EntityName)
? "Unknown Creature"
: meta.EntityName;
component.AbsorbedEntities.Add(targetDna.DNA, new HumanoidData
{
EntityPrototype = prototype,
MetaDataComponent = meta,
AppearanceComponent = appearance,
Name = name,
Dna = targetDna.DNA
});
Dirty(uid, component);
}
/// <summary>
/// Transforms chosen person to another, transferring it's appearance
/// </summary>
/// <param name="target">Transform target</param>
/// <param name="transformData">Transform data</param>
/// <param name="humanoidOverride">Override first check on HumanoidAppearanceComponent</param>
/// <returns>Id of the transformed entity</returns>
private EntityUid? TransformPerson(EntityUid target, HumanoidData transformData, bool humanoidOverride = false)
{
if (!HasComp<HumanoidAppearanceComponent>(target) && !humanoidOverride)
return null;
var polymorphEntity = _polymorph.PolymorphEntity(target, transformData.EntityPrototype.ID);
if (polymorphEntity == null)
return null;
if (!TryComp<HumanoidAppearanceComponent>(polymorphEntity.Value, out var polyAppearance))
return null;
ClonePerson(polymorphEntity.Value, transformData.AppearanceComponent, polyAppearance);
TransferDna(polymorphEntity.Value, transformData.Dna);
if (!TryComp<MetaDataComponent>(polymorphEntity.Value, out var meta))
return null;
_metaData.SetEntityName(polymorphEntity.Value, transformData.MetaDataComponent!.EntityName, meta);
_metaData.SetEntityDescription(polymorphEntity.Value, transformData.MetaDataComponent!.EntityDescription, meta);
_identity.QueueIdentityUpdate(polymorphEntity.Value);
return polymorphEntity;
}
private void TransferDna(EntityUid target, string dna)
{
if (!TryComp<DnaComponent>(target, out var dnaComponent))
return;
dnaComponent.DNA = dna;
}
private void TryTransformChangeling(EntityUid uid, string dna, ChangelingComponent component)
{
if (!component.AbsorbedEntities.TryGetValue(dna, out var person))
return;
EntityUid? reverted = uid;
reverted = component.IsLesserForm
? TransformPerson(reverted.Value, person, humanoidOverride: true)
: TransformPerson(reverted.Value, person);
if (reverted == null)
return;
var toAdd = new ChangelingComponent
{
HiveName = component.HiveName,
ChemicalsBalance = component.ChemicalsBalance,
AbsorbedEntities = component.AbsorbedEntities,
IsInited = component.IsInited
};
EntityManager.AddComponent(reverted.Value, toAdd);
_implantSystem.TransferImplants(uid, reverted.Value);
_actionContainerSystem.TransferAllActionsFiltered(uid, reverted.Value);
_action.GrantContainedActions(reverted.Value,reverted.Value);
if (component.IsLesserForm)
{
//Don't copy IsLesserForm bool, because transferred component, in fact, new. Bool default value if false.
StartUseDelayById(reverted.Value, ChangelingLesserForm);
}
_chemicalsSystem.UpdateAlert(reverted.Value, component);
StartUseDelayById(reverted.Value, ChangelingTransform);
}
/// <summary>
/// Used for cloning appearance
/// </summary>
/// <param name="target">Acceptor</param>
/// <param name="sourceHumanoid">Source appearance</param>
/// <param name="targetHumanoid">Acceptor appearance component</param>
private void ClonePerson(EntityUid target, HumanoidAppearanceComponent sourceHumanoid,
HumanoidAppearanceComponent targetHumanoid)
{
targetHumanoid.Species = sourceHumanoid.Species;
targetHumanoid.SkinColor = sourceHumanoid.SkinColor;
targetHumanoid.EyeColor = sourceHumanoid.EyeColor;
targetHumanoid.Age = sourceHumanoid.Age;
_humanoidAppearance.SetSex(target, sourceHumanoid.Sex, false, targetHumanoid);
_humanoidAppearance.SetSpecies(target, sourceHumanoid.Species);
targetHumanoid.CustomBaseLayers = new Dictionary<HumanoidVisualLayers,
CustomBaseLayerInfo>(sourceHumanoid.CustomBaseLayers);
targetHumanoid.MarkingSet = new MarkingSet(sourceHumanoid.MarkingSet);
targetHumanoid.Gender = sourceHumanoid.Gender;
if (TryComp<GrammarComponent>(target, out var grammar))
{
grammar.Gender = sourceHumanoid.Gender;
}
Dirty(target, targetHumanoid);
}
private void SpawnOrDeleteItem(EntityUid target, string prototypeName)
{
foreach (var eHand in _handsSystem.EnumerateHands(target))
{
if (eHand.HeldEntity == null || !TryComp<MetaDataComponent>(eHand.HeldEntity.Value, out var meta))
continue;
if (meta.EntityPrototype != null && meta.EntityPrototype.ID != prototypeName)
continue;
Del(eHand.HeldEntity);
return;
}
if (!_handsSystem.TryGetEmptyHand(target, out var hand))
{
_popup.PopupEntity("We need to have at least one empty hand!", target);
return;
}
var item = Spawn(prototypeName, Transform(target).Coordinates);
if (!_handsSystem.TryPickup(target, item, hand, animate: false))
{
Del(item);
}
}
private bool TakeChemicals(EntityUid uid, ChangelingComponent component, int quantity)
{
if (!_chemicalsSystem.RemoveChemicals(uid, component, quantity))
{
_popup.PopupEntity("We're lacking of chemicals!", uid);
return false;
}
_popup.PopupEntity($"Used {quantity} of chemicals.", uid);
return true;
}
private void AddCurrency(EntityUid uid, EntityUid absorbed)
{
if (!TryComp<ImplantedComponent>(uid, out var implant))
return;
foreach (var entity in implant.ImplantContainer.ContainedEntities)
{
if (!TryComp<StoreComponent>(entity, out var store))
continue;
if (_mobStateSystem.IsDead(absorbed))
{
var points = _random.Next(1, 3);
var toAdd = new Dictionary<string, FixedPoint2> { { "ChangelingPoint", points } };
_storeSystem.TryAddCurrency(toAdd, entity, store);
}
else
{
var points = _random.Next(2, 4);
var toAdd = new Dictionary<string, FixedPoint2> { { "ChangelingPoint", points } };
_storeSystem.TryAddCurrency(toAdd, entity, store);
}
return;
}
}
#endregion
}

View File

@@ -0,0 +1,24 @@
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Changeling;
using Content.Shared.Implants.Components;
using Robust.Server.GameStates;
using Robust.Server.Placement;
namespace Content.Server.Changeling;
public sealed partial class ChangelingSystem
{
private void InitializeShop()
{
SubscribeLocalEvent<SubdermalImplantComponent, ChangelingShopActionEvent>(OnShop);
}
private void OnShop(EntityUid uid, SubdermalImplantComponent component, ChangelingShopActionEvent args)
{
if(!TryComp<StoreComponent>(uid, out var store))
return;
_storeSystem.ToggleUi(args.Performer, uid, store);
}
}

View File

@@ -0,0 +1,81 @@
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Actions;
using Content.Shared.Changeling;
using Content.Shared.Examine;
using Content.Shared.Implants;
using Content.Shared.Implants.Components;
namespace Content.Server.Changeling;
public sealed partial class ChangelingSystem : EntitySystem
{
[Dependency] private readonly SharedActionsSystem _action = default!;
[Dependency] private readonly ChemicalsSystem _chemicalsSystem = default!;
[Dependency] private readonly SharedSubdermalImplantSystem _implantSystem = default!;
[Dependency] private readonly StoreSystem _storeSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<AbsorbedComponent, ExaminedEvent>(OnExamine);
InitializeAbilities();
InitializeShop();
}
#region Handlers
private void OnInit(EntityUid uid, ChangelingComponent component, ComponentInit args)
{
SetupShop(uid, component);
SetupInitActions(uid, component);
CopyHumanoidData(uid, uid, component);
_chemicalsSystem.UpdateAlert(uid, component);
component.IsInited = true;
}
private void OnExamine(EntityUid uid, AbsorbedComponent component, ExaminedEvent args)
{
args.PushMarkup("[color=#A30000]His juices sucked up![/color]");
}
#endregion
#region Helpers
private void SetupShop(EntityUid uid, ChangelingComponent component)
{
if (component.IsInited)
return;
var coords = Transform(uid).Coordinates;
var implant = Spawn("ChangelingShopImplant", coords);
if(!TryComp<SubdermalImplantComponent>(implant, out var implantComp))
return;
_implantSystem.ForceImplant(uid, implant, implantComp);
if (!TryComp<StoreComponent>(implant, out var implantStore))
return;
implantStore.Balance.Add("ChangelingPoint", component.StartingPointsBalance);
}
private void SetupInitActions(EntityUid uid, ChangelingComponent component)
{
if (component.IsInited)
return;
_action.AddAction(uid, ChangelingAbsorb);
_action.AddAction(uid, ChangelingTransform);
_action.AddAction(uid, ChangelingRegenerate);
}
#endregion
}

View File

@@ -0,0 +1,236 @@
using System.Linq;
using Content.Server.Changeling.Objectives.Components;
using Content.Server.Forensics;
using Content.Server.Mind;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Shuttles.Systems;
using Content.Shared.Changeling;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Robust.Shared.Random;
namespace Content.Server.Changeling.Objectives;
public sealed class ChangelingConditionsSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly MindSystem _mind = default!;
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
public override void Initialize()
{
base.Initialize();
// Absorb DNA condition
SubscribeLocalEvent<AbsorbDnaConditionComponent, ObjectiveAssignedEvent>(OnAbsorbDnaAssigned);
SubscribeLocalEvent<AbsorbDnaConditionComponent, ObjectiveAfterAssignEvent>(OnAbsorbDnaAfterAssigned);
SubscribeLocalEvent<AbsorbDnaConditionComponent, ObjectiveGetProgressEvent>(OnAbsorbDnaGetProgress);
//Absorb more genomes, than others changelings
SubscribeLocalEvent<AbsorbMoreConditionComponent, ObjectiveGetProgressEvent>(OnAbsorbMoreGetProgress);
//Absorb other changeling
SubscribeLocalEvent<PickRandomChangelingComponent, ObjectiveAssignedEvent>(OnAbsorbChangelingAssigned);
SubscribeLocalEvent<AbsorbChangelingConditionComponent, ObjectiveGetProgressEvent>(OnAbsorbChangelingGetProgress);
//Escape with identity
SubscribeLocalEvent<PickRandomIdentityComponent, ObjectiveAssignedEvent>(OnEscapeWithIdentityAssigned);
SubscribeLocalEvent<EscapeWithIdentityConditionComponent, ObjectiveGetProgressEvent>(OnEscapeWithIdentityGetProgress);
}
#region AbsorbDNA
private void OnAbsorbDnaAssigned(EntityUid uid, AbsorbDnaConditionComponent component, ref ObjectiveAssignedEvent args)
{
component.NeedToAbsorb = _random.Next(2, 6);
}
private void OnAbsorbDnaAfterAssigned(EntityUid uid, AbsorbDnaConditionComponent component, ref ObjectiveAfterAssignEvent args)
{
var title = Loc.GetString("objective-condition-absorb-dna", ("count", component.NeedToAbsorb));
_metaData.SetEntityName(uid, title, args.Meta);
}
private void OnAbsorbDnaGetProgress(EntityUid uid, AbsorbDnaConditionComponent component, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetAbsorbProgress(args.Mind, component.NeedToAbsorb);
}
private float GetAbsorbProgress(MindComponent mind, int requiredDna)
{
if (!TryComp<ChangelingComponent>(mind.CurrentEntity, out var changelingComponent))
return 0f;
var absorbed = changelingComponent.AbsorbedEntities.Count - 1; // Because first - it's the owner
if (requiredDna == absorbed)
return 1f;
var progress = MathF.Min(absorbed/(float)requiredDna, 1f);
return progress;
}
#endregion
#region AbsorbMoreDNA
private void OnAbsorbMoreGetProgress(EntityUid uid, AbsorbMoreConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetAbsorbMoreProgress(args.Mind);
}
private float GetAbsorbMoreProgress(MindComponent mind)
{
if (!TryComp<ChangelingComponent>(mind.CurrentEntity, out var changelingComponent))
return 0f;
var selfAbsorbed = changelingComponent.AbsorbedEntities.Count - 1; // Because first - it's the owner
var query = EntityQueryEnumerator<ChangelingComponent>();
List<int> otherAbsorbed = new();
while (query.MoveNext(out var uid, out var comp))
{
if (uid == mind.CurrentEntity)
continue; //don't include self
var absorbed = comp.AbsorbedEntities.Count - 1;
otherAbsorbed.Add(absorbed);
}
if (otherAbsorbed.Count == 0)
return 1f;
var isTheMost = otherAbsorbed.Max() < selfAbsorbed;
return isTheMost ? 1f : 0f;
}
#endregion
#region AbsorbChangeling
private void OnAbsorbChangelingAssigned(EntityUid uid, PickRandomChangelingComponent comp, ref ObjectiveAssignedEvent args)
{
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
if (target.Target != null)
return;
foreach (var changelingRule in EntityQuery<ChangelingRuleComponent>())
{
var changelingMinds = changelingRule.ChangelingMinds
.Except(new List<EntityUid> { args.MindId })
.ToList();
if (changelingMinds.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(changelingMinds), target);
}
}
private void OnAbsorbChangelingGetProgress(EntityUid uid, AbsorbChangelingConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetAbsorbChangelingProgress(args.Mind, target.Value);
}
private float GetAbsorbChangelingProgress(MindComponent mind, EntityUid target)
{
if(!_mind.TryGetMind(mind.CurrentEntity!.Value, out var selfMindId, out _))
return 0f;
if (!TryComp<MindComponent>(target, out var targetMind))
return 0f;
if (!HasComp<ChangelingComponent>(targetMind.CurrentEntity))
return 0f;
if (!TryComp<AbsorbedComponent>(targetMind.CurrentEntity, out var absorbedComponent))
return 0f;
return absorbedComponent.AbsorberMind == selfMindId ? 1f : 0f;
}
#endregion
#region EscapeWithIdentity
private void OnEscapeWithIdentityAssigned(EntityUid uid, PickRandomIdentityComponent component, ref ObjectiveAssignedEvent args)
{
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
if (target.Target != null)
return;
var allHumans = _mind.GetAliveHumansExcept(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(allHumans), target);
}
private void OnEscapeWithIdentityGetProgress(EntityUid uid, EscapeWithIdentityConditionComponent component, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetEscapeWithIdentityProgress(args.Mind, target.Value);
}
private float GetEscapeWithIdentityProgress(MindComponent mind, EntityUid target)
{
var progress = 0f;
if (!TryComp<DnaComponent>(mind.CurrentEntity, out var selfDna))
return 0f;
if (!TryComp<MindComponent>(target, out var targetMind))
return 0f;
if (!TryComp<DnaComponent>(targetMind.CurrentEntity, out var targetDna))
return 0f;
if (!TryComp<ChangelingComponent>(mind.CurrentEntity, out var changeling))
return 0f;
if (!changeling.AbsorbedEntities.ContainsKey(targetDna.DNA))
return 0f;
//Target absorbed by this changeling, so 50% of work is done
progress += 0.5f;
if (_emergencyShuttle.IsTargetEscaping(mind.CurrentEntity.Value) && selfDna.DNA == targetDna.DNA)
progress += 0.5f;
if (_emergencyShuttle.ShuttlesLeft)
return progress;
return progress;
}
#endregion
}

View File

@@ -0,0 +1,6 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class AbsorbChangelingConditionComponent : Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class AbsorbDnaConditionComponent : Component
{
public int NeedToAbsorb;
}

View File

@@ -0,0 +1,6 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class AbsorbMoreConditionComponent: Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class EscapeWithIdentityConditionComponent : Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class PickRandomChangelingComponent : Component
{
}

View File

@@ -0,0 +1,7 @@
namespace Content.Server.Changeling.Objectives.Components;
[RegisterComponent]
public sealed partial class PickRandomIdentityComponent : Component
{
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.Changeling;
namespace Content.Server.Changeling;
public sealed class TentacleGun : SharedTentacleGun
{
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Changeling;
/// <summary>
/// This is used for the uncloneable trait.
/// </summary>
[RegisterComponent]
public sealed partial class UncloneableComponent : Component
{
}

View File

@@ -0,0 +1,43 @@
using Content.Server.Chat.Managers;
using Content.Server.Chat.Systems;
using Content.Shared.Administration;
using Content.Shared.Changeling;
using Robust.Shared.Console;
using Robust.Shared.Enums;
namespace Content.Server.Chat.Commands;
[AnyCommand]
internal sealed class ChangelingChatCommand : IConsoleCommand
{
public string Command => "gsay";
public string Description => "Send changeling Hive message";
public string Help => "gsay <text>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } player)
{
shell.WriteError("This command cannot be run from the server.");
return;
}
if (player.AttachedEntity is not { Valid: true } entity)
return;
if (args.Length < 1)
return;
var entityManager = IoCManager.Resolve<IEntityManager>();
if (!entityManager.HasComponent<ChangelingComponent>(entity))
return;
var message = string.Join(" ", args).Trim();
if (string.IsNullOrEmpty(message))
return;
entityManager.System<ChatSystem>().TrySendInGameOOCMessage(entity, message,
InGameOOCChatType.Changeling, false, shell, player);
}
}

View File

@@ -1,5 +1,6 @@
using Content.Server.Administration.Logs;
using Content.Server.Popups;
using Content.Shared.Changeling;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
@@ -31,6 +32,11 @@ namespace Content.Server.Chat
if (_tagSystem.HasTag(victim, "CannotSuicide"))
return false;
//Miracle edit
if (TryComp<ChangelingComponent>(victim, out var changeling) && changeling.IsRegenerating)
return false;
//Miracle edit end
// Checks to see if the player is dead.
if (!TryComp<MobStateComponent>(victim, out var mobState) || _mobState.IsDead(victim, mobState))
return false;

View File

@@ -14,6 +14,7 @@ using Content.Server._White.PandaSocket.Main;
using Content.Shared.ActionBlocker;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Changeling;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Decals;
@@ -357,6 +358,9 @@ public sealed partial class ChatSystem : SharedChatSystem
case InGameOOCChatType.Looc:
SendLOOC(source, player, message, hideChat);
break;
case InGameOOCChatType.Changeling:
SendChangelingChat(source, player, message, hideChat);
break;
case InGameOOCChatType.Cult:
SendCultChat(source, player, message, hideChat);
break;
@@ -767,6 +771,37 @@ public sealed partial class ChatSystem : SharedChatSystem
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"LOOC from {player:Player}: {message}");
}
private void SendChangelingChat(EntityUid source, ICommonSession player, string message, bool hideChat)
{
if (!TryComp<ChangelingComponent>(source, out var changeling))
return;
var clients = GetChangelingChatClients();
var playerName = changeling.HiveName;
message = $"{char.ToUpper(message[0])}{message[1..]}";
var wrappedMessage = Loc.GetString("chat-manager-send-changeling-chat-wrap-message",
("player", playerName),
("message", FormattedMessage.EscapeText(message)));
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"Changeling chat from {player:Player}-({playerName}): {message}");
_chatManager.ChatMessageToMany(ChatChannel.Changeling, message, wrappedMessage, source,
hideChat, false, clients.ToList());
}
private IEnumerable<INetChannel> GetChangelingChatClients()
{
return Filter.Empty()
.AddWhereAttachedEntity(HasComp<GhostComponent>)
.AddWhereAttachedEntity(HasComp<ChangelingComponent>)
.Recipients
.Select(p => p.Channel);
}
// WD EDIT
private void SendCultChat(EntityUid source, ICommonSession player, string message, bool hideChat)
{
@@ -1243,6 +1278,7 @@ public enum InGameOOCChatType : byte
{
Looc,
Dead,
Changeling,
Cult
}

View File

@@ -1,4 +1,5 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Changeling;
using Content.Server.Chat.Systems;
using Content.Server.Cloning.Components;
using Content.Server.DeviceLinking.Systems;
@@ -177,6 +178,19 @@ namespace Content.Server.Cloning
if (_configManager.GetCVar(CCVars.BiomassEasyMode))
cloningCost = (int) Math.Round(cloningCost * EasyModeCloningCost);
// Check if they have the uncloneable trait
if (TryComp<UncloneableComponent>(bodyToClone, out _))
{
if (clonePod.ConnectedConsole != null)
{
_chatSystem.TrySendInGameICMessage(clonePod.ConnectedConsole.Value,
Loc.GetString("cloning-console-uncloneable-trait-error"),
InGameICChatType.Speak, false);
}
return false;
}
// biomass checks
var biomassAmount = _material.GetMaterialAmount(uid, clonePod.RequiredMaterial);

View File

@@ -31,7 +31,6 @@
<ProjectReference Include="..\Content.Shared\Content.Shared.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Objectives\Interfaces\" />
<Folder Include="Tools\Systems\" />
</ItemGroup>
<Import Project="..\RobustToolbox\MSBuild\Robust.Properties.targets" />

View File

@@ -6,6 +6,7 @@ using Content.Server.GameTicking.Presets;
using Content.Server.Maps;
using Content.Server.Ghost;
using Content.Shared.CCVar;
using Content.Shared.Changeling;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Database;
@@ -229,6 +230,11 @@ namespace Content.Server.GameTicking
return false;
}
//Miracle edit
if (TryComp<ChangelingComponent>(playerEntity, out var changeling) && changeling.IsRegenerating)
return false;
//Miracle edit end
if (HasComp<GhostComponent>(playerEntity))
return false;

View File

@@ -174,6 +174,32 @@ public sealed class ActionContainerSystem : EntitySystem
DebugTools.AssertEqual(oldContainer.Container.Count, 0);
}
//Miracle edit
public void TransferAllActionsFiltered(
EntityUid from,
EntityUid to,
ActionsContainerComponent? oldContainer = null,
ActionsContainerComponent? newContainer = null)
{
if (!Resolve(from, ref oldContainer) || !Resolve(to, ref newContainer))
return;
foreach (var action in oldContainer.Container.ContainedEntities.ToArray())
{
var actions = newContainer.Container.ContainedEntities;
var toAdd = MetaData(action).EntityPrototype?.ID;
if (actions.Select(act => MetaData(act).EntityPrototype?.ID).Any(ext => toAdd == ext))
continue;
TransferAction(action, to, container: newContainer);
}
}
//Miracle edit end
/// <summary>
/// Transfers an actions from one container to another, while changing the attached entity.
/// </summary>

View File

@@ -55,6 +55,7 @@ namespace Content.Shared.Alert
VowOfSilence,
VowBroken,
Essence,
Chemicals,
Corporeal,
Bleed,
Pacified,

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling;
[RegisterComponent, NetworkedComponent]
public sealed partial class AbsorbedComponent : Component
{
public EntityUid AbsorberMind;
}

View File

@@ -0,0 +1,70 @@
using Content.Shared.Humanoid;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Changeling;
[RegisterComponent, NetworkedComponent]
public sealed partial class ChangelingComponent : Component
{
[DataField("chemRegenRate")]
public int ChemicalRegenRate = 2;
[DataField("chemicalCap")]
public int ChemicalCapacity = 75;
[ViewVariables(VVAccess.ReadWrite), DataField("chemicalsBalance")]
public int ChemicalsBalance = 20;
[ViewVariables(VVAccess.ReadWrite), DataField("pointsBalance")]
public int StartingPointsBalance = 10;
[ViewVariables(VVAccess.ReadOnly)]
public float Accumulator;
[ViewVariables(VVAccess.ReadOnly)]
public float UpdateDelay = 6f;
[ViewVariables(VVAccess.ReadOnly)]
public bool IsRegenerating;
[ViewVariables(VVAccess.ReadOnly)]
public bool IsLesserForm;
[ViewVariables(VVAccess.ReadOnly)]
public string HiveName;
[ViewVariables(VVAccess.ReadOnly), DataField("absorbedEntities")]
public Dictionary<string, HumanoidData> AbsorbedEntities = new();
[ViewVariables(VVAccess.ReadWrite), DataField("AbsorbDNACost")]
public int AbsorbDnaCost;
[ViewVariables(VVAccess.ReadWrite), DataField("AbsorbDNADelay")]
public float AbsorbDnaDelay = 10f;
[ViewVariables(VVAccess.ReadWrite), DataField("TransformDelay")]
public float TransformDelay = 2f;
[ViewVariables(VVAccess.ReadWrite), DataField("RegenerateDelay")]
public float RegenerateDelay = 20f;
[ViewVariables(VVAccess.ReadWrite), DataField("LesserFormDelay")]
public float LesserFormDelay = 5f;
public bool IsInited;
}
public struct HumanoidData
{
public EntityPrototype EntityPrototype;
public MetaDataComponent? MetaDataComponent;
public HumanoidAppearanceComponent AppearanceComponent;
public string Name;
public string Dna;
}

View File

@@ -0,0 +1,63 @@
using System.Linq;
using Robust.Shared.Random;
namespace Content.Shared.Changeling;
public sealed class ChangelingNameGenerator
{
[Dependency] private readonly IRobustRandom _random = default!;
private List<string> _used = new();
private readonly List<string> _greekAlphabet = new()
{
"Alpha",
"Beta",
"Gamma",
"Delta",
"Epsilon",
"Zeta",
"Eta",
"Theta",
"Iota",
"Kappa",
"Lambda",
"Mu",
"Nu",
"Xi",
"Omicron",
"Pi",
"Rho",
"Sigma",
"Tau",
"Upsilon",
"Phi",
"Chi",
"Psi",
"Omega"
};
private string GenWhiteLabelName()
{
var number = _random.Next(0,10000);
return $"HiveMember-{number}";
}
public string GetName()
{
_random.Shuffle(_greekAlphabet);
foreach (var selected in _greekAlphabet.Where(selected => !_used.Contains(selected)))
{
_used.Add(selected);
return selected;
}
return GenWhiteLabelName();
}
public void ClearUsed()
{
_used.Clear();
}
}

View File

@@ -0,0 +1,88 @@
using Content.Shared.Alert;
using Content.Shared.Mobs.Systems;
using Robust.Shared.Network;
namespace Content.Shared.Changeling;
public sealed class ChemicalsSystem : EntitySystem
{
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly INetManager _net = default!;
public bool AddChemicals(EntityUid uid, ChangelingComponent component, int quantity)
{
if (_mobStateSystem.IsDead(uid))
return false;
var toAdd = quantity;
if (component.ChemicalsBalance == component.ChemicalCapacity)
return false;
if (component.ChemicalsBalance + toAdd > component.ChemicalCapacity)
{
var overflow = component.ChemicalsBalance + toAdd - component.ChemicalCapacity;
toAdd -= overflow;
component.ChemicalsBalance += toAdd;
}
component.ChemicalsBalance += toAdd;
Dirty(uid, component);
UpdateAlert(uid, component);
return true;
}
public bool RemoveChemicals(EntityUid uid, ChangelingComponent component, int quantity)
{
if (_mobStateSystem.IsDead(uid) && !component.IsRegenerating)
return false;
var toRemove = quantity;
if (component.ChemicalsBalance == 0)
return false;
if (component.ChemicalsBalance - toRemove < 0)
return false;
component.ChemicalsBalance -= toRemove;
Dirty(uid, component);
UpdateAlert(uid, component);
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ChangelingComponent>();
while (query.MoveNext(out var uid, out var component))
{
component.Accumulator += frameTime;
if(component.Accumulator < component.UpdateDelay)
continue;
if (component.IsRegenerating)
continue;
component.Accumulator = 0;
AddChemicals(uid, component, component.ChemicalRegenRate);
}
}
public void UpdateAlert(EntityUid uid, ChangelingComponent component)
{
if(_net.IsServer)
{
_alertsSystem.ShowAlert(uid, AlertType.Chemicals,
(short) Math.Clamp(Math.Round(component.ChemicalsBalance / 10f), 0, 7));
}
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling;
[RegisterComponent, NetworkedComponent]
public sealed partial class LesserFormRestrictedComponent : Component
{
}

View File

@@ -0,0 +1,105 @@
using Content.Shared.Actions;
using Content.Shared.DoAfter;
using Robust.Shared.Serialization;
namespace Content.Shared.Changeling;
[Serializable, NetSerializable]
public sealed partial class AbsorbDnaDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class AbsorbDnaActionEvent : EntityTargetActionEvent
{
}
[Serializable, NetSerializable]
public sealed partial class TransformDoAfterEvent : SimpleDoAfterEvent
{
public string SelectedDna;
}
public sealed partial class TransformActionEvent : InstantActionEvent
{
}
[Serializable, NetSerializable]
public sealed partial class RegenerateDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class RegenerateActionEvent : InstantActionEvent
{
}
[Serializable, NetSerializable]
public sealed partial class LesserFormDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class LesserFormActionEvent : InstantActionEvent
{
}
public sealed partial class TransformStingActionEvent : EntityTargetActionEvent
{
}
public sealed partial class BlindStingActionEvent : EntityTargetActionEvent
{
}
public sealed partial class MuteStingActionEvent : EntityTargetActionEvent
{
}
public sealed partial class HallucinationStingActionEvent : EntityTargetActionEvent
{
}
public sealed partial class CryoStingActionEvent : EntityTargetActionEvent
{
}
public sealed partial class AdrenalineSacsActionEvent : InstantActionEvent
{
}
public sealed partial class FleshmendActionEvent : InstantActionEvent
{
}
public sealed partial class ArmbladeActionEvent : InstantActionEvent
{
}
public sealed partial class OrganicShieldActionEvent : InstantActionEvent
{
}
public sealed partial class ChitinousArmorActionEvent : InstantActionEvent
{
}
public sealed partial class TentacleArmActionEvent : InstantActionEvent
{
}
public sealed partial class ChangelingShopActionEvent : InstantActionEvent
{
}

View File

@@ -0,0 +1,33 @@
namespace Content.Shared.Changeling;
public sealed class SharedChangelingChat : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChangelingComponent, ComponentStartup>(OnInit);
SubscribeLocalEvent<ChangelingComponent, ComponentShutdown>(OnShutdown);
}
private void OnInit(EntityUid uid, ChangelingComponent component, ComponentStartup args)
{
RaiseLocalEvent(new ChangelingUserStart(true));
}
private void OnShutdown(EntityUid uid, ChangelingComponent component, ComponentShutdown args)
{
RaiseLocalEvent(new ChangelingUserStart(false));
}
}
public sealed class ChangelingUserStart
{
public bool Created { get; }
public ChangelingUserStart(bool state)
{
Created = state;
}
}

View File

@@ -0,0 +1,155 @@
using System.Numerics;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Humanoid;
using Content.Shared.Physics;
using Content.Shared.Projectiles;
using Content.Shared.Stunnable;
using Content.Shared.Throwing;
using Content.Shared.Weapons.Misc;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Systems;
using Robust.Shared.Input;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
using Robust.Shared.Random;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Changeling;
public abstract class SharedTentacleGun : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly ITimerManager _timerManager = default!;
[Dependency] private readonly SharedStunSystem _stunSystem = default!;
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TentacleGunComponent, GunShotEvent>(OnTentacleShot);
SubscribeLocalEvent<TentacleProjectileComponent, ProjectileEmbedEvent>(OnTentacleCollide);
}
private void OnTentacleShot(EntityUid uid, TentacleGunComponent component, ref GunShotEvent args)
{
foreach (var (shotUid, _) in args.Ammo)
{
if (!HasComp<TentacleProjectileComponent>(shotUid))
continue;
Dirty(uid, component);
var visuals = EnsureComp<JointVisualsComponent>(shotUid.Value);
visuals.Sprite =
new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Guns/Launchers/tentacle_gun.rsi"), "frope");
visuals.OffsetA = new Vector2(0f, 0.5f);
visuals.Target = uid;
Dirty(shotUid.Value, visuals);
}
TryComp<AppearanceComponent>(uid, out var appearance);
_appearance.SetData(uid, SharedTetherGunSystem.TetherVisualsStatus.Key, false, appearance);
}
private void OnTentacleCollide(EntityUid uid, TentacleProjectileComponent component, ref ProjectileEmbedEvent args)
{
if (!_timing.IsFirstTimePredicted)
return;
if (!HasComp<TentacleGunComponent>(args.Weapon))
{
QueueDel(uid);
return;
}
if (!TryComp<GunComponent>(args.Weapon, out var gun))
{
QueueDel(uid);
return;
}
if (!HasComp<HumanoidAppearanceComponent>(args.Embedded))
{
DeleteProjectile(uid);
return;
}
switch (gun.SelectedMode)
{
case SelectiveFire.PullMob when !PullMob(args):
DeleteProjectile(uid);
return;
case SelectiveFire.PullMob:
_timerManager.AddTimer(new Timer(1500, false, () =>
{
DeleteProjectile(uid);
}));
break;
case SelectiveFire.PullItem:
PullItem(args);
DeleteProjectile(uid);
break;
}
}
private void PullItem(ProjectileEmbedEvent args)
{
foreach (var activeItem in _handsSystem.EnumerateHeld(args.Embedded))
{
if(!TryComp<PhysicsComponent>(activeItem, out var physicsComponent))
return;
var coords = Transform(args.Embedded).Coordinates;
_handsSystem.TryDrop(args.Embedded, coords);
var force = physicsComponent.Mass * 2.5f / 2;
_throwingSystem.TryThrow(activeItem, Transform(args.Shooter!.Value).Coordinates, force);
break;
}
}
private bool PullMob(ProjectileEmbedEvent args)
{
var stunTime = _random.Next(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(8));
if (!_stunSystem.TryParalyze(args.Embedded, stunTime, true))
return false;
_throwingSystem.TryThrow(args.Embedded, Transform(args.Shooter!.Value).Coordinates, 5f);
return true;
}
private void DeleteProjectile(EntityUid projUid)
{
TryComp<AppearanceComponent>(projUid, out var appearance);
if (!Deleted(projUid))
{
if (_netManager.IsServer)
{
QueueDel(projUid);
}
}
_appearance.SetData(projUid, SharedTetherGunSystem.TetherVisualsStatus.Key, true, appearance);
}
[Serializable, NetSerializable]
protected sealed class RequestTentacleMessage : EntityEventArgs
{
public BoundKeyFunction Key;
public RequestTentacleMessage(BoundKeyFunction key)
{
Key = key;
}
}
}

View File

@@ -0,0 +1,8 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling;
[RegisterComponent, NetworkedComponent]
public sealed partial class TentacleGunComponent : Component
{
}

View File

@@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Changeling;
[RegisterComponent, NetworkedComponent]
public sealed partial class TentacleProjectileComponent : Component
{
}

View File

@@ -75,17 +75,22 @@ namespace Content.Shared.Chat
AdminChat = 1 << 12,
/// <summary>
/// Unspecified.
/// Changeling
/// </summary>
Unspecified = 1 << 13,
Changeling = 1 << 13,
//WD EDIT
Cult = 1 << 14,
/// <summary>
/// Unspecified.
/// </summary>
Unspecified = 1 << 15,
/// <summary>
/// Channels considered to be IC.
/// </summary>
IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual,
IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Changeling,
AdminRelated = Admin | AdminAlert | AdminChat,
}

View File

@@ -15,6 +15,7 @@ public static class ChatChannelExtensions
ChatChannel.AdminAlert => Color.Red,
ChatChannel.AdminChat => Color.HotPink,
ChatChannel.Whisper => Color.DarkGray,
ChatChannel.Changeling => Color.Purple,
ChatChannel.Cult => Color.DarkRed, // WD EDIT
_ => Color.LightGray
};

View File

@@ -53,6 +53,8 @@
/// </summary>
Admin = ChatChannel.AdminChat,
Changeling = ChatChannel.Changeling,
Console = ChatChannel.Unspecified
}
}

View File

@@ -21,6 +21,7 @@ public abstract class SharedChatSystem : EntitySystem
public const char AdminPrefix = ']';
public const char WhisperPrefix = ',';
public const char DefaultChannelKey = 'h';
public const char ChangelingPrefix = '<';
public const char CultPrefix = '^'; // WD EDIT
[ValidatePrototypeId<RadioChannelPrototype>]

View File

@@ -8,20 +8,20 @@ namespace Content.Shared.Eye.Blinding.Components;
/// </summary>
[RegisterComponent]
[NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(BlurryVisionSystem))]
// [Access(typeof(BlurryVisionSystem))]
public sealed partial class BlurryVisionComponent : Component
{
/// <summary>
/// Amount of "blurring". Also modifies examine ranges.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("magnitude"), AutoNetworkedField]
public float Magnitude;
public float Magnitude = 4f;
/// <summary>
/// Exponent that controls the magnitude of the effect.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("correctionPower"), AutoNetworkedField]
public float CorrectionPower;
public float CorrectionPower = 2f;
public const float MaxMagnitude = 6;
public const float DefaultCorrectionPower = 2f;

View File

@@ -10,5 +10,6 @@ namespace Content.Shared.Implants.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ImplantedComponent : Component
{
[ViewVariables(VVAccess.ReadOnly)]
public Container ImplantContainer = default!;
}

View File

@@ -117,7 +117,8 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
/// <param name="target">The entity to be implanted</param>
/// <param name="implant"> The implant</param>
/// <param name="component">The implant component</param>
public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component)
/// <param name="containerForce">Should we force inserting in container</param>
public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component, bool containerForce = false)
{
//If the target doesn't have the implanted component, add it.
var implantedComp = EnsureComp<ImplantedComponent>(target);
@@ -175,6 +176,48 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem
RaiseLocalEvent(implant, relayEv);
}
}
//Miracle edit
/// <summary>
/// Transfers all implants from one entity to another.
/// </summary>
/// <remarks>
/// This method transfers all implants from a donor entity to a recipient entity.
/// Implants are moved from the donor's implant container to the recipient's implant container.
/// </remarks>
/// <param name="donor">The entity from which implants will be transferred.</param>
/// <param name="recipient">The entity to which implants will be transferred.</param>
public void TransferImplants(EntityUid donor, EntityUid recipient)
{
// Check if the donor has an ImplantedComponent, indicating the presence of implants
if (!TryComp<ImplantedComponent>(donor, out var donorImplanted))
return;
// Get the implant containers for both the donor and recipient entities
var donorImplantContainer = donorImplanted.ImplantContainer;
// Get all implants from the donor's implant container
var donorImplants = donorImplantContainer.ContainedEntities.ToArray();
// Transfer each implant from the donor to the recipient
foreach (var donorImplant in donorImplants)
{
// Check for any conditions or filters before transferring (if needed)
// For instance, verifying if the recipient can receive specific implants, etc.
// Remove the implant from the donor's implant container
_container.Remove(donorImplant, donorImplantContainer, force: true);
if(!TryComp<SubdermalImplantComponent>(donorImplant, out var subdermal))
return;
// Insert the implant into the recipient's implant container
ForceImplant(recipient, donorImplant, subdermal, true);
}
}
//Miracle edit end
}
public sealed class ImplantRelayEvent<T> where T : notnull

View File

@@ -15,6 +15,7 @@ namespace Content.Shared.Input
public static readonly BoundKeyFunction FocusLocalChat = "FocusLocalChatWindow";
public static readonly BoundKeyFunction FocusEmote = "FocusEmote";
public static readonly BoundKeyFunction FocusWhisperChat = "FocusWhisperChatWindow";
public static readonly BoundKeyFunction FocusChangelingChat = "FocusChangelingChatWindow";
public static readonly BoundKeyFunction FocusCultChat = "FocusCultChatWindow";
public static readonly BoundKeyFunction FocusRadio = "FocusRadioWindow";
public static readonly BoundKeyFunction FocusLOOC = "FocusLOOCWindow";

View File

@@ -1,4 +1,5 @@
using Content.Shared.Humanoid.Markings;
using Content.Shared.Changeling;
using Content.Shared.Humanoid.Markings;
using Content.Shared.Localizations;
using Content.Shared._White.Cult.Systems;
@@ -10,7 +11,7 @@ namespace Content.Shared.IoC
{
IoCManager.Register<MarkingManager, MarkingManager>();
IoCManager.Register<ContentLocalizationManager, ContentLocalizationManager>();
IoCManager.Register<ChangelingNameGenerator, ChangelingNameGenerator>();
// WD EDIT
IoCManager.Register<CultistWordGeneratorManager, CultistWordGeneratorManager>();
// WD EDIT END

View File

@@ -0,0 +1,31 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Miracle.UI;
[Serializable, NetSerializable]
public enum ListViewSelectorUiKeyChangeling
{
Key
}
[Serializable, NetSerializable]
public sealed class ListViewBuiState : BoundUserInterfaceState
{
public Dictionary<string, string> Items { get; set; }
public ListViewBuiState(Dictionary<string, string> items)
{
Items = items;
}
}
[Serializable, NetSerializable]
public sealed class ListViewItemSelectedMessage : BoundUserInterfaceMessage
{
public string SelectedItem { get; private set; }
public ListViewItemSelectedMessage(string selectedItem)
{
SelectedItem = selectedItem;
}
}

View File

@@ -0,0 +1,35 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Miracle.UI;
[Serializable, NetSerializable]
public enum TransformStingSelectorUiKey
{
Key
}
[Serializable, NetSerializable]
public sealed class TransformStingBuiState : BoundUserInterfaceState
{
public Dictionary<string, string> Items { get; set; }
public NetEntity Target { get; set; }
public TransformStingBuiState(Dictionary<string, string> items, NetEntity target)
{
Items = items;
Target = target;
}
}
[Serializable, NetSerializable]
public sealed class TransformStingItemSelectedMessage : BoundUserInterfaceMessage
{
public string SelectedItem { get; private set; }
public NetEntity Target { get; private set; }
public TransformStingItemSelectedMessage(string selectedItem, NetEntity target)
{
SelectedItem = selectedItem;
Target = target;
}
}

View File

@@ -186,4 +186,9 @@ public enum SelectiveFire : byte
SemiAuto = 1 << 0,
Burst = 1 << 1,
FullAuto = 1 << 2, // Not in the building!
//Miracle edit
PullItem = 1 << 3,
PullMob = 1 << 4
//Miracle edit end
}

View File

@@ -22,6 +22,12 @@ public sealed partial class RechargeBasicEntityAmmoComponent : Component
Params = AudioParams.Default.WithVolume(-5f)
};
//Miracle edit
[DataField("playRechargeSound")]
[AutoNetworkedField]
public bool PlayRechargeSound = true;
//Miracle edit end
[ViewVariables(VVAccess.ReadWrite),
DataField("nextCharge", customTypeSerializer:typeof(TimeOffsetSerializer)),
AutoNetworkedField]

View File

@@ -51,7 +51,8 @@ public sealed class RechargeBasicEntityAmmoSystem : EntitySystem
{
// We don't predict this because occasionally on client it may not play.
// PlayPredicted will still be predicted on the client.
if (_netManager.IsServer)
//Miracle edit start/end
if (_netManager.IsServer && recharge.PlayRechargeSound)
_audio.PlayPvs(recharge.RechargeSound, uid);
}

View File

@@ -285,6 +285,10 @@ public abstract partial class SharedGunSystem : EntitySystem
// Don't do this in the loop so we still reset NextFire.
switch (gun.SelectedMode)
{
//Miracle edit
case SelectiveFire.PullItem:
case SelectiveFire.PullMob:
//Miracle edit end
case SelectiveFire.SemiAuto:
shots = Math.Min(shots, 1 - gun.ShotCounter);
break;

Binary file not shown.

View File

@@ -0,0 +1,3 @@
chat-manager-changeling-channel-name = HIVE
hud-chatbox-select-channel-Changeling = Hive
chat-manager-send-changeling-chat-wrap-message = [bold]\[HIVE\][/bold] [font size=13][italic][bold]{ $player }[/bold] hisses, "{ $message }"[/italic][/font]

View File

@@ -1,5 +1,6 @@
verb-categories-antag = Antag ctrl
admin-verb-make-traitor = Make the target into a traitor.
admin-verb-make-changeling = Make the target into a changeling.
admin-verb-make-zombie = Zombifies the target immediately.
admin-verb-make-nuclear-operative = Make target into a lone Nuclear Operative.
admin-verb-make-pirate = Make the target into a pirate. Note this doesn't configure the game rule.
@@ -7,8 +8,9 @@ admin-verb-make-head-rev = Make the target into a Head Revolutionary.
admin-verb-make-thief = Make the target into a thief.
admin-verb-text-make-traitor = Make Traitor
admin-verb-text-make-changeling = Make Changeling
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-head-rev = Make Head Rev
admin-verb-text-make-thief = Make Thief
admin-verb-text-make-thief = Make Thief

View File

@@ -110,3 +110,6 @@ alerts-revenant-essence-desc = The power of souls. It sustains you and is used f
alerts-revenant-corporeal-name = Corporeal
alerts-revenant-corporeal-desc = You have manifested physically. People around you can see and hurt you.
alerts-changeling-chemicals-name = Chemicals
alerts-changeling-chemicals-desc = Our chemicals.

View File

@@ -0,0 +1,13 @@
#Changeling
changeling-title = Changeling
changeling-description = A changeling has boarded the station!
changeling-not-enough-ready-players = Not enough players readied up for the game! There were {$readyPlayersCount} players readied up out of {$minimumPlayers} needed. Can't start Changeling.
changeling-no-one-ready = No players readied up! Can't start Changeling.
changeling-round-end-agent-name = changeling
changeling-role-greeting =
You are a changeling!
Your objectives are listed in the character menu.
Use the evolution shop to gain new abilities.
Death to Nanotrasen!
changeling-role-briefing-short = Use '<' to communicate with other members of the Hive.

View File

@@ -0,0 +1,4 @@
objective-condition-absorb-dna = Absorb {$count} humans.
objective-condition-absorb-more-dna = Absorb more humans, that others in the Hive.
objective-condition-absorb-changeling-title = Absorb {$targetName}, {CAPITALIZE($job)}.
objective-condition-escape-with-identity-title = Escape with {$targetName}, {CAPITALIZE($job)} identity.

View File

@@ -1,6 +1,9 @@
roles-antag-syndicate-agent-name = Syndicate agent
roles-antag-syndicate-agent-objective = Complete your objectives without being caught.
roles-antag-changeling-name = Changeling
roles-antag-changeling-objective = Complete your objectives without being caught.
roles-antag-initial-infected-name = Initial Infected
roles-antag-initial-infected-objective = Once you turn, infect as many other crew members as possible.

View File

@@ -13,5 +13,7 @@ store-category-job = Job
store-category-armor = Armor
store-category-pointless = Pointless
# Revenant
# Revenant && Changeling
store-category-abilities = Abilities
store-category-stings = Stings
store-category-boosters = Boosters

View File

@@ -9,7 +9,7 @@ store-currency-display-debugdollar = {$amount ->
}
store-currency-display-telecrystal = TC
store-currency-display-stolen-essence = Stolen Essence
store-currency-display-changeling-point = DNA Points
store-currency-display-space-cash = {$amount ->
[one] One Dollar
*[other] Dollars

View File

@@ -12,6 +12,8 @@ gun-component-wrong-ammo = Wrong ammo!
gun-SemiAuto = semi-auto
gun-Burst = burst
gun-FullAuto = full-auto
gun-PullItem = grab item
gun-PullMob = grab human
# BallisticAmmoProvider
gun-ballistic-cycle = Cycle

View File

@@ -0,0 +1,208 @@
- type: entity
id: ActionChangelingShop
name: Shop
description: Abilities shop.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/shop.png
event: !type:ChangelingShopActionEvent
- type: LesserFormRestricted
- type: entity
id: ActionChangelingAbsorb
name: Absorb
description: Absorbs the human.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/ling_absorb.png
event: !type:AbsorbDnaActionEvent
canTargetSelf: false
useDelay: 10
- type: LesserFormRestricted
- type: entity
id: ActionChangelingTransform
name: Transform
description: Transform to the chosen person.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/transform.png
event: !type:TransformActionEvent
useDelay: 30
- type: entity
id: ActionChangelingRegenerate
name: Regenerate
description: Enter in a regenerative stasis to heal self.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/reviving_stasis.png
event: !type:RegenerateActionEvent
checkCanInteract: false
useDelay: 120
- type: LesserFormRestricted
- type: entity
id: ActionChangelingLesserForm
name: Lesser Form
description: Become a lesser form of self.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/lesser_form.png
event: !type:LesserFormActionEvent
useDelay: 90
- type: LesserFormRestricted
- type: entity
id: ActionTransformSting
name: Transform Sting
description: Injects a retrovirus that forces human victim to transform into another.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/sting_transform.png
event: !type:TransformStingActionEvent
canTargetSelf: false
useDelay: 120
- type: LesserFormRestricted
- type: entity
id: ActionBlindSting
name: Blind Sting
description: Stings a human, completely blinding them for a short time.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/sting_blind.png
event: !type:BlindStingActionEvent
canTargetSelf: false
useDelay: 30
- type: LesserFormRestricted
- type: entity
id: ActionMuteSting
name: Mute Sting
description: Silently stings a human, temporarily silencing them.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/sting_mute.png
event: !type:MuteStingActionEvent
canTargetSelf: false
useDelay: 30
- type: LesserFormRestricted
- type: entity
id: ActionHallucinationSting
name: Hallucination Sting
description: Injects large doses of hallucinogenic chemicals into their victim.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/sting_hallucination.png
event: !type:HallucinationStingActionEvent
canTargetSelf: false
useDelay: 30
- type: LesserFormRestricted
- type: entity
id: ActionCryoSting
name: Cryogenic Sting
description: Injects the target with a blend of chemicals that begins to turn their blood to ice.
noSpawn: true
components:
- type: EntityTargetAction
itemIconStyle: NoItem
icon: Interface/Actions/sting_cryo.png
event: !type:CryoStingActionEvent
canTargetSelf: false
useDelay: 30
- type: LesserFormRestricted
- type: entity
id: ActionAdrenalineSacs
name: Adrenaline Sacs
description: Allows to make use of additional adrenaline to instantly recover from knockdown.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/adrenaline_sacs.png
event: !type:AdrenalineSacsActionEvent
useDelay: 60
- type: LesserFormRestricted
- type: entity
id: ActionFleshmend
name: Fleshmend
description: Rapidly heal all type of damage.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/fleshmend.png
event: !type:FleshmendActionEvent
useDelay: 60
- type: LesserFormRestricted
- type: entity
id: ActionArmblade
name: Arm Blade
description: Reforms one of the changeling's arms into a grotesque blade made out of bone and flesh.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/arm_blade.png
event: !type:ArmbladeActionEvent
- type: LesserFormRestricted
- type: entity
id: ActionShield
name: Organic Shield
description: Reforms one of the changeling's arms into a large, fleshy shield.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/changeling_shield.png
event: !type:OrganicShieldActionEvent
- type: LesserFormRestricted
- type: entity
id: ActionArmor
name: Chitinous Armor
description: Inflates the changeling's body into an all-consuming chitinous mass of armor.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/changeling_armor.png
event: !type:ChitinousArmorActionEvent
- type: LesserFormRestricted
- type: entity
id: ActionTentacleArm
name: Tentacle Arm
description: Reforms one of the arms into a tentacle. Can grab items or humans depending on mode.
noSpawn: true
components:
- type: InstantAction
itemIconStyle: NoItem
icon: Interface/Actions/tentacle_arm.png
event: !type:TentacleArmActionEvent
- type: LesserFormRestricted

View File

@@ -9,6 +9,7 @@
icon: Interface/Actions/scream.png
event: !type:ScreamActionEvent
checkCanInteract: false
- type: LesserFormRestricted
- type: entity
id: ActionTurnUndead

View File

@@ -10,6 +10,7 @@
- category: Stamina
- alertType: SuitPower
- category: Internals
- alertType: Chemicals
- alertType: Fire
- alertType: Handcuffed
- alertType: Ensnared

View File

@@ -0,0 +1,23 @@
- type: alert
id: Chemicals
icons:
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence0
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence1
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence2
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence3
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence4
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence5
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence6
- sprite: /Textures/Interface/Alerts/essence_counter.rsi
state: essence7
name: alerts-changeling-chemicals-name
description: alerts-changeling-chemicals-desc
minSeverity: 0
maxSeverity: 7

View File

@@ -0,0 +1,158 @@
# Body Transforms
- type: listing
id: ChangelingLesserForm
name: Lesser Form
description: Become a lesser form of self.
productAction: ActionChangelingLesserForm
cost:
ChangelingPoint: 1
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingArmblade
name: Arm Blade
description: Reforms one of the changeling's arms into a grotesque blade made out of bone and flesh.
productAction: ActionArmblade
cost:
ChangelingPoint: 3
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingShield
name: Organic Shield
description: Reforms one of the changeling's arms into a large, fleshy shield.
productAction: ActionShield
cost:
ChangelingPoint: 3
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingArmor
name: Chitinous Armor
description: Inflates the changeling's body into an all-consuming chitinous mass of armor.
productAction: ActionArmor
cost:
ChangelingPoint: 4
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingTentacleArm
name: Tentacle Arm
description: Reforms one of the arms into a tentacle. Can grab items or humans depending on mode.
productAction: ActionTentacleArm
cost:
ChangelingPoint: 4
categories:
- ChangelingAbilities
conditions:
- !type:ListingLimitedStockCondition
stock: 1
# Stings
- type: listing
id: ChangelingTransformSting
name: Transform Sting
description: Injects a retrovirus that forces human victim to transform into another.
productAction: ActionTransformSting
cost:
ChangelingPoint: 3
categories:
- ChangelingStings
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingBlindSting
name: Blind Sting
description: Stings a human, completely blinding them for a short time.
productAction: ActionBlindSting
cost:
ChangelingPoint: 2
categories:
- ChangelingStings
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingMuteSting
name: Mute Sting
description: Silently stings a human, temporarily silencing them.
productAction: ActionMuteSting
cost:
ChangelingPoint: 3
categories:
- ChangelingStings
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingHallucinationSting
name: Hallucination Sting
description: Injects large doses of hallucinogenic chemicals into their victim.
productAction: ActionHallucinationSting
cost:
ChangelingPoint: 1
categories:
- ChangelingStings
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingCryogenicSting
name: Cryogenic Sting
description: Injects the target with a blend of chemicals that begins to turn their blood to ice.
productAction: ActionCryoSting
cost:
ChangelingPoint: 3
categories:
- ChangelingStings
conditions:
- !type:ListingLimitedStockCondition
stock: 1
# Boosters
- type: listing
id: ChangelingAdrenalineSacs
name: Adrenaline Sacs
description: Allows to make use of additional adrenaline to instantly recover from knockdown.
productAction: ActionAdrenalineSacs
cost:
ChangelingPoint: 3
categories:
- ChangelingBoosters
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- type: listing
id: ChangelingFleshmend
name: Fleshmend
description: Rapidly heal all type of damage.
productAction: ActionFleshmend
cost:
ChangelingPoint: 2
categories:
- ChangelingBoosters
conditions:
- !type:ListingLimitedStockCondition
stock: 1

View File

@@ -152,6 +152,19 @@
- type: GroupExamine
- type: GiftIgnore
- type: entity
parent: ClothingOuterArmorHeavy
id: ClothingOuterChangeling
name: chitinous armor
description: Chitinous and flesh mass of armor.
components:
- type: Sprite
sprite: Clothing/OuterClothing/Armor/changeling.rsi
- type: Clothing
sprite: Clothing/OuterClothing/Armor/changeling.rsi
- type: Unremoveable
deleteOnDrop: true
- type: entity
parent: ClothingOuterArmorHeavy
id: ClothingOuterArmorHeavyGreen
@@ -196,6 +209,8 @@
- type: Clothing
sprite: Clothing/OuterClothing/Armor/magusred.rsi
- type: entity
parent: ClothingOuterBaseLarge
id: ClothingOuterArmorScaf

View File

@@ -1175,6 +1175,8 @@
interfaces:
- key: enum.StrippingUiKey.Key
type: StrippableBoundUserInterface
- key: enum.ListViewSelectorUiKey.Key
type: ListViewSelectorBui
- type: Sprite
drawdepth: Mobs
layers:

View File

@@ -201,6 +201,10 @@
type: HumanoidMarkingModifierBoundUserInterface
- key: enum.StrippingUiKey.Key
type: StrippableBoundUserInterface
- key: enum.ListViewSelectorUiKeyChangeling.Key
type: ListViewSelectorBui
- key: enum.TransformStingSelectorUiKey.Key
type: TransformStingSelectorBui
# WD-EDIT
- key: enum.NameSelectorUIKey.Key
type: NameSelectorBUI

View File

@@ -163,6 +163,23 @@
- key: enum.StoreUiKey.Key
type: StoreBoundUserInterface
- type: entity
parent: BaseSubdermalImplant
id: ChangelingShopImplant
name: changeling shop implant
description: Opens evolution shop.
noSpawn: true
components:
- type: SubdermalImplant
permanent: true
implantAction: ActionChangelingShop
- type: Store
preset: StorePresetChangeling
- type: UserInterface
interfaces:
- key: enum.StoreUiKey.Key
type: StoreBoundUserInterface
- type: entity
parent: BaseSubdermalImplant
id: EmpImplant
@@ -177,7 +194,7 @@
range: 1.75
energyConsumption: 50000
disableDuration: 10
- type: entity
parent: BaseSubdermalImplant
id: ScramImplant

View File

@@ -118,6 +118,48 @@
Blunt: 1.5
Piercing: 1.5
#Changeling shield
- type: entity
name: organic shield
parent: BaseShield
id: OrganicShield
description: A large, fleshy shield.
components:
- type: Sprite
state: changeling-icon
- type: Item
heldPrefix: changeling
- type: Blocking
passiveBlockModifier:
coefficients:
Blunt: 0.8
Piercing: 0.8
activeBlockModifier:
coefficients:
Blunt: 0.7
Piercing: 0.7
flatReductions:
Blunt: 1.5
Piercing: 1.5
- type: Unremoveable
deleteOnDrop: true
- type: Destructible
thresholds:
- trigger:
!type:DamageTrigger
damage: 80
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- trigger:
!type:DamageTrigger
damage: 50
behaviors:
- !type:DoActsBehavior
acts: [ "Destruction" ]
- !type:PlaySoundBehavior
sound: /Audio/Effects/gib2.ogg
#Craftable shields
- type: entity

View File

@@ -261,6 +261,43 @@
True: { state: base-unshaded }
False: { state: base-unshaded-off }
- type: entity
name: Tentacle Arm
parent: BaseItem
id: TentacleArmGun
components:
- type: TentacleGun
- type: Gun
soundGunshot: /Audio/Effects/gib1.ogg
fireRate: 0.3
selectedMode: PullItem
availableModes:
- PullItem
- PullMob
- type: BasicEntityAmmoProvider
proto: TentacleProjectile
capacity: 1
count: 1
- type: RechargeBasicEntityAmmo
rechargeCooldown: 0.75
playRechargeSound: false
- type: Sprite
sprite: Objects/Weapons/Guns/Launchers/tentacle_gun.rsi
layers:
- state: base
- type: UseDelay
delay: 1.5
- type: Appearance
- type: EmitSoundOnCollide
sound:
path: /Audio/Effects/gib3.ogg
params:
variation: 0.65
volume: -10
- type: Unremoveable
deleteOnDrop: true
# Admeme
- type: entity
name: tether gun

View File

@@ -883,6 +883,45 @@
- HighImpassable
- type: GrapplingProjectile
- type: entity
id: TentacleProjectile
name: tentacle
noSpawn: true
components:
- type: EmbeddableProjectile
deleteOnRemove: true
- type: Clickable
- type: InteractionOutline
- type: Ammo
muzzleFlash: null
- type: Sprite
noRot: false
sprite: Objects/Weapons/Guns/Launchers/tentacle_gun.rsi
layers:
- state: hook
- type: Physics
bodyType: Dynamic
linearDamping: 0
angularDamping: 0
- type: Projectile
deleteOnCollide: false
damage:
types:
Blunt: 0
soundHit:
path: /Audio/Effects/gib3.ogg
- type: Fixtures
fixtures:
projectile:
shape:
!type:PhysShapeAabb
bounds: "-0.1,-0.1,0.1,0.1"
hard: false
mask:
- Impassable
- BulletImpassable
- type: TentacleProjectile
- type: entity
name : disabler bolt smg
id: BulletDisablerSmg

View File

@@ -22,3 +22,6 @@
qualities:
- Prying
- type: Prying
- type: Unremoveable
deleteOnDrop: true
- type: ToolForcePowered

View File

@@ -79,6 +79,15 @@
- type: ThiefRule #the thieves come as an extension of another gamemode
ruleChance: 0.5
- type: entity
id: Changeling
parent: BaseGameRule
noSpawn: true
components:
- type: ChangelingRule
# - type: TraitorRule
# ruleChance: 0.6
- type: entity
id: Revolutionary
parent: BaseGameRule

View File

@@ -0,0 +1,223 @@
- type: entity
abstract: true
parent: BaseObjective
id: BaseChangelingObjective
components:
- type: Objective
difficulty: 1.5
issuer: syndicate
- type: RoleRequirement
roles:
components:
- ChangelingRole
- type: entity
abstract: true
parent: [BaseChangelingObjective, BaseStealObjective]
id: BaseChangelingStealObjective
components:
- type: StealCondition
verifyMapExistance: false
- type: Objective
difficulty: 2.75
- type: ObjectiveLimit
limit: 1
- type: entity
noSpawn: true
parent: BaseChangelingObjective
id: AbsorbDnaObjective
components:
- type: Objective
icon:
sprite: /Textures/Actions/changeling.rsi # TODO: Change icon
state: absorb
- type: AbsorbDnaCondition
- type: entity
noSpawn: true
parent: BaseChangelingObjective
id: AbsorbMoreDnaObjective
name: Absorb more humans, that others in the Hive.
description: Absorb as many you can.
components:
- type: Objective
difficulty: 2.0
icon:
sprite: /Textures/Actions/changeling.rsi # TODO: Change icon
state: absorb
- type: AbsorbMoreCondition
- type: entity
noSpawn: true
parent: BaseChangelingObjective
id: AbsorbChangelingObjective
components:
- type: Objective
difficulty: 3.0
icon:
sprite: /Textures/Actions/changeling.rsi # TODO: Change icon
state: blade
- type: TargetObjective
title: objective-condition-absorb-changeling-title
- type: PickRandomChangeling
- type: AbsorbChangelingCondition
- type: entity
noSpawn: true
parent: BaseChangelingObjective
id: EscapeWithIdentityObjective
components:
- type: Objective
icon:
sprite: /Textures/Mobs/Species/Human/parts.rsi
state: full
- type: TargetObjective
title: objective-condition-escape-with-identity-title
- type: PickRandomIdentity
- type: EscapeWithIdentityCondition
# steal
## cmo
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: CMOHyposprayStealObjectiveCh
components:
- type: NotJobRequirement
job: ChiefMedicalOfficer
- type: StealCondition
stealGroup: Hypospray
owner: job-name-cmo
## rd
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: RDHardsuitStealObjectiveCh
components:
- type: StealCondition
stealGroup: ClothingOuterHardsuitRd
owner: job-name-rd
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: HandTeleporterStealObjectiveCh
components:
- type: StealCondition
stealGroup: HandTeleporter
owner: job-name-rd
## hos
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: SecretDocumentsStealObjectiveCh
components:
- type: Objective
# hos has a gun ce does not, higher difficulty than most
difficulty: 3
- type: NotJobRequirement
job: HeadOfSecurity
- type: StealCondition
stealGroup: BookSecretDocuments
owner: job-name-hos
## ce
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: MagbootsStealObjectiveCh
components:
- type: NotJobRequirement
job: ChiefEngineer
- type: StealCondition
stealGroup: ClothingShoesBootsMagAdv
owner: job-name-ce
## qm
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: ClipboardStealObjectiveCh
components:
- type: NotJobRequirement
job: Quartermaster
- type: StealCondition
stealGroup: BoxFolderQmClipboard
owner: job-name-qm
## hop
- type: entity
noSpawn: true
parent: BaseChangelingStealObjective
id: CorgiMeatStealObjectiveCh
components:
- type: NotJobRequirement
job: HeadOfPersonnel
- type: ObjectiveLimit
limit: 3 # ian only has 2 slices, 3 obj for drama
- type: StealCondition
stealGroup: FoodMeatCorgi
owner: objective-condition-steal-Ian
## cap
- type: entity
abstract: true
parent: BaseChangelingStealObjective
id: BaseCaptainObjectiveCh
components:
- type: Objective
# sorry ce but your jordans are not as high security as the caps gear
difficulty: 3.5
- type: NotJobRequirement
job: Captain
- type: entity
noSpawn: true
parent: BaseCaptainObjectiveCh
id: CaptainIDStealObjectiveCh
components:
- type: StealCondition
stealGroup: CaptainIDCard
- type: entity
noSpawn: true
parent: BaseCaptainObjectiveCh
id: CaptainJetpackStealObjectiveCh
components:
- type: StealCondition
stealGroup: JetpackCaptainFilled
- type: entity
noSpawn: true
parent: BaseCaptainObjectiveCh
id: CaptainGunStealObjectiveCh
components:
- type: StealCondition
stealGroup: WeaponAntiqueLaser
owner: job-name-captain
- type: entity
noSpawn: true
parent: BaseCaptainObjectiveCh
id: NukeDiskStealObjectiveCh
components:
- type: Objective
# high difficulty since the hardest item both to steal, and to not get caught down the road,
# since anyone with a pinpointer can track you down and kill you
# it's close to being a stealth loneop
difficulty: 4
- type: NotCommandRequirement
- type: StealCondition
stealGroup: NukeDisk
owner: objective-condition-steal-station

View File

@@ -126,4 +126,33 @@
id: ThiefObjectiveGroupEscape
weights:
EscapeThiefShuttleObjective: 1
#Changeling, crew, wizard, when you code it...
# Changeling
- type: weightedRandom
id: ChangelingObjectiveGroups
weights:
ChangelingObjectiveGroupSteal: 1
ChangelingObjectiveGroupKill: 1
- type: weightedRandom
id: ChangelingObjectiveGroupSteal
weights:
CaptainIDStealObjectiveCh: 0.5
CMOHyposprayStealObjectiveCh: 0.5
RDHardsuitStealObjectiveCh: 0.5
NukeDiskStealObjectiveCh: 0.5
MagbootsStealObjectiveCh: 0.5
CorgiMeatStealObjectiveCh: 0.5
ClipboardStealObjectiveCh: 0.5
CaptainGunStealObjectiveCh: 0.5
CaptainJetpackStealObjectiveCh: 0.5
HandTeleporterStealObjectiveCh: 0.5
SecretDocumentsStealObjectiveCh: 0.5
- type: weightedRandom
id: ChangelingObjectiveGroupKill
weights:
AbsorbDnaObjective: 1
AbsorbMoreDnaObjective: 0.75
AbsorbChangelingObjective: 0.25
EscapeWithIdentityObjective: 0.75

View File

@@ -148,3 +148,93 @@
revertOnDeath: true
revertOnCrit: true
duration: 20
- type: polymorph
id: MonkeyChangeling
entity: MobMonkey
forced: true
inventory: Drop
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
- type: polymorph
id: MobHuman
entity: MobHuman
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobArachnid
entity: MobArachnid
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobDwarf
entity: MobDwarf
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobDiona
entity: MobDiona
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobMoth
entity: MobMoth
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobReptilian
entity: MobReptilian
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobVox
entity: MobVox
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer
- type: polymorph
id: MobSlimePerson
entity: MobSlimePerson
forced: true
revertOnCrit: false
revertOnDeath: false
transferDamage: true
allowRepeatedMorphs: true
inventory: Transfer

View File

@@ -0,0 +1,6 @@
- type: antag
id: Changeling
name: roles-antag-changeling-name
antagonist: true
setPreference: true
objective: roles-antag-changeling-objective

View File

@@ -68,3 +68,18 @@
id: RevenantAbilities
name: store-category-abilities
# changeling
- type: storeCategory
id: ChangelingStings
name: store-category-stings
priority: 0
- type: storeCategory
id: ChangelingAbilities
name: store-category-abilities
priority: 1
- type: storeCategory
id: ChangelingBoosters
name: store-category-boosters
priority: 2

View File

@@ -10,6 +10,11 @@
displayName: store-currency-display-stolen-essence
canWithdraw: false
- type: currency
id: ChangelingPoint
displayName: store-currency-display-changeling-point
canWithdraw: false
- type: currency
id: MeatyOreCoin
displayName: Meaty Ore Coin

View File

@@ -23,3 +23,13 @@
minItems: 3
maxItems: 10
salesCategory: UplinkSales
- type: storePreset
id: StorePresetChangeling
storeName: Evolution Shop
categories:
- ChangelingAbilities
- ChangelingStings
- ChangelingBoosters
currencyWhitelist:
- ChangelingPoint

View File

@@ -61,6 +61,17 @@
- Traitor
- BasicStationEventScheduler
- type: gamePreset
id: Changeling
alias:
- changeling
name: changeling-title
description: changeling-description
showInVote: false
rules:
- Changeling
- BasicStationEventScheduler
- type: gamePreset
id: Deathmatch
alias:

View File

@@ -7,4 +7,4 @@
Revolutionary: 0.10
Zombie: 0.05
Survival: 0.05
Changeling: 0.30

View File

@@ -60,7 +60,10 @@
- type: statusEffect
id: StaminaModifier
- type: statusEffect
id: BlurryVision
alwaysAllowed: true
#WD EDIT
- type: statusEffect
id: Incorporeal

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

View File

@@ -0,0 +1,17 @@
{
"version": 1,
"license": null,
"copyright": null,
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "blade"
},
{
"name": "absorb"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

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