Objectives ecs rework (#19967)

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas
2023-09-16 07:18:10 +01:00
committed by GitHub
parent e8c58d1574
commit f7711edbe3
106 changed files with 2121 additions and 1779 deletions

View File

@@ -59,7 +59,7 @@ public sealed class CharacterInfoSystem : EntitySystem
public readonly record struct CharacterData(
EntityUid Entity,
string Job,
Dictionary<string, List<ConditionInfo>> Objectives,
Dictionary<string, List<ObjectiveInfo>> Objectives,
string? Briefing,
string EntityName
);

View File

@@ -0,0 +1,7 @@
using Content.Shared.Objectives.Systems;
namespace Content.Client.Objectives.Systems;
public sealed class ObjectivesSystem : SharedObjectivesSystem
{
}

View File

@@ -128,7 +128,7 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
foreach (var condition in conditions)
{
var conditionControl = new ObjectiveConditionsControl();
conditionControl.ProgressTexture.Texture = _sprite.Frame0(condition.SpriteSpecifier);
conditionControl.ProgressTexture.Texture = _sprite.Frame0(condition.Icon);
conditionControl.ProgressTexture.Progress = condition.Progress;
var titleMessage = new FormattedMessage();
var descriptionMessage = new FormattedMessage();

View File

@@ -3,6 +3,8 @@ using Content.Server.Roles;
using Content.Server.Roles.Jobs;
using Content.Shared.CharacterInfo;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
namespace Content.Server.CharacterInfo;
@@ -11,6 +13,7 @@ public sealed class CharacterInfoSystem : EntitySystem
[Dependency] private readonly JobSystem _jobs = default!;
[Dependency] private readonly MindSystem _minds = default!;
[Dependency] private readonly RoleSystem _roles = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
public override void Initialize()
{
@@ -27,7 +30,7 @@ public sealed class CharacterInfoSystem : EntitySystem
var entity = args.SenderSession.AttachedEntity.Value;
var conditions = new Dictionary<string, List<ConditionInfo>>();
var objectives = new Dictionary<string, List<ObjectiveInfo>>();
var jobTitle = "No Profession";
string? briefing = null;
if (_minds.TryGetMind(entity, out var mindId, out var mind))
@@ -35,13 +38,15 @@ public sealed class CharacterInfoSystem : EntitySystem
// Get objectives
foreach (var objective in mind.AllObjectives)
{
if (!conditions.ContainsKey(objective.Prototype.Issuer))
conditions[objective.Prototype.Issuer] = new List<ConditionInfo>();
foreach (var condition in objective.Conditions)
{
conditions[objective.Prototype.Issuer].Add(new ConditionInfo(condition.Title,
condition.Description, condition.Icon, condition.Progress));
}
var info = _objectives.GetInfo(objective, mindId, mind);
if (info == null)
continue;
// group objectives by their issuer
var issuer = Comp<ObjectiveComponent>(objective).Issuer;
if (!objectives.ContainsKey(issuer))
objectives[issuer] = new List<ObjectiveInfo>();
objectives[issuer].Add(info.Value);
}
if (_jobs.MindTryGetJobName(mindId, out var jobName))
@@ -51,6 +56,6 @@ public sealed class CharacterInfoSystem : EntitySystem
briefing = _roles.MindGetBriefing(mindId);
}
RaiseNetworkEvent(new CharacterInfoEvent(GetNetEntity(entity), jobTitle, conditions, briefing), args.SenderSession);
RaiseNetworkEvent(new CharacterInfoEvent(GetNetEntity(entity), jobTitle, objectives, briefing), args.SenderSession);
}
}

View File

@@ -1,6 +1,5 @@
using Content.Server.Ninja.Systems;
using Content.Shared.Communications;
using Content.Shared.Objectives;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -18,9 +17,9 @@ public sealed partial class NinjaRuleComponent : Component
public List<EntityUid> Minds = new();
/// <summary>
/// List of objective prototype ids to add
/// List of objective entity prototypes to add
/// </summary>
[DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<ObjectivePrototype>))]
[DataField("objectives", required: true, customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))]
public List<string> Objectives = new();
/// <summary>

View File

@@ -12,6 +12,7 @@ using Content.Shared.CCVar;
using Content.Shared.Dataset;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Preferences;
using Content.Shared.Roles;
@@ -299,8 +300,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (objective == null)
continue;
if (_mindSystem.TryAddObjective(mindId, mind, objective))
difficulty += objective.Difficulty;
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
}

View File

@@ -229,7 +229,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
// assign objectives - must happen after spider charge target so that the obj requirement works
foreach (var objective in config.Objectives)
{
if (!_mind.TryAddObjective(mindId, objective, mind))
if (!_mind.TryAddObjective(mindId, mind, objective))
{
Log.Error($"Failed to add {objective} to ninja {mind.OwnedEntity.Value}");
}

View File

@@ -1,7 +1,7 @@
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Components;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
@@ -39,16 +39,17 @@ namespace Content.Server.Objectives.Commands
}
if (!IoCManager.Resolve<IPrototypeManager>()
.TryIndex<ObjectivePrototype>(args[1], out var objectivePrototype))
.TryIndex<EntityPrototype>(args[1], out var proto) ||
!proto.TryGetComponent<ObjectiveComponent>(out _))
{
shell.WriteLine($"Can't find matching ObjectivePrototype {objectivePrototype}");
shell.WriteLine($"Can't find matching objective prototype {args[1]}");
return;
}
var mindSystem = _entityManager.System<SharedMindSystem>();
if (!mindSystem.TryAddObjective(mindId, mind, objectivePrototype))
if (!minds.TryAddObjective(mindId, mind, args[1]))
{
shell.WriteLine("Objective requirements dont allow that objective to be added.");
// can fail for other reasons so dont pretend to be right
shell.WriteLine("Failed to add the objective. Maybe requirements dont allow that objective to be added.");
}
}
}

View File

@@ -2,6 +2,7 @@ using System.Linq;
using Content.Server.Administration;
using Content.Shared.Administration;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
using Robust.Server.Player;
using Robust.Shared.Console;
@@ -25,7 +26,7 @@ namespace Content.Server.Objectives.Commands
}
var minds = _entities.System<SharedMindSystem>();
if (!minds.TryGetMind(player, out _, out var mind))
if (!minds.TryGetMind(player, out var mindId, out var mind))
{
shell.WriteError(LocalizationManager.GetString("shell-target-entity-does-not-have-message", ("missing", "mind")));
return;
@@ -38,9 +39,20 @@ namespace Content.Server.Objectives.Commands
shell.WriteLine("None.");
}
var objectivesSystem = _entities.System<SharedObjectivesSystem>();
for (var i = 0; i < objectives.Count; i++)
{
shell.WriteLine($"- [{i}] {objectives[i].Conditions[0].Title}");
var info = objectivesSystem.GetInfo(objectives[i], mindId, mind);
if (info == null)
{
shell.WriteLine($"- [{i}] {objectives[i]} - INVALID");
}
else
{
var progress = (int) (info.Value.Progress * 100f);
shell.WriteLine($"- [{i}] {objectives[i]} ({info.Value.Title}) ({progress}%)");
}
}
}

View File

@@ -30,7 +30,7 @@ namespace Content.Server.Objectives.Commands
return;
}
if (!minds.TryGetMind(session, out _, out var mind))
if (!minds.TryGetMind(session, out var mindId, out var mind))
{
shell.WriteLine("Can't find the mind.");
return;
@@ -39,7 +39,7 @@ namespace Content.Server.Objectives.Commands
if (int.TryParse(args[1], out var i))
{
var mindSystem = _entityManager.System<SharedMindSystem>();
shell.WriteLine(mindSystem.TryRemoveObjective(mind, i)
shell.WriteLine(mindSystem.TryRemoveObjective(mindId, mind, i)
? "Objective successfully removed!"
: "Objective removing failed. Maybe the index is out of bounds? Check lsobjectives!");
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player dies to be complete.
/// </summary>
[RegisterComponent, Access(typeof(DieConditionSystem))]
public sealed partial class DieConditionComponent : Component
{
}

View File

@@ -0,0 +1,12 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks.
/// Requires <see cref="NumberObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
public sealed partial class DoorjackConditionComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player is on the emergency shuttle's grid when docking to CentCom.
/// </summary>
[RegisterComponent, Access(typeof(EscapeShuttleConditionSystem))]
public sealed partial class EscapeShuttleConditionComponent : Component
{
}

View File

@@ -0,0 +1,12 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that a target completes half of their objectives.
/// Depends on <see cref="TargetObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
public sealed partial class HelpProgressConditionComponent : Component
{
}

View File

@@ -0,0 +1,12 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that a target stays alive.
/// Depends on <see cref="TargetObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
public sealed partial class KeepAliveConditionComponent : Component
{
}

View File

@@ -0,0 +1,17 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that a target dies or, if <see cref="RequireDead"/> is false, is not on the emergency shuttle.
/// Depends on <see cref="TargetObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class KillPersonConditionComponent : Component
{
/// <summary>
/// Whether the target must be truly dead, ignores missing evac.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool RequireDead = false;
}

View File

@@ -0,0 +1,16 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that there are a certain number of other traitors alive for this objective to be given.
/// </summary>
[RegisterComponent, Access(typeof(MultipleTraitorsRequirementSystem))]
public sealed partial class MultipleTraitorsRequirementComponent : Component
{
/// <summary>
/// Number of traitors, excluding yourself, that have to exist.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int Traitors = 2;
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player is not a member of command.
/// </summary>
[RegisterComponent, Access(typeof(NotCommandRequirementSystem))]
public sealed partial class NotCommandRequirementComponent : Component
{
}

View File

@@ -0,0 +1,17 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
/// <summary>
/// Requires that the player not have a certain job to have this objective.
/// </summary>
[RegisterComponent, Access(typeof(NotJobRequirementSystem))]
public sealed partial class NotJobRequirementComponent : Component
{
/// <summary>
/// ID of the job to ban from having this objective.
/// </summary>
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<JobPrototype>))]
public string Job = string.Empty;
}

View File

@@ -0,0 +1,41 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Objective has a target number of something.
/// When the objective is assigned it randomly picks this target from a minimum to a maximum.
/// </summary>
[RegisterComponent, Access(typeof(NumberObjectiveSystem))]
public sealed partial class NumberObjectiveComponent : Component
{
/// <summary>
/// Number to use in the objective condition.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int Target;
/// <summary>
/// Minimum number for target to roll.
/// </summary>
[DataField(required: true)]
public int Min;
/// <summary>
/// Maximum number for target to roll.
/// </summary>
[DataField(required: true)]
public int Max;
/// <summary>
/// Optional title locale id, passed "count" with <see cref="Target"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string? Title;
/// <summary>
/// Optional description locale id, passed "count" with <see cref="Target"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public string? Description;
}

View File

@@ -0,0 +1,15 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Whitelist;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the objective entity has no blacklisted components.
/// Lets you check for incompatible objectives.
/// </summary>
[RegisterComponent, Access(typeof(ObjectiveBlacklistRequirementSystem))]
public sealed partial class ObjectiveBlacklistRequirementComponent : Component
{
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist Blacklist = new();
}

View File

@@ -0,0 +1,12 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random head.
/// If there are no heads it will fallback to any person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomHeadComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="TargetObjectiveComponent"/> to a random person.
/// </summary>
[RegisterComponent, Access(typeof(KillPersonConditionSystem))]
public sealed partial class PickRandomPersonComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="KeepAliveConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(KeepAliveConditionSystem))]
public sealed partial class RandomTraitorAliveComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Sets the target for <see cref="HelpProgressConditionComponent"/> to a random traitor.
/// </summary>
[RegisterComponent, Access(typeof(HelpProgressConditionSystem))]
public sealed partial class RandomTraitorProgressComponent : Component
{
}

View File

@@ -0,0 +1,15 @@
using Content.Server.Objectives.Systems;
using Content.Shared.Whitelist;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player's mind matches a whitelist.
/// Typical use is checking for (antagonist) roles.
/// </summary>
[RegisterComponent, Access(typeof(RoleRequirementSystem))]
public sealed partial class RoleRequirementComponent : Component
{
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public EntityWhitelist Roles = new();
}

View File

@@ -0,0 +1,9 @@
namespace Content.Server.Objectives.Components;
/// <summary>
/// Marker component for social objectives and kill objectives to be mutually exclusive.
/// </summary>
[RegisterComponent]
public sealed partial class SocialObjectiveComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player is a ninja and blew up their spider charge at its target location.
/// </summary>
[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
public sealed partial class SpiderChargeConditionComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case.
/// </summary>
[RegisterComponent, Access(typeof(SpiderChargeTargetRequirementSystem))]
public sealed partial class SpiderChargeTargetRequirementComponent : Component
{
}

View File

@@ -0,0 +1,28 @@
using Content.Server.Objectives.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that you steal a certain item.
/// </summary>
[RegisterComponent, Access(typeof(StealConditionSystem))]
public sealed partial class StealConditionComponent : Component
{
/// <summary>
/// The id of the item to steal.
/// </summary>
/// <remarks>
/// Works by prototype id not tags or anything so it has to be the exact item.
/// </remarks>
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>)), ViewVariables(VVAccess.ReadWrite)]
public string Prototype = string.Empty;
/// <summary>
/// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
/// instead of "steal advanced magboots. Should be a loc string.
/// </summary>
[DataField("owner"), ViewVariables(VVAccess.ReadWrite)]
public string? OwnerText;
}

View File

@@ -0,0 +1,12 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies.
/// Requires <see cref="NumberObjectiveComponent"/> to function.
/// </summary>
[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
public sealed partial class StealResearchConditionComponent : Component
{
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Just requires that the player is not dead, ignores evac and what not.
/// </summary>
[RegisterComponent, Access(typeof(SurviveConditionSystem))]
public sealed partial class SurviveConditionComponent : Component
{
}

View File

@@ -0,0 +1,21 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
[RegisterComponent, Access(typeof(TargetObjectiveSystem))]
public sealed partial class TargetObjectiveComponent : Component
{
/// <summary>
/// Locale id for the objective title.
/// It is passed "targetName" and "job" arguments.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public string Title = string.Empty;
/// <summary>
/// Mind entity id of the target.
/// This must be set by another system using <see cref="TargetObjectiveSystem.SetTarget"/>.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public EntityUid? Target;
}

View File

@@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Requires that the player is a ninja and has called in a threat.
/// </summary>
[RegisterComponent, Access(typeof(NinjaConditionsSystem))]
public sealed partial class TerrorConditionComponent : Component
{
}

View File

@@ -1,55 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class DieCondition : IObjectiveCondition
{
private MindComponent? _mind;
public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
return new DieCondition { _mind = mind };
}
public string Title => Loc.GetString("objective-condition-die-title");
public string Description => Loc.GetString("objective-condition-die-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Mobs/Ghosts/ghost_human.rsi"), "icon");
public float Progress
{
get
{
var entityManager = IoCManager.Resolve<EntityManager>();
var mindSystem = entityManager.System<SharedMindSystem>();
return _mind == null || mindSystem.IsCharacterDeadIc(_mind) ? 1f : 0f;
}
}
public float Difficulty => 0.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is DieCondition condition && Equals(_mind, condition._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DieCondition) obj);
}
public override int GetHashCode()
{
return (_mind != null ? _mind.GetHashCode() : 0);
}
}
}

View File

@@ -1,70 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have doorjacked at least a random number of airlocks.
/// </summary>
[DataDefinition]
public sealed partial class DoorjackCondition : IObjectiveCondition
{
private EntityUid? _mind;
private int _target;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
// TODO: clamp to number of doors on station incase its somehow a shittle or something
return new DoorjackCondition {
_mind = uid,
_target = IoCManager.Resolve<IRobustRandom>().Next(15, 40)
};
}
public string Title => Loc.GetString("objective-condition-doorjack-title", ("count", _target));
public string Description => Loc.GetString("objective-condition-doorjack-description", ("count", _target));
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Tools/emag.rsi"), "icon");
public float Progress
{
get
{
// prevent divide-by-zero
if (_target == 0)
return 1f;
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
if (role.DoorsJacked >= _target)
return 1f;
return (float) role.DoorsJacked / (float) _target;
}
}
public float Difficulty => 1.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is DoorjackCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is DoorjackCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
}
}

View File

@@ -1,80 +0,0 @@
using Content.Server.Shuttles.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class EscapeShuttleCondition : IObjectiveCondition
{
// TODO refactor all of this to be ecs
private MindComponent? _mind;
public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
return new EscapeShuttleCondition
{
_mind = mind,
};
}
public string Title => Loc.GetString("objective-condition-escape-shuttle-title");
public string Description => Loc.GetString("objective-condition-escape-shuttle-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Structures/Furniture/chairs.rsi"), "shuttle");
public float Progress
{
get {
var entMan = IoCManager.Resolve<IEntityManager>();
var mindSystem = entMan.System<SharedMindSystem>();
if (_mind?.OwnedEntity == null
|| !entMan.TryGetComponent<TransformComponent>(_mind.OwnedEntity, out var xform))
return 0f;
if (mindSystem.IsCharacterDeadIc(_mind))
return 0f;
if (entMan.TryGetComponent<CuffableComponent>(_mind.OwnedEntity, out var cuffed)
&& cuffed.CuffedHandCount > 0)
{
// You're not escaping if you're restrained!
return 0f;
}
// Any emergency shuttle counts for this objective, but not pods.
var emergencyShuttle = entMan.System<EmergencyShuttleSystem>();
if (!emergencyShuttle.IsTargetEscaping(_mind.OwnedEntity.Value))
return 0f;
return 1f;
}
}
public float Difficulty => 1.3f;
public bool Equals(IObjectiveCondition? other)
{
return other is EscapeShuttleCondition esc && Equals(_mind, esc._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((EscapeShuttleCondition) obj);
}
public override int GetHashCode()
{
return _mind != null ? _mind.GetHashCode() : 0;
}
}
}

View File

@@ -1,101 +0,0 @@
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives.Interfaces;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
public abstract class KillPersonCondition : IObjectiveCondition
{
// TODO refactor all of this to be ecs
protected IEntityManager EntityManager => IoCManager.Resolve<IEntityManager>();
protected SharedMindSystem Minds => EntityManager.System<SharedMindSystem>();
protected SharedJobSystem Jobs => EntityManager.System<SharedJobSystem>();
protected MobStateSystem MobStateSystem => EntityManager.System<MobStateSystem>();
protected EntityUid? TargetMindId;
protected MindComponent? TargetMind => EntityManager.GetComponentOrNull<MindComponent>(TargetMindId);
public abstract IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind);
/// <summary>
/// Whether the target must be truly dead, ignores missing evac.
/// </summary>
protected bool RequireDead = false;
public string Title
{
get
{
var mind = TargetMind;
var targetName = mind?.CharacterName ?? "Unknown";
var jobName = Jobs.MindTryGetJobName(TargetMindId);
if (TargetMind == null)
return Loc.GetString("objective-condition-kill-person-title", ("targetName", targetName), ("job", jobName));
return Loc.GetString("objective-condition-kill-person-title", ("targetName", targetName), ("job", jobName));
}
}
public string Description => Loc.GetString("objective-condition-kill-person-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Weapons/Guns/Pistols/viper.rsi"), "icon");
public float Progress
{
get
{
if (TargetMindId == null || TargetMind?.OwnedEntity == null)
return 1f;
var entMan = IoCManager.Resolve<EntityManager>();
var mindSystem = entMan.System<SharedMindSystem>();
if (mindSystem.IsCharacterDeadIc(TargetMind))
return 1f;
if (RequireDead)
return 0f;
// if evac is disabled then they really do have to be dead
var configMan = IoCManager.Resolve<IConfigurationManager>();
if (!configMan.GetCVar(CCVars.EmergencyShuttleEnabled))
return 0f;
// target is escaping so you fail
var emergencyShuttle = entMan.System<EmergencyShuttleSystem>();
if (emergencyShuttle.IsTargetEscaping(TargetMind.OwnedEntity.Value))
return 0f;
// evac has left without the target, greentext since the target is afk in space with a full oxygen tank and coordinates off.
if (emergencyShuttle.ShuttlesLeft)
return 1f;
// if evac is still here and target hasn't boarded, show 50% to give you an indicator that you are doing good
return emergencyShuttle.EmergencyShuttleArrived ? 0.5f : 0f;
}
}
public float Difficulty => 1.75f;
public bool Equals(IObjectiveCondition? other)
{
return other is KillPersonCondition kpc && Equals(TargetMindId, kpc.TargetMindId);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((KillPersonCondition) obj);
}
public override int GetHashCode()
{
return TargetMindId?.GetHashCode() ?? 0;
}
}
}

View File

@@ -1,44 +0,0 @@
using System.Linq;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Conditions;
[DataDefinition]
public sealed partial class KillRandomHeadCondition : KillPersonCondition
{
// TODO refactor all of this to be ecs
public override IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
RequireDead = true;
var allHumans = EntityManager.EntityQuery<MindContainerComponent>(true).Where(mc =>
{
var entity = EntityManagerExt.GetComponentOrNull<MindComponent>(EntityManager, (EntityUid?) mc.Mind)?.OwnedEntity;
if (entity == default)
return false;
return EntityManager.TryGetComponent(entity, out MobStateComponent? mobState) &&
MobStateSystem.IsAlive(entity.Value, mobState) &&
mc.Mind != mindId;
}).Select(mc => mc.Mind).ToList();
if (allHumans.Count == 0)
return new DieCondition(); // I guess I'll die
var allHeads = allHumans
.Where(mind => Jobs.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
.ToList();
if (allHeads.Count == 0)
allHeads = allHumans; // fallback to non-head target
return new KillRandomHeadCondition { TargetMindId = IoCManager.Resolve<IRobustRandom>().Pick(allHeads) };
}
public string Description => Loc.GetString("objective-condition-kill-head-description");
}

View File

@@ -1,36 +0,0 @@
using Content.Shared.Humanoid;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Conditions;
[DataDefinition]
public sealed partial class KillRandomPersonCondition : KillPersonCondition
{
public override IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
var allHumans = new List<EntityUid>();
var query = EntityManager.EntityQuery<MindContainerComponent, HumanoidAppearanceComponent>(true);
foreach (var (mc, _) in query)
{
var entity = EntityManager.GetComponentOrNull<MindComponent>(mc.Mind)?.OwnedEntity;
if (entity == default)
continue;
if (EntityManager.TryGetComponent(entity, out MobStateComponent? mobState) &&
MobStateSystem.IsAlive(entity.Value, mobState) &&
mc.Mind != mindId && mc.Mind != null)
{
allHumans.Add(mc.Mind.Value);
}
}
if (allHumans.Count == 0)
return new DieCondition(); // I guess I'll die
return new KillRandomPersonCondition {TargetMindId = IoCManager.Resolve<IRobustRandom>().Pick(allHumans)};
}
}

View File

@@ -1,85 +0,0 @@
using System.Linq;
using Content.Server.GameTicking.Rules;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
[DataDefinition]
public sealed partial class RandomTraitorAliveCondition : IObjectiveCondition
{
private EntityUid? _targetMind;
public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
var entityMgr = IoCManager.Resolve<IEntityManager>();
var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(entityMgr.System<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind));
if (traitors.Count == 0)
return new EscapeShuttleCondition(); //You were made a traitor by admins, and are the first/only.
return new RandomTraitorAliveCondition { _targetMind = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Id };
}
public string Title
{
get
{
var targetName = string.Empty;
var ents = IoCManager.Resolve<IEntityManager>();
var jobs = ents.System<SharedJobSystem>();
var jobName = jobs.MindTryGetJobName(_targetMind);
if (_targetMind == null)
return Loc.GetString("objective-condition-other-traitor-alive-title", ("targetName", targetName), ("job", jobName));
if (ents.TryGetComponent(_targetMind, out MindComponent? mind) &&
mind.OwnedEntity is {Valid: true} owned)
{
targetName = ents.GetComponent<MetaDataComponent>(owned).EntityName;
}
return Loc.GetString("objective-condition-other-traitor-alive-title", ("targetName", targetName), ("job", jobName));
}
}
public string Description => Loc.GetString("objective-condition-other-traitor-alive-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Misc/bureaucracy.rsi"), "folder-white");
public float Progress
{
get
{
var entityManager = IoCManager.Resolve<EntityManager>();
var mindSystem = entityManager.System<SharedMindSystem>();
return !entityManager.TryGetComponent(_targetMind, out MindComponent? mind) ||
!mindSystem.IsCharacterDeadIc(mind)
? 1f
: 0f;
}
}
public float Difficulty => 1.75f;
public bool Equals(IObjectiveCondition? other)
{
return other is RandomTraitorAliveCondition kpc && Equals(_targetMind, kpc._targetMind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is RandomTraitorAliveCondition alive && alive.Equals(this);
}
public override int GetHashCode()
{
return _targetMind?.GetHashCode() ?? 0;
}
}
}

View File

@@ -1,134 +0,0 @@
using System.Linq;
using Content.Server.GameTicking.Rules;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
[DataDefinition]
public sealed partial class RandomTraitorProgressCondition : IObjectiveCondition
{
// TODO ecs all of this
private EntityUid? _targetMind;
public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
//todo shit of a fuck
var entityMgr = IoCManager.Resolve<IEntityManager>();
var traitors = entityMgr.System<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind).ToList();
List<EntityUid> removeList = new();
foreach (var traitor in traitors)
{
foreach (var objective in traitor.Mind.AllObjectives)
{
foreach (var condition in objective.Conditions)
{
if (condition is RandomTraitorProgressCondition)
{
removeList.Add(traitor.Id);
}
}
}
}
foreach (var traitor in removeList)
{
traitors.RemoveAll(t => t.Id == traitor);
}
if (traitors.Count == 0) return new EscapeShuttleCondition{}; //You were made a traitor by admins, and are the first/only.
return new RandomTraitorProgressCondition { _targetMind = IoCManager.Resolve<IRobustRandom>().Pick(traitors).Id };
}
public string Title
{
get
{
var targetName = string.Empty;
var entities = IoCManager.Resolve<IEntityManager>();
var jobs = entities.System<SharedJobSystem>();
var jobName = jobs.MindTryGetJobName(_targetMind);
if (_targetMind == null)
return Loc.GetString("objective-condition-other-traitor-progress-title", ("targetName", targetName), ("job", jobName));
if (entities.TryGetComponent(_targetMind, out MindComponent? mind) &&
mind.OwnedEntity is {Valid: true} owned)
{
targetName = entities.GetComponent<MetaDataComponent>(owned).EntityName;
}
return Loc.GetString("objective-condition-other-traitor-progress-title", ("targetName", targetName), ("job", jobName));
}
}
public string Description => Loc.GetString("objective-condition-other-traitor-progress-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ("Objects/Misc/bureaucracy.rsi"), "folder-white");
public float Progress
{
get
{
float total = 0f; // how much progress they have
float max = 0f; // how much progress is needed for 100%
if (_targetMind == null)
{
Logger.Error("Null target on RandomTraitorProgressCondition.");
return 1f;
}
var entities = IoCManager.Resolve<IEntityManager>();
if (entities.TryGetComponent(_targetMind, out MindComponent? mind))
{
foreach (var objective in mind.AllObjectives)
{
foreach (var condition in objective.Conditions)
{
max++; // things can only be up to 100% complete yeah
total += condition.Progress;
}
}
}
if (max == 0f)
{
Logger.Error("RandomTraitorProgressCondition assigned someone with no objectives to be helped.");
return 1f;
}
var completion = total / max;
if (completion >= 0.5f)
return 1f;
else
return completion / 0.5f;
}
}
public float Difficulty => 2.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is RandomTraitorProgressCondition kpc && Equals(_targetMind, kpc._targetMind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is RandomTraitorProgressCondition alive && alive.Equals(this);
}
public override int GetHashCode()
{
return _targetMind?.GetHashCode() ?? 0;
}
}
}

View File

@@ -1,75 +0,0 @@
using Content.Server.Roles;
using Content.Server.Warps;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have detonated their spider charge.
/// </summary>
[DataDefinition]
public sealed partial class SpiderChargeCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new SpiderChargeCondition {
_mind = uid
};
}
public string Title
{
get
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role)
|| role.SpiderChargeTarget == null
|| !entMan.TryGetComponent<WarpPointComponent>(role.SpiderChargeTarget, out var warp)
|| warp.Location == null)
// this should never really happen but eh
return Loc.GetString("objective-condition-spider-charge-no-target");
return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location));
}
}
public string Description => Loc.GetString("objective-condition-spider-charge-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Weapons/Bombs/spidercharge.rsi"), "icon");
public float Progress
{
get
{
var entMan = IoCManager.Resolve<EntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
return role.SpiderChargeDetonated ? 1f : 0f;
}
}
public float Difficulty => 2.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is SpiderChargeCondition cond && Equals(_mind, cond._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is SpiderChargeCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return _mind?.GetHashCode() ?? 0;
}
}

View File

@@ -1,115 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions
{
// Oh god my eyes
[UsedImplicitly]
[DataDefinition]
public sealed partial class StealCondition : IObjectiveCondition, ISerializationHooks
{
private EntityUid? _mind;
[DataField("prototype")] private string _prototypeId = string.Empty;
/// <summary>
/// Help newer players by saying e.g. "steal the chief engineer's advanced magboots"
/// instead of "steal advanced magboots. Should be a loc string.
/// </summary>
[DataField("owner")] private string? _owner = null;
public IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind)
{
return new StealCondition
{
_mind = mindId,
_prototypeId = _prototypeId,
_owner = _owner
};
}
private string PrototypeName =>
IoCManager.Resolve<IPrototypeManager>().TryIndex<EntityPrototype>(_prototypeId, out var prototype)
? prototype.Name
: "[CANNOT FIND NAME]";
public string Title =>
_owner == null
? Loc.GetString("objective-condition-steal-title-no-owner", ("itemName", Loc.GetString(PrototypeName)))
: Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(_owner)), ("itemName", Loc.GetString(PrototypeName)));
public string Description => Loc.GetString("objective-condition-steal-description",("itemName", Loc.GetString(PrototypeName)));
public SpriteSpecifier Icon => new SpriteSpecifier.EntityPrototype(_prototypeId);
public float Progress
{
get
{
var entMan = IoCManager.Resolve<IEntityManager>();
// TODO make this a container system function
// or: just iterate through transform children, instead of containers?
var metaQuery = entMan.GetEntityQuery<MetaDataComponent>();
var managerQuery = entMan.GetEntityQuery<ContainerManagerComponent>();
var stack = new Stack<ContainerManagerComponent>();
if (!entMan.TryGetComponent(_mind, out MindComponent? mind))
return 0;
if (!metaQuery.TryGetComponent(mind.OwnedEntity, out var meta))
return 0;
if (meta.EntityPrototype?.ID == _prototypeId)
return 1;
if (!managerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager))
return 0;
do
{
foreach (var container in currentManager.Containers.Values)
{
foreach (var entity in container.ContainedEntities)
{
if (metaQuery.GetComponent(entity).EntityPrototype?.ID == _prototypeId)
return 1;
if (!managerQuery.TryGetComponent(entity, out var containerManager))
continue;
stack.Push(containerManager);
}
}
} while (stack.TryPop(out currentManager));
return 0;
}
}
public float Difficulty => 2.25f;
public bool Equals(IObjectiveCondition? other)
{
return other is StealCondition stealCondition &&
Equals(_mind, stealCondition._mind) &&
_prototypeId == stealCondition._prototypeId;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((StealCondition) obj);
}
public override int GetHashCode()
{
return HashCode.Combine(_mind, _prototypeId);
}
}
}

View File

@@ -1,70 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Random;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have stolen at least a random number of technologies.
/// </summary>
[DataDefinition]
public sealed partial class StealResearchCondition : IObjectiveCondition
{
private EntityUid? _mind;
private int _target;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
// TODO: clamp to number of research nodes in a single discipline maybe so easily maintainable
return new StealResearchCondition {
_mind = uid,
_target = IoCManager.Resolve<IRobustRandom>().Next(5, 10)
};
}
public string Title => Loc.GetString("objective-condition-steal-research-title", ("count", _target));
public string Description => Loc.GetString("objective-condition-steal-research-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Structures/Machines/server.rsi"), "server");
public float Progress
{
get
{
// prevent divide-by-zero
if (_target == 0)
return 1f;
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
if (role.DownloadedNodes.Count >= _target)
return 1f;
return (float) role.DownloadedNodes.Count / (float) _target;
}
}
public float Difficulty => 2.5f;
public bool Equals(IObjectiveCondition? other)
{
return other is StealResearchCondition cond && Equals(_mind, cond._mind) && _target == cond._target;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is StealResearchCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return HashCode.Combine(_mind?.GetHashCode() ?? 0, _target);
}
}

View File

@@ -1,58 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Just requires that the player is not dead, ignores evac and what not.
/// </summary>
[DataDefinition]
public sealed partial class SurviveCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new SurviveCondition {_mind = uid};
}
public string Title => Loc.GetString("objective-condition-survive-title");
public string Description => Loc.GetString("objective-condition-survive-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Clothing/Mask/ninja.rsi"), "icon");
public float Difficulty => 0.5f;
public float Progress
{
get
{
var entMan = IoCManager.Resolve<IEntityManager>();
if (!entMan.TryGetComponent<MindComponent>(_mind, out var mind))
return 0f;
var mindSystem = entMan.System<SharedMindSystem>();
return mindSystem.IsCharacterDeadIc(mind) ? 0f : 1f;
}
}
public bool Equals(IObjectiveCondition? other)
{
return other is SurviveCondition condition && Equals(_mind, condition._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((SurviveCondition) obj);
}
public override int GetHashCode()
{
return (_mind != null ? _mind.GetHashCode() : 0);
}
}

View File

@@ -1,57 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Conditions;
/// <summary>
/// Objective condition that requires the player to be a ninja and have called in a threat.
/// </summary>
[DataDefinition]
public sealed partial class TerrorCondition : IObjectiveCondition
{
private EntityUid? _mind;
public IObjectiveCondition GetAssigned(EntityUid uid, MindComponent mind)
{
return new TerrorCondition {_mind = uid};
}
public string Title => Loc.GetString("objective-condition-terror-title");
public string Description => Loc.GetString("objective-condition-terror-description");
public SpriteSpecifier Icon => new SpriteSpecifier.Rsi(new ResPath("Objects/Fun/Instruments/otherinstruments.rsi"), "red_phone");
public float Progress
{
get
{
var entMan = IoCManager.Resolve<EntityManager>();
if (!entMan.TryGetComponent<NinjaRoleComponent>(_mind, out var role))
return 0f;
return role.CalledInThreat ? 1f : 0f;
}
}
public float Difficulty => 2.75f;
public bool Equals(IObjectiveCondition? other)
{
return other is TerrorCondition cond && Equals(_mind, cond._mind);
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj is TerrorCondition cond && cond.Equals(this);
}
public override int GetHashCode()
{
return _mind?.GetHashCode() ?? 0;
}
}

View File

@@ -2,7 +2,8 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Mind;
using Content.Shared.Mind;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Content.Shared.Random;
using Content.Shared.Random.Helpers;
using Robust.Shared.Prototypes;
@@ -11,7 +12,7 @@ using System.Linq;
namespace Content.Server.Objectives;
public sealed class ObjectivesSystem : EntitySystem
public sealed class ObjectivesSystem : SharedObjectivesSystem
{
[Dependency] private readonly GameTicker _gameTicker = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
@@ -30,6 +31,8 @@ public sealed class ObjectivesSystem : EntitySystem
/// </summary>
private void OnRoundEndText(RoundEndTextAppendEvent ev)
{
// go through each gamerule getting data for the roundend summary.
var summaries = new Dictionary<string, Dictionary<string, List<EntityUid>>>();
var query = EntityQueryEnumerator<GameRuleComponent>();
while (query.MoveNext(out var uid, out var gameRule))
{
@@ -41,90 +44,133 @@ public sealed class ObjectivesSystem : EntitySystem
if (info.Minds.Count == 0)
continue;
// first group the gamerules by their agents, for example 2 different dragons
var agent = info.AgentName;
var result = Loc.GetString("objectives-round-end-result", ("count", info.Minds.Count), ("agent", agent));
var prepend = new ObjectivesTextPrependEvent(result);
if (!summaries.ContainsKey(agent))
summaries[agent] = new Dictionary<string, List<EntityUid>>();
var prepend = new ObjectivesTextPrependEvent("");
RaiseLocalEvent(uid, ref prepend);
// space between the start text and player list
result = prepend.Text + "\n";
foreach (var mindId in info.Minds)
// next group them by their prepended texts
// for example with traitor rule, group them by the codewords they share
var summary = summaries[agent];
if (summary.ContainsKey(prepend.Text))
{
if (!TryComp(mindId, out MindComponent? mind))
continue;
// same prepended text (usually empty) so combine them
summary[prepend.Text].AddRange(info.Minds);
}
else
{
summary[prepend.Text] = info.Minds;
}
}
var name = mind.CharacterName;
_mind.TryGetSession(mindId, out var session);
var username = session?.Name;
// convert the data into summary text
foreach (var (agent, summary) in summaries)
{
// first get the total number of players that were in these game rules combined
var total = 0;
foreach (var (_, minds) in summary)
{
total += minds.Count;
}
string title;
if (username != null)
{
if (name != null)
title = Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
else
title = Loc.GetString("objectives-player-user", ("user", username));
}
else
{
// nothing to identify the player by, just give up
if (name == null)
continue;
title = Loc.GetString("objectives-player-named", ("name", name));
}
var result = Loc.GetString("objectives-round-end-result", ("count", total), ("agent", agent));
// next add all the players with its own prepended text
foreach (var (prepend, minds) in summary)
{
if (prepend != string.Empty)
result += prepend;
// add space between the start text and player list
result += "\n";
var objectives = mind.AllObjectives.ToArray();
if (objectives.Length == 0)
{
result += Loc.GetString("objectives-no-objectives", ("title", title), ("agent", agent));
continue;
}
result += Loc.GetString("objectives-with-objectives", ("title", title), ("agent", agent));
foreach (var objectiveGroup in objectives.GroupBy(o => o.Prototype.Issuer))
{
result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}");
foreach (var objective in objectiveGroup)
{
foreach (var condition in objective.Conditions)
{
var progress = condition.Progress;
if (progress > 0.99f)
{
result += "\n- " + Loc.GetString(
"objectives-condition-success",
("condition", condition.Title),
("markupColor", "green")
);
}
else
{
result += "\n- " + Loc.GetString(
"objectives-condition-fail",
("condition", condition.Title),
("progress", (int) (progress * 100)),
("markupColor", "red")
);
}
}
}
}
AddSummary(ref result, agent, minds);
}
ev.AddLine(result + "\n");
}
}
public ObjectivePrototype? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
private void AddSummary(ref string result, string agent, List<EntityUid> minds)
{
foreach (var mindId in minds)
{
if (!TryComp(mindId, out MindComponent? mind))
continue;
var name = mind.CharacterName;
_mind.TryGetSession(mindId, out var session);
var username = session?.Name;
string title;
if (username != null)
{
if (name != null)
title = Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
else
title = Loc.GetString("objectives-player-user", ("user", username));
}
else
{
// nothing to identify the player by, just give up
if (name == null)
continue;
title = Loc.GetString("objectives-player-named", ("name", name));
}
result += "\n";
var objectives = mind.AllObjectives.ToArray();
if (objectives.Length == 0)
{
result += Loc.GetString("objectives-no-objectives", ("title", title), ("agent", agent));
continue;
}
result += Loc.GetString("objectives-with-objectives", ("title", title), ("agent", agent));
foreach (var objectiveGroup in objectives.GroupBy(o => Comp<ObjectiveComponent>(o).Issuer))
{
result += "\n" + Loc.GetString($"objective-issuer-{objectiveGroup.Key}");
foreach (var objective in objectiveGroup)
{
var info = GetInfo(objective, mindId, mind);
if (info == null)
continue;
var objectiveTitle = info.Value.Title;
var progress = info.Value.Progress;
if (progress > 0.99f)
{
result += "\n- " + Loc.GetString(
"objectives-objective-success",
("objective", objectiveTitle),
("markupColor", "green")
);
}
else
{
result += "\n- " + Loc.GetString(
"objectives-objective-fail",
("objective", objectiveTitle),
("progress", (int) (progress * 100)),
("markupColor", "red")
);
}
}
}
}
}
public EntityUid? GetRandomObjective(EntityUid mindId, MindComponent mind, string objectiveGroupProto)
{
if (!_prototypeManager.TryIndex<WeightedRandomPrototype>(objectiveGroupProto, out var groups))
{
Log.Error("Tried to get a random objective, but can't index WeightedRandomPrototype " + objectiveGroupProto);
Log.Error($"Tried to get a random objective, but can't index WeightedRandomPrototype {objectiveGroupProto}");
return null;
}
@@ -137,15 +183,16 @@ public sealed class ObjectivesSystem : EntitySystem
if (!_prototypeManager.TryIndex<WeightedRandomPrototype>(groupName, out var group))
{
Log.Error("Couldn't index objective group prototype" + groupName);
Log.Error($"Couldn't index objective group prototype {groupName}");
return null;
}
if (_prototypeManager.TryIndex<ObjectivePrototype>(group.Pick(_random), out var objective)
&& objective.CanBeAssigned(mindId, mind))
var proto = group.Pick(_random);
var objective = TryCreateObjective(mindId, mind, proto);
if (objective != null)
return objective;
else
tries++;
tries++;
}
return null;

View File

@@ -1,28 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements
{
[DataDefinition]
public sealed partial class IncompatibleConditionsRequirement : IObjectiveRequirement
{
[DataField("conditions")]
private List<string> _incompatibleConditions = new();
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
foreach (var objective in mind.AllObjectives)
{
foreach (var condition in objective.Conditions)
{
foreach (var incompatibleCondition in _incompatibleConditions)
{
if (incompatibleCondition == condition.GetType().Name) return false;
}
}
}
return true;
}
}
}

View File

@@ -1,25 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements
{
[DataDefinition]
public sealed partial class IncompatibleObjectivesRequirement : IObjectiveRequirement
{
[DataField("objectives")]
private List<string> _incompatibleObjectives = new();
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
foreach (var objective in mind.AllObjectives)
{
foreach (var incompatibleObjective in _incompatibleObjectives)
{
if (incompatibleObjective == objective.Prototype.ID) return false;
}
}
return true;
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.GameTicking.Rules;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements
{
[DataDefinition]
public sealed partial class MultipleTraitorsRequirement : IObjectiveRequirement
{
[DataField("traitors")]
private int _requiredTraitors = 2;
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
return EntitySystem.Get<TraitorRuleSystem>().GetOtherTraitorMindsAliveAndConnected(mind).Count >= _requiredTraitors;
}
}
}

View File

@@ -1,18 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements;
/// <summary>
/// Requires the player's mind to have the ninja role component, aka be a ninja.
/// </summary>
[DataDefinition]
public sealed partial class NinjaRequirement : IObjectiveRequirement
{
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
var entMan = IoCManager.Resolve<IEntityManager>();
return entMan.HasComponent<NinjaRoleComponent>(mindId);
}
}

View File

@@ -1,28 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Objectives.Requirements
{
[DataDefinition]
public sealed partial class NotRoleRequirement : IObjectiveRequirement
{
[DataField("roleId", customTypeSerializer:typeof(PrototypeIdSerializer<JobPrototype>), required:true)]
private string _roleId = default!;
/// <summary>
/// This requirement is met if the traitor is NOT the roleId, and fails if they are.
/// </summary>
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
// TODO ECS this shit i keep seeing shitcode everywhere
var entities = IoCManager.Resolve<IEntityManager>();
if (!entities.TryGetComponent(mindId, out JobComponent? job))
return true;
return job.PrototypeId != _roleId;
}
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Server.Objectives.Requirements;
/// <summary>
/// Requires the player to be a ninja that has a spider charge target assigned, which is almost always the case.
/// </summary>
[DataDefinition]
public sealed partial class SpiderChargeTargetRequirement : IObjectiveRequirement
{
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
var entMan = IoCManager.Resolve<IEntityManager>();
entMan.TryGetComponent<NinjaRoleComponent>(mindId, out var role);
return role?.SpiderChargeTarget != null;
}
}

View File

@@ -1,19 +0,0 @@
using Content.Server.Roles;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Content.Shared.Roles;
using JetBrains.Annotations;
namespace Content.Server.Objectives.Requirements
{
[UsedImplicitly]
[DataDefinition]
public sealed partial class TraitorRequirement : IObjectiveRequirement
{
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
var roleSystem = IoCManager.Resolve<IEntityManager>().System<SharedRoleSystem>();
return roleSystem.MindHasRole<TraitorRoleComponent>(mindId);
}
}
}

View File

@@ -0,0 +1,22 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
public sealed class DieConditionSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DieConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, DieConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = _mind.IsCharacterDeadIc(args.Mind) ? 1f : 0f;
}
}

View File

@@ -0,0 +1,39 @@
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.Cuffs.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
public sealed class EscapeShuttleConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EscapeShuttleConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, EscapeShuttleConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(args.MindId, args.Mind);
}
private float GetProgress(EntityUid mindId, MindComponent mind)
{
// not escaping alive if you're deleted/dead
if (mind.OwnedEntity == null || _mind.IsCharacterDeadIc(mind))
return 0f;
// You're not escaping if you're restrained!
if (TryComp<CuffableComponent>(mind.OwnedEntity, out var cuffed) && cuffed.CuffedHandCount > 0)
return 0f;
// Any emergency shuttle counts for this objective, but not pods.
return _emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value) ? 1f : 0f;
}
}

View File

@@ -0,0 +1,111 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles help progress condition logic and picking random help targets.
/// </summary>
public sealed class HelpProgressConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HelpProgressConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<RandomTraitorProgressComponent, ObjectiveAssignedEvent>(OnTraitorAssigned);
}
private void OnGetProgress(EntityUid uid, HelpProgressConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetProgress(target.Value);
}
private void OnTraitorAssigned(EntityUid uid, RandomTraitorProgressComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
var traitors = _traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind)
.Select(pair => pair.Item1)
.ToHashSet();
var removeList = new List<EntityUid>();
// cant help anyone who is tasked with helping:
// 1. thats boring
// 2. no cyclic progress dependencies!!!
foreach (var traitor in traitors)
{
// TODO: replace this with TryComp<ObjectivesComponent>(traitor) or something when objectives are moved out of mind
if (!TryComp<MindComponent>(traitor, out var mind))
continue;
foreach (var objective in mind.AllObjectives)
{
if (HasComp<HelpProgressConditionComponent>(objective))
removeList.Add(traitor);
}
}
foreach (var tot in removeList)
{
traitors.Remove(tot);
}
// no more helpable traitors
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(traitors), target);
}
private float GetProgress(EntityUid target)
{
var total = 0f; // how much progress they have
var max = 0f; // how much progress is needed for 100%
if (TryComp<MindComponent>(target, out var mind))
{
foreach (var objective in mind.AllObjectives)
{
// this has the potential to loop forever, anything setting target has to check that there is no HelpProgressCondition.
var info = _objectives.GetInfo(objective, target, mind);
if (info == null)
continue;
max++; // things can only be up to 100% complete yeah
total += info.Value.Progress;
}
}
// no objectives that can be helped with...
if (max == 0f)
return 1f;
// require 50% completion for this one to be complete
var completion = total / max;
return completion >= 0.5f ? 1f : completion / 0.5f;
}
}

View File

@@ -0,0 +1,66 @@
using Content.Server.Objectives.Components;
using Content.Server.GameTicking.Rules;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Random;
using System.Linq;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles keep alive condition logic and picking random traitors to keep alive.
/// </summary>
public sealed class KeepAliveConditionSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KeepAliveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<RandomTraitorAliveComponent, ObjectiveAssignedEvent>(OnAssigned);
}
private void OnGetProgress(EntityUid uid, KeepAliveConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetProgress(target.Value);
}
private void OnAssigned(EntityUid uid, RandomTraitorAliveComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
var traitors = Enumerable.ToList<(EntityUid Id, MindComponent Mind)>(_traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind));
// You are the first/only traitor.
if (traitors.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(traitors).Id, target);
}
private float GetProgress(EntityUid target)
{
if (!TryComp<MindComponent>(target, out var mind))
return 0f;
return _mind.IsCharacterDeadIc(mind) ? 0f : 1f;
}
}

View File

@@ -0,0 +1,131 @@
using Content.Server.Objectives.Components;
using Content.Server.Shuttles.Systems;
using Content.Shared.CCVar;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.Configuration;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles kill person condition logic and picking random kill targets.
/// </summary>
public sealed class KillPersonConditionSystem : EntitySystem
{
[Dependency] private readonly EmergencyShuttleSystem _emergencyShuttle = default!;
[Dependency] private readonly IConfigurationManager _config = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly TargetObjectiveSystem _target = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<KillPersonConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
SubscribeLocalEvent<PickRandomPersonComponent, ObjectiveAssignedEvent>(OnPersonAssigned);
SubscribeLocalEvent<PickRandomHeadComponent, ObjectiveAssignedEvent>(OnHeadAssigned);
}
private void OnGetProgress(EntityUid uid, KillPersonConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
if (!_target.GetTarget(uid, out var target))
return;
args.Progress = GetProgress(target.Value, comp.RequireDead);
}
private void OnPersonAssigned(EntityUid uid, PickRandomPersonComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid objective prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumansExcept(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
_target.SetTarget(uid, _random.Pick(allHumans), target);
}
private void OnHeadAssigned(EntityUid uid, PickRandomHeadComponent comp, ref ObjectiveAssignedEvent args)
{
// invalid prototype
if (!TryComp<TargetObjectiveComponent>(uid, out var target))
{
args.Cancelled = true;
return;
}
// target already assigned
if (target.Target != null)
return;
// no other humans to kill
var allHumans = _mind.GetAliveHumansExcept(args.MindId);
if (allHumans.Count == 0)
{
args.Cancelled = true;
return;
}
var allHeads = new List<EntityUid>();
foreach (var mind in allHumans)
{
// RequireAdminNotify used as a cheap way to check for command department
if (_job.MindTryGetJob(mind, out _, out var prototype) && prototype.RequireAdminNotify)
allHeads.Add(mind);
}
if (allHeads.Count == 0)
allHeads = allHumans; // fallback to non-head target
_target.SetTarget(uid, _random.Pick(allHeads), target);
}
private float GetProgress(EntityUid target, bool requireDead)
{
// deleted or gibbed or something, counts as dead
if (!TryComp<MindComponent>(target, out var mind) || mind.OwnedEntity == null)
return 1f;
// dead is success
if (_mind.IsCharacterDeadIc(mind))
return 1f;
// if the target has to be dead dead then don't check evac stuff
if (requireDead)
return 0f;
// if evac is disabled then they really do have to be dead
if (!_config.GetCVar(CCVars.EmergencyShuttleEnabled))
return 0f;
// target is escaping so you fail
if (_emergencyShuttle.IsTargetEscaping(mind.OwnedEntity.Value))
return 0f;
// evac has left without the target, greentext since the target is afk in space with a full oxygen tank and coordinates off.
if (_emergencyShuttle.ShuttlesLeft)
return 1f;
// if evac is still here and target hasn't boarded, show 50% to give you an indicator that you are doing good
return _emergencyShuttle.EmergencyShuttleArrived ? 0.5f : 0f;
}
}

View File

@@ -0,0 +1,29 @@
using Content.Server.GameTicking.Rules;
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles requiring multiple traitors being alive for the objective to be given.
/// </summary>
public sealed class MultipleTraitorsRequirementSystem : EntitySystem
{
[Dependency] private readonly TraitorRuleSystem _traitorRule = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MultipleTraitorsRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, MultipleTraitorsRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
if (_traitorRule.GetOtherTraitorMindsAliveAndConnected(args.Mind).Count < comp.Traitors)
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,106 @@
using Content.Server.Roles;
using Content.Server.Objectives.Components;
using Content.Server.Warps;
using Content.Shared.Objectives.Components;
using Robust.Shared.GameObjects;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles the objective conditions that hard depend on ninja.
/// Survive is handled by <see cref="SurviveConditionSystem"/> since it works without being a ninja.
/// </summary>
public sealed class NinjaConditionsSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly NumberObjectiveSystem _number = default!;
public override void Initialize()
{
SubscribeLocalEvent<DoorjackConditionComponent, ObjectiveGetProgressEvent>(OnDoorjackGetProgress);
SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveAfterAssignEvent>(OnSpiderChargeAfterAssign);
SubscribeLocalEvent<SpiderChargeConditionComponent, ObjectiveGetProgressEvent>(OnSpiderChargeGetProgress);
SubscribeLocalEvent<StealResearchConditionComponent, ObjectiveGetProgressEvent>(OnStealResearchGetProgress);
SubscribeLocalEvent<TerrorConditionComponent, ObjectiveGetProgressEvent>(OnTerrorGetProgress);
}
// doorjack
private void OnDoorjackGetProgress(EntityUid uid, DoorjackConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = DoorjackProgress(args.MindId, _number.GetTarget(uid));
}
private float DoorjackProgress(EntityUid mindId, int target)
{
// prevent divide-by-zero
if (target == 0)
return 1f;
if (!TryComp<NinjaRoleComponent>(mindId, out var role))
return 0f;
if (role.DoorsJacked >= target)
return 1f;
return (float) role.DoorsJacked / (float) target;
}
// spider charge
private void OnSpiderChargeAfterAssign(EntityUid uid, SpiderChargeConditionComponent comp, ref ObjectiveAfterAssignEvent args)
{
_metaData.SetEntityName(uid, SpiderChargeTitle(args.MindId), args.Meta);
}
private void OnSpiderChargeGetProgress(EntityUid uid, SpiderChargeConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = TryComp<NinjaRoleComponent>(args.MindId, out var role) && role.SpiderChargeDetonated ? 1f : 0f;
}
private string SpiderChargeTitle(EntityUid mindId)
{
if (!TryComp<NinjaRoleComponent>(mindId, out var role) ||
role.SpiderChargeTarget == null ||
!TryComp<WarpPointComponent>(role.SpiderChargeTarget, out var warp) ||
warp.Location == null)
{
// this should never really happen but eh
return Loc.GetString("objective-condition-spider-charge-title-no-target");
}
return Loc.GetString("objective-condition-spider-charge-title", ("location", warp.Location));
}
// steal research
private void OnStealResearchGetProgress(EntityUid uid, StealResearchConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = StealResearchProgress(args.MindId, _number.GetTarget(uid));
}
private float StealResearchProgress(EntityUid mindId, int target)
{
// prevent divide-by-zero
if (target == 0)
return 1f;
if (!TryComp<NinjaRoleComponent>(mindId, out var role))
return 0f;
if (role.DownloadedNodes.Count >= target)
return 1f;
return (float) role.DownloadedNodes.Count / (float) target;
}
// terror
private void OnTerrorGetProgress(EntityUid uid, TerrorConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = TryComp<NinjaRoleComponent>(args.MindId, out var role) && role.CalledInThreat ? 1f : 0f;
}
}

View File

@@ -0,0 +1,27 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Objectives.Systems;
public sealed class NotCommandRequirementSystem : EntitySystem
{
[Dependency] private readonly SharedJobSystem _job = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NotCommandRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, NotCommandRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
// cheap equivalent to checking that job department is command, since all command members require admin notification when leaving
if (_job.MindTryGetJob(args.MindId, out _, out var prototype) && prototype.RequireAdminNotify)
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,31 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles checking the job blacklist for this objective.
/// </summary>
public sealed class NotJobRequirementSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NotJobRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, NotJobRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
// if player has no job then don't care
if (!TryComp<JobComponent>(args.MindId, out var job))
return;
if (job.PrototypeId == comp.Job)
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,48 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
using Robust.Shared.GameObjects;
using Robust.Shared.Random;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Provides API for other components, handles picking the count and setting the title and description.
/// </summary>
public sealed class NumberObjectiveSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<NumberObjectiveComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<NumberObjectiveComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
}
private void OnAssigned(EntityUid uid, NumberObjectiveComponent comp, ref ObjectiveAssignedEvent args)
{
comp.Target = _random.Next(comp.Min, comp.Max);
}
private void OnAfterAssign(EntityUid uid, NumberObjectiveComponent comp, ref ObjectiveAfterAssignEvent args)
{
if (comp.Title != null)
_metaData.SetEntityName(uid, Loc.GetString(comp.Title, ("count", comp.Target)), args.Meta);
if (comp.Description != null)
_metaData.SetEntityDescription(uid, Loc.GetString(comp.Description, ("count", comp.Target)), args.Meta);
}
/// <summary>
/// Gets the objective's target count.
/// </summary>
public int GetTarget(EntityUid uid, NumberObjectiveComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return 0;
return comp.Target;
}
}

View File

@@ -0,0 +1,26 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles applying the objective component blacklist to the objective entity.
/// </summary>
public sealed class ObjectiveBlacklistRequirementSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ObjectiveBlacklistRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, ObjectiveBlacklistRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
if (comp.Blacklist.IsValid(uid, EntityManager))
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,28 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles role requirement for objectives that require a certain (probably antagonist) role(s).
/// </summary>
public sealed class RoleRequirementSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RoleRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, RoleRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
// this whitelist trick only works because roles are components on the mind and not entities
// if that gets reworked then this will need changing
if (!comp.Roles.IsValid(args.MindId, EntityManager))
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,24 @@
using Content.Server.Objectives.Components;
using Content.Server.Roles;
using Content.Shared.Objectives.Components;
namespace Content.Server.Objectives.Systems;
public sealed class SpiderChargeTargetRequirementSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SpiderChargeTargetRequirementComponent, RequirementCheckEvent>(OnCheck);
}
private void OnCheck(EntityUid uid, SpiderChargeTargetRequirementComponent comp, ref RequirementCheckEvent args)
{
if (args.Cancelled)
return;
if (!TryComp<NinjaRoleComponent>(args.MindId, out var role) || role.SpiderChargeTarget == null)
args.Cancelled = true;
}
}

View File

@@ -0,0 +1,93 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Server.Objectives.Systems;
public sealed class StealConditionSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
private EntityQuery<ContainerManagerComponent> containerQuery;
private EntityQuery<MetaDataComponent> metaQuery;
public override void Initialize()
{
base.Initialize();
containerQuery = GetEntityQuery<ContainerManagerComponent>();
metaQuery = GetEntityQuery<MetaDataComponent>();
SubscribeLocalEvent<StealConditionComponent, ObjectiveAssignedEvent>(OnAssigned);
SubscribeLocalEvent<StealConditionComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
SubscribeLocalEvent<StealConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnAssigned(EntityUid uid, StealConditionComponent comp, ref ObjectiveAssignedEvent args)
{
// cancel if the item to steal doesn't exist
args.Cancelled |= !_proto.HasIndex<EntityPrototype>(comp.Prototype);
}
private void OnAfterAssign(EntityUid uid, StealConditionComponent comp, ref ObjectiveAfterAssignEvent args)
{
var proto = _proto.Index<EntityPrototype>(comp.Prototype);
var title = comp.OwnerText == null
? Loc.GetString("objective-condition-steal-title-no-owner", ("itemName", proto.Name))
: Loc.GetString("objective-condition-steal-title", ("owner", Loc.GetString(comp.OwnerText)), ("itemName", proto.Name));
var description = Loc.GetString("objective-condition-steal-description", ("itemName", proto.Name));
_metaData.SetEntityName(uid, title, args.Meta);
_metaData.SetEntityDescription(uid, description, args.Meta);
_objectives.SetIcon(uid, new SpriteSpecifier.EntityPrototype(comp.Prototype), args.Objective);
}
private void OnGetProgress(EntityUid uid, StealConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = GetProgress(args.Mind, comp.Prototype);
}
private float GetProgress(MindComponent mind, string prototype)
{
// TODO make this a container system function
// or: just iterate through transform children, instead of containers?
if (!metaQuery.TryGetComponent(mind.OwnedEntity, out var meta))
return 0;
// who added this check bruh
if (meta.EntityPrototype?.ID == prototype)
return 1;
if (!containerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager))
return 0;
// recursively check each container for the item
// checks inventory, bag, implants, etc.
var stack = new Stack<ContainerManagerComponent>();
do
{
foreach (var container in currentManager.Containers.Values)
{
foreach (var entity in container.ContainedEntities)
{
// check if this is the item
if (metaQuery.GetComponent(entity).EntityPrototype?.ID == prototype)
return 1;
// if it is a container check its contents
if (containerQuery.TryGetComponent(entity, out var containerManager))
stack.Push(containerManager);
}
}
} while (stack.TryPop(out currentManager));
return 0;
}
}

View File

@@ -0,0 +1,25 @@
using Content.Server.Objectives.Components;
using Content.Shared.Objectives.Components;
using Content.Shared.Mind;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles progress for the survive objective condition.
/// </summary>
public sealed class SurviveConditionSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SurviveConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(EntityUid uid, SurviveConditionComponent comp, ref ObjectiveGetProgressEvent args)
{
args.Progress = _mind.IsCharacterDeadIc(args.Mind) ? 0f : 1f;
}
}

View File

@@ -0,0 +1,68 @@
using Content.Server.Objectives.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Roles.Jobs;
using Robust.Shared.GameObjects;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Provides API for other components and handles setting the title.
/// </summary>
public sealed class TargetObjectiveSystem : EntitySystem
{
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedJobSystem _job = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TargetObjectiveComponent, ObjectiveAfterAssignEvent>(OnAfterAssign);
}
private void OnAfterAssign(EntityUid uid, TargetObjectiveComponent comp, ref ObjectiveAfterAssignEvent args)
{
if (!GetTarget(uid, out var target, comp))
return;
_metaData.SetEntityName(uid, GetTitle(target.Value, comp.Title), args.Meta);
}
/// <summary>
/// Sets the Target field for the title and other components to use.
/// </summary>
public void SetTarget(EntityUid uid, EntityUid target, TargetObjectiveComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.Target = target;
}
/// <summary>
/// Gets the target from the component.
/// </summary>
/// <remarks>
/// If it is null then the prototype is invalid, just return.
/// </remarks>
public bool GetTarget(EntityUid uid, [NotNullWhen(true)] out EntityUid? target, TargetObjectiveComponent? comp = null)
{
target = Resolve(uid, ref comp) ? comp.Target : null;
return target != null;
}
private string GetTitle(EntityUid target, string title)
{
var targetName = "Unknown";
if (TryComp<MindComponent>(target, out var mind) && mind.CharacterName != null)
{
targetName = mind.CharacterName;
}
var jobName = _job.MindTryGetJobName(target);
return Loc.GetString(title, ("targetName", targetName), ("job", jobName));
}
}

View File

@@ -19,10 +19,10 @@ public sealed class CharacterInfoEvent : EntityEventArgs
{
public readonly NetEntity NetEntity;
public readonly string JobTitle;
public readonly Dictionary<string, List<ConditionInfo>> Objectives;
public readonly Dictionary<string, List<ObjectiveInfo>> Objectives;
public readonly string? Briefing;
public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary<string, List<ConditionInfo>> objectives, string? briefing)
public CharacterInfoEvent(NetEntity netEntity, string jobTitle, Dictionary<string, List<ObjectiveInfo>> objectives, string? briefing)
{
NetEntity = netEntity;
JobTitle = jobTitle;

View File

@@ -1,6 +1,5 @@
using Content.Shared.GameTicking;
using Content.Shared.Mind.Components;
using Content.Shared.Objectives;
using Robust.Shared.Network;
using Robust.Shared.Players;
@@ -22,7 +21,7 @@ namespace Content.Shared.Mind
[RegisterComponent]
public sealed partial class MindComponent : Component
{
internal readonly List<Objective> Objectives = new();
internal readonly List<EntityUid> Objectives = new();
/// <summary>
/// The session ID of the player owning this mind.
@@ -78,10 +77,10 @@ namespace Content.Shared.Mind
// TODO move objectives out of mind component
/// <summary>
/// An enumerable over all the objectives this mind has.
/// An enumerable over all the objective entities this mind has.
/// </summary>
[ViewVariables]
public IEnumerable<Objective> AllObjectives => Objectives;
public IEnumerable<EntityUid> AllObjectives => Objectives;
/// <summary>
/// Prevents user from ghosting out

View File

@@ -3,11 +3,13 @@ using Content.Shared.Administration.Logs;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Systems;
using Content.Shared.Players;
using Robust.Shared.Map;
using Robust.Shared.Network;
@@ -20,9 +22,9 @@ namespace Content.Shared.Mind;
public abstract class SharedMindSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPlayerSystem _playerSystem = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedObjectivesSystem _objectives = default!;
[Dependency] private readonly SharedPlayerSystem _player = default!;
// This is dictionary is required to track the minds of disconnected players that may have had their entity deleted.
protected readonly Dictionary<NetUserId, EntityUid> UserMinds = new();
@@ -90,7 +92,7 @@ public abstract class SharedMindSystem : EntitySystem
if (!mindContainer.ShowExamineInfo || !args.IsInDetailsRange)
return;
var dead = _mobStateSystem.IsDead(uid);
var dead = _mobState.IsDead(uid);
var hasSession = CompOrNull<MindComponent>(mindContainer.Mind)?.Session;
if (dead && !mindContainer.HasMind)
@@ -166,7 +168,7 @@ public abstract class SharedMindSystem : EntitySystem
if (targetMobState == null)
return true;
// They might actually be alive.
return _mobStateSystem.IsDead(mind.OwnedEntity.Value, targetMobState);
return _mobState.IsDead(mind.OwnedEntity.Value, targetMobState);
}
public virtual void Visit(EntityUid mindId, EntityUid entity, MindComponent? mind = null)
@@ -215,7 +217,7 @@ public abstract class SharedMindSystem : EntitySystem
public void WipeMind(ICommonSession player)
{
var mind = _playerSystem.ContentData(player)?.Mind;
var mind = _player.ContentData(player)?.Mind;
DebugTools.Assert(GetMind(player.UserId) == mind);
WipeMind(mind);
}
@@ -251,59 +253,44 @@ public abstract class SharedMindSystem : EntitySystem
}
/// <summary>
/// Adds an objective to this mind.
/// Tries to create and add an objective from its prototype id.
/// </summary>
public bool TryAddObjective(EntityUid mindId, MindComponent mind, ObjectivePrototype objectivePrototype)
/// <returns>Returns true if adding the objective succeeded.</returns>
public bool TryAddObjective(EntityUid mindId, MindComponent mind, string proto)
{
if (!objectivePrototype.CanBeAssigned(mindId, mind))
return false;
var objective = objectivePrototype.GetObjective(mindId, mind);
if (mind.Objectives.Contains(objective))
var objective = _objectives.TryCreateObjective(mindId, mind, proto);
if (objective == null)
return false;
foreach (var condition in objective.Conditions)
{
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' added to mind of {MindOwnerLoggingString(mind)}");
}
mind.Objectives.Add(objective);
AddObjective(mindId, mind, objective.Value);
return true;
}
/// <summary>
/// Adds an objective, by id, to this mind.
/// Adds an objective that already exists, and is assumed to have had its requirements checked.
/// </summary>
public bool TryAddObjective(EntityUid mindId, string name, MindComponent? mind = null)
public void AddObjective(EntityUid mindId, MindComponent mind, EntityUid objective)
{
if (!Resolve(mindId, ref mind))
return false;
if (!_proto.TryIndex<ObjectivePrototype>(name, out var objective))
{
Log.Error($"Tried to add unknown objective prototype: {name}");
return false;
}
return TryAddObjective(mindId, mind, objective);
var title = Name(objective);
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) added to mind of {MindOwnerLoggingString(mind)}");
mind.Objectives.Add(objective);
}
/// <summary>
/// Removes an objective to this mind.
/// Removes an objective from this mind.
/// </summary>
/// <returns>Returns true if the removal succeeded.</returns>
public bool TryRemoveObjective(MindComponent mind, int index)
public bool TryRemoveObjective(EntityUid mindId, MindComponent mind, int index)
{
if (index < 0 || index >= mind.Objectives.Count)
return false;
var objective = mind.Objectives[index];
foreach (var condition in objective.Conditions)
{
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"'{condition.Title}' removed from the mind of {MindOwnerLoggingString(mind)}");
}
var title = Name(objective);
_adminLogger.Add(LogType.Mind, LogImpact.Low, $"Objective {objective} ({title}) removed from the mind of {MindOwnerLoggingString(mind)}");
mind.Objectives.Remove(objective);
Del(objective);
return true;
}
@@ -356,7 +343,7 @@ public abstract class SharedMindSystem : EntitySystem
{
mindId = default;
mind = null;
return _playerSystem.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
return _player.ContentData(player) is { } data && TryGetMind(data, out mindId, out mind);
}
/// <summary>
@@ -432,6 +419,30 @@ public abstract class SharedMindSystem : EntitySystem
{
return TryGetMind(userId, out _, out var mind) ? mind.CharacterName : null;
}
/// <summary>
/// Returns a list of every living humanoid player's minds, except for a single one which is exluded.
/// </summary>
public List<EntityUid> GetAliveHumansExcept(EntityUid exclude)
{
var mindQuery = EntityQuery<MindComponent>();
var allHumans = new List<EntityUid>();
// HumanoidAppearanceComponent is used to prevent mice, pAIs, etc from being chosen
var query = EntityQueryEnumerator<MindContainerComponent, MobStateComponent, HumanoidAppearanceComponent>();
while (query.MoveNext(out var uid, out var mc, out var mobState, out _))
{
// the player needs to have a mind and not be the excluded one
if (mc.Mind == null || mc.Mind == exclude)
continue;
// the player has to be alive
if (_mobState.IsAlive(uid, mobState))
allHumans.Add(mc.Mind.Value);
}
return allHumans;
}
}
/// <summary>

View File

@@ -0,0 +1,69 @@
using Content.Shared.Mind;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Utility;
namespace Content.Shared.Objectives.Components;
/// <summary>
/// Required component for an objective entity prototype.
/// </summary>
[RegisterComponent, Access(typeof(SharedObjectivesSystem))]
public sealed partial class ObjectiveComponent : Component
{
/// <summary>
/// Difficulty rating used to avoid assigning too many difficult objectives.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public float Difficulty;
/// <summary>
/// Organisation that issued this objective, used for grouping and as a header above common objectives.
/// </summary>
[DataField(required: true), ViewVariables(VVAccess.ReadWrite)]
public string Issuer = string.Empty;
/// <summary>
/// Unique objectives can only have 1 per prototype id.
/// Set this to false if you want multiple objectives of the same prototype.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool Unique = true;
/// <summary>
/// Icon of this objective to display in the character menu.
/// Can be specified by an <see cref="ObjectiveGetInfoEvent"/> handler but is usually done in the prototype.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public SpriteSpecifier? Icon;
}
/// <summary>
/// Event raised on an objective after spawning it to see if it meets all the requirements.
/// Requirement components should have subscriptions and cancel if the requirements are not met.
/// If a requirement is not met then the objective is deleted.
/// </summary>
[ByRefEvent]
public record struct RequirementCheckEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false);
/// <summary>
/// Event raised on an objective after its requirements have been checked.
/// If <see cref="Cancelled"/> is set to true, the objective is deleted.
/// Use this if the objective cannot be used, like a kill objective with no people alive.
/// </summary>
[ByRefEvent]
public record struct ObjectiveAssignedEvent(EntityUid MindId, MindComponent Mind, bool Cancelled = false);
/// <summary>
/// Event raised on an objective after everything has handled <see cref="ObjectiveAssignedEvent"/>.
/// Use this to set the objective's title description or icon.
/// </summary>
[ByRefEvent]
public record struct ObjectiveAfterAssignEvent(EntityUid MindId, MindComponent Mind, ObjectiveComponent Objective, MetaDataComponent Meta);
/// <summary>
/// Event raised on an objective to update the Progress field.
/// To use this yourself call <see cref="SharedObjectivesSystem.GetInfo"/> with the mind.
/// </summary>
[ByRefEvent]
public record struct ObjectiveGetProgressEvent(EntityUid MindId, MindComponent Mind, float? Progress = null);

View File

@@ -1,22 +0,0 @@
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Objectives
{
[Serializable, NetSerializable]
public sealed class ConditionInfo
{
public string Title { get; }
public string Description { get; }
public SpriteSpecifier SpriteSpecifier { get; }
public float Progress { get; }
public ConditionInfo(string title, string description, SpriteSpecifier spriteSpecifier, float progress)
{
Title = title;
Description = description;
SpriteSpecifier = spriteSpecifier;
Progress = progress;
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Shared.Mind;
using Robust.Shared.Utility;
namespace Content.Shared.Objectives.Interfaces
{
// TODO refactor all of this to be ecs
public interface IObjectiveCondition
{
/// <summary>
/// Returns a copy of the IObjectiveCondition which is assigned to the mind.
/// </summary>
/// <param name="mindId">Mind id to assign to.</param>
/// <param name="mind">Mind to assign to.</param>
/// <returns>The new IObjectiveCondition.</returns>
IObjectiveCondition GetAssigned(EntityUid mindId, MindComponent mind);
/// <summary>
/// Returns the title of the condition.
/// </summary>
string Title { get; }
/// <summary>
/// Returns the description of the condition.
/// </summary>
string Description { get; }
/// <summary>
/// Returns a SpriteSpecifier to be used as an icon for the condition.
/// </summary>
SpriteSpecifier Icon { get; }
/// <summary>
/// Returns the current progress of the condition in % from 0 to 1.
/// </summary>
/// <returns>Current progress in %.</returns>
float Progress { get; }
/// <summary>
/// Returns a difficulty of the condition.
/// </summary>
float Difficulty { get; }
}
}

View File

@@ -1,14 +0,0 @@
using Content.Shared.Mind;
namespace Content.Shared.Objectives.Interfaces
{
// TODO refactor all of this to be ecs
public interface IObjectiveRequirement
{
/// <summary>
/// Checks whether or not the entity & its surroundings are valid to be given the objective.
/// </summary>
/// <returns>Returns true if objective can be given.</returns>
bool CanBeAssigned(EntityUid mindId, MindComponent mind);
}
}

View File

@@ -1,56 +0,0 @@
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
namespace Content.Shared.Objectives
{
public sealed class Objective : IEquatable<Objective>
{
[ViewVariables]
public readonly EntityUid MindId;
[ViewVariables]
public readonly MindComponent Mind;
[ViewVariables]
public readonly ObjectivePrototype Prototype;
private readonly List<IObjectiveCondition> _conditions = new();
[ViewVariables]
public IReadOnlyList<IObjectiveCondition> Conditions => _conditions;
public Objective(ObjectivePrototype prototype, EntityUid mindId, MindComponent mind)
{
Prototype = prototype;
MindId = mindId;
Mind = mind;
foreach (var condition in prototype.Conditions)
{
_conditions.Add(condition.GetAssigned(mindId, mind));
}
}
public bool Equals(Objective? other)
{
if (other is null) return false;
if (ReferenceEquals(this, other)) return true;
if (!Equals(Mind, other.Mind) || !Equals(Prototype, other.Prototype)) return false;
if (_conditions.Count != other._conditions.Count) return false;
for (var i = 0; i < _conditions.Count; i++)
{
if (!_conditions[i].Equals(other._conditions[i])) return false;
}
return true;
}
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((Objective) obj);
}
public override int GetHashCode()
{
return HashCode.Combine(Mind, Prototype, _conditions);
}
}
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
namespace Content.Shared.Objectives;
/// <summary>
/// Info about objectives visible in the character menu and on round end.
/// Description and icon are displayed only in the character menu.
/// Progress is a percentage from 0.0 to 1.0.
/// </summary>
/// <remarks>
/// All of these fields must eventually be set by condition event handlers.
/// Everything but progress can be set to static data in yaml on the entity and <see cref="ObjectiveComponent"/>.
/// If anything is null it will be logged and return null.
/// </remarks>
[Serializable, NetSerializable]
public record struct ObjectiveInfo(string Title, string Description, SpriteSpecifier Icon, float Progress);

View File

@@ -1,63 +0,0 @@
using System.Linq;
using Content.Shared.Mind;
using Content.Shared.Objectives.Interfaces;
using Robust.Shared.Prototypes;
namespace Content.Shared.Objectives
{
/// <summary>
/// Prototype for objectives. Remember that to be assigned, it should be added to one or more objective groups in prototype. E.g. crew, traitor, wizard
/// </summary>
[Prototype("objective")]
public sealed class ObjectivePrototype : IPrototype
{
[ViewVariables]
[IdDataField]
public string ID { get; private set; } = default!;
[DataField("issuer")] public string Issuer { get; private set; } = "Unknown";
[ViewVariables]
public float Difficulty => _difficultyOverride ?? _conditions.Sum(c => c.Difficulty);
[DataField("conditions", serverOnly: true)]
private List<IObjectiveCondition> _conditions = new();
[DataField("requirements")]
private List<IObjectiveRequirement> _requirements = new();
[ViewVariables]
public IReadOnlyList<IObjectiveCondition> Conditions => _conditions;
[DataField("canBeDuplicate")]
public bool CanBeDuplicateAssignment { get; private set; }
[ViewVariables(VVAccess.ReadWrite)]
[DataField("difficultyOverride")]
private float? _difficultyOverride = null;
public bool CanBeAssigned(EntityUid mindId, MindComponent mind)
{
foreach (var requirement in _requirements)
{
if (!requirement.CanBeAssigned(mindId, mind))
return false;
}
if (!CanBeDuplicateAssignment)
{
foreach (var objective in mind.AllObjectives)
{
if (objective.Prototype.ID == ID)
return false;
}
}
return true;
}
public Objective GetObjective(EntityUid mindId, MindComponent mind)
{
return new Objective(this, mindId, mind);
}
}
}

View File

@@ -0,0 +1,130 @@
using Content.Shared.Mind;
using Content.Shared.Objectives;
using Content.Shared.Objectives.Components;
using Robust.Shared.Utility;
namespace Content.Shared.Objectives.Systems;
/// <summary>
/// Provides API for creating and interacting with objectives.
/// </summary>
public abstract class SharedObjectivesSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
private EntityQuery<MetaDataComponent> _metaQuery;
public override void Initialize()
{
base.Initialize();
_metaQuery = GetEntityQuery<MetaDataComponent>();
}
/// <summary>
/// Checks requirements and duplicate objectives to see if an objective can be assigned.
/// </summary>
public bool CanBeAssigned(EntityUid uid, EntityUid mindId, MindComponent mind, ObjectiveComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return false;
var ev = new RequirementCheckEvent(mindId, mind);
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled)
return false;
// only check for duplicate prototypes if it's unique
if (comp.Unique)
{
var proto = _metaQuery.GetComponent(uid).EntityPrototype?.ID;
foreach (var objective in mind.AllObjectives)
{
if (_metaQuery.GetComponent(objective).EntityPrototype?.ID == proto)
return false;
}
}
return true;
}
/// <summary>
/// Spawns and assigns an objective for a mind.
/// The objective is not added to the mind's objectives, mind system does that in TryAddObjective.
/// If the objective could not be assigned the objective is deleted and null is returned.
/// </summary>
public EntityUid? TryCreateObjective(EntityUid mindId, MindComponent mind, string proto)
{
var uid = Spawn(proto);
if (!TryComp<ObjectiveComponent>(uid, out var comp))
{
Del(uid);
Log.Error($"Invalid objective prototype {proto}, missing ObjectiveComponent");
return null;
}
Log.Debug($"Created objective {proto} ({uid})");
if (!CanBeAssigned(uid, mindId, mind, comp))
{
Del(uid);
Log.Warning($"Objective {uid} did not match the requirements for {_mind.MindOwnerLoggingString(mind)}, deleted it");
return null;
}
var ev = new ObjectiveAssignedEvent(mindId, mind);
RaiseLocalEvent(uid, ref ev);
if (ev.Cancelled)
{
Del(uid);
Log.Warning($"Could not assign objective {uid}, deleted it");
return null;
}
// let the title description and icon be set by systems
var afterEv = new ObjectiveAfterAssignEvent(mindId, mind, comp, MetaData(uid));
RaiseLocalEvent(uid, ref afterEv);
return uid;
}
/// <summary>
/// Get the title, description, icon and progress of an objective using <see cref="ObjectiveGetInfoEvent"/>.
/// If any of them are null it is logged and null is returned.
/// </summary>
/// <param name="uid"/>ID of the condition entity</param>
/// <param name="mindId"/>ID of the player's mind entity</param>
/// <param name="mind"/>Mind component of the player's mind</param>
public ObjectiveInfo? GetInfo(EntityUid uid, EntityUid mindId, MindComponent? mind = null)
{
if (!Resolve(mindId, ref mind))
return null;
var ev = new ObjectiveGetProgressEvent(mindId, mind);
RaiseLocalEvent(uid, ref ev);
var comp = Comp<ObjectiveComponent>(uid);
var meta = MetaData(uid);
var title = meta.EntityName;
var description = meta.EntityDescription;
if (comp.Icon == null || ev.Progress == null)
{
Log.Error($"An objective {ToPrettyString(uid):objective} of {_mind.MindOwnerLoggingString(mind)} is missing icon or progress ({ev.Progress})");
return null;
}
return new ObjectiveInfo(title, description, comp.Icon, ev.Progress.Value);
}
/// <summary>
/// Sets the objective's icon to the one specified.
/// Intended for <see cref="ObjectiveAfterAssignEvent"/> handlers to set an icon.
/// </summary>
public void SetIcon(EntityUid uid, SpriteSpecifier icon, ObjectiveComponent? comp = null)
{
if (!Resolve(uid, ref comp))
return;
comp.Icon = icon;
}
}

View File

@@ -1,2 +0,0 @@
objective-condition-die-title = Die a glorious death
objective-condition-die-description = Die.

View File

@@ -1,2 +1,2 @@
objective-condition-doorjack-title = Doorjack {$count} doors on the station.
objective-condition-doorjack-description = Your gloves can emag airlocks. Do this {$count} doors on the station.
objective-condition-doorjack-description = Your gloves can emag airlocks. Do this to {$count} doors on the station.

View File

@@ -1,2 +0,0 @@
objective-condition-escape-shuttle-title = Escape to centcom alive and unrestrained.
objective-condition-escape-shuttle-description = One of our undercover agents will debrief you when you arrive. Don't show up in cuffs.

View File

@@ -1 +0,0 @@
objective-condition-kill-head-description = We need this head gone and you probably know why. Good luck, agent.

View File

@@ -0,0 +1 @@
objective-condition-kill-head-title = Kill {$targetName}, {CAPITALIZE($job)}

View File

@@ -1,2 +0,0 @@
objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)}
objective-condition-kill-person-description = Do it however you like, just make sure they don't make it to centcom.

View File

@@ -0,0 +1 @@
objective-condition-kill-person-title = Kill or maroon {$targetName}, {CAPITALIZE($job)}

View File

@@ -1,2 +1 @@
objective-condition-other-traitor-alive-title = Ensure fellow traitor {$targetName}, {CAPITALIZE($job)} stays alive.
objective-condition-other-traitor-alive-description = Identify yourself at your own risk. We just need them alive.

View File

@@ -1,2 +1 @@
objective-condition-other-traitor-progress-title = Ensure fellow traitor {$targetName}, {CAPITALIZE($job)} achieves at least half their objectives.
objective-condition-other-traitor-progress-description = Identify yourself at your own risk. We just need them to succeed.

View File

@@ -1,3 +0,0 @@
objective-condition-spider-charge-title = Detonate the spider clan charge in {$location}
objective-condition-spider-charge-no-target = Detonate the spider clan charge... somewhere?
objective-condition-spider-charge-description = This bomb can be detonated in a specific location. Note that the bomb will not work anywhere else!

View File

@@ -0,0 +1 @@
objective-condition-spider-charge-title = Detonate the spider clan charge in {$location}

View File

@@ -1,2 +0,0 @@
objective-condition-steal-research-title = Steal {$count} technologies.
objective-condition-steal-research-description = Your gloves can be used to hack a research server and steal its precious data. If science has been slacking you'll have to get to work.

View File

@@ -0,0 +1 @@
objective-condition-steal-research-title = Steal {$count} technologies.

View File

@@ -1,2 +0,0 @@
objective-condition-survive-title = Survive
objective-condition-survive-description = You wouldn't be a very good ninja if you died, now would you?

View File

@@ -1,2 +0,0 @@
objective-condition-terror-title = Call in a threat
objective-condition-terror-description = Use your gloves on a communication console in order to bring another threat to the station.

View File

@@ -10,5 +10,5 @@ objectives-player-named = [color=White]{$name}[/color]
objectives-no-objectives = {$title} was a {$agent}.
objectives-with-objectives = {$title} was a {$agent} who had the following objectives:
objectives-condition-success = {$condition} | [color={$markupColor}]Success![/color]
objectives-condition-fail = {$condition} | [color={$markupColor}]Failure![/color] ({$progress}%)
objectives-objective-success = {$objective} | [color={$markupColor}]Success![/color]
objectives-objective-fail = {$objective} | [color={$markupColor}]Failure![/color] ({$progress}%)

View File

@@ -11,7 +11,7 @@
- DoorjackObjective
- SpiderChargeObjective
- TerrorObjective
- SurviveObjective
- NinjaSurviveObjective
threats:
- announcement: terror-dragon
rule: Dragon

Some files were not shown because too many files have changed in this diff Show More