antag objective issuing refactor (#28216)

* add AntagObjectives from GenericAntag

* add AntagRandomObjectives that traitor and thief can use

* make ObjectivesSystem use initial character name which AntagSelection passes

* make thief and traitor use AntagRandomObjectives

* remove now unused locale

* make sleeper agents rule use baseTraitorRule

* restore dragon rule oop

* bandaid for genericantag

* real

* typo

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas
2024-05-25 20:14:48 +00:00
committed by GitHub
parent f1c70d69f2
commit a06ea58249
18 changed files with 231 additions and 153 deletions

View File

@@ -0,0 +1,35 @@
using Content.Server.Antag.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using Content.Shared.Objectives.Systems;
namespace Content.Server.Antag;
/// <summary>
/// Adds fixed objectives to an antag made with <c>AntagObjectivesComponent</c>.
/// </summary>
public sealed class AntagObjectivesSystem : EntitySystem
{
[Dependency] private readonly SharedMindSystem _mind = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
}
private void OnAntagSelected(Entity<AntagObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
{
Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
return;
}
foreach (var id in ent.Comp.Objectives)
{
_mind.TryAddObjective(mindId, mind, id);
}
}
}

View File

@@ -0,0 +1,52 @@
using Content.Server.Antag.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using Content.Shared.Objectives.Systems;
using Robust.Shared.Random;
namespace Content.Server.Antag;
/// <summary>
/// Adds fixed objectives to an antag made with <c>AntagRandomObjectivesComponent</c>.
/// </summary>
public sealed class AntagRandomObjectivesSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AntagRandomObjectivesComponent, AfterAntagEntitySelectedEvent>(OnAntagSelected);
}
private void OnAntagSelected(Entity<AntagRandomObjectivesComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (!_mind.TryGetMind(args.Session, out var mindId, out var mind))
{
Log.Error($"Antag {ToPrettyString(args.EntityUid):player} was selected by {ToPrettyString(ent):rule} but had no mind attached!");
return;
}
var difficulty = 0f;
foreach (var set in ent.Comp.Sets)
{
if (!_random.Prob(set.Prob))
continue;
for (var pick = 0; pick < set.MaxPicks && ent.Comp.MaxDifficulty > difficulty; pick++)
{
if (_objectives.GetRandomObjective(mindId, mind, set.Groups) is not {} objective)
continue;
_mind.AddObjective(mindId, mind, objective);
var adding = Comp<ObjectiveComponent>(objective).Difficulty;
difficulty += adding;
Log.Debug($"Added objective {ToPrettyString(objective):objective} to {ToPrettyString(args.EntityUid):player} with {adding} difficulty");
}
}
}
}

View File

@@ -124,7 +124,7 @@ public sealed partial class AntagSelectionSystem
}
/// <remarks>
/// Helper specifically for <see cref="ObjectivesTextGetInfoEvent"/>
/// Helper to get just the mind entities and not names.
/// </remarks>
public List<EntityUid> GetAntagMindEntityUids(Entity<AntagSelectionComponent?> ent)
{

View File

@@ -7,6 +7,7 @@ using Content.Server.GameTicking.Rules;
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind;
using Content.Server.Objectives;
using Content.Server.Preferences.Managers;
using Content.Server.Roles;
using Content.Server.Roles.Jobs;
@@ -50,6 +51,8 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
SubscribeLocalEvent<GhostRoleAntagSpawnerComponent, TakeGhostRoleEvent>(OnTakeGhostRole);
SubscribeLocalEvent<AntagSelectionComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<RulePlayerSpawningEvent>(OnPlayerSpawning);
SubscribeLocalEvent<RulePlayerJobsAssignedEvent>(OnJobsAssigned);
SubscribeLocalEvent<PlayerSpawnCompleteEvent>(OnSpawnComplete);
@@ -423,6 +426,15 @@ public sealed partial class AntagSelectionSystem : GameRuleSystem<AntagSelection
return true;
}
private void OnObjectivesTextGetInfo(Entity<AntagSelectionComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
if (ent.Comp.AgentName is not {} name)
return;
args.Minds = ent.Comp.SelectedMinds;
args.AgentName = Loc.GetString(name);
}
}
/// <summary>

View File

@@ -0,0 +1,18 @@
using Content.Server.Antag;
using Content.Shared.Objectives.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Gives antags selected by this rule a fixed list of objectives.
/// </summary>
[RegisterComponent, Access(typeof(AntagObjectivesSystem))]
public sealed partial class AntagObjectivesComponent : Component
{
/// <summary>
/// List of static objectives to give.
/// </summary>
[DataField(required: true)]
public List<EntProtoId<ObjectiveComponent>> Objectives = new();
}

View File

@@ -0,0 +1,52 @@
using Content.Server.Antag;
using Content.Shared.Random;
using Robust.Shared.Prototypes;
namespace Content.Server.Antag.Components;
/// <summary>
/// Gives antags selected by this rule a random list of objectives.
/// </summary>
[RegisterComponent, Access(typeof(AntagRandomObjectivesSystem))]
public sealed partial class AntagRandomObjectivesComponent : Component
{
/// <summary>
/// Each set of objectives to add.
/// </summary>
[DataField(required: true)]
public List<AntagObjectiveSet> Sets = new();
/// <summary>
/// If the total difficulty of the currently given objectives exceeds, no more will be given.
/// </summary>
[DataField(required: true)]
public float MaxDifficulty;
}
/// <summary>
/// A set of objectives to try picking.
/// Difficulty is checked over all sets, but each set has its own probability and pick count.
/// </summary>
[DataRecord]
public record struct AntagObjectiveSet()
{
/// <summary>
/// The grouping used by the objective system to pick random objectives.
/// First a group is picked from these, then an objective from that group.
/// </summary>
[DataField(required: true)]
public ProtoId<WeightedRandomPrototype> Groups = string.Empty;
/// <summary>
/// Probability of this set being used.
/// </summary>
[DataField]
public float Prob = 1f;
/// <summary>
/// Number of times to try picking objectives from this set.
/// Even if there is enough difficulty remaining, no more will be given after this.
/// </summary>
[DataField]
public int MaxPicks = 20;
}

View File

@@ -42,6 +42,13 @@ public sealed partial class AntagSelectionComponent : Component
/// Is not serialized.
/// </summary>
public HashSet<ICommonSession> SelectedSessions = new();
/// <summary>
/// Locale id for the name of the antag.
/// If this is set then the antag is listed in the round-end summary.
/// </summary>
[DataField]
public LocId? AgentName;
}
[DataDefinition]

View File

@@ -8,23 +8,4 @@ namespace Content.Server.GameTicking.Rules.Components;
/// Stores data for <see cref="ThiefRuleSystem"/>.
/// </summary>
[RegisterComponent, Access(typeof(ThiefRuleSystem))]
public sealed partial class ThiefRuleComponent : Component
{
[DataField]
public ProtoId<WeightedRandomPrototype> BigObjectiveGroup = "ThiefBigObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> SmallObjectiveGroup = "ThiefObjectiveGroups";
[DataField]
public ProtoId<WeightedRandomPrototype> EscapeObjectiveGroup = "ThiefEscapeObjectiveGroups";
[DataField]
public float BigObjectiveChance = 0.7f;
[DataField]
public float MaxObjectiveDifficulty = 2.5f;
[DataField]
public int MaxStealObjectives = 10;
}
public sealed partial class ThiefRuleComponent : Component;

View File

@@ -22,9 +22,6 @@ public sealed partial class TraitorRuleComponent : Component
[DataField]
public ProtoId<NpcFactionPrototype> SyndicateFaction = "Syndicate";
[DataField]
public ProtoId<WeightedRandomPrototype> ObjectiveGroup = "TraitorObjectiveGroups";
[DataField]
public ProtoId<DatasetPrototype> CodewordAdjectives = "adjectives";
@@ -72,7 +69,4 @@ public sealed partial class TraitorRuleComponent : Component
/// </summary>
[DataField]
public int StartingBalance = 20;
[DataField]
public int MaxDifficulty = 5;
}

View File

@@ -1,6 +1,8 @@
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Objectives;
using Content.Shared.Mind;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
namespace Content.Server.GameTicking.Rules;
@@ -47,7 +49,8 @@ public sealed class GenericAntagRuleSystem : GameRuleSystem<GenericAntagRuleComp
private void OnObjectivesTextGetInfo(EntityUid uid, GenericAntagRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = comp.Minds;
// just temporary until this is deleted
args.Minds = comp.Minds.Select(mindId => (mindId, Comp<MindComponent>(mindId).CharacterName ?? "?")).ToList();
args.AgentName = Loc.GetString(comp.AgentName);
}
}

View File

@@ -24,7 +24,6 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
SubscribeLocalEvent<ThiefRuleComponent, AfterAntagEntitySelectedEvent>(AfterAntagSelected);
SubscribeLocalEvent<ThiefRoleComponent, GetBriefingEvent>(OnGetBriefing);
SubscribeLocalEvent<ThiefRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
}
private void AfterAntagSelected(Entity<ThiefRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
@@ -33,41 +32,9 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
return;
//Generate objectives
GenerateObjectives(mindId, mind, ent);
_antag.SendBriefing(args.EntityUid, MakeBriefing(args.EntityUid), null, null);
}
private void GenerateObjectives(EntityUid mindId, MindComponent mind, ThiefRuleComponent thiefRule)
{
// Give thieves their objectives
var difficulty = 0f;
if (_random.Prob(thiefRule.BigObjectiveChance)) // 70% chance to 1 big objective (structure or animal)
{
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.BigObjectiveGroup);
if (objective != null)
{
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
}
for (var i = 0; i < thiefRule.MaxStealObjectives && thiefRule.MaxObjectiveDifficulty > difficulty; i++) // Many small objectives
{
var objective = _objectives.GetRandomObjective(mindId, mind, thiefRule.SmallObjectiveGroup);
if (objective == null)
continue;
_mindSystem.AddObjective(mindId, mind, objective.Value);
difficulty += Comp<ObjectiveComponent>(objective.Value).Difficulty;
}
//Escape target
var escapeObjective = _objectives.GetRandomObjective(mindId, mind, thiefRule.EscapeObjectiveGroup);
if (escapeObjective != null)
_mindSystem.AddObjective(mindId, mind, escapeObjective.Value);
}
//Add mind briefing
private void OnGetBriefing(Entity<ThiefRoleComponent> thief, ref GetBriefingEvent args)
{
@@ -87,10 +54,4 @@ public sealed class ThiefRuleSystem : GameRuleSystem<ThiefRuleComponent>
briefing += "\n \n" + Loc.GetString("thief-role-greeting-equipment") + "\n";
return briefing;
}
private void OnObjectivesTextGetInfo(Entity<ThiefRuleComponent> ent, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = _antag.GetAntagMindEntityUids(ent.Owner);
args.AgentName = Loc.GetString("thief-round-end-agent-name");
}
}

View File

@@ -31,15 +31,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
[Dependency] private readonly SharedJobSystem _jobs = default!;
[Dependency] private readonly ObjectivesSystem _objectives = default!;
public const int MaxPicks = 20;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TraitorRuleComponent, AfterAntagEntitySelectedEvent>(AfterEntitySelected);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextGetInfoEvent>(OnObjectivesTextGetInfo);
SubscribeLocalEvent<TraitorRuleComponent, ObjectivesTextPrependEvent>(OnObjectivesTextPrepend);
}
@@ -67,7 +64,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
}
}
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true, bool giveObjectives = true)
public bool MakeTraitor(EntityUid traitor, TraitorRuleComponent component, bool giveUplink = true)
{
//Grab the mind if it wasnt provided
if (!_mindSystem.TryGetMind(traitor, out var mindId, out var mind))
@@ -112,37 +109,16 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
_npcFaction.RemoveFaction(traitor, component.NanoTrasenFaction, false);
_npcFaction.AddFaction(traitor, component.SyndicateFaction);
// Give traitors their objectives
if (giveObjectives)
{
var difficulty = 0f;
for (var pick = 0; pick < MaxPicks && component.MaxDifficulty > difficulty; pick++)
{
var objective = _objectives.GetRandomObjective(mindId, mind, component.ObjectiveGroup);
if (objective == null)
continue;
_mindSystem.AddObjective(mindId, mind, objective.Value);
var adding = Comp<ObjectiveComponent>(objective.Value).Difficulty;
difficulty += adding;
Log.Debug($"Added objective {ToPrettyString(objective):objective} with {adding} difficulty");
}
}
return true;
}
private void OnObjectivesTextGetInfo(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextGetInfoEvent args)
{
args.Minds = _antag.GetAntagMindEntityUids(uid);
args.AgentName = Loc.GetString("traitor-round-end-agent-name");
}
// TODO: AntagCodewordsComponent
private void OnObjectivesTextPrepend(EntityUid uid, TraitorRuleComponent comp, ref ObjectivesTextPrependEvent args)
{
args.Text += "\n" + Loc.GetString("traitor-round-end-codewords", ("codewords", string.Join(", ", comp.Codewords)));
}
// TODO: figure out how to handle this? add priority to briefing event?
private string GenerateBriefing(string[] codewords, Note[]? uplinkCode, string? objectiveIssuer = null)
{
var sb = new StringBuilder();

View File

@@ -36,14 +36,14 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
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 summaries = new Dictionary<string, Dictionary<string, List<(EntityUid, string)>>>();
var query = EntityQueryEnumerator<GameRuleComponent>();
while (query.MoveNext(out var uid, out var gameRule))
{
if (!_gameTicker.IsGameRuleAdded(uid, gameRule))
continue;
var info = new ObjectivesTextGetInfoEvent(new List<EntityUid>(), string.Empty);
var info = new ObjectivesTextGetInfoEvent(new List<(EntityUid, string)>(), string.Empty);
RaiseLocalEvent(uid, ref info);
if (info.Minds.Count == 0)
continue;
@@ -51,7 +51,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
// first group the gamerules by their agents, for example 2 different dragons
var agent = info.AgentName;
if (!summaries.ContainsKey(agent))
summaries[agent] = new Dictionary<string, List<EntityUid>>();
summaries[agent] = new Dictionary<string, List<(EntityUid, string)>>();
var prepend = new ObjectivesTextPrependEvent("");
RaiseLocalEvent(uid, ref prepend);
@@ -79,7 +79,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
foreach (var (_, minds) in summary)
{
total += minds.Count;
totalInCustody += minds.Where(m => IsInCustody(m)).Count();
totalInCustody += minds.Where(pair => IsInCustody(pair.Item1)).Count();
}
var result = new StringBuilder();
@@ -104,19 +104,16 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
}
}
private void AddSummary(StringBuilder result, string agent, List<EntityUid> minds)
private void AddSummary(StringBuilder result, string agent, List<(EntityUid, string)> minds)
{
var agentSummaries = new List<(string summary, float successRate, int completedObjectives)>();
foreach (var mindId in minds)
foreach (var (mindId, name) in minds)
{
if (!TryComp(mindId, out MindComponent? mind))
continue;
var title = GetTitle(mindId, mind);
if (title == null)
if (!TryComp<MindComponent>(mindId, out var mind))
continue;
var title = GetTitle((mindId, mind), name);
var custody = IsInCustody(mindId, mind) ? Loc.GetString("objectives-in-custody") : string.Empty;
var objectives = mind.Objectives;
@@ -238,34 +235,18 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
/// <summary>
/// Get the title for a player's mind used in round end.
/// Pass in the original entity name which is shown alongside username.
/// </summary>
public string? GetTitle(EntityUid mindId, MindComponent? mind = null)
public string GetTitle(Entity<MindComponent?> mind, string name)
{
if (!Resolve(mindId, ref mind))
return null;
var name = mind.CharacterName;
var username = (string?) null;
if (mind.OriginalOwnerUserId != null &&
_player.TryGetPlayerData(mind.OriginalOwnerUserId.Value, out var sessionData))
if (Resolve(mind, ref mind.Comp) &&
mind.Comp.OriginalOwnerUserId != null &&
_player.TryGetPlayerData(mind.Comp.OriginalOwnerUserId.Value, out var sessionData))
{
username = sessionData.UserName;
var username = sessionData.UserName;
return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
}
if (username != null)
{
if (name != null)
return Loc.GetString("objectives-player-user-named", ("user", username), ("name", name));
return Loc.GetString("objectives-player-user", ("user", username));
}
// nothing to identify the player by, just give up
if (name == null)
return null;
return Loc.GetString("objectives-player-named", ("name", name));
}
}
@@ -279,7 +260,7 @@ public sealed class ObjectivesSystem : SharedObjectivesSystem
/// The objectives system already checks if the game rule is added so you don't need to check that in this event's handler.
/// </remarks>
[ByRefEvent]
public record struct ObjectivesTextGetInfoEvent(List<EntityUid> Minds, string AgentName);
public record struct ObjectivesTextGetInfoEvent(List<(EntityUid, string)> Minds, string AgentName);
/// <summary>
/// Raised on the game rule before text for each agent's objectives is added, letting you prepend something.

View File

@@ -6,7 +6,6 @@ objectives-round-end-result = {$count ->
objectives-round-end-result-in-custody = {$custody} out of {$count} {MAKEPLURAL($agent)} were in custody.
objectives-player-user-named = [color=White]{$name}[/color] ([color=gray]{$user}[/color])
objectives-player-user = [color=gray]{$user}[/color]
objectives-player-named = [color=White]{$name}[/color]
objectives-no-objectives = {$custody}{$title} was a {$agent}.

View File

@@ -428,9 +428,9 @@
prototype: Nukeops
- type: entity
id: SleeperAgentsRule
parent: BaseGameRule
noSpawn: true
parent: BaseTraitorRule
id: SleeperAgentsRule
components:
- type: StationEvent
earliestStart: 30
@@ -441,7 +441,6 @@
startAudio:
path: /Audio/Announcements/intercept.ogg
- type: AlertLevelInterceptionRule
- type: TraitorRule
- type: AntagSelection
definitions:
- prefRoles: [ Traitor ]

View File

@@ -35,7 +35,19 @@
id: Thief
components:
- type: ThiefRule
- type: AntagObjectives
objectives:
- EscapeThiefShuttleObjective
- type: AntagRandomObjectives
sets:
- groups: ThiefBigObjectiveGroups
prob: 0.7
maxPicks: 1
- groups: ThiefObjectiveGroups
maxPicks: 10
maxDifficulty: 2.5
- type: AntagSelection
agentName: thief-round-end-agent-name
definitions:
- prefRoles: [ Thief ]
maxRange:
@@ -53,14 +65,3 @@
prototype: Thief
briefing:
sound: "/Audio/Misc/thief_greeting.ogg"
#- type: entity
# noSpawn: true
# parent: BaseGameRule
# id: Exterminator
# components:
# - type: GenericAntagRule
# agentName: terminator-round-end-agent-name
# objectives:
# - TerminateObjective
# - ShutDownObjective

View File

@@ -134,16 +134,30 @@
prototype: Nukeops
- type: entity
id: Traitor
abstract: true
parent: BaseGameRule
id: BaseTraitorRule
components:
- type: TraitorRule
# TODO: codewords in yml
# TODO: uplink in yml
- type: AntagRandomObjectives
sets:
- groups: TraitorObjectiveGroups
maxDifficulty: 5
- type: AntagSelection
agentName: traitor-round-end-agent-name
- type: entity
noSpawn: true
parent: BaseTraitorRule
id: Traitor
components:
- type: GameRule
minPlayers: 5
delay:
min: 240
max: 420
- type: TraitorRule
- type: AntagSelection
definitions:
- prefRoles: [ Traitor ]

View File

@@ -55,13 +55,6 @@
ThiefObjectiveGroupStructure: 0 #Temporarily disabled until obvious ways to steal structures are added
ThiefObjectiveGroupAnimal: 2
- type: weightedRandom
id: ThiefEscapeObjectiveGroups
weights:
ThiefObjectiveGroupEscape: 1
- type: weightedRandom
id: ThiefObjectiveGroupCollection
weights: