diff --git a/Content.Client/Chat/Managers/ChatManager.cs b/Content.Client/Chat/Managers/ChatManager.cs index 4f73a9ba80..55e1b0a5c6 100644 --- a/Content.Client/Chat/Managers/ChatManager.cs +++ b/Content.Client/Chat/Managers/ChatManager.cs @@ -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(localEntity)) + _consoleHost.ExecuteCommand($"gsay \"{CommandParsing.Escape(text)}\""); + break; + + default: throw new ArgumentOutOfRangeException(nameof(channel), channel, null); } diff --git a/Content.Client/Miracle/Changeling/TentacleGun.cs b/Content.Client/Miracle/Changeling/TentacleGun.cs new file mode 100644 index 0000000000..8e582c0efe --- /dev/null +++ b/Content.Client/Miracle/Changeling/TentacleGun.cs @@ -0,0 +1,8 @@ +using Content.Shared.Changeling; + +namespace Content.Client.Miracle.Changeling; + +public sealed class TentacleGun : SharedTentacleGun +{ + +} diff --git a/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml new file mode 100644 index 0000000000..4a2aa3fbb3 --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml.cs b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml.cs new file mode 100644 index 0000000000..d439a192ba --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewChangelingSelectorWindow.xaml.cs @@ -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? ItemSelected; + + public ListViewChangelingSelectorWindow() + { + RobustXamlLoader.Load(this); + } + + public void PopulateList(Dictionary 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); + } + } +} diff --git a/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewSelectorBUI.cs b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewSelectorBUI.cs new file mode 100644 index 0000000000..b7de804f1c --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/ListViewSelector/ListViewSelectorBUI.cs @@ -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(); + } +} diff --git a/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml new file mode 100644 index 0000000000..4a2aa3fbb3 --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml @@ -0,0 +1,9 @@ + + + + + diff --git a/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml.cs b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml.cs new file mode 100644 index 0000000000..2402976248 --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingSelectorWindow.xaml.cs @@ -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? ItemSelected; + + public TransformStingSelectorWindow() + { + RobustXamlLoader.Load(this); + } + + public void PopulateList(Dictionary 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); + } + } +} diff --git a/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingUIBUI.cs b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingUIBUI.cs new file mode 100644 index 0000000000..5c77d84669 --- /dev/null +++ b/Content.Client/Miracle/Changeling/UI/TransformStingUI/TransformStingUIBUI.cs @@ -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(); + } +} diff --git a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs index 0f1fbcea60..82e38ecb35 100644 --- a/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs +++ b/Content.Client/UserInterface/Systems/Chat/ChatUIController.cs @@ -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 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(OnUpdateChangelingChat); // WD EDIT SubscribeLocalEvent(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) { diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs index 185e48aea2..a2168ae4cb 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelFilterPopup.xaml.cs @@ -23,6 +23,7 @@ public sealed partial class ChannelFilterPopup : Popup ChatChannel.AdminAlert, ChatChannel.AdminChat, ChatChannel.Server, + ChatChannel.Changeling, ChatChannel.Cult // WD EDIT }; diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs index 455f70918b..1b1644e2d0 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorButton.cs @@ -64,6 +64,7 @@ public sealed class ChannelSelectorButton : ChatPopupButton Color.LightSkyBlue, ChatSelectChannel.Dead => Color.MediumPurple, ChatSelectChannel.Admin => Color.HotPink, + ChatSelectChannel.Changeling => Color.Purple, ChatSelectChannel.Cult => Color.DarkRed, _ => Color.DarkGray }; diff --git a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs index f9ff9f07fa..5ab5540b44 100644 --- a/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs +++ b/Content.Client/UserInterface/Systems/Chat/Controls/ChannelSelectorPopup.cs @@ -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 /. diff --git a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs index 13853eaab3..5c04970daa 100644 --- a/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs +++ b/Content.Server/Administration/Systems/AdminVerbSystem.Antags.cs @@ -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(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"), diff --git a/Content.Server/Changeling/ChangelingRoleComponent.cs b/Content.Server/Changeling/ChangelingRoleComponent.cs new file mode 100644 index 0000000000..6f347a7b26 --- /dev/null +++ b/Content.Server/Changeling/ChangelingRoleComponent.cs @@ -0,0 +1,8 @@ +using Content.Shared.Roles; + +namespace Content.Server.Changeling; + +[RegisterComponent] +public sealed partial class ChangelingRoleComponent : AntagonistRoleComponent +{ +} diff --git a/Content.Server/Changeling/ChangelingRuleComponent.cs b/Content.Server/Changeling/ChangelingRuleComponent.cs new file mode 100644 index 0000000000..c96547e91f --- /dev/null +++ b/Content.Server/Changeling/ChangelingRuleComponent.cs @@ -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 ChangelingMinds = new(); + + [DataField("changelingPrototypeId", customTypeSerializer: typeof(PrototypeIdSerializer))] + 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 StartCandidates = new(); + + /// + /// Path to antagonist alert sound. + /// + [DataField("greetSoundNotification")] + public SoundSpecifier GreetSoundNotification = new SoundPathSpecifier("/Audio/Ambience/Antag/changeling_start.ogg"); +} diff --git a/Content.Server/Changeling/ChangelingRuleSystem.cs b/Content.Server/Changeling/ChangelingRuleSystem.cs new file mode 100644 index 0000000000..55b4e7ba18 --- /dev/null +++ b/Content.Server/Changeling/ChangelingRuleSystem.cs @@ -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 +{ + [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(OnStartAttempt); + SubscribeLocalEvent(OnPlayersSpawned); + SubscribeLocalEvent(HandleLatejoin); + SubscribeLocalEvent(ClearUsedNames); + + SubscribeLocalEvent(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(); + 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(); + 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().FirstOrDefault(); + if (changelingRule == null) + { + GameTicker.StartGameRule("Changeling", out var ruleEntity); + changelingRule = Comp(ruleEntity); + } + + if (!_mindSystem.TryGetMind(changeling, out var mindId, out var mind)) + { + Log.Info("Failed getting mind for picked changeling."); + return false; + } + + if (HasComp(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(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(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(); + 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(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(); + } +} diff --git a/Content.Server/Changeling/ChangelingSystem.Abilities.cs b/Content.Server/Changeling/ChangelingSystem.Abilities.cs new file mode 100644 index 0000000000..e1aef75291 --- /dev/null +++ b/Content.Server/Changeling/ChangelingSystem.Abilities.cs @@ -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(OnAbsorb); + SubscribeLocalEvent(OnTransform); + SubscribeLocalEvent(OnRegenerate); + SubscribeLocalEvent(OnLesserForm); + + SubscribeLocalEvent(OnTransformSting); + SubscribeLocalEvent(OnTransformStingMessage); + SubscribeLocalEvent(OnBlindSting); + SubscribeLocalEvent(OnMuteSting); + SubscribeLocalEvent(OnHallucinationSting); + SubscribeLocalEvent(OnCryoSting); + + SubscribeLocalEvent(OnAdrenalineSacs); + SubscribeLocalEvent(OnFleshMend); + SubscribeLocalEvent(OnArmBlade); + SubscribeLocalEvent(OnShield); + SubscribeLocalEvent(OnArmor); + SubscribeLocalEvent(OnTentacleArm); + + SubscribeLocalEvent(OnTransformDoAfter); + SubscribeLocalEvent(OnAbsorbDoAfter); + SubscribeLocalEvent(OnRegenerateDoAfter); + SubscribeLocalEvent(OnLesserFormDoAfter); + + SubscribeLocalEvent(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(args.Target)) + { + _popup.PopupEntity("You can't absorb not humans!", args.Performer); + return; + } + + if (HasComp(args.Target)) + { + _popup.PopupEntity("This person already absorbed!", args.Performer); + return; + } + + if (!TryComp(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(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(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 state; + + if (TryComp(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(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(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(args.Target)) + { + _popup.PopupEntity("We can't transform that!", args.Performer); + return; + } + + if (!TryComp(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(uid, out var actorComponent)) + return; + + if (!_ui.TryGetUi(user, TransformStingSelectorUiKey.Key, out var bui)) + return; + + if (HasComp(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(args.Target) || + !HasComp(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(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(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(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(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(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(args.Target.Value, out var absorbedComponent); + + absorbedComponent.AbsorberMind = mindId; + + EnsureComp(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(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(uid, out var actionsComponent)) + return; + + foreach (var action in actionsComponent.Actions.ToArray()) + { + if (!HasComp(action)) + continue; + + _action.RemoveAction(uid, action); + } + } + + private void StartUseDelayById(EntityUid performer, string actionProto) + { + if (!TryComp(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(target, out var targetMeta)) + return; + if (!TryComp(target, out var targetAppearance)) + return; + if (!TryComp(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); + } + + /// + /// Transforms chosen person to another, transferring it's appearance + /// + /// Transform target + /// Transform data + /// Override first check on HumanoidAppearanceComponent + /// Id of the transformed entity + private EntityUid? TransformPerson(EntityUid target, HumanoidData transformData, bool humanoidOverride = false) + { + if (!HasComp(target) && !humanoidOverride) + return null; + + var polymorphEntity = _polymorph.PolymorphEntity(target, transformData.EntityPrototype.ID); + + if (polymorphEntity == null) + return null; + + if (!TryComp(polymorphEntity.Value, out var polyAppearance)) + return null; + + ClonePerson(polymorphEntity.Value, transformData.AppearanceComponent, polyAppearance); + TransferDna(polymorphEntity.Value, transformData.Dna); + + if (!TryComp(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(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); + } + + /// + /// Used for cloning appearance + /// + /// Acceptor + /// Source appearance + /// Acceptor appearance component + 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(sourceHumanoid.CustomBaseLayers); + targetHumanoid.MarkingSet = new MarkingSet(sourceHumanoid.MarkingSet); + + targetHumanoid.Gender = sourceHumanoid.Gender; + if (TryComp(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(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(uid, out var implant)) + return; + + foreach (var entity in implant.ImplantContainer.ContainedEntities) + { + if (!TryComp(entity, out var store)) + continue; + + if (_mobStateSystem.IsDead(absorbed)) + { + var points = _random.Next(1, 3); + var toAdd = new Dictionary { { "ChangelingPoint", points } }; + _storeSystem.TryAddCurrency(toAdd, entity, store); + } + else + { + var points = _random.Next(2, 4); + var toAdd = new Dictionary { { "ChangelingPoint", points } }; + _storeSystem.TryAddCurrency(toAdd, entity, store); + } + + return; + } + } + + #endregion +} diff --git a/Content.Server/Changeling/ChangelingSystem.Shop.cs b/Content.Server/Changeling/ChangelingSystem.Shop.cs new file mode 100644 index 0000000000..b0b6eb789f --- /dev/null +++ b/Content.Server/Changeling/ChangelingSystem.Shop.cs @@ -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(OnShop); + } + + private void OnShop(EntityUid uid, SubdermalImplantComponent component, ChangelingShopActionEvent args) + { + if(!TryComp(uid, out var store)) + return; + + _storeSystem.ToggleUi(args.Performer, uid, store); + } +} diff --git a/Content.Server/Changeling/ChangelingSystem.cs b/Content.Server/Changeling/ChangelingSystem.cs new file mode 100644 index 0000000000..049a305be5 --- /dev/null +++ b/Content.Server/Changeling/ChangelingSystem.cs @@ -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(OnInit); + + SubscribeLocalEvent(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(implant, out var implantComp)) + return; + + _implantSystem.ForceImplant(uid, implant, implantComp); + + if (!TryComp(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 +} diff --git a/Content.Server/Changeling/Objectives/ChangelingConditionsSystem.cs b/Content.Server/Changeling/Objectives/ChangelingConditionsSystem.cs new file mode 100644 index 0000000000..06bee9af8c --- /dev/null +++ b/Content.Server/Changeling/Objectives/ChangelingConditionsSystem.cs @@ -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(OnAbsorbDnaAssigned); + SubscribeLocalEvent(OnAbsorbDnaAfterAssigned); + SubscribeLocalEvent(OnAbsorbDnaGetProgress); + + + //Absorb more genomes, than others changelings + SubscribeLocalEvent(OnAbsorbMoreGetProgress); + + //Absorb other changeling + SubscribeLocalEvent(OnAbsorbChangelingAssigned); + SubscribeLocalEvent(OnAbsorbChangelingGetProgress); + + //Escape with identity + SubscribeLocalEvent(OnEscapeWithIdentityAssigned); + SubscribeLocalEvent(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(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(mind.CurrentEntity, out var changelingComponent)) + return 0f; + + var selfAbsorbed = changelingComponent.AbsorbedEntities.Count - 1; // Because first - it's the owner + + var query = EntityQueryEnumerator(); + + List 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(uid, out var target)) + { + args.Cancelled = true; + return; + } + + if (target.Target != null) + return; + + foreach (var changelingRule in EntityQuery()) + { + var changelingMinds = changelingRule.ChangelingMinds + .Except(new List { 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(target, out var targetMind)) + return 0f; + + if (!HasComp(targetMind.CurrentEntity)) + return 0f; + + if (!TryComp(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(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(mind.CurrentEntity, out var selfDna)) + return 0f; + + if (!TryComp(target, out var targetMind)) + return 0f; + + if (!TryComp(targetMind.CurrentEntity, out var targetDna)) + return 0f; + + if (!TryComp(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 +} diff --git a/Content.Server/Changeling/Objectives/Components/AbsorbChangelingConditionComponent.cs b/Content.Server/Changeling/Objectives/Components/AbsorbChangelingConditionComponent.cs new file mode 100644 index 0000000000..7db9c8bc3c --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/AbsorbChangelingConditionComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class AbsorbChangelingConditionComponent : Component +{ +} diff --git a/Content.Server/Changeling/Objectives/Components/AbsorbDnaConditionComponent.cs b/Content.Server/Changeling/Objectives/Components/AbsorbDnaConditionComponent.cs new file mode 100644 index 0000000000..163e4a9f3d --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/AbsorbDnaConditionComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class AbsorbDnaConditionComponent : Component +{ + public int NeedToAbsorb; +} diff --git a/Content.Server/Changeling/Objectives/Components/AbsorbMoreConditionComponent.cs b/Content.Server/Changeling/Objectives/Components/AbsorbMoreConditionComponent.cs new file mode 100644 index 0000000000..3fd0d41f7a --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/AbsorbMoreConditionComponent.cs @@ -0,0 +1,6 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class AbsorbMoreConditionComponent: Component +{ +} diff --git a/Content.Server/Changeling/Objectives/Components/EscapeWithIdentityConditionComponent.cs b/Content.Server/Changeling/Objectives/Components/EscapeWithIdentityConditionComponent.cs new file mode 100644 index 0000000000..2a774facd2 --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/EscapeWithIdentityConditionComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class EscapeWithIdentityConditionComponent : Component +{ + +} diff --git a/Content.Server/Changeling/Objectives/Components/PickRandomChangelingComponent.cs b/Content.Server/Changeling/Objectives/Components/PickRandomChangelingComponent.cs new file mode 100644 index 0000000000..33338a1e28 --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/PickRandomChangelingComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class PickRandomChangelingComponent : Component +{ + +} diff --git a/Content.Server/Changeling/Objectives/Components/PickRandomIdentityComponent.cs b/Content.Server/Changeling/Objectives/Components/PickRandomIdentityComponent.cs new file mode 100644 index 0000000000..af98b5acf3 --- /dev/null +++ b/Content.Server/Changeling/Objectives/Components/PickRandomIdentityComponent.cs @@ -0,0 +1,7 @@ +namespace Content.Server.Changeling.Objectives.Components; + +[RegisterComponent] +public sealed partial class PickRandomIdentityComponent : Component +{ + +} diff --git a/Content.Server/Changeling/TentacleGun.cs b/Content.Server/Changeling/TentacleGun.cs new file mode 100644 index 0000000000..faf803d2e5 --- /dev/null +++ b/Content.Server/Changeling/TentacleGun.cs @@ -0,0 +1,8 @@ +using Content.Shared.Changeling; + +namespace Content.Server.Changeling; + +public sealed class TentacleGun : SharedTentacleGun +{ + +} diff --git a/Content.Server/Changeling/UncloneableComponent.cs b/Content.Server/Changeling/UncloneableComponent.cs new file mode 100644 index 0000000000..4252f1383e --- /dev/null +++ b/Content.Server/Changeling/UncloneableComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Server.Changeling; + +/// +/// This is used for the uncloneable trait. +/// +[RegisterComponent] +public sealed partial class UncloneableComponent : Component +{ +} diff --git a/Content.Server/Chat/Commands/ChangelingChatCommand.cs b/Content.Server/Chat/Commands/ChangelingChatCommand.cs new file mode 100644 index 0000000000..a6224c9779 --- /dev/null +++ b/Content.Server/Chat/Commands/ChangelingChatCommand.cs @@ -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 "; + 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(); + + if (!entityManager.HasComponent(entity)) + return; + + + var message = string.Join(" ", args).Trim(); + if (string.IsNullOrEmpty(message)) + return; + + entityManager.System().TrySendInGameOOCMessage(entity, message, + InGameOOCChatType.Changeling, false, shell, player); + } +} diff --git a/Content.Server/Chat/SuicideSystem.cs b/Content.Server/Chat/SuicideSystem.cs index 131d19c523..f0bb4e5d8d 100644 --- a/Content.Server/Chat/SuicideSystem.cs +++ b/Content.Server/Chat/SuicideSystem.cs @@ -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(victim, out var changeling) && changeling.IsRegenerating) + return false; + //Miracle edit end + // Checks to see if the player is dead. if (!TryComp(victim, out var mobState) || _mobState.IsDead(victim, mobState)) return false; diff --git a/Content.Server/Chat/Systems/ChatSystem.cs b/Content.Server/Chat/Systems/ChatSystem.cs index 50cded7c05..8cbf052693 100644 --- a/Content.Server/Chat/Systems/ChatSystem.cs +++ b/Content.Server/Chat/Systems/ChatSystem.cs @@ -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(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 GetChangelingChatClients() + { + return Filter.Empty() + .AddWhereAttachedEntity(HasComp) + .AddWhereAttachedEntity(HasComp) + .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 } diff --git a/Content.Server/Cloning/CloningSystem.cs b/Content.Server/Cloning/CloningSystem.cs index 040c88dffa..59f0b29e80 100644 --- a/Content.Server/Cloning/CloningSystem.cs +++ b/Content.Server/Cloning/CloningSystem.cs @@ -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(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); diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj index 30541fdb08..5b7e28e908 100644 --- a/Content.Server/Content.Server.csproj +++ b/Content.Server/Content.Server.csproj @@ -31,7 +31,6 @@ - diff --git a/Content.Server/GameTicking/GameTicker.GamePreset.cs b/Content.Server/GameTicking/GameTicker.GamePreset.cs index 8cf3b7bf01..a308e4e123 100644 --- a/Content.Server/GameTicking/GameTicker.GamePreset.cs +++ b/Content.Server/GameTicking/GameTicker.GamePreset.cs @@ -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(playerEntity, out var changeling) && changeling.IsRegenerating) + return false; + //Miracle edit end + if (HasComp(playerEntity)) return false; diff --git a/Content.Shared/Actions/ActionContainerSystem.cs b/Content.Shared/Actions/ActionContainerSystem.cs index 17bcf11bff..41decf75c9 100644 --- a/Content.Shared/Actions/ActionContainerSystem.cs +++ b/Content.Shared/Actions/ActionContainerSystem.cs @@ -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 + /// /// Transfers an actions from one container to another, while changing the attached entity. /// diff --git a/Content.Shared/Alert/AlertType.cs b/Content.Shared/Alert/AlertType.cs index 2e9133c008..76d31a2168 100644 --- a/Content.Shared/Alert/AlertType.cs +++ b/Content.Shared/Alert/AlertType.cs @@ -55,6 +55,7 @@ namespace Content.Shared.Alert VowOfSilence, VowBroken, Essence, + Chemicals, Corporeal, Bleed, Pacified, diff --git a/Content.Shared/Changeling/AbsorbedComponent.cs b/Content.Shared/Changeling/AbsorbedComponent.cs new file mode 100644 index 0000000000..c0239e5fec --- /dev/null +++ b/Content.Shared/Changeling/AbsorbedComponent.cs @@ -0,0 +1,10 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling; + + +[RegisterComponent, NetworkedComponent] +public sealed partial class AbsorbedComponent : Component +{ + public EntityUid AbsorberMind; +} diff --git a/Content.Shared/Changeling/ChangelingComponent.cs b/Content.Shared/Changeling/ChangelingComponent.cs new file mode 100644 index 0000000000..bd4ff0b370 --- /dev/null +++ b/Content.Shared/Changeling/ChangelingComponent.cs @@ -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 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; +} diff --git a/Content.Shared/Changeling/ChangelingNameGenerator.cs b/Content.Shared/Changeling/ChangelingNameGenerator.cs new file mode 100644 index 0000000000..cc9f8b80ec --- /dev/null +++ b/Content.Shared/Changeling/ChangelingNameGenerator.cs @@ -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 _used = new(); + + private readonly List _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(); + } +} diff --git a/Content.Shared/Changeling/ChemicalsSystem.cs b/Content.Shared/Changeling/ChemicalsSystem.cs new file mode 100644 index 0000000000..304892d81b --- /dev/null +++ b/Content.Shared/Changeling/ChemicalsSystem.cs @@ -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(); + + 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)); + } + } +} diff --git a/Content.Shared/Changeling/LesserFormRestrictedComponent.cs b/Content.Shared/Changeling/LesserFormRestrictedComponent.cs new file mode 100644 index 0000000000..80a875aea2 --- /dev/null +++ b/Content.Shared/Changeling/LesserFormRestrictedComponent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling; + +[RegisterComponent, NetworkedComponent] +public sealed partial class LesserFormRestrictedComponent : Component +{ +} diff --git a/Content.Shared/Changeling/SharedChangeling.cs b/Content.Shared/Changeling/SharedChangeling.cs new file mode 100644 index 0000000000..3346a3819d --- /dev/null +++ b/Content.Shared/Changeling/SharedChangeling.cs @@ -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 +{ + +} diff --git a/Content.Shared/Changeling/SharedChangelingChat.cs b/Content.Shared/Changeling/SharedChangelingChat.cs new file mode 100644 index 0000000000..eaf8520149 --- /dev/null +++ b/Content.Shared/Changeling/SharedChangelingChat.cs @@ -0,0 +1,33 @@ +namespace Content.Shared.Changeling; + +public sealed class SharedChangelingChat : EntitySystem +{ + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + SubscribeLocalEvent(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; + } +} diff --git a/Content.Shared/Changeling/SharedTentacleGun.cs b/Content.Shared/Changeling/SharedTentacleGun.cs new file mode 100644 index 0000000000..191fb1da7b --- /dev/null +++ b/Content.Shared/Changeling/SharedTentacleGun.cs @@ -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(OnTentacleShot); + SubscribeLocalEvent(OnTentacleCollide); + } + + private void OnTentacleShot(EntityUid uid, TentacleGunComponent component, ref GunShotEvent args) + { + foreach (var (shotUid, _) in args.Ammo) + { + if (!HasComp(shotUid)) + continue; + + Dirty(uid, component); + var visuals = EnsureComp(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(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(args.Weapon)) + { + QueueDel(uid); + return; + } + + if (!TryComp(args.Weapon, out var gun)) + { + QueueDel(uid); + return; + } + + if (!HasComp(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(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(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; + } + } +} diff --git a/Content.Shared/Changeling/TentacleGunComponent.cs b/Content.Shared/Changeling/TentacleGunComponent.cs new file mode 100644 index 0000000000..efa8f4c50b --- /dev/null +++ b/Content.Shared/Changeling/TentacleGunComponent.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling; + +[RegisterComponent, NetworkedComponent] +public sealed partial class TentacleGunComponent : Component +{ +} diff --git a/Content.Shared/Changeling/TentacleProjectileComponent.cs b/Content.Shared/Changeling/TentacleProjectileComponent.cs new file mode 100644 index 0000000000..2088ad332e --- /dev/null +++ b/Content.Shared/Changeling/TentacleProjectileComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Changeling; + + +[RegisterComponent, NetworkedComponent] +public sealed partial class TentacleProjectileComponent : Component +{ +} diff --git a/Content.Shared/Chat/ChatChannel.cs b/Content.Shared/Chat/ChatChannel.cs index 67f6b64a48..66fedea558 100644 --- a/Content.Shared/Chat/ChatChannel.cs +++ b/Content.Shared/Chat/ChatChannel.cs @@ -75,17 +75,22 @@ namespace Content.Shared.Chat AdminChat = 1 << 12, /// - /// Unspecified. + /// Changeling /// - Unspecified = 1 << 13, + Changeling = 1 << 13, //WD EDIT Cult = 1 << 14, + /// + /// Unspecified. + /// + Unspecified = 1 << 15, + /// /// Channels considered to be IC. /// - IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual, + IC = Local | Whisper | Radio | Dead | Emotes | Damage | Visual | Changeling, AdminRelated = Admin | AdminAlert | AdminChat, } diff --git a/Content.Shared/Chat/ChatChannelExtensions.cs b/Content.Shared/Chat/ChatChannelExtensions.cs index 4b6267559f..374a9437b2 100644 --- a/Content.Shared/Chat/ChatChannelExtensions.cs +++ b/Content.Shared/Chat/ChatChannelExtensions.cs @@ -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 }; diff --git a/Content.Shared/Chat/ChatSelectChannel.cs b/Content.Shared/Chat/ChatSelectChannel.cs index 4a7ca5aec8..1871ed0b71 100644 --- a/Content.Shared/Chat/ChatSelectChannel.cs +++ b/Content.Shared/Chat/ChatSelectChannel.cs @@ -53,6 +53,8 @@ /// Admin = ChatChannel.AdminChat, + Changeling = ChatChannel.Changeling, + Console = ChatChannel.Unspecified } } diff --git a/Content.Shared/Chat/SharedChatSystem.cs b/Content.Shared/Chat/SharedChatSystem.cs index d86053c43d..0338f36e8e 100644 --- a/Content.Shared/Chat/SharedChatSystem.cs +++ b/Content.Shared/Chat/SharedChatSystem.cs @@ -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] diff --git a/Content.Shared/Eye/Blinding/Components/BlurryVisionComponent.cs b/Content.Shared/Eye/Blinding/Components/BlurryVisionComponent.cs index faff4b9e52..5e90f0cc74 100644 --- a/Content.Shared/Eye/Blinding/Components/BlurryVisionComponent.cs +++ b/Content.Shared/Eye/Blinding/Components/BlurryVisionComponent.cs @@ -8,20 +8,20 @@ namespace Content.Shared.Eye.Blinding.Components; /// [RegisterComponent] [NetworkedComponent, AutoGenerateComponentState] -[Access(typeof(BlurryVisionSystem))] +// [Access(typeof(BlurryVisionSystem))] public sealed partial class BlurryVisionComponent : Component { /// /// Amount of "blurring". Also modifies examine ranges. /// [ViewVariables(VVAccess.ReadWrite), DataField("magnitude"), AutoNetworkedField] - public float Magnitude; + public float Magnitude = 4f; /// /// Exponent that controls the magnitude of the effect. /// [ViewVariables(VVAccess.ReadWrite), DataField("correctionPower"), AutoNetworkedField] - public float CorrectionPower; + public float CorrectionPower = 2f; public const float MaxMagnitude = 6; public const float DefaultCorrectionPower = 2f; diff --git a/Content.Shared/Implants/Components/ImplantedComponent.cs b/Content.Shared/Implants/Components/ImplantedComponent.cs index 727213907a..2744d0291b 100644 --- a/Content.Shared/Implants/Components/ImplantedComponent.cs +++ b/Content.Shared/Implants/Components/ImplantedComponent.cs @@ -10,5 +10,6 @@ namespace Content.Shared.Implants.Components; [RegisterComponent, NetworkedComponent] public sealed partial class ImplantedComponent : Component { + [ViewVariables(VVAccess.ReadOnly)] public Container ImplantContainer = default!; } diff --git a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs index 8c3aa00561..7b1893b7a9 100644 --- a/Content.Shared/Implants/SharedSubdermalImplantSystem.cs +++ b/Content.Shared/Implants/SharedSubdermalImplantSystem.cs @@ -117,7 +117,8 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem /// The entity to be implanted /// The implant /// The implant component - public void ForceImplant(EntityUid target, EntityUid implant, SubdermalImplantComponent component) + /// Should we force inserting in container + 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(target); @@ -175,6 +176,48 @@ public abstract class SharedSubdermalImplantSystem : EntitySystem RaiseLocalEvent(implant, relayEv); } } + + //Miracle edit + + /// + /// Transfers all implants from one entity to another. + /// + /// + /// 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. + /// + /// The entity from which implants will be transferred. + /// The entity to which implants will be transferred. + public void TransferImplants(EntityUid donor, EntityUid recipient) + { + // Check if the donor has an ImplantedComponent, indicating the presence of implants + if (!TryComp(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(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 where T : notnull diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index 02ab921ed7..37876fd629 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -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"; diff --git a/Content.Shared/IoC/SharedContentIoC.cs b/Content.Shared/IoC/SharedContentIoC.cs index c08410cab9..f5ae9106c1 100644 --- a/Content.Shared/IoC/SharedContentIoC.cs +++ b/Content.Shared/IoC/SharedContentIoC.cs @@ -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(); IoCManager.Register(); - + IoCManager.Register(); // WD EDIT IoCManager.Register(); // WD EDIT END diff --git a/Content.Shared/Miracle/UI/ListViewBUI.cs b/Content.Shared/Miracle/UI/ListViewBUI.cs new file mode 100644 index 0000000000..2039093510 --- /dev/null +++ b/Content.Shared/Miracle/UI/ListViewBUI.cs @@ -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 Items { get; set; } + + public ListViewBuiState(Dictionary items) + { + Items = items; + } +} + +[Serializable, NetSerializable] +public sealed class ListViewItemSelectedMessage : BoundUserInterfaceMessage +{ + public string SelectedItem { get; private set; } + + public ListViewItemSelectedMessage(string selectedItem) + { + SelectedItem = selectedItem; + } +} diff --git a/Content.Shared/Miracle/UI/TransformStingBUI.cs b/Content.Shared/Miracle/UI/TransformStingBUI.cs new file mode 100644 index 0000000000..f247fe4fb3 --- /dev/null +++ b/Content.Shared/Miracle/UI/TransformStingBUI.cs @@ -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 Items { get; set; } + public NetEntity Target { get; set; } + + public TransformStingBuiState(Dictionary 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; + } +} diff --git a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs index 7035f17978..305d5a8df4 100644 --- a/Content.Shared/Weapons/Ranged/Components/GunComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/GunComponent.cs @@ -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 } diff --git a/Content.Shared/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs b/Content.Shared/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs index 923f95e207..a1f8b4e96e 100644 --- a/Content.Shared/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs +++ b/Content.Shared/Weapons/Ranged/Components/RechargeBasicEntityAmmoComponent.cs @@ -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] diff --git a/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs b/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs index 536f3da811..e796fb99c5 100644 --- a/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/RechargeBasicEntityAmmoSystem.cs @@ -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); } diff --git a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs index 199b90ab7f..e09d8a4e5f 100644 --- a/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs +++ b/Content.Shared/Weapons/Ranged/Systems/SharedGunSystem.cs @@ -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; diff --git a/Resources/Audio/Ambience/Antag/changeling_start.ogg b/Resources/Audio/Ambience/Antag/changeling_start.ogg new file mode 100644 index 0000000000..05c86a6dcd Binary files /dev/null and b/Resources/Audio/Ambience/Antag/changeling_start.ogg differ diff --git a/Resources/Locale/en-US/Miracle/changeling.ftl b/Resources/Locale/en-US/Miracle/changeling.ftl new file mode 100644 index 0000000000..9193eea5cc --- /dev/null +++ b/Resources/Locale/en-US/Miracle/changeling.ftl @@ -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] diff --git a/Resources/Locale/en-US/administration/antag.ftl b/Resources/Locale/en-US/administration/antag.ftl index 535659f27e..1375969e79 100644 --- a/Resources/Locale/en-US/administration/antag.ftl +++ b/Resources/Locale/en-US/administration/antag.ftl @@ -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 \ No newline at end of file +admin-verb-text-make-thief = Make Thief diff --git a/Resources/Locale/en-US/alerts/alerts.ftl b/Resources/Locale/en-US/alerts/alerts.ftl index 795d740141..db4a58275f 100644 --- a/Resources/Locale/en-US/alerts/alerts.ftl +++ b/Resources/Locale/en-US/alerts/alerts.ftl @@ -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. diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-changeling.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-changeling.ftl new file mode 100644 index 0000000000..be664bc249 --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-changeling.ftl @@ -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. + diff --git a/Resources/Locale/en-US/objectives/conditions/absorb-dna.ftl b/Resources/Locale/en-US/objectives/conditions/absorb-dna.ftl new file mode 100644 index 0000000000..c7df4e2666 --- /dev/null +++ b/Resources/Locale/en-US/objectives/conditions/absorb-dna.ftl @@ -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. diff --git a/Resources/Locale/en-US/prototypes/roles/antags.ftl b/Resources/Locale/en-US/prototypes/roles/antags.ftl index d12f70cda2..7ccfc78ee4 100644 --- a/Resources/Locale/en-US/prototypes/roles/antags.ftl +++ b/Resources/Locale/en-US/prototypes/roles/antags.ftl @@ -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. diff --git a/Resources/Locale/en-US/store/categories.ftl b/Resources/Locale/en-US/store/categories.ftl index 437fc03ae0..95e9f24fe1 100644 --- a/Resources/Locale/en-US/store/categories.ftl +++ b/Resources/Locale/en-US/store/categories.ftl @@ -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 diff --git a/Resources/Locale/en-US/store/currency.ftl b/Resources/Locale/en-US/store/currency.ftl index ed21ab4792..af2efc249e 100644 --- a/Resources/Locale/en-US/store/currency.ftl +++ b/Resources/Locale/en-US/store/currency.ftl @@ -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 diff --git a/Resources/Locale/en-US/weapons/ranged/gun.ftl b/Resources/Locale/en-US/weapons/ranged/gun.ftl index 3fbf5f77e7..0a9d5705cb 100644 --- a/Resources/Locale/en-US/weapons/ranged/gun.ftl +++ b/Resources/Locale/en-US/weapons/ranged/gun.ftl @@ -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 diff --git a/Resources/Prototypes/Actions/changeling.yml b/Resources/Prototypes/Actions/changeling.yml new file mode 100644 index 0000000000..0618871bb3 --- /dev/null +++ b/Resources/Prototypes/Actions/changeling.yml @@ -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 diff --git a/Resources/Prototypes/Actions/types.yml b/Resources/Prototypes/Actions/types.yml index ed3029e43a..5abcaf9bc3 100644 --- a/Resources/Prototypes/Actions/types.yml +++ b/Resources/Prototypes/Actions/types.yml @@ -9,6 +9,7 @@ icon: Interface/Actions/scream.png event: !type:ScreamActionEvent checkCanInteract: false + - type: LesserFormRestricted - type: entity id: ActionTurnUndead diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 11b9beec3b..57040cc9be 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -10,6 +10,7 @@ - category: Stamina - alertType: SuitPower - category: Internals + - alertType: Chemicals - alertType: Fire - alertType: Handcuffed - alertType: Ensnared diff --git a/Resources/Prototypes/Alerts/changeling.yml b/Resources/Prototypes/Alerts/changeling.yml new file mode 100644 index 0000000000..f007cd0b01 --- /dev/null +++ b/Resources/Prototypes/Alerts/changeling.yml @@ -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 diff --git a/Resources/Prototypes/Catalog/changeling_catalog.yml b/Resources/Prototypes/Catalog/changeling_catalog.yml new file mode 100644 index 0000000000..475d3a93af --- /dev/null +++ b/Resources/Prototypes/Catalog/changeling_catalog.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml index 2bc864e88e..aba1c7399a 100644 --- a/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml +++ b/Resources/Prototypes/Entities/Clothing/OuterClothing/armor.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index abf57a405f..50544837fb 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -1175,6 +1175,8 @@ interfaces: - key: enum.StrippingUiKey.Key type: StrippableBoundUserInterface + - key: enum.ListViewSelectorUiKey.Key + type: ListViewSelectorBui - type: Sprite drawdepth: Mobs layers: diff --git a/Resources/Prototypes/Entities/Mobs/Species/base.yml b/Resources/Prototypes/Entities/Mobs/Species/base.yml index 2aa85a86a6..3b8cf5a912 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/base.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/base.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml index dd58bcabc9..003ad3feab 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/subdermal_implants.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Shields/shields.yml b/Resources/Prototypes/Entities/Objects/Shields/shields.yml index 7bc600e87f..aef48e42d6 100644 --- a/Resources/Prototypes/Entities/Objects/Shields/shields.yml +++ b/Resources/Prototypes/Entities/Objects/Shields/shields.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml index ecabe2a4ab..11accf2b36 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Launchers/launchers.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml index d8e3176731..9f195302bf 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/projectiles.yml @@ -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 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml index 497876f359..3bc3b9e765 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/armblade.yml @@ -22,3 +22,6 @@ qualities: - Prying - type: Prying + - type: Unremoveable + deleteOnDrop: true + - type: ToolForcePowered diff --git a/Resources/Prototypes/GameRules/roundstart.yml b/Resources/Prototypes/GameRules/roundstart.yml index b35615b4cf..3a494bb955 100644 --- a/Resources/Prototypes/GameRules/roundstart.yml +++ b/Resources/Prototypes/GameRules/roundstart.yml @@ -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 diff --git a/Resources/Prototypes/Objectives/changeling.yml b/Resources/Prototypes/Objectives/changeling.yml new file mode 100644 index 0000000000..17c9b02ef4 --- /dev/null +++ b/Resources/Prototypes/Objectives/changeling.yml @@ -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 diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index f2d900afe6..82d7561102 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -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 diff --git a/Resources/Prototypes/Polymorphs/polymorph.yml b/Resources/Prototypes/Polymorphs/polymorph.yml index 46590248ae..bdbd273873 100644 --- a/Resources/Prototypes/Polymorphs/polymorph.yml +++ b/Resources/Prototypes/Polymorphs/polymorph.yml @@ -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 diff --git a/Resources/Prototypes/Roles/Antags/changeling.yml b/Resources/Prototypes/Roles/Antags/changeling.yml new file mode 100644 index 0000000000..3b347a452c --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/changeling.yml @@ -0,0 +1,6 @@ +- type: antag + id: Changeling + name: roles-antag-changeling-name + antagonist: true + setPreference: true + objective: roles-antag-changeling-objective diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml index c16972c8a3..89f4973528 100644 --- a/Resources/Prototypes/Store/categories.yml +++ b/Resources/Prototypes/Store/categories.yml @@ -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 diff --git a/Resources/Prototypes/Store/currency.yml b/Resources/Prototypes/Store/currency.yml index e26d0c39ec..f61daa685d 100644 --- a/Resources/Prototypes/Store/currency.yml +++ b/Resources/Prototypes/Store/currency.yml @@ -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 diff --git a/Resources/Prototypes/Store/presets.yml b/Resources/Prototypes/Store/presets.yml index 3e370a4fab..3f18fb70d3 100644 --- a/Resources/Prototypes/Store/presets.yml +++ b/Resources/Prototypes/Store/presets.yml @@ -23,3 +23,13 @@ minItems: 3 maxItems: 10 salesCategory: UplinkSales + +- type: storePreset + id: StorePresetChangeling + storeName: Evolution Shop + categories: + - ChangelingAbilities + - ChangelingStings + - ChangelingBoosters + currencyWhitelist: + - ChangelingPoint diff --git a/Resources/Prototypes/game_presets.yml b/Resources/Prototypes/game_presets.yml index dbdeebb627..bbd30e541f 100644 --- a/Resources/Prototypes/game_presets.yml +++ b/Resources/Prototypes/game_presets.yml @@ -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: diff --git a/Resources/Prototypes/secret_weights.yml b/Resources/Prototypes/secret_weights.yml index 9fc1d576be..d994c26b95 100644 --- a/Resources/Prototypes/secret_weights.yml +++ b/Resources/Prototypes/secret_weights.yml @@ -7,4 +7,4 @@ Revolutionary: 0.10 Zombie: 0.05 Survival: 0.05 - + Changeling: 0.30 diff --git a/Resources/Prototypes/status_effects.yml b/Resources/Prototypes/status_effects.yml index bc84dfbaf1..1c2bd7fbf8 100644 --- a/Resources/Prototypes/status_effects.yml +++ b/Resources/Prototypes/status_effects.yml @@ -60,7 +60,10 @@ - type: statusEffect id: StaminaModifier +- type: statusEffect + id: BlurryVision + alwaysAllowed: true + #WD EDIT - type: statusEffect id: Incorporeal - diff --git a/Resources/Textures/Actions/changeling.rsi/absorb.png b/Resources/Textures/Actions/changeling.rsi/absorb.png new file mode 100644 index 0000000000..1eb4a0bb0b Binary files /dev/null and b/Resources/Textures/Actions/changeling.rsi/absorb.png differ diff --git a/Resources/Textures/Actions/changeling.rsi/blade.png b/Resources/Textures/Actions/changeling.rsi/blade.png new file mode 100644 index 0000000000..792dde4500 Binary files /dev/null and b/Resources/Textures/Actions/changeling.rsi/blade.png differ diff --git a/Resources/Textures/Actions/changeling.rsi/meta.json b/Resources/Textures/Actions/changeling.rsi/meta.json new file mode 100644 index 0000000000..3c3625c5a0 --- /dev/null +++ b/Resources/Textures/Actions/changeling.rsi/meta.json @@ -0,0 +1,17 @@ +{ + "version": 1, + "license": null, + "copyright": null, + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "blade" + }, + { + "name": "absorb" + } + ] +} diff --git a/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/equipped-OUTERCLOTHING.png b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/equipped-OUTERCLOTHING.png new file mode 100644 index 0000000000..9e6b3115a7 Binary files /dev/null and b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/equipped-OUTERCLOTHING.png differ diff --git a/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/icon.png b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/icon.png new file mode 100644 index 0000000000..9c0c95f128 Binary files /dev/null and b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/icon.png differ diff --git a/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/meta.json b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/meta.json new file mode 100644 index 0000000000..99ad2c99eb --- /dev/null +++ b/Resources/Textures/Clothing/OuterClothing/Armor/changeling.rsi/meta.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "license": null, + "copyright": null, + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "icon" + }, + { + "name": "equipped-OUTERCLOTHING", + "directions": 4 + } + ] +} diff --git a/Resources/Textures/Interface/Actions/adrenaline_sacs.png b/Resources/Textures/Interface/Actions/adrenaline_sacs.png new file mode 100644 index 0000000000..e25800a112 Binary files /dev/null and b/Resources/Textures/Interface/Actions/adrenaline_sacs.png differ diff --git a/Resources/Textures/Interface/Actions/arm_blade.png b/Resources/Textures/Interface/Actions/arm_blade.png new file mode 100644 index 0000000000..792dde4500 Binary files /dev/null and b/Resources/Textures/Interface/Actions/arm_blade.png differ diff --git a/Resources/Textures/Interface/Actions/changeling_armor.png b/Resources/Textures/Interface/Actions/changeling_armor.png new file mode 100644 index 0000000000..a33caba7bb Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling_armor.png differ diff --git a/Resources/Textures/Interface/Actions/changeling_shield.png b/Resources/Textures/Interface/Actions/changeling_shield.png new file mode 100644 index 0000000000..d99e0651a2 Binary files /dev/null and b/Resources/Textures/Interface/Actions/changeling_shield.png differ diff --git a/Resources/Textures/Interface/Actions/fleshmend.png b/Resources/Textures/Interface/Actions/fleshmend.png new file mode 100644 index 0000000000..74c0212411 Binary files /dev/null and b/Resources/Textures/Interface/Actions/fleshmend.png differ diff --git a/Resources/Textures/Interface/Actions/lesser_form.png b/Resources/Textures/Interface/Actions/lesser_form.png new file mode 100644 index 0000000000..f418fe55a7 Binary files /dev/null and b/Resources/Textures/Interface/Actions/lesser_form.png differ diff --git a/Resources/Textures/Interface/Actions/ling_absorb.png b/Resources/Textures/Interface/Actions/ling_absorb.png new file mode 100644 index 0000000000..1eb4a0bb0b Binary files /dev/null and b/Resources/Textures/Interface/Actions/ling_absorb.png differ diff --git a/Resources/Textures/Interface/Actions/reviving_stasis.png b/Resources/Textures/Interface/Actions/reviving_stasis.png new file mode 100644 index 0000000000..bc1820f933 Binary files /dev/null and b/Resources/Textures/Interface/Actions/reviving_stasis.png differ diff --git a/Resources/Textures/Interface/Actions/sting_blind.png b/Resources/Textures/Interface/Actions/sting_blind.png new file mode 100644 index 0000000000..520fb18c59 Binary files /dev/null and b/Resources/Textures/Interface/Actions/sting_blind.png differ diff --git a/Resources/Textures/Interface/Actions/sting_cryo.png b/Resources/Textures/Interface/Actions/sting_cryo.png new file mode 100644 index 0000000000..a732cfc6da Binary files /dev/null and b/Resources/Textures/Interface/Actions/sting_cryo.png differ diff --git a/Resources/Textures/Interface/Actions/sting_hallucination.png b/Resources/Textures/Interface/Actions/sting_hallucination.png new file mode 100644 index 0000000000..7cff4fa73c Binary files /dev/null and b/Resources/Textures/Interface/Actions/sting_hallucination.png differ diff --git a/Resources/Textures/Interface/Actions/sting_mute.png b/Resources/Textures/Interface/Actions/sting_mute.png new file mode 100644 index 0000000000..36bb5c4e18 Binary files /dev/null and b/Resources/Textures/Interface/Actions/sting_mute.png differ diff --git a/Resources/Textures/Interface/Actions/sting_transform.png b/Resources/Textures/Interface/Actions/sting_transform.png new file mode 100644 index 0000000000..559d12d798 Binary files /dev/null and b/Resources/Textures/Interface/Actions/sting_transform.png differ diff --git a/Resources/Textures/Interface/Actions/tentacle_arm.png b/Resources/Textures/Interface/Actions/tentacle_arm.png new file mode 100644 index 0000000000..5dc17c582f Binary files /dev/null and b/Resources/Textures/Interface/Actions/tentacle_arm.png differ diff --git a/Resources/Textures/Interface/Actions/transform.png b/Resources/Textures/Interface/Actions/transform.png new file mode 100644 index 0000000000..0e9e02588f Binary files /dev/null and b/Resources/Textures/Interface/Actions/transform.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/base.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/base.png new file mode 100644 index 0000000000..9226c273d4 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/base.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/frope.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/frope.png new file mode 100644 index 0000000000..5f72836fd2 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/frope.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/hook.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/hook.png new file mode 100644 index 0000000000..5c1411be3d Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/hook.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-left.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-left.png new file mode 100644 index 0000000000..c2cf0f4b5b Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-right.png b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-right.png new file mode 100644 index 0000000000..35fbe866b1 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/meta.json b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/meta.json new file mode 100644 index 0000000000..f877bb35f1 --- /dev/null +++ b/Resources/Textures/Objects/Weapons/Guns/Launchers/tentacle_gun.rsi/meta.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "license": null, + "copyright": null, + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "base" + }, + { + "name": "inhand-left", + "directions": 4 + }, + { + "name": "inhand-right", + "directions": 4 + }, + { + "name": "frope" + }, + { + "name": "hook", + "delays": [ + [ + 0.07, + 0.07, + 0.07 + ] + ] + } + ] +} diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-icon.png b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-icon.png new file mode 100644 index 0000000000..0d906cf447 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-icon.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-left.png b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-left.png new file mode 100644 index 0000000000..65ca7b4a56 Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-left.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-right.png b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-right.png new file mode 100644 index 0000000000..c18168848e Binary files /dev/null and b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/changeling-inhand-right.png differ diff --git a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json index 2d2663c158..28c8bac1cd 100644 --- a/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json +++ b/Resources/Textures/Objects/Weapons/Melee/shields.rsi/meta.json @@ -168,6 +168,17 @@ { "name": "teleriot-inhand-left-on", "directions": 4 - } + }, + { + "name": "changeling-icon" + }, + { + "name": "changeling-inhand-left", + "directions": 4 + }, + { + "name": "changeling-inhand-right", + "directions": 4 + } ] } diff --git a/SpaceStation14.sln.DotSettings b/SpaceStation14.sln.DotSettings index 620de42253..32bf0b80ce 100644 --- a/SpaceStation14.sln.DotSettings +++ b/SpaceStation14.sln.DotSettings @@ -585,6 +585,7 @@ public sealed partial class $CLASS$ : Shared$CLASS$ { True True True + True True True True @@ -675,6 +676,7 @@ public sealed partial class $CLASS$ : Shared$CLASS$ { True True True + True True True True