diff --git a/Content.Server/Explosion/Components/ExplosiveComponent.cs b/Content.Server/Explosion/Components/ExplosiveComponent.cs index 04a08955a3..2b27a89d9d 100644 --- a/Content.Server/Explosion/Components/ExplosiveComponent.cs +++ b/Content.Server/Explosion/Components/ExplosiveComponent.cs @@ -81,6 +81,13 @@ public sealed partial class ExplosiveComponent : Component [DataField("deleteAfterExplosion")] public bool? DeleteAfterExplosion; + /// + /// Whether to not set to true, allowing it to explode multiple times. + /// This should never be used if it is damageable. + /// + [DataField] + public bool Repeatable; + /// /// Avoid somehow double-triggering this explosion (e.g. by damaging this entity from its own explosion. /// diff --git a/Content.Server/Explosion/Components/RepeatingTriggerComponent.cs b/Content.Server/Explosion/Components/RepeatingTriggerComponent.cs new file mode 100644 index 0000000000..cc08de53f9 --- /dev/null +++ b/Content.Server/Explosion/Components/RepeatingTriggerComponent.cs @@ -0,0 +1,25 @@ +using Content.Server.Explosion.EntitySystems; +using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom; + +namespace Content.Server.Explosion.Components; + +/// +/// Constantly triggers after being added to an entity. +/// +[RegisterComponent, Access(typeof(TriggerSystem))] +[AutoGenerateComponentPause] +public sealed partial class RepeatingTriggerComponent : Component +{ + /// + /// How long to wait between triggers. + /// The first trigger starts this long after the component is added. + /// + [DataField] + public TimeSpan Delay = TimeSpan.FromSeconds(1); + + /// + /// When the next trigger will be. + /// + [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField] + public TimeSpan NextTrigger; +} diff --git a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs index 23543895e6..8734c054d6 100644 --- a/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs +++ b/Content.Server/Explosion/EntitySystems/ExplosionSystem.cs @@ -160,7 +160,7 @@ public sealed partial class ExplosionSystem : EntitySystem if (explosive.Exploded) return; - explosive.Exploded = true; + explosive.Exploded = !explosive.Repeatable; // Override the explosion intensity if optional arguments were provided. if (radius != null) diff --git a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs index 3675c214b1..8e0a75ea24 100644 --- a/Content.Server/Explosion/EntitySystems/TriggerSystem.cs +++ b/Content.Server/Explosion/EntitySystems/TriggerSystem.cs @@ -94,6 +94,7 @@ namespace Content.Server.Explosion.EntitySystems SubscribeLocalEvent(OnStepTriggered); SubscribeLocalEvent(OnSlipTriggered); SubscribeLocalEvent(OnEmptyTriggered); + SubscribeLocalEvent(OnRepeatInit); SubscribeLocalEvent(OnSpawnTrigger); SubscribeLocalEvent(HandleDeleteTrigger); @@ -241,6 +242,11 @@ namespace Content.Server.Explosion.EntitySystems Trigger(uid, args.EmptyGun); } + private void OnRepeatInit(Entity ent, ref MapInitEvent args) + { + ent.Comp.NextTrigger = _timing.CurTime + ent.Comp.Delay; + } + public bool Trigger(EntityUid trigger, EntityUid? user = null) { var triggerEvent = new TriggerEvent(trigger, user); @@ -323,6 +329,7 @@ namespace Content.Server.Explosion.EntitySystems UpdateProximity(); UpdateTimer(frameTime); UpdateTimedCollide(frameTime); + UpdateRepeat(); } private void UpdateTimer(float frameTime) @@ -357,5 +364,19 @@ namespace Content.Server.Explosion.EntitySystems _appearance.SetData(uid, TriggerVisuals.VisualState, TriggerVisualState.Unprimed, appearance); } } + + private void UpdateRepeat() + { + var now = _timing.CurTime; + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var comp)) + { + if (comp.NextTrigger > now) + continue; + + comp.NextTrigger = now + comp.Delay; + Trigger(uid); + } + } } } diff --git a/Content.Shared/Weapons/Ranged/Components/ActionGunComponent.cs b/Content.Shared/Weapons/Ranged/Components/ActionGunComponent.cs new file mode 100644 index 0000000000..112339efd7 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Components/ActionGunComponent.cs @@ -0,0 +1,37 @@ +using Content.Shared.Actions; +using Content.Shared.Weapons.Ranged.Systems; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared.Weapons.Ranged.Components; + +/// +/// Lets you shoot a gun using an action. +/// +[RegisterComponent, NetworkedComponent, Access(typeof(ActionGunSystem))] +public sealed partial class ActionGunComponent : Component +{ + /// + /// Action to create, must use . + /// + [DataField(required: true)] + public EntProtoId Action = string.Empty; + + [DataField] + public EntityUid? ActionEntity; + + /// + /// Prototype of gun entity to spawn. + /// Deleted when this component is removed. + /// + [DataField(required: true)] + public EntProtoId GunProto = string.Empty; + + [DataField] + public EntityUid? Gun; +} + +/// +/// Action event for to shoot at a position. +/// +public sealed partial class ActionGunShootEvent : WorldTargetActionEvent; diff --git a/Content.Shared/Weapons/Ranged/Systems/ActionGunSystem.cs b/Content.Shared/Weapons/Ranged/Systems/ActionGunSystem.cs new file mode 100644 index 0000000000..f3dfe8a2a0 --- /dev/null +++ b/Content.Shared/Weapons/Ranged/Systems/ActionGunSystem.cs @@ -0,0 +1,41 @@ +using Content.Shared.Actions; +using Content.Shared.Weapons.Ranged.Components; + +namespace Content.Shared.Weapons.Ranged.Systems; + +public sealed class ActionGunSystem : EntitySystem +{ + [Dependency] private readonly SharedActionsSystem _actions = default!; + [Dependency] private readonly SharedGunSystem _gun = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMapInit); + SubscribeLocalEvent(OnShutdown); + SubscribeLocalEvent(OnShoot); + } + + private void OnMapInit(Entity ent, ref MapInitEvent args) + { + if (string.IsNullOrEmpty(ent.Comp.Action)) + return; + + _actions.AddAction(ent, ref ent.Comp.ActionEntity, ent.Comp.Action); + ent.Comp.Gun = Spawn(ent.Comp.GunProto); + } + + private void OnShutdown(Entity ent, ref ComponentShutdown args) + { + if (ent.Comp.Gun is {} gun) + QueueDel(gun); + } + + private void OnShoot(Entity ent, ref ActionGunShootEvent args) + { + if (TryComp(ent.Comp.Gun, out var gun)) + _gun.AttemptShoot(ent, ent.Comp.Gun.Value, gun, args.Target); + } +} + diff --git a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml index ee0db34fc2..cb9dc1c911 100644 --- a/Resources/Prototypes/Entities/Mobs/Player/dragon.yml +++ b/Resources/Prototypes/Entities/Mobs/Player/dragon.yml @@ -91,6 +91,12 @@ speedModifierThresholds: 250: 0.7 400: 0.5 + # disable taking damage from fire, since its a fire breathing dragon + - type: Flammable + damage: + types: {} + - type: Temperature + heatDamageThreshold: 800 - type: Metabolizer solutionOnBody: false updateInterval: 0.25 @@ -141,10 +147,33 @@ spawnRiftAction: ActionSpawnRift - type: GenericAntag rule: Dragon + - type: ActionGun + action: ActionDragonsBreath + gunProto: DragonsBreathGun - type: GuideHelp guides: - MinorAntagonists +- type: entity + noSpawn: true + id: DragonsBreathGun + name: dragon's lung + description: For dragon's breathing + components: + - type: RechargeBasicEntityAmmo + rechargeCooldown: 5 + rechargeSound: + path: /Audio/Animals/space_dragon_roar.ogg + - type: BasicEntityAmmoProvider + proto: ProjectileDragonsBreath + capacity: 1 + count: 1 + - type: Gun + soundGunshot: + path: /Audio/Animals/space_dragon_roar.ogg + soundEmpty: null + projectileSpeed: 5 + - type: entity parent: BaseMobDragon id: MobDragonDungeon @@ -183,6 +212,7 @@ state: icon event: !type:DragonSpawnRiftActionEvent useDelay: 1 + priority: 3 - type: entity id: ActionDevour @@ -194,3 +224,18 @@ icon: { sprite : Interface/Actions/devour.rsi, state: icon } iconOn: { sprite : Interface/Actions/devour.rsi, state: icon-on } event: !type:DevourActionEvent + priority: 1 + +- type: entity + noSpawn: true + id: ActionDragonsBreath + name: "[color=orange]Dragon's Breath[/color]" + description: Spew out flames at anyone foolish enough to attack you! + components: + - type: WorldTargetAction + # TODO: actual sprite + icon: { sprite : Objects/Weapons/Guns/Projectiles/magic.rsi, state: fireball } + event: !type:ActionGunShootEvent + priority: 2 + checkCanAccess: false + range: 0 diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml index 3556d1c8f8..d6adcd614e 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Guns/Projectiles/magic.yml @@ -30,6 +30,39 @@ - type: IgniteOnCollide fireStacks: 0.35 +- type: entity + noSpawn: true + parent: BaseBulletTrigger + id: ProjectileDragonsBreath + name: dragon's breath + description: Try not to get toasted. + components: + - type: PointLight + color: "#E25822" + radius: 3.0 + energy: 5.0 + - type: Sprite + sprite: Objects/Weapons/Guns/Projectiles/magic.rsi + layers: + - state: fireball + shader: unshaded + - type: IgnitionSource + temperature: 1000 + ignited: true + - type: RepeatingTrigger + delay: 0.5 # line of fire as well as if it hits something + - type: ExplodeOnTrigger + - type: Explosive + explosionType: FireBomb + totalIntensity: 5 # low intensity, the point is to burn attackers not to break open walls, dragons can just eat them + intensitySlope: 1 + maxIntensity: 3 + canCreateVacuum: false + deleteAfterExplosion: false + repeatable: true + - type: TimedDespawn + lifetime: 5 + - type: entity id: ProjectileAnomalyFireball name: fireball