diff --git a/Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs b/Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs new file mode 100644 index 0000000000..5e6cc203d2 --- /dev/null +++ b/Content.Server/GameTicking/Rules/Components/SurvivorRuleComponent.cs @@ -0,0 +1,8 @@ +namespace Content.Server.GameTicking.Rules.Components; + +/// +/// Component for the SurvivorRuleSystem. Game rule that turns everyone into a survivor and gives them the objective to escape centcom alive. +/// Started by Wizard Summon Guns/Magic spells. +/// +[RegisterComponent, Access(typeof(SurvivorRuleSystem))] +public sealed partial class SurvivorRuleComponent : Component; diff --git a/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs new file mode 100644 index 0000000000..00f652b6c6 --- /dev/null +++ b/Content.Server/GameTicking/Rules/SurvivorRuleSystem.cs @@ -0,0 +1,108 @@ +using Content.Server.Antag; +using Content.Server.GameTicking.Rules.Components; +using Content.Server.Mind; +using Content.Server.Roles; +using Content.Server.Shuttles.Systems; +using Content.Shared.GameTicking.Components; +using Content.Shared.Mind; +using Content.Shared.Mobs.Systems; +using Content.Shared.Survivor.Components; +using Content.Shared.Tag; +using Robust.Server.GameObjects; + +namespace Content.Server.GameTicking.Rules; + +public sealed class SurvivorRuleSystem : GameRuleSystem +{ + [Dependency] private readonly RoleSystem _role = default!; + [Dependency] private readonly MindSystem _mind = default!; + [Dependency] private readonly AntagSelectionSystem _antag = default!; + [Dependency] private readonly TransformSystem _xform = default!; + [Dependency] private readonly EmergencyShuttleSystem _eShuttle = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly MobStateSystem _mobState = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnGetBriefing); + } + + // TODO: Planned rework post wizard release when RandomGlobalSpawnSpell becomes a gamerule + protected override void Started(EntityUid uid, SurvivorRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args) + { + base.Started(uid, component, gameRule, args); + + var allAliveHumanMinds = _mind.GetAliveHumans(); + + foreach (var humanMind in allAliveHumanMinds) + { + if (!humanMind.Comp.OwnedEntity.HasValue) + continue; + + var mind = humanMind.Owner; + var ent = humanMind.Comp.OwnedEntity.Value; + + if (HasComp(mind) || _tag.HasTag(mind, "InvalidForSurvivorAntag")) + continue; + + EnsureComp(mind); + _role.MindAddRole(mind, "MindRoleSurvivor"); + _antag.SendBriefing(ent, Loc.GetString("survivor-role-greeting"), Color.Olive, null); + } + } + + private void OnGetBriefing(Entity ent, ref GetBriefingEvent args) + { + args.Append(Loc.GetString("survivor-role-greeting")); + } + + protected override void AppendRoundEndText(EntityUid uid, + SurvivorRuleComponent component, + GameRuleComponent gameRule, + ref RoundEndTextAppendEvent args) + { + base.AppendRoundEndText(uid, component, gameRule, ref args); + + // Using this instead of alive antagonists to make checking for shuttle & if the ent is alive easier + var existingSurvivors = AllEntityQuery(); + + var deadSurvivors = 0; + var aliveMarooned = 0; + var aliveOnShuttle = 0; + var eShuttle = _eShuttle.GetShuttle(); + + while (existingSurvivors.MoveNext(out _, out _, out var mindComp)) + { + // If their brain is gone or they respawned/became a ghost role + if (mindComp.CurrentEntity is null) + { + deadSurvivors++; + continue; + } + + var survivor = mindComp.CurrentEntity.Value; + + if (!_mobState.IsAlive(survivor)) + { + deadSurvivors++; + continue; + } + + if (eShuttle != null && eShuttle.Value.IsValid() && (Transform(eShuttle.Value).MapID == _xform.GetMapCoordinates(survivor).MapId)) + { + aliveOnShuttle++; + continue; + } + + aliveMarooned++; + } + + args.AddLine(Loc.GetString("survivor-round-end-dead-count", ("deadCount", deadSurvivors))); + args.AddLine(Loc.GetString("survivor-round-end-alive-count", ("aliveCount", aliveMarooned))); + args.AddLine(Loc.GetString("survivor-round-end-alive-on-shuttle-count", ("aliveCount", aliveOnShuttle))); + + // Player manifest at EOR shows who's a survivor so no need for extra info here. + } +} diff --git a/Content.Server/Magic/MagicSystem.cs b/Content.Server/Magic/MagicSystem.cs index a7e5e96786..34c12954c6 100644 --- a/Content.Server/Magic/MagicSystem.cs +++ b/Content.Server/Magic/MagicSystem.cs @@ -1,12 +1,20 @@ using Content.Server.Chat.Systems; +using Content.Server.GameTicking; +using Content.Server.GameTicking.Rules.Components; using Content.Shared.Magic; using Content.Shared.Magic.Events; +using Content.Shared.Mind; +using Content.Shared.Tag; +using Robust.Shared.Prototypes; namespace Content.Server.Magic; public sealed class MagicSystem : SharedMagicSystem { [Dependency] private readonly ChatSystem _chat = default!; + [Dependency] private readonly GameTicker _gameTicker = default!; + [Dependency] private readonly TagSystem _tag = default!; + [Dependency] private readonly SharedMindSystem _mind = default!; public override void Initialize() { @@ -32,4 +40,20 @@ public sealed class MagicSystem : SharedMagicSystem Spawn(ev.Effect, perfXForm.Coordinates); Spawn(ev.Effect, targetXForm.Coordinates); } + + protected override void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev) + { + base.OnRandomGlobalSpawnSpell(ev); + + if (!ev.MakeSurvivorAntagonist) + return; + + if (_mind.TryGetMind(ev.Performer, out var mind, out _) && !_tag.HasTag(mind, "InvalidForSurvivorAntag")) + _tag.AddTag(mind, "InvalidForSurvivorAntag"); + + EntProtoId survivorRule = "Survivor"; + + if (!_gameTicker.IsGameRuleActive()) + _gameTicker.StartGameRule(survivorRule); + } } diff --git a/Content.Server/Roles/SurvivorRoleComponent.cs b/Content.Server/Roles/SurvivorRoleComponent.cs new file mode 100644 index 0000000000..e5e6dd9f87 --- /dev/null +++ b/Content.Server/Roles/SurvivorRoleComponent.cs @@ -0,0 +1,9 @@ +using Content.Shared.Roles; + +namespace Content.Server.Roles; + +/// +/// Adds to a mind role ent to tag they're a Survivor +/// +[RegisterComponent] +public sealed partial class SurvivorRoleComponent : BaseMindRoleComponent; diff --git a/Content.Server/Zombies/ZombieSystem.Transform.cs b/Content.Server/Zombies/ZombieSystem.Transform.cs index 7acfe9dbbd..82c9e2dacc 100644 --- a/Content.Server/Zombies/ZombieSystem.Transform.cs +++ b/Content.Server/Zombies/ZombieSystem.Transform.cs @@ -35,6 +35,7 @@ using Content.Shared.Prying.Components; using Content.Shared.Traits.Assorted; using Robust.Shared.Audio.Systems; using Content.Shared.Ghost.Roles.Components; +using Content.Shared.Tag; namespace Content.Server.Zombies; @@ -58,6 +59,7 @@ public sealed partial class ZombieSystem [Dependency] private readonly MovementSpeedModifierSystem _movementSpeedModifier = default!; [Dependency] private readonly NPCSystem _npc = default!; [Dependency] private readonly SharedRoleSystem _roles = default!; + [Dependency] private readonly TagSystem _tag = default!; /// /// Handles an entity turning into a zombie when they die or go into crit @@ -275,5 +277,9 @@ public sealed partial class ZombieSystem RaiseLocalEvent(target, ref ev, true); //zombies get slowdown once they convert _movementSpeedModifier.RefreshMovementSpeedModifiers(target); + + //Need to prevent them from getting an item, they have no hands. + // Also prevents them from becoming a Survivor. They're undead. + _tag.AddTag(target, "InvalidForGlobalSpawnSpell"); } } diff --git a/Content.Shared/Magic/Events/RandomGlobalSpawnSpellEvent.cs b/Content.Shared/Magic/Events/RandomGlobalSpawnSpellEvent.cs index c77607562a..eb39c0cd08 100644 --- a/Content.Shared/Magic/Events/RandomGlobalSpawnSpellEvent.cs +++ b/Content.Shared/Magic/Events/RandomGlobalSpawnSpellEvent.cs @@ -20,4 +20,11 @@ public sealed partial class RandomGlobalSpawnSpellEvent : InstantActionEvent, IS [DataField] public string? Speech { get; private set; } + + /// + /// Should this Global spawn spell turn its targets into a Survivor Antagonist? + /// Ignores the caster for this. + /// + [DataField] + public bool MakeSurvivorAntagonist = false; } diff --git a/Content.Shared/Magic/SharedMagicSystem.cs b/Content.Shared/Magic/SharedMagicSystem.cs index 0be5646f4b..49f0825ddf 100644 --- a/Content.Shared/Magic/SharedMagicSystem.cs +++ b/Content.Shared/Magic/SharedMagicSystem.cs @@ -469,7 +469,8 @@ public abstract class SharedMagicSystem : EntitySystem #endregion #region Global Spells - private void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev) + // TODO: Change this into a "StartRuleAction" when actions with multiple events are supported + protected virtual void OnRandomGlobalSpawnSpell(RandomGlobalSpawnSpellEvent ev) { if (!_net.IsServer || ev.Handled || !PassesSpellPrerequisites(ev.Action, ev.Performer) || ev.Spawns is not { } spawns) return; @@ -486,6 +487,9 @@ public abstract class SharedMagicSystem : EntitySystem var ent = human.Comp.OwnedEntity.Value; + if (_tag.HasTag(ent, "InvalidForGlobalSpawnSpell")) + continue; + var mapCoords = _transform.GetMapCoordinates(ent); foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random)) { diff --git a/Content.Shared/Survivor/Components/SurvivorComponent.cs b/Content.Shared/Survivor/Components/SurvivorComponent.cs new file mode 100644 index 0000000000..140f688077 --- /dev/null +++ b/Content.Shared/Survivor/Components/SurvivorComponent.cs @@ -0,0 +1,9 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared.Survivor.Components; + +/// +/// Component to keep track of which entities are a Survivor antag. +/// +[RegisterComponent, NetworkedComponent] +public sealed partial class SurvivorComponent : Component; diff --git a/Resources/Locale/en-US/game-ticking/game-presets/preset-wizard.ftl b/Resources/Locale/en-US/game-ticking/game-presets/preset-wizard.ftl new file mode 100644 index 0000000000..c355f4b9b0 --- /dev/null +++ b/Resources/Locale/en-US/game-ticking/game-presets/preset-wizard.ftl @@ -0,0 +1,36 @@ +## Survivor + +roles-antag-survivor-name = Survivor +# It's a Halo reference +roles-antag-survivor-objective = Current Objective: Survive + +survivor-role-greeting = + You are a Survivor. + Above all you need to make it back to CentComm alive. + Collect as much firepower as needed to guarantee your survival. + Trust no one. + +survivor-round-end-dead-count = +{ + $deadCount -> + [one] [color=red]{$deadCount}[/color] survivor died. + *[other] [color=red]{$deadCount}[/color] survivors died. +} + +survivor-round-end-alive-count = +{ + $aliveCount -> + [one] [color=yellow]{$aliveCount}[/color] survivor was marooned on the station. + *[other] [color=yellow]{$aliveCount}[/color] survivors were marooned on the station. +} + +survivor-round-end-alive-on-shuttle-count = +{ + $aliveCount -> + [one] [color=green]{$aliveCount}[/color] survivor made it out alive. + *[other] [color=green]{$aliveCount}[/color] survivors made it out alive. +} + +## TODO: Wizard + +## TODO: Wizard Apprentice (Coming sometime post-wizard release) diff --git a/Resources/Prototypes/GameRules/survivor.yml b/Resources/Prototypes/GameRules/survivor.yml new file mode 100644 index 0000000000..e8f73a2e27 --- /dev/null +++ b/Resources/Prototypes/GameRules/survivor.yml @@ -0,0 +1,5 @@ +- type: entity + id: Survivor + parent: BaseGameRule + components: + - type: SurvivorRule diff --git a/Resources/Prototypes/Magic/event_spells.yml b/Resources/Prototypes/Magic/event_spells.yml index 87b3a57107..a72dc8b217 100644 --- a/Resources/Prototypes/Magic/event_spells.yml +++ b/Resources/Prototypes/Magic/event_spells.yml @@ -26,6 +26,7 @@ sprite: Objects/Weapons/Guns/Rifles/ak.rsi state: base event: !type:RandomGlobalSpawnSpellEvent + makeSurvivorAntagonist: true spawns: - id: WeaponPistolViper orGroup: Guns @@ -158,7 +159,7 @@ - id: RevolverCapGunFake orGroup: Guns speech: action-speech-spell-summon-guns - + - type: entity id: ActionSummonMagic name: Summon Magic @@ -172,6 +173,7 @@ sprite: Objects/Magic/magicactions.rsi state: magicmissile event: !type:RandomGlobalSpawnSpellEvent + makeSurvivorAntagonist: true spawns: - id: SpawnSpellbook orGroup: Magics diff --git a/Resources/Prototypes/Roles/Antags/wizard.yml b/Resources/Prototypes/Roles/Antags/wizard.yml new file mode 100644 index 0000000000..e8ffe649e7 --- /dev/null +++ b/Resources/Prototypes/Roles/Antags/wizard.yml @@ -0,0 +1,8 @@ +# TODO: Actual wizard coming later, this is just for the survival antags + +- type: antag + id: Survivor + name: roles-antag-survivor-name + antagonist: true + objective: roles-antag-survivor-objective + # guides: [ ] diff --git a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml index e32c173870..ded64895f0 100644 --- a/Resources/Prototypes/Roles/MindRoles/mind_roles.yml +++ b/Resources/Prototypes/Roles/MindRoles/mind_roles.yml @@ -196,6 +196,17 @@ - type: MindRole antagPrototype: Rev +# Survivors (Wizard) +- type: entity + parent: BaseMindRoleAntag + id: MindRoleSurvivor + name: Survivor Role + components: + - type: MindRole + antagPrototype: Survivor + roleType: FreeAgent + - type: SurvivorRole + # Thief - type: entity parent: BaseMindRoleAntag diff --git a/Resources/Prototypes/Roles/role_types.yml b/Resources/Prototypes/Roles/role_types.yml index 326abe4ee4..97e11d80b2 100644 --- a/Resources/Prototypes/Roles/role_types.yml +++ b/Resources/Prototypes/Roles/role_types.yml @@ -35,3 +35,4 @@ id: SiliconAntagonist name: role-type-silicon-antagonist-name color: '#c832e6' + diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index 958caccfe7..f5eba41c70 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -752,6 +752,12 @@ - type: Tag id: IntercomElectronics +- type: Tag + id: InvalidForGlobalSpawnSpell + +- type: Tag + id: InvalidForSurvivorAntag + - type: Tag id: JawsOfLife