diff --git a/Content.Server/Objectives/Components/StealAreaComponent.cs b/Content.Server/Objectives/Components/StealAreaComponent.cs new file mode 100644 index 0000000000..26e752f2f2 --- /dev/null +++ b/Content.Server/Objectives/Components/StealAreaComponent.cs @@ -0,0 +1,23 @@ +using Content.Server.Objectives.Systems; +using Content.Server.Thief.Systems; + +namespace Content.Server.Objectives.Components; + +/// +/// An abstract component that allows other systems to count adjacent objects as "stolen" when controlling other systems +/// +[RegisterComponent, Access(typeof(StealConditionSystem), typeof(ThiefBeaconSystem))] +public sealed partial class StealAreaComponent : Component +{ + [DataField] + public bool Enabled = true; + + [DataField] + public float Range = 1f; + + /// + /// all the minds that will be credited with stealing from this area. + /// + [DataField] + public HashSet Owners = new(); +} diff --git a/Content.Server/Objectives/Components/StealConditionComponent.cs b/Content.Server/Objectives/Components/StealConditionComponent.cs index f8d68063f8..cdade8cc8d 100644 --- a/Content.Server/Objectives/Components/StealConditionComponent.cs +++ b/Content.Server/Objectives/Components/StealConditionComponent.cs @@ -22,11 +22,18 @@ public sealed partial class StealConditionComponent : Component [DataField] public bool VerifyMapExistence = true; + /// + /// If true, counts objects that are close to steal areas. + /// + [DataField] + public bool CheckStealAreas = false; + /// /// If the target may be alive but has died, it will not be counted /// [DataField] public bool CheckAlive = false; + /// /// The minimum number of items you need to steal to fulfill a objective /// diff --git a/Content.Server/Objectives/Systems/StealConditionSystem.cs b/Content.Server/Objectives/Systems/StealConditionSystem.cs index 42a296ab92..2c9244cf7d 100644 --- a/Content.Server/Objectives/Systems/StealConditionSystem.cs +++ b/Content.Server/Objectives/Systems/StealConditionSystem.cs @@ -21,16 +21,15 @@ public sealed class StealConditionSystem : EntitySystem [Dependency] private readonly MetaDataSystem _metaData = default!; [Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly SharedObjectivesSystem _objectives = default!; + [Dependency] private readonly EntityLookupSystem _lookup = default!; private EntityQuery _containerQuery; - private EntityQuery _metaQuery; public override void Initialize() { base.Initialize(); _containerQuery = GetEntityQuery(); - _metaQuery = GetEntityQuery(); SubscribeLocalEvent(OnAssigned); SubscribeLocalEvent(OnAfterAssign); @@ -96,25 +95,33 @@ public sealed class StealConditionSystem : EntitySystem if (!_containerQuery.TryGetComponent(mind.OwnedEntity, out var currentManager)) return 0; - var stack = new Stack(); + var containerStack = new Stack(); var count = 0; + //check stealAreas + if (condition.CheckStealAreas) + { + var areasQuery = AllEntityQuery(); + while (areasQuery.MoveNext(out var uid, out var area)) + { + if (!area.Owners.Contains(mind.Owner)) + continue; + + var nearestEnt = _lookup.GetEntitiesInRange(uid, area.Range); + foreach (var ent in nearestEnt) + { + CheckEntity(ent, condition, ref containerStack, ref count); + } + } + } + //check pulling object if (TryComp(mind.OwnedEntity, out var pull)) //TO DO: to make the code prettier? don't like the repetition { var pulledEntity = pull.Pulling; if (pulledEntity != null) { - // check if this is the item - count += CheckStealTarget(pulledEntity.Value, condition); - - //we don't check the inventories of sentient entity - if (!HasComp(pulledEntity)) - { - // if it is a container check its contents - if (_containerQuery.TryGetComponent(pulledEntity, out var containerManager)) - stack.Push(containerManager); - } + CheckEntity(pulledEntity.Value, condition, ref containerStack, ref count); } } @@ -131,16 +138,30 @@ public sealed class StealConditionSystem : EntitySystem // if it is a container check its contents if (_containerQuery.TryGetComponent(entity, out var containerManager)) - stack.Push(containerManager); + containerStack.Push(containerManager); } } - } while (stack.TryPop(out currentManager)); + } while (containerStack.TryPop(out currentManager)); var result = count / (float) condition.CollectionSize; result = Math.Clamp(result, 0, 1); return result; } + private void CheckEntity(EntityUid entity, StealConditionComponent condition, ref Stack containerStack, ref int counter) + { + // check if this is the item + counter += CheckStealTarget(entity, condition); + + //we don't check the inventories of sentient entity + if (!TryComp(entity, out var pullMind)) + { + // if it is a container check its contents + if (_containerQuery.TryGetComponent(entity, out var containerManager)) + containerStack.Push(containerManager); + } + } + private int CheckStealTarget(EntityUid entity, StealConditionComponent condition) { // check if this is the target diff --git a/Content.Server/Thief/Components/ThiefBeaconComponent.cs b/Content.Server/Thief/Components/ThiefBeaconComponent.cs new file mode 100644 index 0000000000..65db79f861 --- /dev/null +++ b/Content.Server/Thief/Components/ThiefBeaconComponent.cs @@ -0,0 +1,17 @@ +using Content.Server.Thief.Systems; +using Robust.Shared.Audio; + +namespace Content.Server.Thief.Components; + +/// +/// working together with StealAreaComponent, allows the thief to count objects near the beacon as stolen when setting up. +/// +[RegisterComponent, Access(typeof(ThiefBeaconSystem))] +public sealed partial class ThiefBeaconComponent : Component +{ + [DataField] + public SoundSpecifier LinkSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg"); + + [DataField] + public SoundSpecifier UnlinkSound = new SoundPathSpecifier("/Audio/Machines/beep.ogg"); +} diff --git a/Content.Server/Thief/Systems/ThiefBeaconSystem.cs b/Content.Server/Thief/Systems/ThiefBeaconSystem.cs new file mode 100644 index 0000000000..e004212124 --- /dev/null +++ b/Content.Server/Thief/Systems/ThiefBeaconSystem.cs @@ -0,0 +1,95 @@ +using Content.Server.Mind; +using Content.Server.Objectives.Components; +using Content.Server.Roles; +using Content.Server.Thief.Components; +using Content.Shared.Examine; +using Content.Shared.Foldable; +using Content.Shared.Popups; +using Content.Shared.Verbs; +using Robust.Shared.Audio.Systems; + +namespace Content.Server.Thief.Systems; + +/// +/// +/// +public sealed class ThiefBeaconSystem : EntitySystem +{ + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + [Dependency] private readonly MindSystem _mind = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent>(OnGetInteractionVerbs); + SubscribeLocalEvent(OnFolded); + SubscribeLocalEvent(OnExamined); + } + + private void OnGetInteractionVerbs(Entity beacon, ref GetVerbsEvent args) + { + if (!args.CanAccess || !args.CanInteract || args.Hands is null) + return; + + if (TryComp(beacon, out var foldable) && foldable.IsFolded) + return; + + var mind = _mind.GetMind(args.User); + if (!HasComp(mind)) + return; + + var user = args.User; + args.Verbs.Add(new() + { + Act = () => + { + SetCoordinate(beacon, mind.Value); + }, + Message = Loc.GetString("thief-fulton-verb-message"), + Text = Loc.GetString("thief-fulton-verb-text"), + }); + } + + private void OnFolded(Entity beacon, ref FoldedEvent args) + { + if (args.IsFolded) + ClearCoordinate(beacon); + } + + private void OnExamined(Entity beacon, ref ExaminedEvent args) + { + if (!TryComp(beacon, out var area)) + return; + + args.PushText(Loc.GetString(area.Owners.Count == 0 + ? "thief-fulton-examined-unset" + : "thief-fulton-examined-set")); + } + + private void SetCoordinate(Entity beacon, EntityUid mind) + { + if (!TryComp(beacon, out var area)) + return; + + _audio.PlayPvs(beacon.Comp.LinkSound, beacon); + _popup.PopupEntity(Loc.GetString("thief-fulton-set"), beacon); + area.Owners.Clear(); //We only reconfigure the beacon for ourselves, we don't need multiple thieves to steal from the same beacon. + area.Owners.Add(mind); + } + + private void ClearCoordinate(Entity beacon) + { + if (!TryComp(beacon, out var area)) + return; + + if (area.Owners.Count == 0) + return; + + _audio.PlayPvs(beacon.Comp.UnlinkSound, beacon); + _popup.PopupEntity(Loc.GetString("thief-fulton-clear"), beacon); + area.Owners.Clear(); + } +} diff --git a/Resources/Locale/en-US/thief/beacon.ftl b/Resources/Locale/en-US/thief/beacon.ftl new file mode 100644 index 0000000000..9fbbcaf419 --- /dev/null +++ b/Resources/Locale/en-US/thief/beacon.ftl @@ -0,0 +1,8 @@ +thief-fulton-set = Delivery coordinates are set. +thief-fulton-clear = Delivery coordinates cleared. + +thief-fulton-examined-set = Coordinates entered. Bluespace teleportation of the nearest objects will be performed when the evacuation shuttle departs. +thief-fulton-examined-unset = Beacon coordinates are not set. + +thief-fulton-verb-text = Set coordinates +thief-fulton-verb-message = Set the coordinates of your thief's hideout, where all nearby items will be sent at the end of the round. \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Objects/Tools/thief_beacon.yml b/Resources/Prototypes/Entities/Objects/Tools/thief_beacon.yml new file mode 100644 index 0000000000..c35e66127d --- /dev/null +++ b/Resources/Prototypes/Entities/Objects/Tools/thief_beacon.yml @@ -0,0 +1,38 @@ +- type: entity + id: ThiefBeacon + name: thieving beacon + description: A device that will teleport everything around it to the thief's vault at the end of the shift. + components: + - type: ThiefBeacon + - type: StealArea + - type: Item + size: Normal + - type: Physics + bodyType: Dynamic + - type: Fixtures + fixtures: + fix1: + shape: + !type:PhysShapeAabb + bounds: "-0.25,-0.4,0.25,0.1" + density: 20 + mask: + - Impassable + - type: Foldable + folded: true + - type: Clickable + - type: InteractionOutline + - type: Appearance + - type: GenericVisualizer + visuals: + enum.FoldedVisuals.State: + foldedLayer: + True: { state: folded_extraction } + False: { state: extraction_point } + - type: Sprite + sprite: Objects/Tools/thief_beacon.rsi + drawdepth: SmallObjects + noRot: true + layers: + - state: extraction_point + map: [ "foldedLayer" ] \ No newline at end of file diff --git a/Resources/Prototypes/Objectives/objectiveGroups.yml b/Resources/Prototypes/Objectives/objectiveGroups.yml index bb74c92da3..e62aa9fdf6 100644 --- a/Resources/Prototypes/Objectives/objectiveGroups.yml +++ b/Resources/Prototypes/Objectives/objectiveGroups.yml @@ -52,8 +52,8 @@ - type: weightedRandom id: ThiefBigObjectiveGroups weights: - ThiefObjectiveGroupStructure: 0 #Temporarily disabled until obvious ways to steal structures are added - ThiefObjectiveGroupAnimal: 2 + ThiefObjectiveGroupStructure: 1 + ThiefObjectiveGroupAnimal: 1 - type: weightedRandom id: ThiefObjectiveGroupCollection @@ -91,7 +91,6 @@ weights: NuclearBombStealObjective: 0.5 FaxMachineCaptainStealObjective: 1 - VehicleSecwayStealObjective: 1 ChemDispenserStealObjective: 1 XenoArtifactStealObjective: 1 FreezerHeaterStealObjective: 1 diff --git a/Resources/Prototypes/Objectives/thief.yml b/Resources/Prototypes/Objectives/thief.yml index c29641081b..672f9b2ba7 100644 --- a/Resources/Prototypes/Objectives/thief.yml +++ b/Resources/Prototypes/Objectives/thief.yml @@ -18,30 +18,29 @@ - type: StealCondition verifyMapExistence: false descriptionText: objective-condition-thief-description + checkStealAreas: true - type: entity abstract: true - parent: [BaseThiefObjective, BaseStealObjective] + parent: [BaseThiefObjective, BaseThiefStealObjective] id: BaseThiefStealCollectionObjective components: - type: StealCondition verifyMapExistence: true - descriptionText: objective-condition-thief-description - type: entity abstract: true - parent: [BaseThiefObjective, BaseStealObjective] + parent: [BaseThiefObjective, BaseThiefStealObjective] id: BaseThiefStealStructureObjective components: - type: StealCondition verifyMapExistence: true - descriptionText: objective-condition-thief-description - type: Objective difficulty: 2 # it's hard to hide - type: entity abstract: true - parent: [BaseThiefObjective, BaseStealObjective] + parent: [BaseThiefObjective, BaseThiefStealObjective] id: BaseThiefStealAnimalObjective components: - type: StealCondition diff --git a/Resources/Prototypes/Roles/Antags/Thief.yml b/Resources/Prototypes/Roles/Antags/Thief.yml index d85d366f6b..f0d0c12470 100644 --- a/Resources/Prototypes/Roles/Antags/Thief.yml +++ b/Resources/Prototypes/Roles/Antags/Thief.yml @@ -10,4 +10,5 @@ storage: back: - ToolboxThief - - ClothingHandsChameleonThief \ No newline at end of file + - ClothingHandsChameleonThief + - ThiefBeacon \ No newline at end of file diff --git a/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point.png b/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point.png new file mode 100644 index 0000000000..b986a6ae28 Binary files /dev/null and b/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point.png differ diff --git a/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point_light.png b/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point_light.png new file mode 100644 index 0000000000..f2e9f984d2 Binary files /dev/null and b/Resources/Textures/Objects/Tools/thief_beacon.rsi/extraction_point_light.png differ diff --git a/Resources/Textures/Objects/Tools/thief_beacon.rsi/folded_extraction.png b/Resources/Textures/Objects/Tools/thief_beacon.rsi/folded_extraction.png new file mode 100644 index 0000000000..43ead68bb2 Binary files /dev/null and b/Resources/Textures/Objects/Tools/thief_beacon.rsi/folded_extraction.png differ diff --git a/Resources/Textures/Objects/Tools/thief_beacon.rsi/meta.json b/Resources/Textures/Objects/Tools/thief_beacon.rsi/meta.json new file mode 100644 index 0000000000..ba7e003575 --- /dev/null +++ b/Resources/Textures/Objects/Tools/thief_beacon.rsi/meta.json @@ -0,0 +1,32 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Taken from https://github.com/austation/austation/commit/e2a4fefd01e702f48d3d4cc8d6a2686d54d104fa and edited by TheShuEd", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "folded_extraction" + }, + { + "name": "extraction_point", + "delays": [ + [ + 0.5, + 0.5 + ] + ] + }, + { + "name": "extraction_point_light", + "delays": [ + [ + 0.5, + 0.5 + ] + ] + } + ] +} \ No newline at end of file