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