Hijack the Automated Trade Station objective (#42135)

* predict TradeStationComponent

* hijack beacon first pass

* add Objective uplink category

* add HijackTradeStationCondition, objective, and uplink entry

* add sprite and cleanups

* fix + make cooldown sane

* use TimeSpans and update logic accordingly + private state-related fields

* remove temporary reward

* tiny loc changes

* buncha changes

* Change success message, beacon physics properties, formatting

---------

Co-authored-by: seanpimble <149889301+seanpimble@users.noreply.github.com>
Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: SlamBamActionman <slambamactionman@gmail.com>
This commit is contained in:
alexalexmax 2026-04-03 06:21:04 -04:00 • committed by GitHub
parent 9504c1e0bb
commit 17ccf1dc70
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 648 additions and 16 deletions

View file

@ -1,10 +0,0 @@
namespace Content.Server.Cargo.Components;
/// <summary>
/// Target for approved orders to spawn at.
/// </summary>
[RegisterComponent]
public sealed partial class TradeStationComponent : Component
{
}

View file

@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.CCVar;
using Content.Shared.HijackBeacon;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@ -14,7 +15,7 @@ namespace Content.Server.Cargo.Systems;
public sealed partial class CargoSystem
{
/*
* Handles cargo shuttle / trade mechanics.
* Handles automated trade station / trade mechanics.
*/
private static readonly SoundPathSpecifier ApproveSound = new("/Audio/Effects/Cargo/ping.ogg");

View file

@ -7,6 +7,7 @@ using Content.Shared.Access.Systems;
using Content.Shared.Administration.Logs;
using Content.Server.Radio.EntitySystems;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Mobs.Components;
using Content.Shared.Paper;

View file

@ -0,0 +1,11 @@
using Content.Server.Objectives.Systems;
namespace Content.Server.Objectives.Components;
/// <summary>
/// Objective condition that requires the player to hijack the trade station.
/// </summary>
[RegisterComponent, Access(typeof(HijackTradeStationConditionSystem))]
public sealed partial class HijackTradeStationConditionComponent : Component
{
}

View file

@ -0,0 +1,35 @@
using Content.Server.Objectives.Components;
using Content.Shared.Cargo.Components;
using Content.Shared.Mind;
using Content.Shared.Objectives.Components;
using NetCord;
namespace Content.Server.Objectives.Systems;
/// <summary>
/// Handles the Hijack Trade Station objective.
/// </summary>
public sealed class HijackTradeStationConditionSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HijackTradeStationConditionComponent, ObjectiveGetProgressEvent>(OnGetProgress);
}
private void OnGetProgress(Entity<HijackTradeStationConditionComponent> ent, ref ObjectiveGetProgressEvent args)
{
var enumerator = EntityQueryEnumerator<TradeStationComponent>();
args.Progress = 0f;
// If there's any hacked trade station, succeed.
while (enumerator.MoveNext(out var comp))
{
if (!comp.Hacked)
continue;
args.Progress = 1f;
return;
}
}
}

View file

@ -0,0 +1,17 @@
using Content.Shared.HijackBeacon;
using Robust.Shared.GameStates;
namespace Content.Shared.Cargo.Components;
/// <summary>
/// Target for approved orders to spawn at.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class TradeStationComponent : Component
{
/// <summary>
/// The Trade Station's current hijack state. Modified by HijackBeaconSystem.
/// </summary>
[DataField, AutoNetworkedField]
public bool Hacked = false;
}

View file

@ -1,5 +1,6 @@
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.HijackBeacon;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
@ -17,6 +18,7 @@ public abstract class SharedCargoSystem : EntitySystem
base.Initialize();
SubscribeLocalEvent<StationBankAccountComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<HijackBeaconSuccessEvent>(OnHijackSuccess);
}
private void OnMapInit(Entity<StationBankAccountComponent> ent, ref MapInitEvent args)
@ -25,6 +27,23 @@ public abstract class SharedCargoSystem : EntitySystem
Dirty(ent);
}
private void OnHijackSuccess(ref HijackBeaconSuccessEvent args)
{
var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
while (stationQuery.MoveNext(out var uid, out var comp))
{
foreach (var (account, cash) in comp.Accounts)
{
comp.Accounts[account] = cash - args.Fine;
args.Total += args.Fine;
}
var ev = new BankBalanceUpdatedEvent(uid, comp.Accounts);
RaiseLocalEvent(uid, ref ev, true);
Dirty(uid, comp);
}
}
/// <summary>
/// For a given station, retrieves the balance in a specific account.
/// </summary>

View file

@ -235,12 +235,8 @@ public sealed partial class AnchorableSystem : EntitySystem
// Log anchor attempt (server only)
_adminLogger.Add(LogType.Anchor, LogImpact.Low, $"{ToPrettyString(userUid):user} is trying to anchor {ToPrettyString(uid):entity} to {transform.Coordinates:targetlocation}");
if (TryComp<PhysicsComponent>(uid, out var anchorBody) &&
!TileFree(transform.Coordinates, anchorBody))
{
_popup.PopupClient(Loc.GetString("anchorable-occupied"), uid, userUid);
if (!CanAnchorAt(uid, transform.Coordinates, userUid))
return;
}
if (AnyUnstackable(uid, transform.Coordinates))
{
@ -285,6 +281,23 @@ public sealed partial class AnchorableSystem : EntitySystem
return !attempt.Cancelled;
}
public bool CanAnchorAt(Entity<PhysicsComponent?> entity, EntityUid? user = null)
{
return CanAnchorAt(entity, Transform(entity).Coordinates, user);
}
public bool CanAnchorAt(Entity<PhysicsComponent?> entity, EntityCoordinates coordinates, EntityUid? user = null)
{
if (!Resolve(entity, ref entity.Comp))
return true;
if (TileFree(coordinates, entity.Comp))
return true;
_popup.PopupClient(Loc.GetString("anchorable-occupied"), entity, user);
return false;
}
/// <summary>
/// Returns true if no hard anchored entities exist on the coordinate tile that would collide with the provided physics body.
/// </summary>

View file

@ -0,0 +1,16 @@
using Robust.Shared.GameStates;
namespace Content.Shared.HijackBeacon;
/// <summary>
/// This is used for tracking a <see cref="HijackBeaconComponent"/> that is currently activated or on cooldown.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ActiveHijackBeaconComponent : Component
{
/// <summary>
/// Remaining time until the hijack is completed.
/// </summary>
[DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
public TimeSpan CompletionTime = TimeSpan.Zero;
}

View file

@ -0,0 +1,53 @@
using Content.Shared.DoAfter;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Prototypes;
namespace Content.Shared.HijackBeacon;
/// <summary>
/// Component for hijack beacons, meant to be planted on the ATS to drain station funds.
/// </summary>
/// <remarks>
/// Status and timer fields are private so the state machine is preserved.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class HijackBeaconComponent : Component
{
/// <summary>
/// Current state of the beacon.
/// </summary>
[DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
public HijackBeaconStatus Status = HijackBeaconStatus.AwaitActivate;
/// <summary>
/// How long it takes to deactivate the beacon.
/// </summary>
[DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
public TimeSpan DeactivationLength = TimeSpan.FromSeconds(5);
/// <summary>
/// Remaining time until the hijack is completed.
/// </summary>
[DataField, Access(typeof(HijackBeaconSystem))]
public TimeSpan RemainingTime = TimeSpan.FromSeconds(200);
/// <summary>
/// Default amount of time before the beacon can be re-activated, if it is disarmed.
/// </summary>
[DataField, Access(typeof(HijackBeaconSystem))]
public TimeSpan Cooldown = TimeSpan.FromSeconds(20);
/// <summary>
/// Remaining cooldown time before the beacon can be reactivated.
/// </summary>
[DataField, AutoNetworkedField, Access(typeof(HijackBeaconSystem))]
public TimeSpan CooldownTime = TimeSpan.Zero;
/// <summary>
/// How much cash should be withdrawn from each department account?
/// </summary>
[DataField]
public int Fine = 5000;
}

View file

@ -0,0 +1,353 @@
using Content.Shared.Cargo.Components;
using Content.Shared.Chat;
using Content.Shared.Construction.Components;
using Content.Shared.Construction.EntitySystems;
using Content.Shared.Database;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Audio;
using Robust.Shared.Serialization;
using Robust.Shared.Timing;
namespace Content.Shared.HijackBeacon;
public sealed class HijackBeaconSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly AnchorableSystem _anchor = default!;
[Dependency] private readonly SharedChatSystem _chat = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public readonly SoundSpecifier AnnounceSound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg");
public readonly SoundSpecifier DeactivateSound = new SoundPathSpecifier("/Audio/Misc/notice2.ogg");
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<HijackBeaconComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAltVerbs);
SubscribeLocalEvent<HijackBeaconComponent, UnanchorAttemptEvent>(OnUnanchorAttempt);
SubscribeLocalEvent<HijackBeaconComponent, AnchorStateChangedEvent>(OnAnchorChanged);
SubscribeLocalEvent<HijackBeaconComponent, HijackBeaconDeactivateDoAfterEvent>(OnDeactivateDoAfter);
SubscribeLocalEvent<HijackBeaconComponent, ExaminedEvent>(OnExaminedEvent);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveHijackBeaconComponent, HijackBeaconComponent>();
while (query.MoveNext(out var uid, out var active, out var comp))
{
switch (comp.Status)
{
case HijackBeaconStatus.Armed:
if (_gameTiming.CurTime < active.CompletionTime)
return;
HijackFinish((uid, comp));
Dirty(uid, comp);
break;
case HijackBeaconStatus.Cooldown:
if (comp.CooldownTime < _gameTiming.CurTime)
{
comp.Status = HijackBeaconStatus.AwaitActivate;
RemCompDeferred<ActiveHijackBeaconComponent>(uid);
Dirty(uid, comp);
}
break;
}
}
}
#region Event Subs
/// <summary>
/// Deactivate beacon if it gets unanchored(via a bomb or something)
/// </summary>
private void OnAnchorChanged(Entity<HijackBeaconComponent> ent, ref AnchorStateChangedEvent args)
{
// Unanchoring the beacon deactivates it. This is to prevent people from bombing the tile the beacon is on and running away with it for a free activation.
if (!args.Anchored && ent.Comp.Status == HijackBeaconStatus.Armed)
DeactivateBeacon(ent);
}
private void OnUnanchorAttempt(Entity<HijackBeaconComponent> entity, ref UnanchorAttemptEvent args)
{
if (entity.Comp.Status == HijackBeaconStatus.Armed)
args.Cancel();
}
/// <summary>
/// Get the activation and deactivation verbs.
/// </summary>
private void OnGetAltVerbs(Entity<HijackBeaconComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract || args.Hands is null)
return;
if (ent.Comp.Status == HijackBeaconStatus.AwaitActivate)
{
args.Verbs.Add(new()
{
Act = () =>
{
ActivateBeacon(ent);
},
Text = Loc.GetString("hijack-beacon-verb-activate-text"),
Message = Loc.GetString("hijack-beacon-verb-activate-message"),
Disabled = ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent),
TextStyleClass = "InteractionVerb",
Impact = LogImpact.High,
});
}
if (ent.Comp.Status == HijackBeaconStatus.Armed)
{
var user = args.User;
args.Verbs.Add(new()
{
Act = () =>
{
DeactivateBeaconDoAfter(ent, user);
},
Text = Loc.GetString("hijack-beacon-verb-deactivate-text"),
Message = Loc.GetString("hijack-beacon-verb-deactivate-message"),
TextStyleClass = "InteractionVerb",
Impact = LogImpact.High,
});
}
}
/// <summary>
/// When it's examined.
/// </summary>
private void OnExaminedEvent(Entity<HijackBeaconComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
switch (ent.Comp.Status)
{
case HijackBeaconStatus.AwaitActivate:
args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-activate"));
break;
case HijackBeaconStatus.Armed:
args.PushMarkup(Loc.GetString("defusable-examine-live",
("name", ent),
("time", GetRemainingTime(ent.Owner))));
break;
case HijackBeaconStatus.Cooldown:
args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-cooldown"));
break;
case HijackBeaconStatus.HijackComplete:
args.PushMarkup(Loc.GetString("hijack-beacon-examine-await-hijack-complete"));
break;
}
}
/// <summary>
/// What happens when you deactivate the beacon.
/// </summary>
private void OnDeactivateDoAfter(Entity<HijackBeaconComponent> ent, ref HijackBeaconDeactivateDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
DeactivateBeacon(ent);
args.Handled = true;
}
#endregion
/// <summary>
/// Arming the beacon. Should only occur if on the ATS.
/// </summary>
private void ActivateBeacon(Entity<HijackBeaconComponent> ent)
{
if (ent.Comp.Status != HijackBeaconStatus.AwaitActivate || !CanActivate(ent))
return;
// Activate and start countdown.
// Remaining time is adjusted by current time to simplify the update loop.
EnsureComp<ActiveHijackBeaconComponent>(ent, out var activeComp);
activeComp.CompletionTime = _gameTiming.CurTime + ent.Comp.RemainingTime;
ent.Comp.Status = HijackBeaconStatus.Armed;
//global announcement
var sender = Loc.GetString("hijack-beacon-announcement-sender");
var message = Loc.GetString("hijack-beacon-announcement-activated", ("time", GetRemainingTime((ent.Owner, activeComp))));
_chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Yellow);
//Anchor. Anchoring is tied to activation.
Anchor(ent);
Dirty(ent);
}
/// <summary>
/// Deactivates the beacon.
/// </summary>
private void DeactivateBeacon(Entity<HijackBeaconComponent> ent)
{
if (ent.Comp.Status != HijackBeaconStatus.Armed)
return;
// Put beacon on cooldown
ent.Comp.CooldownTime = ent.Comp.Cooldown + _gameTiming.CurTime;
ent.Comp.Status = HijackBeaconStatus.Cooldown;
//global announcement
var sender = Loc.GetString("hijack-beacon-announcement-sender");
var message = Loc.GetString("hijack-beacon-announcement-deactivated");
_chat.DispatchGlobalAnnouncement(message, sender, true, DeactivateSound, Color.Green);
// Unanchor. we want anchoring to be tied to activation here so we just call this.
Unanchor(ent);
Dirty(ent);
}
/// <summary>
/// Complete the hijack. The beacon equivalent to a nuke detonation
/// </summary>
private void HijackFinish(Entity<HijackBeaconComponent> ent)
{
if (ent.Comp.Status != HijackBeaconStatus.Armed)
return;
// Hijack is completed and can't be reattempted
ent.Comp.Status = HijackBeaconStatus.HijackComplete;
RemCompDeferred<ActiveHijackBeaconComponent>(ent);
var beaconXForm = Transform(ent);
// Ensure we are on the trade station still
if (!TryComp<TradeStationComponent>(beaconXForm.GridUid, out var station))
{
Log.Warning($"Trade station hijack tried to succeed on non-trade station grid {beaconXForm.GridUid}!");
DeactivateBeacon(ent);
return;
}
if (station.Hacked)
{
Log.Warning("Hack succeeded on an already hacked trade station!");
return;
}
// Mark the station as hacked.
station.Hacked = true;
Dirty(beaconXForm.GridUid.Value, station);
// Broadcast that the ATS has been hacked.
var ev = new HijackBeaconSuccessEvent(ent.Comp.Fine);
RaiseLocalEvent(ref ev);
//global announcement
var sender = Loc.GetString("hijack-beacon-announcement-sender");
var message = Loc.GetString("hijack-beacon-announcement-success", ("fine", ev.Total));
_chat.DispatchGlobalAnnouncement(message, sender, true, AnnounceSound, Color.Red);
// Unanchoring must occur after updating the status, or it will disarm the beacon
Unanchor(ent, beaconXForm);
Dirty(ent);
}
/// <summary>
/// Starts the deactivation doafter.
/// </summary>
private void DeactivateBeaconDoAfter(Entity<HijackBeaconComponent> beacon, EntityUid user)
{
var doAfter = new DoAfterArgs(EntityManager, user, beacon.Comp.DeactivationLength, new HijackBeaconDeactivateDoAfterEvent(), beacon)
{
BreakOnDamage = true,
BreakOnMove = true,
NeedHand = true,
};
// (try to) start doafter
_doAfter.TryStartDoAfter(doAfter);
}
#region Helpers
/// <summary>
/// Check if the beacon is on the Trade Station and if the Trade Station has not been hijacked already.
/// </summary>
private bool CanActivate(Entity<HijackBeaconComponent> ent)
{
return TryComp(Transform(ent).GridUid, out TradeStationComponent? tradeStation) && !tradeStation.Hacked && _anchor.CanAnchorAt(ent.Owner);
}
/// <summary>
/// Anchoring helper
/// </summary>
private void Anchor(Entity<HijackBeaconComponent> ent, TransformComponent? beaconXForm = null)
{
beaconXForm ??= Transform(ent);
if (beaconXForm.Anchored || beaconXForm.GridUid == null)
return;
_transform.AnchorEntity(ent, beaconXForm);
_popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-anchor"), ent, null);
}
/// <summary>
/// Unanchoring helper
/// </summary>
private void Unanchor(Entity<HijackBeaconComponent> ent, TransformComponent? beaconXForm = null)
{
beaconXForm ??= Transform(ent);
if (!beaconXForm.Anchored)
return;
_transform.Unanchor(ent, beaconXForm);
_popup.PopupPredicted(Loc.GetString("hijack-beacon-popup-unanchor"), ent, null);
}
/// <summary>
/// Turns time values into usable values for announcements/examine messages
/// </summary>
private int GetRemainingTime(Entity<ActiveHijackBeaconComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return 69420; // Mature error code
return (int) (ent.Comp.CompletionTime - _gameTiming.CurTime).TotalSeconds;
}
#endregion
}
public enum HijackBeaconStatus : byte
{
AwaitActivate,
Armed,
Cooldown,
HijackComplete
}
/// <summary>
/// DoAfter event raised when the hijack beacon is deactivated.
/// </summary>
[Serializable, NetSerializable]
public sealed partial class HijackBeaconDeactivateDoAfterEvent : SimpleDoAfterEvent;
/// <summary>
/// Event raised when the hijack beacon succeeds in hijacking the ATS.
/// </summary>
[ByRefEvent]
public record struct HijackBeaconSuccessEvent(int Fine)
{
public int Total = 0;
};

View file

@ -0,0 +1,16 @@
hijack-beacon-announcement-sender = Automated Trade Station
hijack-beacon-announcement-activated = Attention! An Attempted Breach of the Automated Trade Station's firewall has been detected! Estimated {$time} seconds until firewall breach!
hijack-beacon-announcement-deactivated = Firewall breach failed. Firewall integrity partially restored. Have a nice day!
hijack-beacon-announcement-success = Successfully disengaged Automated Trade Station firewall. {$fine} spessos has been transferred from station funds to [%ERROR%]. Your trade station warranty is now void. This incident has been reported.
hijack-beacon-examine-await-activate = The beacon is [color=green]ready to activate[/color].
hijack-beacon-examine-await-cooldown = The beacon is [color=red]on cooldown[/color].
hijack-beacon-examine-await-hijack-complete = The beacon is [color=red]spent[/color].
hijack-beacon-popup-anchor = The beacon anchors itself into the ground!
hijack-beacon-popup-unanchor = The beacon unanchors itself from the ground.
hijack-beacon-verb-activate-text = Activate
hijack-beacon-verb-activate-message = The beacon can only be armed on the Automated Trade Station, on an unoccupied tile.
hijack-beacon-verb-deactivate-text = Deactivate
hijack-beacon-verb-deactivate-message = The beacon isn't going to deactivate itself, you know.

View file

@ -12,6 +12,7 @@ store-category-allies = Allies
store-category-job = Job
store-category-wearables = Wearables
store-category-pointless = Pointless
store-category-objective = Objective
store-discounted-items = Discounts
# Revenant

View file

@ -520,3 +520,7 @@ uplink-briefcase-gun-desc = An indistinct briefcase with a highly compact C-20K
uplink-energycrossbow-name = Mini Energy Crossbow
uplink-energycrossbow-desc = The go-to sidearm of any operative who prefers their victims not to be moving. Fires regenerating toxic arrows that floors victims in an instant.
#Objective items
uplink-hijack-beacon-name = Hijack Beacon
uplink-hijack-beacon-desc = A syndicate-brand hijack beacon designed to get around the firewalls of Nanotrasen-brand Automated Trade Stations. They take 200 seconds to work and Trade Stations will announce they are being hacked, so prepare accordingly.

View file

@ -2268,3 +2268,20 @@
- !type:BuyerJobCondition
whitelist:
- Lawyer
#Objective items
- type: listing
id: uplinkHijackBeacon
name: uplink-hijack-beacon-name
description: uplink-hijack-beacon-desc
productEntity: HijackBeacon
categories:
- UplinkObjective
conditions:
- !type:ListingLimitedStockCondition
stock: 1
- !type:BuyerObjectiveWhitelistCondition
whitelist:
components:
- HijackTradeStationCondition

View file

@ -0,0 +1,34 @@
- type: entity
parent: [ BaseItem, BaseSyndicateContraband ]
id: HijackBeacon
name: hijack beacon
description: A device that bypasses the firewall on Nanotrasen-brand Automated Trade Stations.
components:
- type: Transform
anchored: false
noRot: true
- type: Physics
bodyType: Dynamic
- type: Anchorable
- type: Item
size: Normal
- type: HijackBeacon
- type: Fixtures
fixtures:
fix1:
shape:
!type:PhysShapeAabb
bounds: "-0.3,-0.3,0.3,0.3"
density: 190
mask:
- MachineMask
layer:
- MachineLayer
- type: Clickable
- type: InteractionOutline
- type: Appearance
- type: Sprite
sprite: Objects/Tools/hijack_beacon.rsi
noRot: true
layers:
- state: extraction_point

View file

@ -6,6 +6,7 @@
TraitorObjectiveGroupKill: 1
TraitorObjectiveGroupState: 1 #As in, something about your character. Alive, dead, arrested, gained an ability...
TraitorObjectiveGroupSocial: 1 #Involves helping/harming others without killing them or stealing their stuff
TraitorObjectiveGroupOther: 1
- type: weightedRandom
id: TraitorObjectiveGroupSteal
@ -43,6 +44,13 @@
# RandomTraitorAliveObjective: 1 - Removed because the objective was boring and didn't progress the round.
RandomTraitorProgressObjective: 1
#misc objectives that don't fall other another category
- type: weightedRandom
id: TraitorObjectiveGroupOther
weights:
HijackTradeStationObjective: 1
#Thief groups
- type: weightedRandom
id: ThiefObjectiveGroups

View file

@ -352,3 +352,20 @@
- type: StealCondition
stealGroup: NukeDisk
owner: objective-condition-steal-station
# other
- type: entity
parent: BaseTraitorObjective
id: HijackTradeStationObjective
name: Hijack the Automated Trade Station.
description: Your uplink has been authorized one hijack beacon. Deploy it on the Automated Trade Station and defend it whilst it hijacks the trade station.
components:
- type: Objective
difficulty: 3
icon:
sprite: Objects/Tools/hijack_beacon.rsi
state: extraction_point
- type: HijackTradeStationCondition
- type: ObjectiveLimit
# There is only one trade station so there should never be more than one of these objectives.
limit: 1

View file

@ -89,6 +89,11 @@
name: store-category-pointless
priority: 10
- type: storeCategory
id: UplinkObjective
name: store-category-objective
priority: 11
#nukie delivery
- type: storeCategory

View file

@ -16,6 +16,7 @@
- UplinkWearables
- UplinkJob
- UplinkPointless
- UplinkObjective
currencyWhitelist:
- Telecrystal
balance:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -0,0 +1,20 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
"copyright": "Taken from https://github.com/austation/austation/commit/e2a4fefd01e702f48d3d4cc8d6a2686d54d104fa and edited by TheShuEd. Recolored by alexalexmax",
"size": {
"x": 32,
"y": 32
},
"states": [
{
"name": "extraction_point",
"delays": [
[
0.5,
0.5
]
]
}
]
}