fugitives redux (#1413)

* add fugitive rule logic

* add fugitive role

* add fugitive event

* remove nyano spawner and make midround antag spawner work real

* fix announcement logic

* fix the funny bug i think

* fix crimes

* fix crime count

* 1 less min crime

* fix sex (real)

* thief bag unhardcode MaxSelectedSets

* :trollface:

* :trollface:

* add fugitive stash

* fix

* add misc stuff and fugitive stash

* m

---------

Co-authored-by: deltanedas <@deltanedas:kde.org>
This commit is contained in:
deltanedas 2024-06-30 19:40:33 +00:00 committed by GitHub
parent 78cc6939bc
commit 540febcbad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 500 additions and 26 deletions

View File

@ -5,7 +5,7 @@
MinSize="700 700">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<!-- First Informational panel -->
<Label Text="{Loc 'thief-backpack-window-description'}" Margin="5 5"/>
<Label Name="Description" Margin="5 5"/>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5"/>
<Label Name="SelectedSets" Text="{Loc 'thief-backpack-window-selected'}" Margin="5 5"/>

View File

@ -50,6 +50,7 @@ public sealed partial class ThiefBackpackMenu : FancyWindow
selectedNumber++;
}
Description.Text = Loc.GetString("thief-backpack-window-description", ("maxCount", state.MaxSelectedSets));
SelectedSets.Text = Loc.GetString("thief-backpack-window-selected", ("selectedCount", selectedNumber), ("maxCount", state.MaxSelectedSets));
ApproveButton.Disabled = selectedNumber == state.MaxSelectedSets ? false : true;
}

View File

@ -0,0 +1,87 @@
using Content.Shared.Dataset;
using Content.Server.StationEvents.Events;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Utility;
namespace Content.Server.StationEvents.Components;
/// <summary>
/// Makes a GALPOL announcement and creates a report some time after an antag spawns.
/// Removed after this is done.
/// </summary>
[RegisterComponent, Access(typeof(FugitiveRule))]
[AutoGenerateComponentPause]
public sealed partial class FugitiveRuleComponent : Component
{
[DataField]
public LocId Announcement = "station-event-fugitive-hunt-announcement";
[DataField]
public LocId Sender = "fugitive-announcement-GALPOL";
[DataField]
public Color Color = Color.Yellow;
/// <summary>
/// Report paper to spawn. Its content is generated from the fugitive.
/// </summary>
[DataField]
public EntProtoId ReportPaper = "PaperFugitiveReport";
/// <summary>
/// How long to wait after the antag spawns before announcing it.
/// </summary>
[DataField]
public TimeSpan AnnounceDelay = TimeSpan.FromMinutes(5);
/// <summary>
/// Station to give the report to.
/// </summary>
[DataField]
public EntityUid? Station;
/// <summary>
/// The report generated for the spawned fugitive.
/// </summary>
[DataField]
public string Report = string.Empty;
/// <summary>
/// When the announcement will be made, if an antag has spawned yet.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan? NextAnnounce;
/// <summary>
/// Dataset to pick crimes on the report from.
/// </summary>
[DataField]
public ProtoId<LocalizedDatasetPrototype> CrimesDataset = "FugitiveCrimes";
/// <summary>
/// Max number of unique crimes they can be charged with.
/// Does not affect the counts of each crime.
/// </summary>
[DataField]
public int MinCrimes = 2;
/// <summary>
/// Min number of unique crimes they can be charged with.
/// Does not affect the counts of each crime.
/// </summary>
[DataField]
public int MaxCrimes = 7;
/// <summary>
/// Min counts of each crime that can be rolled.
/// </summary>
[DataField]
public int MinCounts = 1;
/// <summary>
/// Max counts of each crime that can be rolled.
/// </summary>
[DataField]
public int MaxCounts = 4;
}

View File

@ -0,0 +1,129 @@
using Content.Server.Antag;
using Content.Server.Communications;
using Content.Server.GameTicking.Components; // TODO: Shared when upstream merged
using Content.Server.Paper;
using Content.Server.StationEvents.Components;
using Content.Shared.Ghost;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Prototypes;
using Content.Shared.Popups;
using Content.Shared.Random.Helpers;
using Robust.Shared.Physics.Components;
using Robust.Shared.Utility;
namespace Content.Server.StationEvents.Events;
public sealed class FugitiveRule : StationEventSystem<FugitiveRuleComponent>
{
[Dependency] private readonly PaperSystem _paper = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<FugitiveRuleComponent, AfterAntagEntitySelectedEvent>(OnEntitySelected);
}
protected override void ActiveTick(EntityUid uid, FugitiveRuleComponent comp, GameRuleComponent rule, float frameTime)
{
if (comp.NextAnnounce is not {} next || next > Timing.CurTime)
return;
var announcement = Loc.GetString(comp.Announcement);
var sender = Loc.GetString(comp.Sender);
ChatSystem.DispatchGlobalAnnouncement(announcement, sender: sender, colorOverride: comp.Color);
// send the report to every comms console on the station
var query = EntityQueryEnumerator<TransformComponent, CommunicationsConsoleComponent>();
var consoles = new List<TransformComponent>();
while (query.MoveNext(out var console, out var xform, out _))
{
if (StationSystem.GetOwningStation(console, xform) != comp.Station || HasComp<GhostComponent>(console))
continue;
consoles.Add(xform);
}
foreach (var xform in consoles)
{
var report = Spawn(comp.ReportPaper, xform.Coordinates);
_paper.SetContent(report, comp.Report);
}
// prevent any possible funnies
comp.NextAnnounce = null;
RemCompDeferred(uid, comp);
}
private void OnEntitySelected(Entity<FugitiveRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.NextAnnounce != null)
{
Log.Error("Fugitive rule spawning multiple fugitives isn't supported, sorry.");
return;
}
var fugi = args.EntityUid;
ent.Comp.Report = GenerateReport(fugi, ent.Comp).ToMarkup();
ent.Comp.Station = StationSystem.GetOwningStation(fugi);
ent.Comp.NextAnnounce = Timing.CurTime + ent.Comp.AnnounceDelay;
_popup.PopupEntity(Loc.GetString("fugitive-spawn"), fugi, fugi);
}
private FormattedMessage GenerateReport(EntityUid uid, FugitiveRuleComponent rule)
{
var report = new FormattedMessage();
report.PushMarkup(Loc.GetString("fugitive-report-title", ("name", uid)));
report.PushNewline();
report.PushMarkup(Loc.GetString("fugitive-report-first-line", ("name", uid)));
report.PushNewline();
if (!TryComp<HumanoidAppearanceComponent>(uid, out var humanoid))
{
report.AddMarkup(Loc.GetString("fugitive-report-inhuman", ("name", uid)));
return report;
}
var species = PrototypeManager.Index(humanoid.Species);
report.PushMarkup(Loc.GetString("fugitive-report-morphotype", ("species", Loc.GetString(species.Name))));
report.PushMarkup(Loc.GetString("fugitive-report-age", ("age", humanoid.Age)));
report.PushMarkup(Loc.GetString("fugitive-report-sex", ("sex", humanoid.Sex.ToString())));
if (TryComp<PhysicsComponent>(uid, out var physics))
report.PushMarkup(Loc.GetString("fugitive-report-weight", ("weight", Math.Round(physics.FixturesMass))));
report.PushNewline();
report.PushMarkup(Loc.GetString("fugitive-report-crimes-header"));
// generate some random crimes to avoid this situation
// "officer what are my charges?"
// "uh i dunno a piece of paper said to arrest you thats it"
AddCharges(report, rule);
report.PushNewline();
report.AddMarkup(Loc.GetString("fugitive-report-last-line"));
return report;
}
private void AddCharges(FormattedMessage report, FugitiveRuleComponent rule)
{
var crimeTypes = PrototypeManager.Index(rule.CrimesDataset);
var crimes = new HashSet<LocId>();
var total = RobustRandom.Next(rule.MinCrimes, rule.MaxCrimes + 1);
while (crimes.Count < total)
{
crimes.Add(RobustRandom.Pick(crimeTypes));
}
foreach (var crime in crimes)
{
var count = RobustRandom.Next(rule.MinCounts, rule.MaxCounts + 1);
report.PushMarkup(Loc.GetString("fugitive-report-crime", ("crime", Loc.GetString(crime)), ("count", count)));
}
}
}

View File

@ -0,0 +1,9 @@
using Content.Shared.Roles;
namespace Content.Server.Roles;
/// <summary>
/// DeltaV - fugitive antag role
/// </summary>
[RegisterComponent, ExclusiveAntagonist]
public sealed partial class FugitiveRoleComponent : AntagonistRoleComponent;

View File

@ -23,4 +23,10 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component
[DataField]
public SoundSpecifier ApproveSound = new SoundPathSpecifier("/Audio/Effects/rustle1.ogg");
/// <summary>
/// Max number of sets you can select.
/// </summary>
[DataField]
public int MaxSelectedSets = 2;
}

View File

@ -18,7 +18,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
private const int MaxSelectedSets = 2;
public override void Initialize()
{
base.Initialize();
@ -35,7 +34,7 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
private void OnApprove(Entity<ThiefUndeterminedBackpackComponent> backpack, ref ThiefBackpackApproveMessage args)
{
if (backpack.Comp.SelectedSets.Count != MaxSelectedSets)
if (backpack.Comp.SelectedSets.Count != backpack.Comp.MaxSelectedSets)
return;
foreach (var i in backpack.Comp.SelectedSets)
@ -79,6 +78,6 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
data.Add(i, info);
}
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, MaxSelectedSets));
_ui.SetUiState(uid, ThiefBackpackUIKey.Key, new ThiefBackpackBoundUserInterfaceState(data, component.MaxSelectedSets));
}
}

View File

@ -0,0 +1,24 @@
fugitive-set-hitman-name = hitman's kit
fugitive-set-hitman-description =
You've taken lives before and are prepared to do so again if necessary.
Comes with a loaded viper and a spare mag.
fugitive-set-saboteur-name = saboteur's kit
fugitive-set-saboteur-description =
Making engineers miserable is your life's mission.
Comes with EMP grenades and a brick of C4.
fugitive-set-ghost-name = ghost's kit
fugitive-set-ghost-description =
Disappear in the middle of a chase to secure your freedom!
Comes with 2 smoke grenades and a scram implanter that can teleport you.
fugitive-set-leverage-name = leverage kit
fugitive-set-leverage-description =
Your years in the clown college taught you to slip security very well.
Use a death adicifier on you or a "friend" to get what you want!
fugitive-set-infiltrator-name = infiltrator's kit
fugitive-set-infiltrator-description =
Use an Agent ID to steal access from others and go anywhere.
Your freedom implanter can be used as a plan B if all else fails.

View File

@ -0,0 +1,48 @@
fugitive-round-end-agent-name = Fugitive
fugitive-spawn = You fall from the ceiling!
station-event-fugitive-hunt-announcement = Please check communications consoles for a sensitive message.
fugitive-announcement-GALPOL = GALPOL
fugitive-report-title = WANTED: {$name}
fugitive-report-first-line = Escaped fugitive {$name} has been spotted in the sector. They may be a stowaway on a station somewhere.
fugitive-report-inhuman = {CAPITALIZE(THE($name))} {CONJUGATE-BE($name)} inhuman. We have no further details.
fugitive-report-morphotype = MORPHOTYPE: {$species}
fugitive-report-age = AGE: {$age}
fugitive-report-sex = SEX: {$sex ->
[Male] M
[Female] F
*[none] N/A
}
fugitive-report-weight = WEIGHT: {$weight} kg
fugitive-report-crimes-header = The above individual is wanted across the sector for the following:
fugitive-report-crime = - {$count ->
[1] One count
*[other] {$count} counts
} of {$crime}
fugitive-report-last-line = GALPOL prefers the fugitive to be returned alive so they may face trial at Central Command.
# All (non erp) felonies and capital crimes in Space Law as of June 2024
fugitive-crime-1 = Murder
fugitive-crime-2 = Terrorism
fugitive-crime-3 = Grand Sabotage
fugitive-crime-4 = Decorporealisation
fugitive-crime-5 = Kidnapping
fugitive-crime-6 = Sedition
fugitive-crime-7 = Manslaughter
fugitive-crime-8 = Grand Theft
fugitive-crime-9 = Black Marketeering
fugitive-crime-10 = Sabotage
fugitive-crime-11 = Mindbreaking
fugitive-crime-12 = Assault
fugitive-crime-13 = Abuse of Power
fugitive-crime-14 = Possession
fugitive-crime-15 = Endangerment
fugitive-crime-16 = Breaking and Entering
fugitive-crime-17 = Rioting
fugitive-crime-18 = Contempt of Court
fugitive-crime-19 = Perjury
fugitive-crime-20 = False Report
fugitive-crime-21 = Obstruction of Justice

View File

@ -10,3 +10,7 @@ ghost-role-information-listeningop-rules = You are a Syndicate Operative tasked
ghost-role-information-paradox-anomaly-name = Paradox Anomaly
ghost-role-information-paradox-anomaly-description = Replace your double, or befriend them.
ghost-role-information-paradox-anomaly-rules = Try and replace your twin with this funny roleplay antag rather than plasma flooding the station or something. You can also just befriend them.
ghost-role-information-fugitive-name = Fugitive
ghost-role-information-fugitive-description = You're an escaped prisoner. Make it out alive.
ghost-role-information-fugitive-rules = You are the lightest of antags, focus on laying low rather than engaging security directly. Don't murderbone.

View File

@ -3,7 +3,10 @@ thief-backpack-window-title = thief toolbox
thief-backpack-window-description =
This toolbox is filled with unspecified contents.
Now you need to remember what you put in it.
Choose 2 different sets from the list.
Choose {$maxCount} different {$maxCount ->
[1] set
*[other] sets
} from the list.
thief-backpack-window-selected = Kits selected: ({$selectedCount}/{$maxCount})

View File

@ -0,0 +1,61 @@
- type: thiefBackpackSet
id: FugitiveHitman
name: fugitive-set-hitman-name
description: fugitive-set-hitman-description
sprite:
sprite: Objects/Weapons/Guns/Pistols/viper.rsi
state: icon
content:
- WeaponPistolViper
- MagazinePistol
- BriefcaseBrown
- type: thiefBackpackSet
id: FugitiveSaboteur
name: fugitive-set-saboteur-name
description: fugitive-set-saboteur-description
sprite:
sprite: Objects/Weapons/Grenades/empgrenade.rsi
state: icon
content:
- EmpGrenade
- EmpGrenade
- C4
- ClothingMaskGas
- type: thiefBackpackSet
id: FugitiveGhost
name: fugitive-set-ghost-name
description: fugitive-set-ghost-description
sprite:
sprite: Objects/Weapons/Grenades/smoke.rsi
state: icon
content:
- SmokeGrenade
- SmokeGrenade
- ScramImplanter
- ClothingOuterGhostSheet
- type: thiefBackpackSet
id: FugitiveLeverage
name: fugitive-set-leverage-name
description: fugitive-set-leverage-description
sprite:
sprite: Objects/Misc/handcuffs.rsi
state: handcuff
content:
- DeathAcidifierImplanter
- Handcuffs
- Handcuffs
- Bola
- type: thiefBackpackSet
id: FugitiveInfiltrator
name: fugitive-set-infiltrator-name
description: fugitive-set-infiltrator-description
sprite:
entity: AgentIDCard
content:
- AgentIDCard
- FreedomImplanter
- ClothingMaskGasSyndicate

View File

@ -0,0 +1,5 @@
- type: localizedDataset
id: FugitiveCrimes
values:
prefix: fugitive-crime-
count: 21

View File

@ -52,3 +52,18 @@
name: ghost-role-information-paradox-anomaly-name
description: ghost-role-information-paradox-anomaly-description
rules: ghost-role-information-paradox-anomaly-rules
- type: entity
noSpawn: true
parent: BaseAntagSpawner
id: SpawnPointGhostFugitive
name: fugitive spawn point
components:
- type: GhostRole
name: ghost-role-information-fugitive-name
description: ghost-role-information-fugitive-description
rules: ghost-role-information-fugitive-rules
requirements:
- !type:DepartmentTimeRequirement
department: Security
time: 3600 # 1 hour

View File

@ -23,3 +23,10 @@
backgroundModulate: "#e0bc99"
backgroundPatchMargin: 16.0, 16.0, 16.0, 16.0
contentMargin: 32.0, 16.0, 32.0, 0.0
# contents set by FugitiveRule or a creative admeme
- type: entity
parent: PaperStationWarrant
id: PaperFugitiveReport
name: fugitive report
description: An arrest warrant for a space fugitive sent from GALPOL.

View File

@ -0,0 +1,15 @@
- type: entity
parent: ToolboxThief
id: FugitiveStash
name: fugitive's stash
description: "These supplies got you out of jail and hopefully they'll keep you out of it."
components:
# TODO: make the sprite a hobo bindle or something wysi
- type: ThiefUndeterminedBackpack
maxSelectedSets: 1
possibleSets:
- FugitiveHitman
- FugitiveSaboteur
- FugitiveGhost
- FugitiveLeverage
- FugitiveInfiltrator

View File

@ -133,3 +133,34 @@
briefing: paradox-anomaly-role-briefing
- type: TerminatorRole
prototype: Terminator
- type: entity
parent: BaseMidRoundAntag
id: Fugitive
components:
- type: StationEvent
minimumPlayers: 40 # it's really easy to find fugitives on lowpop
duration: null
- type: FugitiveRule
- type: AntagLoadProfileRule
- type: AntagObjectives
objectives:
- FugitiveEscapeObjective
- type: AntagSelection
agentName: fugitive-round-end-agent-name
definitions:
- spawnerPrototype: SpawnPointGhostFugitive
min: 1
max: 1
pickPlayer: false
startingGear: FugitiveGear
components:
- type: RandomMetadata
nameSegments:
- fake_human_first
- fake_human_last
- type: EmitSoundOnSpawn # fell out of the ceiling
sound: /Audio/Effects/clang.ogg
mindComponents:
- type: FugitiveRole
prototype: Fugitive

View File

@ -0,0 +1,24 @@
- type: entity
abstract: true
parent: BaseObjective
id: BaseFugitiveObjective
components:
- type: Objective
issuer: self
difficulty: 1
- type: RoleRequirement
roles:
components:
- FugitiveRole
- type: entity
parent: [BaseFugitiveObjective, BaseLivingObjective]
id: FugitiveEscapeObjective
name: Evade law enforcement
description: You will never atone for your crimes, make sure of it by blending into the crowd.
components:
- type: Objective
icon:
sprite: Markers/jobs.rsi
state: prisoner
- type: EscapeShuttleCondition

View File

@ -0,0 +1,26 @@
- type: antag
id: Fugitive
name: roles-antag-fugitive-name
antagonist: true
objective: roles-antag-fugitive-objective
# keep these in sync with the spawner
requirements:
- !type:DepartmentTimeRequirement
department: Security
time: 3600 # 1 hour
- type: startingGear
id: FugitiveGear
equipment:
jumpsuit: ClothingUniformJumpsuitPrisoner
ears: ClothingHeadsetGrey
gloves: ClothingHandsGlovesColorYellow
back: ClothingBackpackFilled
shoes: ClothingShoesChameleonNoSlips
id: PassengerPDA
#innerClothingSkirt: ClothingUniformJumpskirtPrisoner # FIXME doesnt work
inhand:
- ToolboxElectricalFilled
storage:
back:
- FugitiveStash

View File

@ -18,26 +18,6 @@
- sprite: Mobs/Animals/bat.rsi
state: bat
- type: entity
id: SpawnPointGhostFugitive
name: ghost role spawn point
parent: MarkerBase
noSpawn: true
components:
# - type: GhostRoleMobSpawner
# prototype: MobHumanFugitive # Todo
- type: GhostRole
name: Fugitive
description: You're an escaped prisoner. Make it out alive.
rules: |
You are the lightest of antags.
Murderboning = ban and whitelist removal.
- type: Sprite
sprite: Markers/jobs.rsi
layers:
- state: green
- state: prisoner
- type: entity
id: SpawnPointLocationMidRoundAntag
name: possible spawn location
@ -49,7 +29,7 @@
layers:
- state: green
- state: prisoner
# - type: MidRoundAntagSpawnLocation # When MidRoundAntag?
- type: MidRoundAntagSpawnLocation
# - type: entity
# id: SpawnPointGhostVampSpider