diff --git a/Content.Server/Spider/SpiderSystem.cs b/Content.Server/Spider/SpiderSystem.cs deleted file mode 100644 index 449e43984c37..000000000000 --- a/Content.Server/Spider/SpiderSystem.cs +++ /dev/null @@ -1,77 +0,0 @@ -using System.Linq; -using Content.Server.Popups; -using Content.Shared.Spider; -using Content.Shared.Maps; -using Robust.Server.GameObjects; -using Robust.Shared.Map; - -namespace Content.Server.Spider; - -public sealed class SpiderSystem : SharedSpiderSystem -{ - [Dependency] private readonly PopupSystem _popup = default!; - - public override void Initialize() - { - base.Initialize(); - SubscribeLocalEvent(OnSpawnNet); - } - - private void OnSpawnNet(EntityUid uid, SpiderComponent component, SpiderWebActionEvent args) - { - if (args.Handled) - return; - - var transform = Transform(uid); - - if (transform.GridUid == null) - { - _popup.PopupEntity(Loc.GetString("spider-web-action-nogrid"), args.Performer, args.Performer); - return; - } - - var coords = transform.Coordinates; - - // TODO generic way to get certain coordinates - - var result = false; - // Spawn web in center - if (!IsTileBlockedByWeb(coords)) - { - Spawn(component.WebPrototype, coords); - result = true; - } - - // Spawn web in other directions - for (var i = 0; i < 4; i++) - { - var direction = (DirectionFlag) (1 << i); - coords = transform.Coordinates.Offset(direction.AsDir().ToVec()); - - if (!IsTileBlockedByWeb(coords)) - { - Spawn(component.WebPrototype, coords); - result = true; - } - } - - if (result) - { - _popup.PopupEntity(Loc.GetString("spider-web-action-success"), args.Performer, args.Performer); - args.Handled = true; - } - else - _popup.PopupEntity(Loc.GetString("spider-web-action-fail"), args.Performer, args.Performer); - } - - private bool IsTileBlockedByWeb(EntityCoordinates coords) - { - foreach (var entity in coords.GetEntitiesInTile()) - { - if (HasComp(entity)) - return true; - } - return false; - } -} - diff --git a/Content.Server/WebPlacer/WebPlacerSystem.cs b/Content.Server/WebPlacer/WebPlacerSystem.cs new file mode 100644 index 000000000000..365374e783d4 --- /dev/null +++ b/Content.Server/WebPlacer/WebPlacerSystem.cs @@ -0,0 +1,99 @@ +using Content.Server.Popups; +using Content.Shared.Maps; +using Content.Shared.WebPlacer; +using Content.Shared.Whitelist; +using Robust.Server.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Audio.Systems; +using Robust.Shared.Map; +using Robust.Shared.Map.Components; + +namespace Content.Server.WebPlacer; + +/// +/// Spawns entities (probably webs) around the component owner when using the component's action. +/// +/// +public sealed class WebPlacerSystem : SharedWebPlacerSystem +{ + [Dependency] private readonly EntityLookupSystem _lookup = default!; + [Dependency] private readonly EntityWhitelistSystem _whitelist = default!; + [Dependency] private readonly ITileDefinitionManager _tile = default!; + [Dependency] private readonly PopupSystem _popup = default!; + [Dependency] private readonly SharedAudioSystem _audio = default!; + [Dependency] private readonly SharedMapSystem _map = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnSpawnWeb); + } + + private void OnSpawnWeb(Entity webPlacer, ref SpiderWebActionEvent args) + { + if (args.Handled) + return; + + var xform = Transform(webPlacer.Owner); + var grid = xform.GridUid; + + // Instantly fail in space. + if (!TryComp(grid, out var gridComp)) + { + _popup.PopupEntity(Loc.GetString(webPlacer.Comp.MessageOffGrid), args.Performer, args.Performer); + return; + } + + // Get coordinates and spawn Comp.WebPrototype if the coordinates are valid. + bool success = false; + foreach (var vect in webPlacer.Comp.OffsetVectors) + { + var pos = xform.Coordinates.Offset(vect); + if (!IsValidTile(pos, webPlacer.Comp.DestinationWhitelist, webPlacer.Comp.DestinationBlacklist, (grid.Value, gridComp))) + continue; + + Spawn(webPlacer.Comp.WebPrototype, pos); + success = true; + } + + // Return unhandled if nothing was spawned so that the action doesn't go on cooldown. + if (!success) + { + _popup.PopupEntity(Loc.GetString(webPlacer.Comp.MessageNoSpawn), args.Performer, args.Performer); + return; + } + + args.Handled = true; + _popup.PopupEntity(Loc.GetString(webPlacer.Comp.MessageSuccess), args.Performer, args.Performer); + + if (webPlacer.Comp.WebSound != null) + _audio.PlayPvs(webPlacer.Comp.WebSound, webPlacer.Owner); + } + + private bool IsValidTile(EntityCoordinates coords, EntityWhitelist? whitelist, EntityWhitelist? blacklist, Entity mapGrid) + { + // Don't place webs in space + if (!_map.TryGetTileRef(mapGrid.Owner, mapGrid.Comp, coords, out var tileRef) || + tileRef.IsSpace(_tile)) + return false; + + // Don't place webs on webs + if (blacklist != null) + foreach (var entity in _lookup.GetEntitiesIntersecting(coords, LookupFlags.Uncontained)) + if (_whitelist.IsBlacklistPass(blacklist, entity)) + return false; + + // Only place webs on webs + if (whitelist != null) + { + foreach (var entity in _lookup.GetEntitiesIntersecting(coords, LookupFlags.Uncontained)) + if (_whitelist.IsWhitelistPass(whitelist, entity)) + return true; + + return false; + } + + return true; + } +} diff --git a/Content.Shared/IgnoreSpiderWeb/IgnoreSpiderWebComponent.cs b/Content.Shared/IgnoreSpiderWeb/IgnoreSpiderWebComponent.cs new file mode 100644 index 000000000000..9748cd6a1324 --- /dev/null +++ b/Content.Shared/IgnoreSpiderWeb/IgnoreSpiderWebComponent.cs @@ -0,0 +1,9 @@ +namespace Content.Shared.IgnoreSpiderWeb; + +/// +/// Marker component for entities that ignore spiderwebs. +/// +[RegisterComponent] +public sealed partial class IgnoreSpiderWebComponent : Component +{ +} diff --git a/Content.Shared/Spider/IgnoreSpiderWebComponent.cs b/Content.Shared/Spider/IgnoreSpiderWebComponent.cs deleted file mode 100644 index 55eb1aa50f0f..000000000000 --- a/Content.Shared/Spider/IgnoreSpiderWebComponent.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Content.Shared.Spider; - -[RegisterComponent] -public sealed partial class IgnoreSpiderWebComponent : Component -{ - -} diff --git a/Content.Shared/Spider/SharedSpiderSystem.cs b/Content.Shared/Spider/SharedSpiderSystem.cs deleted file mode 100644 index 33473303aa91..000000000000 --- a/Content.Shared/Spider/SharedSpiderSystem.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Content.Shared.Actions; -using Robust.Shared.Network; -using Robust.Shared.Random; - -namespace Content.Shared.Spider; - -public abstract class SharedSpiderSystem : EntitySystem -{ - [Dependency] private readonly SharedActionsSystem _action = default!; - [Dependency] private readonly IRobustRandom _robustRandom = default!; - [Dependency] private readonly SharedAppearanceSystem _appearance = default!; - - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent(OnInit); - SubscribeLocalEvent(OnWebStartup); - } - - private void OnInit(EntityUid uid, SpiderComponent component, MapInitEvent args) - { - _action.AddAction(uid, ref component.Action, component.WebAction, uid); - } - - private void OnWebStartup(EntityUid uid, SpiderWebObjectComponent component, ComponentStartup args) - { - // TODO dont use this. use some general random appearance system - _appearance.SetData(uid, SpiderWebVisuals.Variant, _robustRandom.Next(1, 3)); - } -} diff --git a/Content.Shared/Spider/SpiderComponent.cs b/Content.Shared/Spider/SpiderComponent.cs deleted file mode 100644 index 42213adcb10b..000000000000 --- a/Content.Shared/Spider/SpiderComponent.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Content.Shared.Actions; -using Robust.Shared.GameStates; -using Robust.Shared.Prototypes; -using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; - -namespace Content.Shared.Spider; - -[RegisterComponent, NetworkedComponent] -[Access(typeof(SharedSpiderSystem))] -public sealed partial class SpiderComponent : Component -{ - [ViewVariables(VVAccess.ReadWrite)] - [DataField("webPrototype", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string WebPrototype = "SpiderWeb"; - - [ViewVariables(VVAccess.ReadWrite)] - [DataField("webAction", customTypeSerializer: typeof(PrototypeIdSerializer))] - public string WebAction = "ActionSpiderWeb"; - - [DataField] public EntityUid? Action; -} - -public sealed partial class SpiderWebActionEvent : InstantActionEvent { } diff --git a/Content.Shared/Spider/SpiderWebObjectComponent.cs b/Content.Shared/Spider/SpiderWebObjectComponent.cs deleted file mode 100644 index 3f05888083d7..000000000000 --- a/Content.Shared/Spider/SpiderWebObjectComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.GameStates; - -namespace Content.Shared.Spider; - -[RegisterComponent, NetworkedComponent] -[Access(typeof(SharedSpiderSystem))] -public sealed partial class SpiderWebObjectComponent : Component -{ -} diff --git a/Content.Shared/Spider/SpiderWebVisualsComponent.cs b/Content.Shared/Spider/SpiderWebVisualsComponent.cs deleted file mode 100644 index 2d6fb5881592..000000000000 --- a/Content.Shared/Spider/SpiderWebVisualsComponent.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Robust.Shared.Serialization; - -namespace Content.Shared.Spider; - -[Serializable, NetSerializable] -public enum SpiderWebVisuals -{ - Variant -} diff --git a/Content.Shared/WebPlacer/SharedWebPlacerSystem.cs b/Content.Shared/WebPlacer/SharedWebPlacerSystem.cs new file mode 100644 index 000000000000..4538f69b987a --- /dev/null +++ b/Content.Shared/WebPlacer/SharedWebPlacerSystem.cs @@ -0,0 +1,25 @@ +using Content.Shared.Actions; + +namespace Content.Shared.WebPlacer; + +/// +/// Gives the component owner (probably a spider) an action to spawn entites around itself. +/// Spawning is handled by . +/// +/// +public abstract class SharedWebPlacerSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _action = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnInit); + } + + private void OnInit(Entity webPlacer, ref MapInitEvent args) + { + _action.AddAction(webPlacer.Owner, ref webPlacer.Comp.ActionEntity, webPlacer.Comp.SpawnWebAction, webPlacer.Owner); + } +} diff --git a/Content.Shared/WebPlacer/WebPlacerComponent.cs b/Content.Shared/WebPlacer/WebPlacerComponent.cs new file mode 100644 index 000000000000..7a5e50bad614 --- /dev/null +++ b/Content.Shared/WebPlacer/WebPlacerComponent.cs @@ -0,0 +1,94 @@ +using Content.Shared.Actions; +using Content.Shared.Whitelist; +using Robust.Shared.Audio; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.WebPlacer; + +/// +/// Gives the entity (probably a spider) an action to spawn entities (probably webs) around itself. +/// +/// +/// +[RegisterComponent, NetworkedComponent] +[Access(typeof(SharedWebPlacerSystem))] +public sealed partial class WebPlacerComponent : Component +{ + /// + /// Id of the entity getting spawned. + /// + [DataField] + public EntProtoId WebPrototype = "SpiderWeb"; + + /// + /// Id of the action that will be given. + /// + [DataField] + public EntProtoId SpawnWebAction = "ActionSpiderWeb"; + + /// + /// Action given to the player. + /// + [DataField] + public EntityUid? ActionEntity; + + /// + /// Whitelist of entities proto will only spawn on. + /// + [DataField] + public EntityWhitelist? DestinationWhitelist; + + /// + /// Blacklist of entities proto won't spawn on. + /// + [DataField] + public EntityWhitelist? DestinationBlacklist; + + /// + /// Sound played when successfully spawning something. + /// + [DataField] + public SoundSpecifier? WebSound = + new SoundPathSpecifier("/Audio/Effects/spray3.ogg") + { + Params = AudioParams.Default.WithVariation(0.125f), + }; + + /// + /// Vectors determining where the entities will spawn. + /// + [DataField] + public List OffsetVectors = new() + { + Vector2i.Zero, + Vector2i.Up, + Vector2i.Down, + Vector2i.Left, + Vector2i.Right, + }; + + #region Localization + /// + /// Webs cannot be placed because the component owner is not on a valid grid (e.g. in space). + /// + [DataField] + public LocId MessageOffGrid = "spider-web-action-off-grid"; + + /// + /// At least one web was placed. + /// + [DataField] + public LocId MessageSuccess = "spider-web-action-success"; + + /// + /// Webs failed to be placed (e.g. no valid spawn destination). + /// + [DataField] + public LocId MessageNoSpawn = "spider-web-action-no-spawn"; + #endregion +} + +public sealed partial class SpiderWebActionEvent : InstantActionEvent +{ +} diff --git a/Resources/Locale/en-US/actions/actions/spider.ftl b/Resources/Locale/en-US/actions/actions/spider.ftl index e7a41c883aa7..44f022f5ac91 100644 --- a/Resources/Locale/en-US/actions/actions/spider.ftl +++ b/Resources/Locale/en-US/actions/actions/spider.ftl @@ -1,5 +1,5 @@ -spider-web-action-nogrid = There is no floor under you! -spider-web-action-success = You place webs around you. -spider-web-action-fail = You can't place webs here! All cardinal directions already have webs! +spider-web-action-off-grid = There is no floor under you! +spider-web-action-success = You spin webs around you. +spider-web-action-no-spawn = There is no room to spin your webs! sericulture-failure-hunger = Your stomach is too empty to make any more webs! diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index e71902db6b08..014d57740b51 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -2349,7 +2349,11 @@ interactSuccessSound: path: /Audio/Animals/snake_hiss.ogg - type: NoSlip - - type: Spider + - type: WebPlacer + webPrototype: SpiderWeb + destinationBlacklist: + tags: + - SpiderWeb - type: IgnoreSpiderWeb - type: Bloodstream bloodMaxVolume: 150 @@ -2481,8 +2485,15 @@ thresholds: 0: Alive 180: Dead - - type: Spider + - type: WebPlacer webPrototype: SpiderWebClown + webSound: + collection: BikeHorn + params: + variation: 0.125 + destinationBlacklist: + tags: + - SpiderWeb - type: MeleeWeapon altDisarm: false angle: 0 diff --git a/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml b/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml index 02feda953f8c..5cd7854dd37b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/spider_web.yml @@ -16,15 +16,14 @@ sprite: Objects/Misc/spiderweb.rsi layers: - state: spider_web_1 - map: ["spiderWebLayer"] + map: [ "enum.DamageStateVisualLayers.Base" ] drawdepth: WallMountedItems + - type: RandomSprite + available: + - enum.DamageStateVisualLayers.Base: + spider_web_1: "" + spider_web_2: "" - type: Appearance - - type: GenericVisualizer - visuals: - enum.SpiderWebVisuals.Variant: - spiderWebLayer: - 1: {state: spider_web_1} - 2: {state: spider_web_2} - type: Clickable - type: Transform anchored: true @@ -69,13 +68,15 @@ groups: Flammable: [Touch] Extinguish: [Touch] - - type: SpiderWebObject - type: SpeedModifierContacts walkSpeedModifier: 0.5 sprintSpeedModifier: 0.5 ignoreWhitelist: components: - IgnoreSpiderWeb + - type: Tag + tags: + - SpiderWeb - type: entity id: SpiderWebClown @@ -95,15 +96,14 @@ sprite: Objects/Misc/spiderweb.rsi layers: - state: spider_web_clown_1 - map: ["spiderWebLayer"] + map: [ "enum.DamageStateVisualLayers.Base" ] drawdepth: WallMountedItems + - type: RandomSprite + available: + - enum.DamageStateVisualLayers.Base: + spider_web_clown_1: "" + spider_web_clown_2: "" - type: Appearance - - type: GenericVisualizer - visuals: - enum.SpiderWebVisuals.Variant: - spiderWebLayer: - 1: {state: spider_web_clown_1} - 2: {state: spider_web_clown_2} - type: Clickable - type: Transform anchored: true @@ -152,7 +152,6 @@ groups: Flammable: [Touch] Extinguish: [Touch] - - type: SpiderWebObject - type: FlavorProfile flavors: - sweet @@ -164,3 +163,6 @@ reagents: - ReagentId: Sugar Quantity: 2 + - type: Tag + tags: + - SpiderWeb diff --git a/Resources/Prototypes/tags.yml b/Resources/Prototypes/tags.yml index ea2dffbe6a30..6e6a61d6e56e 100644 --- a/Resources/Prototypes/tags.yml +++ b/Resources/Prototypes/tags.yml @@ -1177,6 +1177,9 @@ - type: Tag id: SpiderCraft +- type: Tag + id: SpiderWeb + - type: Tag id: SpookyFog