Changeling Escape as Department Objective (#44220)

* insanity

* changeling real pliz

* the wizard

* no conflict plz :3

* Update Content.Server/Objectives/Systems/ChangelingEscapeDepartmentConditionSystem.cs

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>

* linq is hell

* Update Content.Server/Objectives/Systems/ChangelingEscapeDepartmentConditionSystem.cs

Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>

* comment & API

* wokism is real and is coming for you in this pride month

* i am stoopid and dont know how to code :3

* cleanup, self review, usual stuff

* oops

* comments and difficulty

* fuck everything

* add to rolls

* FFS

* self ontag

* filters? you should kill your mom, here's why women never fuck you here's how you could build a bomb.

* ncat

* Update Content.Shared/Roles/Jobs/SharedJobSystem.cs

eqweqwewqe

Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>

* address slam,s reviews idfk this will be squashed anyway

* from captain sprite (apparently removed on master) to atmosian sprite

* Minor cleanup changes

---------

Co-authored-by: nomdéraisonnablementlong <you@example.com>
Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: Pok <113675512+Pok27@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
Co-authored-by: SlamBamActionman <83650252+SlamBamActionman@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
This commit is contained in:
KeTuFaisPiKiNut 2026-08-13 13:03:37 +00:00 • committed by GitHub
parent ba23ea370d
commit 5a6bf259d6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 279 additions and 6 deletions

View file

@ -0,0 +1,23 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the changeling:
/// 1. Is on the emergency shuttle when docking to CentComm
/// 2. Is currently transformed into the identity of someone in the target department
/// 3. Is wearing an ID card with the matching dept
/// Depends on <see cref="TargetObjectiveComponent"/> to know which target department to check for.
/// </summary>
[RegisterComponent]
[Access(typeof(ChangelingEscapeDepartmentConditionSystem))]
public sealed partial class ChangelingEscapeDepartmentConditionComponent : Component
{
/// <summary>
/// The department that is targeted by the objective.
/// </summary>
[DataField]
public ProtoId<DepartmentPrototype>? Department;
}

View file

@ -7,7 +7,8 @@ public sealed partial class TargetObjectiveComponent : Component
{
/// <summary>
/// Locale id for the objective title.
/// It is passed "targetName" and "job" arguments.
/// It is passed "targetName", "job" and "department" arguments.
/// Standing for the name of the target, their job, and their job's department.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public string Title = string.Empty;

View file

@ -0,0 +1,92 @@
using System.Diagnostics;
using System.Linq;
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.Access.Systems;
using Content.Shared.Changeling.Systems;
using Content.Shared.Cuffs;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Makes it so that <see cref="ChangelingEscapeDepartmentConditionComponent"/> works.
/// </summary>
public sealed partial class ChangelingEscapeDepartmentConditionSystem : EntitySystem
{
[Dependency] private EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private SharedChangelingIdentitySystem _changelingIdentity = default!;
[Dependency] private SharedCuffableSystem _cuffable = default!;
[Dependency] private SharedIdCardSystem _idCard = default!;
[Dependency] private SharedMindSystem _mind = default!;
[Dependency] private TargetObjectiveSystem _target = default!;
[Dependency] private SharedJobSystem _job = default!;
[Dependency] private IRobustRandom _random = default!;
[SubscribeLocalEvent]
private void OnAfterAssign(Entity<ChangelingEscapeDepartmentConditionComponent> ent, ref ObjectiveAfterAssignEvent args)
{
if (!_target.GetTarget(ent, out var target))
return;
// Does our target have a valid job?
if (!_job.MindTryGetJobId(target.Value, out var jobPrototype) || !jobPrototype.HasValue)
return;
// We get the department of the job for our target, prioritizing the secondary departments (such as Command)
// We do this to allow the objective to roll Command.
// Otherwise RD/HoS/QM would return Science/Security/Cargo instead of Command.
if (!_job.TryGetSecondaryDepartmentsOrFallback(jobPrototype.Value, out var departmentPrototypes))
return;
Debug.Assert(departmentPrototypes.Count > 0, $"Attempted to assign a job without departments to objective {MetaData(ent).EntityPrototype}!");
// Get a random department.
// This is all done on the server, so no need to worry about mispredictions.
ent.Comp.Department = _random.Pick(departmentPrototypes);
}
[SubscribeLocalEvent]
private void OnGetProgress(Entity<ChangelingEscapeDepartmentConditionComponent> ent, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(ent, (args.MindId, args.Mind));
}
private float GetProgress(Entity<ChangelingEscapeDepartmentConditionComponent> ent, Entity<MindComponent> mind)
{
var ownedEntity = mind.Comp.OwnedEntity;
if (ownedEntity == null || _mind.IsCharacterDeadIc(mind))
return 0f;
// Check 1: Must have transformed into a target with a job that is in the department
if (!_changelingIdentity.TryGetCurrentIdentityData(ownedEntity.Value, out var identityData))
return 0f; // this should not happen
// Our identity must have a job.
var job = identityData.OriginalJob;
if (!job.HasValue)
return 0f;
if (ent.Comp.Department == null || !_job.JobIsInDepartment(job.Value, ent.Comp.Department.Value))
return 0f; // The job of our identity is not in the target department or the department doesn't exist.
// Check 2: Must escape alive.
if (!_emergencyShuttle.IsTargetEscaping(ownedEntity.Value))
return 0.5f;
if (_cuffable.IsCuffed(ownedEntity.Value))
return 0.5f;
// Check 3: Must wear an ID card with the target's name on it.
if (!_idCard.TryFindIdCard(ownedEntity.Value, out var idCard))
return 0.75f; // is not wearing an id
if (idCard.Comp.JobDepartments.All(idDept => idDept.Id != ent.Comp.Department))
return 0.75f; // found ID is not of the correct department.
return 1f;
}
}

View file

@ -62,7 +62,16 @@ public sealed partial class TargetObjectiveSystem : EntitySystem
}
var jobName = _job.MindTryGetJobName(target);
return Loc.GetString(title, ("targetName", targetName), ("job", jobName));
var deptName = Loc.GetString("department-Unknown");
if (_job.MindTryGetJobId(target, out var jobId))
{
if (jobId.HasValue && _job.TryGetDepartment(jobId.Value, out var deptProto))
{
deptName = Loc.GetString(deptProto.Name);
}
}
return Loc.GetString(title, ("targetName", targetName), ("job", jobName), ("department", deptName));
}
}

View file

@ -0,0 +1,58 @@
using System.Linq;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Throwing;
using Robust.Shared.Prototypes;
namespace Content.Shared.EntityConditions.Conditions.Mind;
public sealed partial class DifferentDepartmentConditionSystem : EntityConditionSystem<MindComponent, DifferentDepartmentCondition>
{
[Dependency] private SharedRoleSystem _roleSystem = default!;
[Dependency] private SharedJobSystem _jobSystem = default!;
protected override void Condition(Entity<MindComponent> entity, ref EntityConditionEvent<DifferentDepartmentCondition> args)
{
args.Result = !IsInvalid(entity, args.SourceEnt);
}
private bool IsInvalid(Entity<MindComponent> mind, EntityUid? exclude)
{
// no entity to exclude depts, so all depts are valid
if (!exclude.HasValue)
return false;
if (!_jobSystem.MindTryGetJobId(exclude.Value, out var objJob))
return false; // in no department, so all departments are valid
if (!_jobSystem.MindTryGetJobId(mind.Owner, out var job))
return false; // target in no department, so all depts are valid
if (!objJob.HasValue || !job.HasValue)
throw new Exception("Unreachable statement after getting job proto from mind.");
// get all departments
if (!_jobSystem.TryGetAllDepartments(objJob.Value, out var deptsA) || !_jobSystem.TryGetAllDepartments(job.Value, out var deptsB))
throw new Exception("Job didn't have any department assigned.");
// perform the department check
if (deptsA.Select(dept => dept.ID).Intersect(deptsB.Select(dept => dept.ID)).Any())
return true;
return false;
}
}
/// <summary>
/// A condition that requires minds to have a job with a different department from the excluded entity's.
/// This uses mind roles, not ID cards.
/// </summary>
public sealed partial class DifferentDepartmentCondition : EntityConditionBase<DifferentDepartmentCondition>
{
public override string EntityConditionGuidebookText(IPrototypeManager prototype)
{
return string.Empty;
}
}

View file

@ -117,6 +117,68 @@ public abstract partial class SharedJobSystem : EntitySystem
return false;
}
/// <summary>
/// Like <see cref="TryGetPrimaryDepartment"/> but tries to get any non-primary department(s) first.
/// For all heads (including the captain), it will return Command, but for John Scientist it will return Science.
/// </summary>
/// <returns> True if a department was found, false if not.</returns>
public bool TryGetSecondaryDepartmentsOrFallback(ProtoId<JobPrototype> jobProto, [NotNullWhen(true)] out List<DepartmentPrototype>? departmentPrototypes)
{
departmentPrototypes = new List<DepartmentPrototype>();
// not sorting it since there should only be 1 primary department for a job.
// this is enforced by the job tests.
var departmentProtos = ProtoMan.EnumeratePrototypes<DepartmentPrototype>();
foreach (var department in departmentProtos)
{
if (!department.Primary && department.Roles.Contains(jobProto))
{
departmentPrototypes.Add(department);
}
}
if (departmentPrototypes.Count > 0)
return true;
// if we don't have any secondary dept, try to get a primary
if (TryGetPrimaryDepartment(jobProto, out var primaryDept))
{
departmentPrototypes.Add(primaryDept);
return true;
}
// no department was found
return false;
}
/// <summary>
/// Gets all departments for a mind's job (if any) and checks if the chosen department is among them.
/// </summary>
/// <param name="mind">The mind to check for.</param>
/// <param name="deptProto">The department proto ID to check for.</param>
/// <returns>True if the mind is a part of the department.</returns>
public bool MindIsInDepartment(EntityUid mind, ProtoId<DepartmentPrototype> deptProto)
{
if (!MindTryGetJobId(mind, out var job))
return false;
if (!job.HasValue)
return false;
return JobIsInDepartment(job.Value, deptProto);
}
/// <summary>
/// Checks if the job is contained within a department, and returns true if it does.
/// </summary>
public bool JobIsInDepartment(ProtoId<JobPrototype> jobProto, ProtoId<DepartmentPrototype> deptProto)
{
if (!TryGetAllDepartments(jobProto, out var depts))
return false;
return depts.Any(dept => dept.ID == deptProto);
}
/// <summary>
/// Tries to get all the departments for a given job. Will return an empty list if none are found.
/// </summary>

View file

@ -2,3 +2,5 @@ objective-unique-identity-name = Devour {$count} unique humanoids.
objective-unique-identity-description = We must devour {$count} different humanoids for sustenance.
objective-escape-identity-title = Escape as {$targetName}, {CAPITALIZE($job)}.
objective-escape-department-title = Escape as a member of the {CAPITALIZE($department)} department.

View file

@ -17,7 +17,7 @@
description: We must stay alive at all cost.
components:
- type: Objective
difficulty: 1
difficulty: 0.5
icon:
sprite: Mobs/Species/Human/organs.rsi
state: heart-on
@ -53,11 +53,37 @@
- type: ChangelingEscapeIdentityCondition
- type: PickRandomPerson
conditions:
- !type:ObjectiveTargetCondition
- !type:RoleCondition
inverted: true # Do not roll changelings here. Their initial identity will be basically impossible to get after they transform.
whitelist:
components:
- ChangelingRole
- type: entity
parent: [BaseChangelingObjective, BaseLivingObjective]
id: ChangelingEscapeDepartmentObjective
description: We must escape with the identity & ID card of any member of this department.
components:
- type: Objective
difficulty: 1
icon:
sprite: Objects/Fun/Plushies/atmosian.rsi
state: icon
- type: TargetObjective
title: objective-escape-department-title
- type: ChangelingEscapeDepartmentCondition
- type: PickRandomPerson
conditions:
- !type:RoleCondition # Do not roll changelings here. It will cause problems with weird job mindroles as they transform n stuff.
inverted: true
whitelist:
components:
- ChangelingRole
- !type:RoleCondition
whitelist:
components:
- JobRole # Our target has to be employed, obviously.
- !type:DifferentDepartmentCondition # I love spawning as a scientist and having to escape as a scientist
- type: entity
parent: [BaseChangelingObjective, BaseKillObjective]

View file

@ -157,8 +157,8 @@
id: ChangelingObjectiveGroupEscape
weights:
ChangelingSurviveObjective: 0.5
ChangelingEscapeIdentityObjective: 1
# eventually: escape as a job/dept
ChangelingEscapeDepartmentObjective: 1.0
ChangelingEscapeIdentityObjective: 1.5
- type: weightedRandom
id: ChangelingObjectiveGroupKillDummy