Paradox Clone (#35794)

* polymorph fixes

* paradox clone

* forensics cleanup

* bump doors

* 4

* attribution

* polymorphn't

* clean up objectives

* Update Resources/ServerInfo/Guidebook/Antagonist/MinorAntagonists.xml

* review

* add virtual items to blacklist

* allow them to roll sleeper agent
This commit is contained in:
slarticodefast
2025-03-13 18:09:07 +01:00
committed by GitHub
parent 1b2d3d6ab9
commit bce9b4b05b
23 changed files with 498 additions and 17 deletions

View File

@@ -0,0 +1,23 @@
using Content.Shared.Cloning;
using Robust.Shared.Prototypes;
namespace Content.Server.GameTicking.Rules.Components;
/// <summary>
/// Gamerule component for spawning a paradox clone antagonist.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRuleComponent : Component
{
/// <summary>
/// Cloning settings to be used.
/// </summary>
[DataField]
public ProtoId<CloningSettingsPrototype> Settings = "BaseClone";
/// <summary>
/// Visual effect spawned when gibbing at round end.
/// </summary>
[DataField]
public EntProtoId GibProto = "MobParadoxTimed";
}

View File

@@ -0,0 +1,83 @@
using Content.Server.Antag;
using Content.Server.Cloning;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives.Components;
using Content.Shared.GameTicking.Components;
using Content.Shared.Gibbing.Components;
using Content.Shared.Mind;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.GameTicking.Rules;
public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComponent>
{
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly CloningSystem _cloning = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ParadoxCloneRuleComponent, AntagSelectEntityEvent>(OnAntagSelectEntity);
}
protected override void Started(EntityUid uid, ParadoxCloneRuleComponent component, GameRuleComponent gameRule, GameRuleStartedEvent args)
{
base.Started(uid, component, gameRule, args);
// check if we got enough potential cloning targets, otherwise cancel the gamerule so that the ghost role does not show up
var allHumans = _mind.GetAliveHumans();
if (allHumans.Count == 0)
{
Log.Info("Could not find any alive players to create a paradox clone from! Ending gamerule.");
ForceEndSelf(uid, gameRule);
}
}
// we have to do the spawning here so we can transfer the mind to the correct entity and can assign the objectives correctly
private void OnAntagSelectEntity(Entity<ParadoxCloneRuleComponent> ent, ref AntagSelectEntityEvent args)
{
if (args.Session?.AttachedEntity is not { } spawner)
return;
if (!_prototypeManager.TryIndex(ent.Comp.Settings, out var settings))
{
Log.Error($"Used invalid cloning settings {ent.Comp.Settings} for ParadoxCloneRule");
return;
}
// get possible targets
var allHumans = _mind.GetAliveHumans();
// we already checked when starting the gamerule, but someone might have died since then.
if (allHumans.Count == 0)
{
Log.Warning("Could not find any alive players to create a paradox clone from!");
return;
}
// pick a random player
var playerToClone = _random.Pick(allHumans);
var bodyToClone = playerToClone.Comp.OwnedEntity;
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), settings, out var clone))
{
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
return;
}
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
targetComp.Target = playerToClone.Owner; // set the kill target
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
gibComp.SpawnProto = ent.Comp.GibProto;
gibComp.PreventGibbingObjectives = new() { "ParadoxCloneKillObjective" }; // don't gib them if they killed the original.
args.Entity = clone;
}
}

View File

@@ -0,0 +1,55 @@
using Content.Shared.GameTicking;
using Content.Shared.Gibbing.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
using Content.Server.Body.Systems;
namespace Content.Server.Gibbing.Systems;
public sealed class GibOnRoundEndSystem : EntitySystem
{
[Dependency] private readonly BodySystem _body = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
// this is raised after RoundEndTextAppendEvent, so they can successfully greentext before we gib them
SubscribeLocalEvent<RoundEndMessageEvent>(OnRoundEnd);
}
private void OnRoundEnd(RoundEndMessageEvent args)
{
var gibQuery = EntityQueryEnumerator<GibOnRoundEndComponent>();
// gib everyone with the component
while (gibQuery.MoveNext(out var uid, out var gibComp))
{
var gib = false;
// if they fulfill all objectives given in the component they are not gibbed
if (_mind.TryGetMind(uid, out var mindId, out var mindComp))
{
foreach (var objectiveId in gibComp.PreventGibbingObjectives)
{
if (!_mind.TryFindObjective((mindId, mindComp), objectiveId, out var objective)
|| !_objectives.IsCompleted(objective.Value, (mindId, mindComp)))
{
gib = true;
break;
}
}
}
else
gib = true;
if (!gib)
continue;
if (gibComp.SpawnProto != null)
SpawnAtPosition(gibComp.SpawnProto, Transform(uid).Coordinates);
_body.GibBody(uid, splatModifier: 5f);
}
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// Added to mind role entities to tag that they are a paradox clone.
/// </summary>
[RegisterComponent]
public sealed partial class ParadoxCloneRoleComponent : BaseMindRoleComponent;

View File

@@ -0,0 +1,22 @@
using Robust.Shared.Prototypes;
namespace Content.Shared.Gibbing.Components;
/// <summary>
/// Gibs an entity on round end.
/// </summary>
[RegisterComponent]
public sealed partial class GibOnRoundEndComponent : Component
{
/// <summary>
/// If the entity has all these objectives fulfilled they won't be gibbed.
/// </summary>
[DataField]
public HashSet<EntProtoId> PreventGibbingObjectives = new();
/// <summary>
/// Entity to spawn when gibbed. Can be used for effects.
/// </summary>
[DataField]
public EntProtoId? SpawnProto;
}

View File

@@ -8,6 +8,11 @@
copyright: "Created by Chillyconmor"
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
- files: ["nparadox_clone_greeting.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Created by ps3moira"
source: "https://github.com/space-wizards/space-station-14/blob/master/Resources/Audio/Misc/ninja_greeting.ogg"
- files: ["thief_greeting.ogg"]
license: "CC-BY-NC-4.0"
copyright: "Taken from SergeQuadrado via freesound.org, edit and mono by TheShuEd"

Binary file not shown.

View File

@@ -211,6 +211,9 @@ ghost-role-information-BreadDog-description = You are the chef's favorite child.
ghost-role-information-space-ninja-name = Space Ninja
ghost-role-information-space-ninja-description = Use stealth and deception to sabotage the station.
ghost-role-information-paradox-clone-name = Paradox Clone
ghost-role-information-paradox-clone-description = A freak space-time anomaly has teleported you into another reality! Now you have to find your counterpart and kill and replace them.
ghost-role-information-syndicate-reinforcement-name = Syndicate Agent
ghost-role-information-syndicate-reinforcement-description = Someone needs reinforcements. You, the first person the syndicate could find, will help them.
ghost-role-information-syndicate-reinforcement-rules = You are a [color=red][bold]Team Antagonist[/bold][/color] with the agent who summoned you.

View File

@@ -0,0 +1,5 @@
paradox-clone-round-end-agent-name = paradox clone
objective-issuer-paradox = [color=lightblue]Paradox[/color]
paradox-clone-role-greeting = A freak space-time anomaly has teleported you into another reality! Now you have to find your counterpart and kill and replace them. Only one of you two can survive.

View File

@@ -31,6 +31,9 @@ roles-antag-subverted-silicon-objective = Follow your new laws and do bad unto t
roles-antag-space-ninja-name = Space Ninja
roles-antag-space-ninja-objective = Use your stealth to sabotage the station, nom on electrical wires.
roles-antag-paradox-clone-name = Paradox Clone
roles-antag-paradox-clone-objective = A freak space-time anomaly has teleported you into another reality! Now you have to find your counterpart and kill and replace them.
roles-antag-thief-name = Thief
roles-antag-thief-objective = Add some NT property to your personal collection without using violence.

View File

@@ -22,7 +22,7 @@
prob: 0.15
- id: HarmonicaInstrument
prob: 0.15
- type: entity
id: ToolboxElectricalFilled
name: electrical toolbox
@@ -82,7 +82,7 @@
- id: MysteryFigureBox
prob: 0.5
- id: BookRandom
amount: 2
amount: 2
- id: CrayonMime
- id: CrayonRainbow
@@ -105,6 +105,21 @@
- id: ClothingHeadHatHardhatBlue
prob: 0.5
- type: entity
id: ToolboxMechanicalFilledAllTools
name: mechanical toolbox
suffix: Filled, all tools
parent: ToolboxMechanical
components:
- type: StorageFill
contents:
- id: Crowbar
- id: Wrench
- id: Screwdriver
- id: Wirecutter
- id: Welder
- id: Multitool
- type: entity
parent: ToolboxSyndicate
id: ToolboxSyndicateFilled

View File

@@ -191,6 +191,26 @@
- sprite: Objects/Weapons/Melee/energykatana.rsi
state: icon
- type: entity
categories: [ HideSpawnMenu, Spawner ]
parent: BaseAntagSpawner
id: SpawnPointGhostParadoxClone
components:
- type: GhostRole
name: ghost-role-information-paradox-clone-name
description: ghost-role-information-paradox-clone-description
rules: ghost-role-information-antagonist-rules
mindRoles:
- MindRoleGhostRoleSoloAntagonist
raffle:
settings: default
- type: Sprite
layers:
- sprite: Markers/jobs.rsi
state: green
- sprite: Mobs/Aliens/paradox_clone.rsi
state: preview
- type: entity
categories: [ HideSpawnMenu, Spawner ]
parent: BaseAntagSpawner

View File

@@ -56,6 +56,7 @@
blacklist:
components:
- AttachedClothing # helmets, which are part of the suit
- VirtualItem
- type: cloningSettings
id: Antag
@@ -78,7 +79,7 @@
suffix: Non-Antag
components:
- type: Sprite
sprite: Markers/paradox_clone.rsi
sprite: Mobs/Aliens/paradox_clone.rsi
state: preview
- type: RandomCloneSpawner
settings: BaseClone

View File

@@ -0,0 +1,37 @@
# Mob
- type: entity
parent: BaseMob
id: MobParadox
name: space-time paradox
description: A big ball of wibbly wobbly, timey wimey stuff.
components:
- type: Sprite
drawdepth: Mobs
layers:
- sprite: Mobs/Aliens/paradox_clone.rsi
state: paradox
shader: unshaded
- type: RotationVisuals
horizontalRotation: 90
- type: Pullable
- type: Tag
tags:
- DoorBumpOpener
- type: entity
parent: MobParadox
id: MobParadoxTimed # visual effect when gibbing on round end
components:
- type: TimedDespawn
lifetime: 10
# guidebook dummy
- type: entity
id: ParadoxCloneDummy
categories: [ HideSpawnMenu ]
name: Paradox Clone
components:
- type: Sprite
sprite: Mobs/Aliens/paradox_clone.rsi
state: preview

View File

@@ -232,6 +232,39 @@
mindRoles:
- MindRoleNinja
- type: entity
parent: BaseGameRule
id: ParadoxCloneSpawn
components:
- type: StationEvent
weight: 4 # should not happen every round
duration: null
earliestStart: 30
reoccurrenceDelay: 20
minimumPlayers: 15
- type: ParadoxCloneRule
- type: AntagObjectives
objectives:
- ParadoxCloneKillObjective
- ParadoxCloneLivingObjective
- type: AntagRandomSpawn # TODO: improve spawning so they only start in maints
- type: AntagSelection
agentName: paradox-clone-round-end-agent-name
definitions:
- spawnerPrototype: SpawnPointGhostParadoxClone
min: 1
max: 1
pickPlayer: false
startingGear: ParadoxCloneGear
roleLoadout:
- RoleSurvivalStandard # give vox something to breath in case they don't get a copy
briefing:
text: paradox-clone-role-greeting
color: lightblue
sound: /Audio/Misc/paradox_clone_greeting.ogg
mindRoles:
- MindRoleParadoxClone
- type: entity
parent: BaseGameRule
id: RevenantSpawn

View File

@@ -0,0 +1,38 @@
- type: entity
abstract: true
parent: BaseObjective
id: BaseParadoxCloneObjective
components:
- type: Objective
# required but not used
difficulty: 1
issuer: objective-issuer-paradox
- type: RoleRequirement
roles:
mindRoles:
- ParadoxCloneRole
- type: entity
parent: [BaseParadoxCloneObjective, BaseLivingObjective]
id: ParadoxCloneLivingObjective
name: Escape to centcomm alive and unrestrained.
description: Return to your old life.
components:
- type: Objective
icon:
sprite: Structures/Furniture/chairs.rsi
state: shuttle
- type: EscapeShuttleCondition
- type: entity
parent: [BaseParadoxCloneObjective, BaseKillObjective]
id: ParadoxCloneKillObjective
name: Fix the space-time paradox.
description: Replace your original to fix the paradox. Remember, your mission is to blend in, do not kill anyone else unless you have to!
components:
- type: PickSpecificPerson
- type: KillPersonCondition
requireDead: true # don't count missing evac as killing
- type: TargetObjective
title: objective-condition-kill-head-title # kill <name>, <job>

View File

@@ -0,0 +1,16 @@
- type: antag
id: ParadoxClone
name: roles-antag-paradox-clone-name
antagonist: true
objective: roles-antag-paradox-clone-objective
setPreference: false
# they need to be able to espace when spawning inside a locked room
# TODO: remove this once we got a better spawning loacation selection
- type: startingGear
id: ParadoxCloneGear
inhand:
- ToolboxMechanicalFilledAllTools
storage:
back:
- ClothingHandsGlovesColorYellow

View File

@@ -143,6 +143,17 @@
exclusiveAntag: true
- type: NinjaRole
# Paradox Clone
- type: entity
parent: BaseMindRoleAntag
id: MindRoleParadoxClone
name: Paradox Clone Role
components:
- type: MindRole
antagPrototype: ParadoxClone
roleType: SoloAntagonist
- type: ParadoxCloneRole
# Nukies
- type: entity
parent: BaseMindRoleAntag

View File

@@ -3,6 +3,14 @@
Most if not all Minor Antagonists are ghost-controlled roles that gives dead people new ways to cause chaos around the station. They are spawned by random events.
# Paradox Clone
<Box>
<GuideEntityEmbed Entity="ParadoxCloneDummy"/>
</Box>
A perfect copy of a random crewmember that was thrown into our universe by a space-time anomaly. To fix the paradox and survive they have to find and kill their original counterpart. They have no special abilities, but are have identical appearance, DNA, fingerprints, clothing, equipment and IDs, making it hard to figure out who is the orginal and who is the clone, unless you know your fellow crewmembers well.
# Revenant
<Box>

View File

@@ -1,14 +0,0 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "preview"
}
]
}

View File

@@ -0,0 +1,108 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "preview combined from Mobs/Species/Human/parts.rsi, Clothing/Uniforms/Jumpsuit/janitor.rsi, Clothing/Shoes/Specific/galoshes.rsi, Clothing/Belt/janitor.rsi, Clothing/Hands/Gloves/janitor.rsi and Clothing/Head/Soft/purplesoft.rsi by slarticodefast, paradox made by ps3moira (github)",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "preview"
},
{
"name": "paradox",
"directions": 4,
"delays": [
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
],
[
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB