Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions Content.Server/Lightning/Components/LightningTargetComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,26 @@ public sealed partial class LightningTargetComponent : Component
/// </summary>
[DataField]
public FixedPoint2 DamageFromLightning = 1;

// Triad: electrocution path. The Structural damage above no-ops on Biological damage containers,
// so mobs on the strike table took nothing. These make a strike electrocute the target instead;
// insulated gloves apply through the normal electrocution attempt.

/// <summary>
/// Triad: whether a lightning strike electrocutes this target (stun + shock damage).
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool ElectrocuteOnStrike;

/// <summary>
/// Triad: shock damage dealt by the electrocution.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int ElectrocutionShockDamage = 15;

/// <summary>
/// Triad: how long the electrocution lasts.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public TimeSpan ElectrocutionTime = TimeSpan.FromSeconds(5);
}
40 changes: 32 additions & 8 deletions Content.Server/Lightning/LightningSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,25 +106,37 @@ private void ShootRandomLightnings(EntityUid user, float range, int boltCount, E
// several hashsets every time

var targets = _lookup.GetEntitiesInRange<LightningTargetComponent>(_transform.GetMapCoordinates(user), range).ToList();
_random.Shuffle(targets);
targets.Sort((x, y) => y.Comp.Priority.CompareTo(x.Comp.Priority));
// Triad: strike-attempt hook. Each candidate may adjust its effective priority and hit chance
// from live state (tesla coils bid by charge headroom) before the volley is sorted and rolled.
// Targets with no subscriber keep their static values, so behavior is unchanged for them.
// _random.Shuffle(targets);
// targets.Sort((x, y) => y.Comp.Priority.CompareTo(x.Comp.Priority));
var candidates = new List<(Entity<LightningTargetComponent> Target, int Priority, float HitProbability)>(targets.Count);
foreach (var target in targets)
{
var attempt = new LightningStrikeAttemptEvent(user, target.Comp.Priority, target.Comp.HitProbability);
RaiseLocalEvent(target, ref attempt);
candidates.Add((target, attempt.Priority, Math.Clamp(attempt.HitProbability, 0f, 1f)));
}
_random.Shuffle(candidates);
candidates.Sort((x, y) => y.Priority.CompareTo(x.Priority));

int shootedCount = 0;
int count = -1;
while (shootedCount < boltCount)
{
count++;

if (count >= targets.Count) { break; }
if (count >= candidates.Count) { break; }

var curTarget = targets[count];
if (!_random.Prob(curTarget.Comp.HitProbability)) //Chance to ignore target
var curTarget = candidates[count];
if (!_random.Prob(curTarget.HitProbability)) //Chance to ignore target
continue;

ShootLightning(user, targets[count].Owner, spawnOnHit, lightningPrototype, triggerLightningEvents);
if (arcDepth - targets[count].Comp.LightningResistance > 0)
ShootLightning(user, curTarget.Target.Owner, spawnOnHit, lightningPrototype, triggerLightningEvents);
if (arcDepth - curTarget.Target.Comp.LightningResistance > 0)
{
ShootRandomLightnings(targets[count].Owner, range, 1, spawnOnHit, lightningPrototype, arcDepth - targets[count].Comp.LightningResistance, triggerLightningEvents);
ShootRandomLightnings(curTarget.Target.Owner, range, 1, spawnOnHit, lightningPrototype, arcDepth - curTarget.Target.Comp.LightningResistance, triggerLightningEvents);
}
shootedCount++;
}
Expand All @@ -138,3 +150,15 @@ private void ShootRandomLightnings(EntityUid user, float range, int boltCount, E
/// <param name="Target">The entity that was struck by lightning.</param>
[ByRefEvent]
public readonly record struct HitByLightningEvent(EntityUid Source, EntityUid Target);

/// <summary>
/// Triad: raised directed on each candidate target before a lightning volley is sorted and rolled.
/// Subscribers may adjust <see cref="Priority"/> and <see cref="HitProbability"/> from live state
/// (e.g. a tesla coil bidding by charge headroom). Both start at the target's static
/// LightningTargetComponent values; HitProbability is clamped to [0, 1] after the event.
/// </summary>
/// <param name="Source">The entity shooting the lightning volley</param>
/// <param name="Priority">Effective sort priority for this volley; higher is struck first</param>
/// <param name="HitProbability">Effective chance this target is not skipped by the roll</param>
[ByRefEvent]
public record struct LightningStrikeAttemptEvent(EntityUid Source, int Priority, float HitProbability);
6 changes: 6 additions & 0 deletions Content.Server/Lightning/LightningTargetSystem.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Content.Server.Electrocution; // Triad
using Content.Server.Explosion.EntitySystems;
using Content.Server.Lightning;
using Content.Server.Lightning.Components;
Expand All @@ -12,6 +13,7 @@ namespace Content.Server.Tesla.EntitySystems;
public sealed class LightningTargetSystem : EntitySystem
{
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly ElectrocutionSystem _electrocution = default!; // Triad
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly TransformSystem _transform = default!;

Expand All @@ -24,6 +26,10 @@ public override void Initialize()

private void OnHitByLightning(Entity<LightningTargetComponent> uid, ref HitByLightningEvent args)
{
// Triad: electrocution path for biological targets, which the Structural damage below no-ops on.
if (uid.Comp.ElectrocuteOnStrike)
_electrocution.TryDoElectrocution(uid, args.Source, uid.Comp.ElectrocutionShockDamage, uid.Comp.ElectrocutionTime, refresh: true);

DamageSpecifier damage = new();
damage.DamageDict.Add("Structural", uid.Comp.DamageFromLightning);
_damageable.TryChangeDamage(uid, damage, true);
Expand Down
17 changes: 17 additions & 0 deletions Content.Server/Tesla/Components/TeslaCoilComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,21 @@ public sealed partial class TeslaCoilComponent : Component
// To Do: Different lightning bolts have different powers and generate different amounts of energy
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float ChargeFromLightning = 50000f;

/// <summary>
/// Triad: strike-chance floor once the battery is full. Lightning favors the biggest potential
/// difference, so the coil's effective hit chance scales with charge headroom: empty = 1.0
/// (plus a priority bump above every static target), full = this floor. A floored coil still
/// outranks grounding rods in the sort but gets skipped on almost every roll, so overflow
/// falls through to the rods.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float SaturatedHitProbability = 0.05f;

/// <summary>
/// Triad: priority added on top of the coil's static LightningTarget priority while the battery
/// is completely empty, so a fresh coil catches the next bolt ahead of every charged coil.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int EmptyPriorityBonus = 1;
}
8 changes: 8 additions & 0 deletions Content.Server/Tesla/Components/TeslaEnergyBallComponent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ public sealed partial class TeslaEnergyBallComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float EnergyToDespawn = -100f;

/// <summary>
/// Triad: energy bled passively per second while the ball exists. 0 disables decay (upstream
/// behavior, the default). The tesla prototype pins this to the effective Level-1 PA feed rate
/// so Level 1 idles the ball: alive, essentially no motes. See energyball.yml for the tuning.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
public float PassiveEnergyDecay;

/// <summary>
/// Played when energy reaches the lower limit (and entity destroyed)
/// </summary>
Expand Down
31 changes: 31 additions & 0 deletions Content.Server/Tesla/EntitySystem/TeslaCoilSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Content.Server.Power.EntitySystems;
using Content.Server.Tesla.Components;
using Content.Server.Lightning;
using Content.Shared.Power; // Triad

namespace Content.Server.Tesla.EntitySystems;

Expand All @@ -11,12 +12,15 @@ namespace Content.Server.Tesla.EntitySystems;
public sealed class TeslaCoilSystem : EntitySystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; // Triad

public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<TeslaCoilComponent, HitByLightningEvent>(OnHitByLightning);
SubscribeLocalEvent<TeslaCoilComponent, LightningStrikeAttemptEvent>(OnLightningStrikeAttempt); // Triad
SubscribeLocalEvent<TeslaCoilComponent, ChargeChangedEvent>(OnChargeChanged); // Triad
}

//When struck by lightning, charge the internal battery
Expand All @@ -27,4 +31,31 @@ private void OnHitByLightning(Entity<TeslaCoilComponent> coil, ref HitByLightnin
_battery.SetCharge(coil, batteryComponent.CurrentCharge + coil.Comp.ChargeFromLightning);
}
}

// Triad: bid for the strike by charge headroom. An empty coil is a guaranteed catch and outbids
// every static target; a full coil floors at SaturatedHitProbability so bolts fall through to
// the grounding rods. At half charge the effective chance is ~0.52, close to the old static 0.5.
private void OnLightningStrikeAttempt(Entity<TeslaCoilComponent> coil, ref LightningStrikeAttemptEvent args)
{
if (!TryComp<BatteryComponent>(coil, out var battery) || battery.MaxCharge <= 0f)
return;

if (battery.CurrentCharge <= 0f)
{
args.Priority += coil.Comp.EmptyPriorityBonus;
args.HitProbability = 1f;
return;
}

var headroom = 1f - battery.CurrentCharge / battery.MaxCharge;
args.HitProbability = MathHelper.Lerp(coil.Comp.SaturatedHitProbability, 1f, headroom);
}

// Triad: hold the arcing indicator while the coil can't bank a full strike, so engineers can see
// saturation at a glance. Drains through the power net raise ChargeChangedEvent too, so the
// indicator clears on its own as the coil pushes charge into the grid.
private void OnChargeChanged(Entity<TeslaCoilComponent> coil, ref ChargeChangedEvent args)
{
_appearance.SetData(coil, TeslaCoilVisuals.Charged, args.Charge + coil.Comp.ChargeFromLightning > args.MaxCharge);
}
}
16 changes: 16 additions & 0 deletions Content.Server/Tesla/EntitySystem/TeslaEnergyBallSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,22 @@ public override void Initialize()
SubscribeLocalEvent<TeslaEnergyBallComponent, EntityConsumedByEventHorizonEvent>(OnConsumed);
}

// Triad: passive energy decay. The ball bleeds energy continuously so it needs a running PA to
// sustain it; with no feed it drains to EnergyToDespawn and collapses.
public override void Update(float frameTime)
{
base.Update(frameTime);

var query = EntityQueryEnumerator<TeslaEnergyBallComponent>();
while (query.MoveNext(out var uid, out var teslaEnergyBall))
{
if (teslaEnergyBall.PassiveEnergyDecay <= 0f)
continue;

AdjustEnergy(uid, teslaEnergyBall, -teslaEnergyBall.PassiveEnergyDecay * frameTime);
}
}

private void OnConsumed(Entity<TeslaEnergyBallComponent> tesla, ref EntityConsumedByEventHorizonEvent args)
{
Spawn(tesla.Comp.ConsumeEffectProto, Transform(args.Entity).Coordinates);
Expand Down
3 changes: 2 additions & 1 deletion Content.Shared/Power/TeslaCoilVisuals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@ namespace Content.Shared.Power;
public enum TeslaCoilVisuals : byte
{
Enabled,
Lightning
Lightning,
Charged // Triad: coil battery too full to accept a full strike; shows the persistent arcing indicator
}
39 changes: 0 additions & 39 deletions Resources/Maps/_NF/POI/edison.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36438,23 +36438,6 @@ entities:
- type: Transform
pos: 33.5,-24.5
parent: 1
- proto: CrateEngineeringTeslaCoil
entities:
- uid: 6050
components:
- type: Transform
pos: 43.5,-23.5
parent: 1
- uid: 6051
components:
- type: Transform
pos: 42.5,-23.5
parent: 1
- uid: 6052
components:
- type: Transform
pos: 44.5,-23.5
parent: 1
- proto: CrateEngineeringTeslaCoilBulk
entities:
- uid: 6053
Expand Down Expand Up @@ -80498,28 +80481,6 @@ entities:
- type: Transform
pos: 25.5,17.5
parent: 1
- proto: TeslaCoilFlatpack
entities:
- uid: 12219
components:
- type: Transform
pos: 46.438305,-23.349659
parent: 1
- uid: 12220
components:
- type: Transform
pos: 46.713493,-23.386375
parent: 1
- uid: 12221
components:
- type: Transform
pos: 46.493343,-23.569962
parent: 1
- uid: 12222
components:
- type: Transform
pos: 46.823566,-23.569962
parent: 1
- proto: TeslaGenerator
entities:
- uid: 12223
Expand Down
3 changes: 3 additions & 0 deletions Resources/Prototypes/Entities/Mobs/base.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@
- type: LightningTarget
priority: 1 # Mono
lightningExplode: false
# Triad: bolts that overflow the coils and rods electrocute whoever they land on.
# Insulated gloves apply; lightningResistance stays 1 so the bolt can arc once more onward.
electrocuteOnStrike: true

# Used for mobs that can enter combat mode and can attack.
- type: entity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@
- state: coilhit
visible: false
map: ["hit"]
- state: coilhit # Triad: persistent arc while saturated; own layer so the strike flash can't clobber it
visible: false
map: ["charged"]
- type: Appearance
- type: GenericVisualizer
visuals:
Expand All @@ -41,6 +44,10 @@
hit:
True: { visible: true }
False: { visible: false }
enum.TeslaCoilVisuals.Charged: # Triad
charged:
True: { visible: true }
False: { visible: false }
- type: LightningSparking
- type: AnimationPlayer
- type: NodeContainer
Expand All @@ -56,19 +63,31 @@
sprite: Structures/Power/Generation/Tesla/coil.rsi
state: coil
- type: Battery
maxCharge: 1000000
# Triad: wild-tesla economy, capacitor model. One strike fills the bank; the full coil's strike
# bid floors (see TeslaCoilSystem) so the next bolt round-robins to a drained coil, and the bank
# trickles out through the throttled supply below. Tower count is therefore the throughput knob:
# Monte Carlo at a hacked Level-3 PA: ~5.3 MW with 12 towers, peaking at ~7 MW with 24; towers
# beyond 24 add no output, only spread wear (~2.6 MW at stock Level 2 with 12).
maxCharge: 2000000
startingCharge: 0
- type: BatteryDischarger
- type: TeslaCoil
chargeFromLightning: 2000000
chargeFromLightning: 2000000 # Triad: equals maxCharge on purpose — one strike saturates, see Battery note
- type: LightningTarget
priority: 4
hitProbability: 0.5
hitProbability: 0.5 # Triad: fallback only; with a working battery the coil bids by charge headroom (see TeslaCoilSystem)
lightningResistance: 10
lightningExplode: false
# Triad: 1500 strikes to destruction. Simulated: first tower death ~2h on a max-output 24-tower
# array at a hacked Level 3, ~3.4h at stock Level 2 with 12 towers. Weld (Repairable) on a
# service rotation, or add towers beyond 24 to spread wear.
damageFromLightning: 0.15
- type: PowerNetworkBattery
maxSupply: 1000000
supplyRampTolerance: 1000000
# Triad: throttled discharge is what makes tower count matter — each tower trickles its bank at
# 500 kW (4s recovery), sized so harvest at a hacked Level-3 PA peaks at a 24-tower array. The
# per-tower buffer also smooths the strike lumps into near-constant grid supply.
maxSupply: 500000
supplyRampTolerance: 500000
- type: Anchorable
- type: Rotatable
- type: Pullable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@
consumeEntities: false
- type: TeslaEnergyBall
spawnProto: TeslaMiniEnergyBall
# Triad: wild-tesla economy. Decay pinned to the EFFECTIVE Level-1 PA feed (~7.3/s from Monte
# Carlo, not the ideal 3 lanes * 20 / 6s = 10/s: the ball's wander in a 3-wide cage drops the far
# side lane about a third of the time). Level 1 idles the ball (alive, a stray mote every few
# minutes), Standby/Level 0 starves it, Level 2+ makes motes. The deep despawn floor gives ~85s
# of grace under full starvation, since feed lands in 6-second lumps.
passiveEnergyDecay: 7
energyToDespawn: -600
# Mote count self-regulates: spawn rate = net feed / needEnergyToSpawn, motes live 120s.
# Simulated ladder at these numbers: Level 1 ~0.5 motes, Level 2 ~6, Level 3 ~22 sustained.
needEnergyToSpawn: 80
soundCollapse:
path: /Audio/Effects/tesla_collapse.ogg
params:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,12 +196,12 @@
id: CrateEngineeringTeslaCoilBulk
parent: CrateEngineeringSecure
name: bulk tesla coil crate
description: A collection of six tesla coils. Attracts lightning and generates energy from it.
description: A dozen flatpacked tesla coils. Attract lightning and generate energy from it.
components:
- type: StorageFill
contents:
- id: TeslaCoilFlatpack
amount: 6
amount: 12 # Triad: one crate = a starter array; two = the 24-tower max-output build

- type: entity
id: CrateEngineeringSingularityCollectorBulk
Expand Down
Loading
Loading