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