Predict powercells, chargers and PowerCellDraw (#41379)

* cleanup

* fix fixtures

* prediction

* fix test

* review

* fix svalinn visuals

* fix chargers

* fix portable recharger and its unlit visuals

* fix borgs

* oomba review

* fix examination prediction
This commit is contained in:
slarticodefast
2025-11-24 17:52:11 +01:00
committed by GitHub
parent d209b97686
commit e2ff167062
116 changed files with 2338 additions and 1579 deletions

View File

@@ -1,5 +0,0 @@
using Content.Shared.Power.EntitySystems;
namespace Content.Client.Power.EntitySystems;
public sealed class ChargerSystem : SharedChargerSystem;

View File

@@ -1,67 +0,0 @@
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;
[UsedImplicitly]
public sealed class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellVisualsComponent, AppearanceChangeEvent>(OnPowerCellVisualsChange);
}
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanUse;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return battery.CanDraw;
}
private void OnPowerCellVisualsChange(EntityUid uid, PowerCellVisualsComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!_sprite.LayerExists((uid, args.Sprite), PowerCellVisualLayers.Unshaded))
return;
// If no appearance data is set, rely on whatever existing sprite state is set being correct.
if (!_appearance.TryGetData<byte>(uid, PowerCellVisuals.ChargeLevel, out var level, args.Component))
return;
var positiveCharge = level > 0;
_sprite.LayerSetVisible((uid, args.Sprite), PowerCellVisualLayers.Unshaded, positiveCharge);
if (positiveCharge)
_sprite.LayerSetRsiState((uid, args.Sprite), PowerCellVisualLayers.Unshaded, $"o{level}");
}
private enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}
}

View File

@@ -0,0 +1,11 @@
namespace Content.Client.PowerCell;
/// <summary>
/// Sprite layers for power cells.
/// For use with the generic visualizer.
/// </summary>
public enum PowerCellVisualLayers : byte
{
Base,
Unshaded,
}

View File

@@ -1,4 +0,0 @@
namespace Content.Client.PowerCell;
[RegisterComponent]
public sealed partial class PowerCellVisualsComponent : Component {}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Client.PowerCell;
@@ -9,15 +9,13 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <summary>
/// The base sprite state used if the power cell charger does not contain a power cell.
/// </summary>
[DataField("emptyState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string EmptyState = "empty";
/// <summary>
/// The base sprite state used if the power cell charger contains a power cell.
/// </summary>
[DataField("occupiedState")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public string OccupiedState = "full";
/// <summary>
@@ -27,8 +25,7 @@ public sealed partial class PowerChargerVisualsComponent : Component
/// <see cref="CellChargerStatus.Charging"/> Maps to the state used when the charger is charging a power cell.
/// <see cref="CellChargerStatus.Charged"/> Maps to the state used when the charger contains a fully charged power cell.
/// </summary>
[DataField("lightStates")]
[ViewVariables(VVAccess.ReadWrite)]
[DataField]
public Dictionary<CellChargerStatus, string> LightStates = new()
{
[CellChargerStatus.Off] = "light-off",

View File

@@ -1,4 +1,4 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Robust.Client.GameObjects;
namespace Content.Client.PowerCell;

View File

@@ -7,23 +7,20 @@ public sealed partial class GunSystem
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, UpdateAmmoCounterEvent>(OnAmmoCountUpdate);
SubscribeLocalEvent<BatteryAmmoProviderComponent, AmmoCounterControlEvent>(OnControl);
}
private void OnAmmoCountUpdate(EntityUid uid, BatteryAmmoProviderComponent component, UpdateAmmoCounterEvent args)
private void OnAmmoCountUpdate(Entity<BatteryAmmoProviderComponent> ent, ref UpdateAmmoCounterEvent args)
{
if (args.Control is not BoxesStatusControl boxes) return;
if (args.Control is not BoxesStatusControl boxes)
return;
boxes.Update(component.Shots, component.Capacity);
boxes.Update(ent.Comp.Shots, ent.Comp.Capacity);
}
private void OnControl(EntityUid uid, BatteryAmmoProviderComponent component, AmmoCounterControlEvent args)
private void OnControl(Entity<BatteryAmmoProviderComponent> ent, ref AmmoCounterControlEvent args)
{
args.Control = new BoxesStatusControl();
}

View File

@@ -80,7 +80,6 @@ public sealed partial class GunSystem : SharedGunSystem
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<AmmoCounterComponent, ItemStatusCollectMessage>(OnAmmoCounterCollect);
SubscribeLocalEvent<AmmoCounterComponent, UpdateClientAmmoEvent>(OnUpdateClientAmmo);
SubscribeAllEvent<MuzzleFlashEvent>(OnMuzzleFlash);
// Plays animated effects on the client.
@@ -90,10 +89,6 @@ public sealed partial class GunSystem : SharedGunSystem
InitializeSpentAmmo();
}
private void OnUpdateClientAmmo(EntityUid uid, AmmoCounterComponent ammoComp, ref UpdateClientAmmoEvent args)
{
UpdateAmmoCount(uid, ammoComp);
}
private void OnMuzzleFlash(MuzzleFlashEvent args)
{
@@ -158,6 +153,8 @@ public sealed partial class GunSystem : SharedGunSystem
public override void Update(float frameTime)
{
base.Update(frameTime);
if (!Timing.IsFirstTimePredicted)
return;

View File

@@ -8,6 +8,7 @@ using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Chat;
using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Popups;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;

View File

@@ -25,6 +25,7 @@ using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.PDA;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks;
using Content.Shared.Station.Components;
using Content.Shared.Verbs;
@@ -52,6 +53,7 @@ public sealed partial class AdminVerbSystem
[Dependency] private readonly StationJobsSystem _stationJobsSystem = default!;
[Dependency] private readonly JointSystem _jointSystem = default!;
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly GunSystem _gun = default!;
@@ -160,6 +162,57 @@ public sealed partial class AdminVerbSystem
args.Verbs.Add(makeVulnerable);
}
if (TryComp<PredictedBatteryComponent>(args.Target, out var pBattery))
{
Verb refillBattery = new()
{
Text = Loc.GetString("admin-verbs-refill-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/fill_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), pBattery.MaxCharge);
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-refill-battery-description"),
Priority = (int)TricksVerbPriorities.RefillBattery,
};
args.Verbs.Add(refillBattery);
Verb drainBattery = new()
{
Text = Loc.GetString("admin-verbs-drain-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/drain_battery.png")),
Act = () =>
{
_predictedBatterySystem.SetCharge((args.Target, pBattery), 0);
},
Impact = LogImpact.Medium,
Priority = (int)TricksVerbPriorities.DrainBattery,
};
args.Verbs.Add(drainBattery);
Verb infiniteBattery = new()
{
Text = Loc.GetString("admin-verbs-infinite-battery"),
Category = VerbCategory.Tricks,
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/AdminActions/infinite_battery.png")),
Act = () =>
{
var recharger = EnsureComp<PredictedBatterySelfRechargerComponent>(args.Target);
recharger.AutoRechargeRate = pBattery.MaxCharge; // Instant refill.
recharger.AutoRechargePauseTime = TimeSpan.Zero; // No delay.
Dirty(args.Target, recharger);
_predictedBatterySystem.RefreshChargeRate((args.Target, pBattery));
},
Impact = LogImpact.Medium,
Message = Loc.GetString("admin-trick-infinite-battery-object-description"),
Priority = (int)TricksVerbPriorities.InfiniteBattery,
};
args.Verbs.Add(infiniteBattery);
}
if (TryComp<BatteryComponent>(args.Target, out var battery))
{
Verb refillBattery = new()

View File

@@ -48,7 +48,7 @@ public sealed partial class BuildMech : IGraphAction
var cell = container.ContainedEntities[0];
if (!entityManager.TryGetComponent<BatteryComponent>(cell, out var batteryComponent))
if (!entityManager.TryGetComponent<PredictedBatteryComponent>(cell, out var batteryComponent))
{
Logger.Warning($"Mech construct entity {uid} had an invalid entity in container \"{Container}\"! Aborting build mech action.");
return;

View File

@@ -1,8 +1,7 @@
using Content.Shared.Examine;
using Content.Shared.Coordinates.Helpers;
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.Interaction;
using Content.Shared.Power.Components;
using Content.Shared.Storage;
namespace Content.Server.Holosign;
@@ -23,9 +22,8 @@ public sealed class HolosignSystem : EntitySystem
{
// TODO: This should probably be using an itemstatus
// TODO: I'm too lazy to do this rn but it's literally copy-paste from emag.
_powerCell.TryGetBatteryFromSlot(uid, out var battery);
var charges = UsesRemaining(component, battery);
var maxCharges = MaxUses(component, battery);
var charges = _powerCell.GetRemainingUses(uid, component.ChargeUse);
var maxCharges = _powerCell.GetMaxUses(uid, component.ChargeUse);
using (args.PushGroup(nameof(HolosignProjectorComponent)))
{
@@ -52,25 +50,10 @@ public sealed class HolosignSystem : EntitySystem
// overlapping of the same holo on one tile remains allowed to allow holofan refreshes
var holoUid = Spawn(component.SignProto, args.ClickLocation.SnapToGrid(EntityManager));
var xform = Transform(holoUid);
// TODO: Just make the prototype anchored
if (!xform.Anchored)
_transform.AnchorEntity(holoUid, xform); // anchor to prevent any tempering with (don't know what could even interact with it)
args.Handled = true;
}
private int UsesRemaining(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.CurrentCharge / component.ChargeUse);
}
private int MaxUses(HolosignProjectorComponent component, BatteryComponent? battery = null)
{
if (battery == null ||
component.ChargeUse == 0f) return 0;
return (int) (battery.MaxCharge / component.ChargeUse);
}
}

View File

@@ -111,16 +111,4 @@ namespace Content.Server.Kitchen.Components
[DataField, ViewVariables(VVAccess.ReadWrite)]
public bool CanMicrowaveIdsSafely = true;
}
public sealed class BeingMicrowavedEvent : HandledEntityEventArgs
{
public EntityUid Microwave;
public EntityUid? User;
public BeingMicrowavedEvent(EntityUid microwave, EntityUid? user)
{
Microwave = microwave;
User = user;
}
}
}

View File

@@ -1,12 +1,12 @@
using Content.Server.Actions;
using Content.Server.Popups;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared.Actions;
using Content.Shared.Examine;
using Content.Shared.Interaction;
using Content.Shared.Light;
using Content.Shared.Light.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Rounding;
using Content.Shared.Toggleable;
using JetBrains.Annotations;
@@ -25,7 +25,7 @@ namespace Content.Server.Light.EntitySystems
[Dependency] private readonly ActionContainerSystem _actionContainer = default!;
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedPointLightSystem _lights = default!;
@@ -108,13 +108,15 @@ namespace Content.Server.Light.EntitySystems
// Curently every single flashlight has the same number of levels for status and that's all it uses the charge for
// Thus we'll just check if the level changes.
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery))
if (!_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery))
return null;
if (MathHelper.CloseToPercent(battery.CurrentCharge, 0) || ent.Comp.Wattage > battery.CurrentCharge)
var currentCharge = _battery.GetCharge(battery.Value.AsNullable());
if (MathHelper.CloseToPercent(currentCharge, 0) || ent.Comp.Wattage > currentCharge)
return 0;
return (byte?) ContentHelpers.RoundToNearestLevels(battery.CurrentCharge / battery.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
return (byte?)ContentHelpers.RoundToNearestLevels(currentCharge / battery.Value.Comp.MaxCharge * 255, 255, HandheldLightComponent.StatusLevels);
}
private void OnRemove(Entity<HandheldLightComponent> ent, ref ComponentRemove args)
@@ -153,6 +155,8 @@ namespace Content.Server.Light.EntitySystems
_activeLights.Clear();
}
// TODO: Very important: Make this charge rate based instead of instantly removing charge each update step.
// See PredictedBatteryComponent
public override void Update(float frameTime)
{
var toRemove = new RemQueue<Entity<HandheldLightComponent>>();
@@ -199,8 +203,7 @@ namespace Content.Server.Light.EntitySystems
return false;
}
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery) &&
!TryComp(uid, out battery))
if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
{
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-missing-message"), uid, user);
@@ -210,7 +213,7 @@ namespace Content.Server.Light.EntitySystems
// To prevent having to worry about frame time in here.
// Let's just say you need a whole second of charge before you can turn it on.
// Simple enough.
if (component.Wattage > battery.CurrentCharge)
if (component.Wattage > _battery.GetCharge(battery.Value.AsNullable()))
{
_audio.PlayPvs(_audio.ResolveSound(component.TurnOnFailSound), uid);
_popup.PopupEntity(Loc.GetString("handheld-light-component-cell-dead-message"), uid, user);
@@ -227,19 +230,15 @@ namespace Content.Server.Light.EntitySystems
public void TryUpdate(Entity<HandheldLightComponent> uid, float frameTime)
{
var component = uid.Comp;
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery, null) &&
!TryComp(uid, out battery))
if (!_powerCell.TryGetBatteryFromSlot(uid.Owner, out var battery))
{
TurnOff(uid, false);
return;
}
if (batteryUid == null)
return;
var appearanceComponent = EntityManager.GetComponentOrNull<AppearanceComponent>(uid);
var fraction = battery.CurrentCharge / battery.MaxCharge;
var fraction = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
if (fraction >= 0.30)
{
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.FullPower, appearanceComponent);
@@ -253,7 +252,7 @@ namespace Content.Server.Light.EntitySystems
_appearance.SetData(uid, HandheldLightVisuals.Power, HandheldLightPowerStates.Dying, appearanceComponent);
}
if (component.Activated && !_battery.TryUseCharge((batteryUid.Value, battery), component.Wattage * frameTime))
if (component.Activated && !_battery.TryUseCharge(battery.Value.AsNullable(), component.Wattage * frameTime))
TurnOff(uid, false);
UpdateLevel(uid);

View File

@@ -2,7 +2,6 @@ using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Mech.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.ActionBlocker;
using Content.Shared.Damage.Systems;
using Content.Shared.DoAfter;
@@ -14,6 +13,7 @@ using Content.Shared.Mech.EntitySystems;
using Content.Shared.Movement.Events;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Tools;
using Content.Shared.Tools.Components;
using Content.Shared.Tools.Systems;
@@ -33,7 +33,7 @@ public sealed partial class MechSystem : SharedMechSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly AtmosphereSystem _atmosphere = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
@@ -88,7 +88,7 @@ public sealed partial class MechSystem : SharedMechSystem
if (TryComp<WiresPanelComponent>(uid, out var panel) && !panel.Open)
return;
if (component.BatterySlot.ContainedEntity == null && TryComp<BatteryComponent>(args.Used, out var battery))
if (component.BatterySlot.ContainedEntity == null && TryComp<PredictedBatteryComponent>(args.Used, out var battery))
{
InsertBattery(uid, args.Used, component, battery);
_actionBlocker.UpdateCanMove(uid);
@@ -109,10 +109,10 @@ public sealed partial class MechSystem : SharedMechSystem
private void OnInsertBattery(EntityUid uid, MechComponent component, EntInsertedIntoContainerMessage args)
{
if (args.Container != component.BatterySlot || !TryComp<BatteryComponent>(args.Entity, out var battery))
if (args.Container != component.BatterySlot || !TryComp<PredictedBatteryComponent>(args.Entity, out var battery))
return;
component.Energy = battery.CurrentCharge;
component.Energy = _battery.GetCharge((args.Entity, battery));
component.MaxEnergy = battery.MaxCharge;
Dirty(uid, component);
@@ -337,21 +337,23 @@ public sealed partial class MechSystem : SharedMechSystem
if (battery == null)
return false;
if (!TryComp<BatteryComponent>(battery, out var batteryComp))
if (!TryComp<PredictedBatteryComponent>(battery, out var batteryComp))
return false;
_battery.SetCharge((battery.Value, batteryComp), batteryComp.CurrentCharge + delta.Float());
if (batteryComp.CurrentCharge != component.Energy) //if there's a discrepency, we have to resync them
_battery.SetCharge((battery.Value, batteryComp), _battery.GetCharge((battery.Value, batteryComp)) + delta.Float());
// TODO: Power cells are predicted now, so no need to duplicate the charge level
var charge = _battery.GetCharge((battery.Value, batteryComp));
if (charge != component.Energy) //if there's a discrepency, we have to resync them
{
Log.Debug($"Battery charge was not equal to mech charge. Battery {batteryComp.CurrentCharge}. Mech {component.Energy}");
component.Energy = batteryComp.CurrentCharge;
Log.Debug($"Battery charge was not equal to mech charge. Battery {charge}. Mech {component.Energy}");
component.Energy = charge;
Dirty(uid, component);
}
_actionBlocker.UpdateCanMove(uid);
return true;
}
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, BatteryComponent? battery = null)
public void InsertBattery(EntityUid uid, EntityUid toInsert, MechComponent? component = null, PredictedBatteryComponent? battery = null)
{
if (!Resolve(uid, ref component, false))
return;
@@ -360,7 +362,7 @@ public sealed partial class MechSystem : SharedMechSystem
return;
_container.Insert(toInsert, component.BatterySlot);
component.Energy = battery.CurrentCharge;
component.Energy = _battery.GetCharge((toInsert, battery));
component.MaxEnergy = battery.MaxCharge;
_actionBlocker.UpdateCanMove(uid);

View File

@@ -1,7 +1,7 @@
using System.Linq;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Medical.CrewMonitoring;

View File

@@ -5,7 +5,7 @@ using Content.Server.Electrocution;
using Content.Server.EUI;
using Content.Server.Ghost;
using Content.Server.Popups;
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.Traits.Assorted;
using Content.Shared.Chat;
using Content.Shared.Damage.Components;

View File

@@ -1,5 +1,4 @@
using Content.Server.Medical.Components;
using Content.Server.PowerCell;
using Content.Shared.Body.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Components;
@@ -12,6 +11,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.MedicalScanner;
using Content.Shared.Mobs.Components;
using Content.Shared.Popups;
using Content.Shared.PowerCell;
using Content.Shared.Temperature.Components;
using Content.Shared.Traits.Assorted;
using Robust.Server.GameObjects;
@@ -81,7 +81,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
/// </summary>
private void OnAfterInteract(Entity<HealthAnalyzerComponent> uid, ref AfterInteractEvent args)
{
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid, user: args.User))
if (args.Target == null || !args.CanReach || !HasComp<MobStateComponent>(args.Target) || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
_audio.PlayPvs(uid.Comp.ScanningBeginSound, uid);
@@ -101,7 +101,7 @@ public sealed class HealthAnalyzerSystem : EntitySystem
private void OnDoAfter(Entity<HealthAnalyzerComponent> uid, ref HealthAnalyzerDoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid, user: args.User))
if (args.Handled || args.Cancelled || args.Target == null || !_cell.HasDrawCharge(uid.Owner, user: args.User))
return;
if (!uid.Comp.Silent)

View File

@@ -7,6 +7,7 @@ using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Audio.Systems;
namespace Content.Server.Ninja.Systems;
@@ -17,6 +18,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -37,7 +39,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
var (uid, comp) = ent;
var target = args.Target;
if (args.Handled || comp.BatteryUid is not {} battery || !HasComp<PowerNetworkBatteryComponent>(target))
if (args.Handled || comp.BatteryUid is not { } battery || !HasComp<PowerNetworkBatteryComponent>(target))
return;
// handles even if battery is full so you can actually see the poup
@@ -70,7 +72,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
{
base.OnDoAfterAttempt(ent, ref args);
if (ent.Comp.BatteryUid is not {} battery || _battery.IsFull(battery))
if (ent.Comp.BatteryUid is not { } battery || _battery.IsFull(battery))
args.Cancel();
}
@@ -78,7 +80,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
protected override bool TryDrainPower(Entity<BatteryDrainerComponent> ent, EntityUid target)
{
var (uid, comp) = ent;
if (comp.BatteryUid == null || !TryComp<BatteryComponent>(comp.BatteryUid.Value, out var battery))
if (comp.BatteryUid == null || !TryComp<PredictedBatteryComponent>(comp.BatteryUid.Value, out var battery))
return false;
if (!TryComp<BatteryComponent>(target, out var targetBattery) || !TryComp<PowerNetworkBatteryComponent>(target, out var pnb))
@@ -91,7 +93,7 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
}
var available = targetBattery.CurrentCharge;
var required = battery.MaxCharge - battery.CurrentCharge;
var required = battery.MaxCharge - _predictedBattery.GetCharge((comp.BatteryUid.Value, battery));
// higher tier storages can charge more
var maxDrained = pnb.MaxSupply * comp.DrainTime;
var input = Math.Min(Math.Min(available, required / comp.DrainEfficiency), maxDrained);
@@ -99,13 +101,15 @@ public sealed class BatteryDrainerSystem : SharedBatteryDrainerSystem
return false;
var output = input * comp.DrainEfficiency;
_battery.SetCharge((comp.BatteryUid.Value, battery), battery.CurrentCharge + output);
// PowerCells use PredictedBatteryComponent
// SMES, substations and APCs use BatteryComponent
_predictedBattery.ChangeCharge((comp.BatteryUid.Value, battery), output);
// TODO: create effect message or something
Spawn("EffectSparks", Transform(target).Coordinates);
_audio.PlayPvs(comp.SparkSound, target);
_popup.PopupEntity(Loc.GetString("battery-drainer-success", ("battery", target)), uid, uid);
// repeat the doafter until battery is full
return !_battery.IsFull((comp.BatteryUid.Value, battery));
return !_predictedBattery.IsFull((comp.BatteryUid.Value, battery));
}
}

View File

@@ -1,15 +1,15 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Ninja.Systems;
public sealed class ItemCreatorSystem : SharedItemCreatorSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
@@ -24,7 +24,7 @@ public sealed class ItemCreatorSystem : SharedItemCreatorSystem
private void OnCreateItem(Entity<ItemCreatorComponent> ent, ref CreateItemEvent args)
{
var (uid, comp) = ent;
if (comp.Battery is not {} battery)
if (comp.Battery is not { } battery)
return;
args.Handled = true;

View File

@@ -1,11 +1,10 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.Components;
using Content.Server.PowerCell;
using Content.Shared.Emp;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Robust.Shared.Containers;
@@ -13,6 +12,7 @@ namespace Content.Server.Ninja.Systems;
/// <summary>
/// Handles power cell upgrading and actions.
/// TODO: Move all of this to shared and predict it
/// </summary>
public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
{
@@ -51,8 +51,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
RaiseLocalEvent(user, ref ev);
}
// TODO: if/when battery is in shared, put this there too
// TODO: or put MaxCharge in shared along with powercellslot
private void OnSuitInsertAttempt(EntityUid uid, NinjaSuitComponent comp, ContainerIsInsertingAttemptEvent args)
{
// this is for handling battery upgrading, not stopping actions from being added
@@ -61,10 +59,10 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
return;
// no power cell for some reason??? allow it
if (!_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
if (!_powerCell.TryGetBatteryFromSlot(uid, out var battery))
return;
if (!TryComp<BatteryComponent>(args.EntityUid, out var inserting))
if (!TryComp<PredictedBatteryComponent>(args.EntityUid, out var inserting))
{
args.Cancel();
return;
@@ -73,7 +71,7 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
var user = Transform(uid).ParentUid;
// can only upgrade power cell, not swap to recharge instantly otherwise ninja could just swap batteries with flashlights in maints for easy power
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(batteryUid.Value, battery))
if (GetCellScore(args.EntityUid, inserting) <= GetCellScore(battery.Value, battery.Value))
{
args.Cancel();
Popup.PopupEntity(Loc.GetString("ninja-cell-downgrade"), user, user);
@@ -90,11 +88,11 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
}
// this function assigns a score to a power cell depending on the capacity, to be used when comparing which cell is better.
private float GetCellScore(EntityUid uid, BatteryComponent battcomp)
private float GetCellScore(EntityUid uid, PredictedBatteryComponent battcomp)
{
// if a cell is able to automatically recharge, boost the score drastically depending on the recharge rate,
// this is to ensure a ninja can still upgrade to a micro reactor cell even if they already have a medium or high.
if (TryComp<BatterySelfRechargerComponent>(uid, out var selfcomp) && selfcomp.AutoRecharge)
if (TryComp<PredictedBatterySelfRechargerComponent>(uid, out var selfcomp))
return battcomp.MaxCharge + selfcomp.AutoRechargeRate * AutoRechargeValue;
return battcomp.MaxCharge;
}
@@ -136,7 +134,6 @@ public sealed class NinjaSuitSystem : SharedNinjaSuitSystem
Popup.PopupEntity(Loc.GetString(message), user, user);
}
// TODO: Move this to shared when power cells are predicted.
private void OnEmp(Entity<NinjaSuitComponent> ent, ref NinjaEmpEvent args)
{
var (uid, comp) = ent;

View File

@@ -2,8 +2,6 @@ using Content.Server.Communications;
using Content.Server.CriminalRecords.Systems;
using Content.Server.Objectives.Components;
using Content.Server.Objectives.Systems;
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Server.Research.Systems;
using Content.Shared.Alert;
using Content.Shared.Doors.Components;
@@ -12,6 +10,8 @@ using Content.Shared.Mind;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Popups;
using Content.Shared.Rounding;
using System.Diagnostics.CodeAnalysis;
@@ -24,7 +24,7 @@ namespace Content.Server.Ninja.Systems;
public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
{
[Dependency] private readonly AlertsSystem _alerts = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly CodeConditionSystem _codeCondition = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly SharedMindSystem _mind = default!;
@@ -39,6 +39,8 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
SubscribeLocalEvent<SpaceNinjaComponent, CriminalRecordsHackedEvent>(OnCriminalRecordsHacked);
}
// TODO: Make this charge rate based instead of updating it every single tick.
// Or make it client side, since power cells are predicted.
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<SpaceNinjaComponent>();
@@ -62,7 +64,7 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return newCount - oldCount;
}
// TODO: can probably copy paste borg code here
// TODO: Generic charge indicator that is combined with borg code.
/// <summary>
/// Update the alert for the ninja's suit power indicator.
/// </summary>
@@ -75,10 +77,10 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
return;
}
if (GetNinjaBattery(uid, out _, out var battery))
if (GetNinjaBattery(uid, out var batteryUid, out var batteryComp))
{
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, battery.CurrentCharge), battery.MaxCharge, 8);
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short) severity);
var severity = ContentHelpers.RoundToLevels(MathF.Max(0f, _battery.GetCharge((batteryUid.Value, batteryComp))), batteryComp.MaxCharge, 8);
_alerts.ShowAlert(uid, comp.SuitPowerAlert, (short)severity);
}
else
{
@@ -89,17 +91,19 @@ public sealed class SpaceNinjaSystem : SharedSpaceNinjaSystem
/// <summary>
/// Get the battery component in a ninja's suit, if it's worn.
/// </summary>
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? uid, [NotNullWhen(true)] out BatteryComponent? battery)
public bool GetNinjaBattery(EntityUid user, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out PredictedBatteryComponent? batteryComp)
{
if (TryComp<SpaceNinjaComponent>(user, out var ninja)
&& ninja.Suit != null
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out uid, out battery))
&& _powerCell.TryGetBatteryFromSlot(ninja.Suit.Value, out var battery))
{
batteryUid = battery.Value.Owner;
batteryComp = battery.Value.Comp;
return true;
}
uid = null;
battery = null;
batteryUid = null;
batteryComp = null;
return false;
}

View File

@@ -1,10 +1,10 @@
using Content.Server.Ninja.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Damage.Systems;
using Content.Shared.Interaction;
using Content.Shared.Ninja.Components;
using Content.Shared.Ninja.Systems;
using Content.Shared.Popups;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stunnable;
using Content.Shared.Timing;
using Content.Shared.Whitelist;
@@ -17,7 +17,7 @@ namespace Content.Server.Ninja.Systems;
/// </summary>
public sealed class StunProviderSystem : SharedStunProviderSystem
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;

View File

@@ -2,7 +2,6 @@ using Content.Server.AlertLevel;
using Content.Server.Audio;
using Content.Server.Chat.Systems;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Pinpointer;
using Content.Server.Popups;
using Content.Server.Station.Systems;
@@ -11,6 +10,7 @@ using Content.Shared.Containers.ItemSlots;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.DoAfter;
using Content.Shared.Examine;
using Content.Shared.Kitchen;
using Content.Shared.Maps;
using Content.Shared.Nuke;
using Content.Shared.Popups;

View File

@@ -1,9 +1,9 @@
using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments;
using Content.Server.Kitchen.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.Kitchen;
using Content.Shared.PAI;
using Content.Shared.Popups;
using Content.Shared.Instruments;

View File

@@ -1,4 +1,4 @@
using Content.Server.PowerCell;
using Content.Shared.PowerCell;
using Content.Shared.Pinpointer;
using Robust.Server.GameObjects;
using Robust.Shared.Player;

View File

@@ -1,10 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Power;
namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ActiveChargerComponent : Component
{
}
}

View File

@@ -1,6 +0,0 @@
namespace Content.Server.Power.Components
{
[RegisterComponent]
public sealed partial class ExaminableBatteryComponent : Component
{}
}

View File

@@ -1,8 +1,14 @@
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="BatteryComponent"/>.
/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class BatterySystem
{
public override float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
@@ -12,6 +18,10 @@ public sealed partial class BatterySystem
var newValue = Math.Clamp(ent.Comp.CurrentCharge + amount, 0, ent.Comp.MaxCharge);
var delta = newValue - ent.Comp.CurrentCharge;
if (delta == 0f)
return delta;
ent.Comp.CurrentCharge = newValue;
TrySetChargeCooldown(ent.Owner);
@@ -23,8 +33,8 @@ public sealed partial class BatterySystem
public override float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
if (amount <= 0 || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
return 0;
if (amount <= 0f || !Resolve(ent, ref ent.Comp) || ent.Comp.CurrentCharge == 0)
return 0f;
return ChangeCharge(ent, -amount);
}
@@ -69,6 +79,45 @@ public sealed partial class BatterySystem
RaiseLocalEvent(ent, ref ev);
}
/// <summary>
/// Gets the battery's current charge.
/// </summary>
public float GetCharge(Entity<BatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
return ent.Comp.CurrentCharge;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
public int GetRemainingUses(Entity<BatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.CurrentCharge / cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
public int GetMaxUses(Entity<BatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.MaxCharge / cost);
}
public override void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))

View File

@@ -11,6 +11,11 @@ using Robust.Shared.Timing;
namespace Content.Server.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="BatteryComponent"/>.
/// Unpredicted equivalent of <see cref="PredictedBatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
[UsedImplicitly]
public sealed partial class BatterySystem : SharedBatterySystem
{
@@ -20,7 +25,8 @@ public sealed partial class BatterySystem : SharedBatterySystem
{
base.Initialize();
SubscribeLocalEvent<ExaminableBatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<BatteryComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<BatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<BatteryComponent, RejuvenateEvent>(OnBatteryRejuvenate);
SubscribeLocalEvent<PowerNetworkBatteryComponent, RejuvenateEvent>(OnNetBatteryRejuvenate);
SubscribeLocalEvent<BatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
@@ -31,27 +37,31 @@ public sealed partial class BatterySystem : SharedBatterySystem
SubscribeLocalEvent<NetworkBatteryPostSync>(PostSync);
}
private void OnInit(Entity<BatteryComponent> ent, ref ComponentInit args)
{
DebugTools.Assert(!HasComp<PredictedBatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
}
private void OnNetBatteryRejuvenate(Entity<PowerNetworkBatteryComponent> ent, ref RejuvenateEvent args)
{
ent.Comp.NetworkBattery.CurrentStorage = ent.Comp.NetworkBattery.Capacity;
}
private void OnBatteryRejuvenate(Entity<BatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
private void OnExamine(Entity<ExaminableBatteryComponent> ent, ref ExaminedEvent args)
private void OnExamine(Entity<BatteryComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (!TryComp<BatteryComponent>(ent, out var battery))
if (!HasComp<ExaminableBatteryComponent>(ent))
return;
var chargePercentRounded = 0;
if (battery.MaxCharge != 0)
chargePercentRounded = (int)(100 * battery.CurrentCharge / battery.MaxCharge);
if (ent.Comp.MaxCharge != 0)
chargePercentRounded = (int)(100 * ent.Comp.CurrentCharge / ent.Comp.MaxCharge);
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",

View File

@@ -1,259 +0,0 @@
using Content.Server.Power.Components;
using Content.Shared.Examine;
using Content.Server.PowerCell;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell.Components;
using Content.Shared.Emp;
using JetBrains.Annotations;
using Robust.Shared.Containers;
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Storage.Components;
using Robust.Server.Containers;
using Content.Shared.Whitelist;
namespace Content.Server.Power.EntitySystems;
[UsedImplicitly]
public sealed class ChargerSystem : SharedChargerSystem
{
[Dependency] private readonly ContainerSystem _container = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
}
private void OnStartup(EntityUid uid, ChargerComponent component, ComponentStartup args)
{
UpdateStatus(uid, component);
}
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
{
using (args.PushGroup(nameof(ChargerComponent)))
{
// rate at which the charger charges
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
// try to get contents of the charger
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
if (HasComp<PowerCellSlotComponent>(uid))
return;
// if charger is empty and not a power cell type charger, add empty message
// power cells have their own empty message by default, for things like flash lights
if (container.ContainedEntities.Count == 0)
{
args.PushMarkup(Loc.GetString("charger-empty"));
}
else
{
// add how much each item is charged it
foreach (var contained in container.ContainedEntities)
{
if (!TryComp<BatteryComponent>(contained, out var battery))
continue;
var chargePercentage = (battery.CurrentCharge / battery.MaxCharge) * 100;
args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
}
}
}
}
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveChargerComponent, ChargerComponent, ContainerManagerComponent>();
while (query.MoveNext(out var uid, out _, out var charger, out var containerComp))
{
if (!_container.TryGetContainer(uid, charger.SlotId, out var container, containerComp))
continue;
if (charger.Status == CellChargerStatus.Empty || charger.Status == CellChargerStatus.Charged || container.ContainedEntities.Count == 0)
continue;
foreach (var contained in container.ContainedEntities)
{
TransferPower(uid, contained, charger, frameTime);
}
}
}
private void OnPowerChanged(EntityUid uid, ChargerComponent component, ref PowerChangedEvent args)
{
UpdateStatus(uid, component);
}
private void OnInserted(EntityUid uid, ChargerComponent component, EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
UpdateStatus(uid, component);
}
private void OnRemoved(EntityUid uid, ChargerComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.SlotId)
return;
UpdateStatus(uid, component);
}
/// <summary>
/// Verify that the entity being inserted is actually rechargeable.
/// </summary>
private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancel();
}
private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (!component.Initialized || args.Cancelled)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancelled = true;
}
private void UpdateStatus(EntityUid uid, ChargerComponent component)
{
var status = GetStatus(uid, component);
TryComp(uid, out AppearanceComponent? appearance);
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
_appearance.SetData(uid, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
if (component.Status == status || !TryComp(uid, out ApcPowerReceiverComponent? receiver))
return;
component.Status = status;
if (component.Status == CellChargerStatus.Charging)
{
AddComp<ActiveChargerComponent>(uid);
}
else
{
RemComp<ActiveChargerComponent>(uid);
}
switch (component.Status)
{
case CellChargerStatus.Off:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Off, appearance);
break;
case CellChargerStatus.Empty:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Empty, appearance);
break;
case CellChargerStatus.Charging:
receiver.Load = component.ChargeRate;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charging, appearance);
break;
case CellChargerStatus.Charged:
receiver.Load = 0;
_appearance.SetData(uid, CellVisual.Light, CellChargerStatus.Charged, appearance);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private CellChargerStatus GetStatus(EntityUid uid, ChargerComponent component)
{
if (!component.Portable)
{
if (!TryComp(uid, out TransformComponent? transformComponent) || !transformComponent.Anchored)
return CellChargerStatus.Off;
}
if (!TryComp(uid, out ApcPowerReceiverComponent? apcPowerReceiverComponent))
return CellChargerStatus.Off;
if (!component.Portable && !apcPowerReceiverComponent.Powered)
return CellChargerStatus.Off;
if (HasComp<EmpDisabledComponent>(uid))
return CellChargerStatus.Off;
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return CellChargerStatus.Off;
if (container.ContainedEntities.Count == 0)
return CellChargerStatus.Empty;
if (!SearchForBattery(container.ContainedEntities[0], out var heldEnt, out var heldBattery))
return CellChargerStatus.Off;
if (_battery.IsFull((heldEnt.Value, heldBattery)))
return CellChargerStatus.Charged;
return CellChargerStatus.Charging;
}
private void TransferPower(EntityUid uid, EntityUid targetEntity, ChargerComponent component, float frameTime)
{
if (!TryComp(uid, out ApcPowerReceiverComponent? receiverComponent))
return;
if (!receiverComponent.Powered)
return;
if (_whitelistSystem.IsWhitelistFail(component.Whitelist, targetEntity))
return;
if (!SearchForBattery(targetEntity, out var batteryUid, out var heldBattery))
return;
_battery.SetCharge((batteryUid.Value, heldBattery), heldBattery.CurrentCharge + component.ChargeRate * frameTime);
UpdateStatus(uid, component);
}
private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out EntityUid? batteryUid, [NotNullWhen(true)] out BatteryComponent? component)
{
// try get a battery directly on the inserted entity
if (!TryComp(uid, out component))
{
// or by checking for a power cell slot on the inserted entity
return _powerCell.TryGetBatteryFromSlot(uid, out batteryUid, out component);
}
batteryUid = uid;
return true;
}
}

View File

@@ -161,11 +161,6 @@ namespace Content.Server.Power.EntitySystems
return !_recQuery.Resolve(uid, ref receiver, false) || receiver.Powered;
}
public void SetLoad(ApcPowerReceiverComponent comp, float load)
{
comp.Load = load;
}
public override bool ResolveApc(EntityUid entity, [NotNullWhen(true)] ref SharedApcPowerReceiverComponent? component)
{
if (component != null)

View File

@@ -1,10 +1,12 @@
using Content.Server.Administration.Logs;
using Content.Server.Explosion.EntitySystems;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Database;
using Content.Shared.Kitchen;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Rejuvenate;
namespace Content.Server.Power.EntitySystems;
@@ -16,6 +18,7 @@ public sealed class RiggableSystem : EntitySystem
{
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
public override void Initialize()
{
@@ -23,6 +26,8 @@ public sealed class RiggableSystem : EntitySystem
SubscribeLocalEvent<RiggableComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<RiggableComponent, BeingMicrowavedEvent>(OnMicrowaved);
SubscribeLocalEvent<RiggableComponent, SolutionContainerChangedEvent>(OnSolutionChanged);
SubscribeLocalEvent<RiggableComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<RiggableComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
}
private void OnRejuvenate(Entity<RiggableComponent> entity, ref RejuvenateEvent args)
@@ -34,14 +39,22 @@ public sealed class RiggableSystem : EntitySystem
{
if (TryComp<BatteryComponent>(entity, out var batteryComponent))
{
if (batteryComponent.CurrentCharge == 0)
if (batteryComponent.CurrentCharge == 0f)
return;
Explode(entity, batteryComponent.CurrentCharge);
args.Handled = true;
}
args.Handled = true;
if (TryComp<PredictedBatteryComponent>(entity, out var predictedBatteryComponent))
{
var charge = _predictedBattery.GetCharge((entity, predictedBatteryComponent));
if (charge == 0f)
return;
// What the fuck are you doing???
Explode(entity.Owner, batteryComponent, args.User);
Explode(entity, charge);
args.Handled = true;
}
}
private void OnSolutionChanged(Entity<RiggableComponent> entity, ref SolutionContainerChangedEvent args)
@@ -59,14 +72,42 @@ public sealed class RiggableSystem : EntitySystem
}
}
public void Explode(EntityUid uid, BatteryComponent? battery = null, EntityUid? cause = null)
public void Explode(EntityUid uid, float charge, EntityUid? cause = null)
{
if (!Resolve(uid, ref battery))
return;
var radius = MathF.Min(5, MathF.Sqrt(charge) / 9);
var radius = MathF.Min(5, MathF.Sqrt(battery.CurrentCharge) / 9);
_explosionSystem.TriggerExplosive(uid, radius: radius, user:cause);
_explosionSystem.TriggerExplosive(uid, radius: radius, user: cause);
QueueDel(uid);
}
// non-predicted batteries
private void OnChargeChanged(Entity<RiggableComponent> ent, ref ChargeChangedEvent args)
{
if (!ent.Comp.IsRigged)
return;
if (TryComp<BatteryComponent>(ent, out var batteryComponent))
{
if (batteryComponent.CurrentCharge == 0f)
return;
Explode(ent, batteryComponent.CurrentCharge);
}
}
// predicted batteries
private void OnChargeChanged(Entity<RiggableComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
if (!ent.Comp.IsRigged)
return;
if (TryComp<PredictedBatteryComponent>(ent, out var predictedBatteryComponent))
{
var charge = _predictedBattery.GetCharge((ent.Owner, predictedBatteryComponent));
if (charge == 0f)
return;
Explode(ent, charge);
}
}
}

View File

@@ -2,6 +2,7 @@ using Content.Server.Administration;
using Content.Server.Power.EntitySystems;
using Content.Shared.Administration;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Console;
namespace Content.Server.Power
@@ -10,6 +11,7 @@ namespace Content.Server.Power
public sealed class SetBatteryPercentCommand : LocalizedEntityCommands
{
[Dependency] private readonly BatterySystem _batterySystem = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBatterySystem = default!;
public override string Command => "setbatterypercent";
@@ -35,12 +37,15 @@ namespace Content.Server.Power
return;
}
if (!EntityManager.TryGetComponent<BatteryComponent>(id, out var battery))
if (EntityManager.TryGetComponent<BatteryComponent>(id, out var battery))
_batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
else if (EntityManager.TryGetComponent<PredictedBatteryComponent>(id, out var pBattery))
_predictedBatterySystem.SetCharge((id.Value, pBattery), pBattery.MaxCharge * percent / 100);
else
{
shell.WriteLine(Loc.GetString($"cmd-setbatterypercent-battery-not-found", ("id", id)));
return;
}
_batterySystem.SetCharge((id.Value, battery), battery.MaxCharge * percent / 100);
// Don't acknowledge b/c people WILL forall this
}
}

View File

@@ -1,72 +0,0 @@
using Content.Shared.Power;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
namespace Content.Server.PowerCell;
public sealed partial class PowerCellSystem
{
/*
* Handles PowerCellDraw
*/
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<PowerCellDrawComponent, PowerCellSlotComponent>();
while (query.MoveNext(out var uid, out var comp, out var slot))
{
if (!comp.Enabled)
continue;
if (Timing.CurTime < comp.NextUpdateTime)
continue;
comp.NextUpdateTime += comp.Delay;
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, slot))
continue;
if (_battery.TryUseCharge((batteryEnt.Value, battery), comp.DrawRate * (float)comp.Delay.TotalSeconds))
continue;
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
}
private void OnDrawChargeChanged(EntityUid uid, PowerCellDrawComponent component, ref ChargeChangedEvent args)
{
// Update the bools for client prediction.
var canUse = component.UseRate <= 0f || args.Charge > component.UseRate;
var canDraw = component.DrawRate <= 0f || args.Charge > 0f;
if (canUse != component.CanUse || canDraw != component.CanDraw)
{
component.CanDraw = canDraw;
component.CanUse = canUse;
Dirty(uid, component);
}
}
private void OnDrawCellChanged(EntityUid uid, PowerCellDrawComponent component, PowerCellChangedEvent args)
{
var canDraw = !args.Ejected && HasCharge(uid, float.MinValue);
var canUse = !args.Ejected && HasActivatableCharge(uid, component);
if (!canDraw)
{
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
if (canUse != component.CanUse || canDraw != component.CanDraw)
{
component.CanDraw = canDraw;
component.CanUse = canUse;
Dirty(uid, component);
}
}
}

View File

@@ -1,257 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Kitchen.Components;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rounding;
using Content.Shared.UserInterface;
using Robust.Shared.Containers;
namespace Content.Server.PowerCell;
/// <summary>
/// Handles Power cells
/// </summary>
public sealed partial class PowerCellSystem : SharedPowerCellSystem
{
[Dependency] private readonly ActivatableUISystem _activatable = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _sharedAppearanceSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly RiggableSystem _riggableSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellDrawComponent, ChargeChangedEvent>(OnDrawChargeChanged);
SubscribeLocalEvent<PowerCellDrawComponent, PowerCellChangedEvent>(OnDrawCellChanged);
SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
// funny
SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(OnSlotMicrowaved);
SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(OnChangeCharge);
}
private void OnSlotMicrowaved(EntityUid uid, PowerCellSlotComponent component, BeingMicrowavedEvent args)
{
if (!_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out var slot))
return;
if (slot.Item == null)
return;
RaiseLocalEvent(slot.Item.Value, args);
}
private void OnChargeChanged(EntityUid uid, PowerCellComponent component, ref ChargeChangedEvent args)
{
if (TryComp<RiggableComponent>(uid, out var rig) && rig.IsRigged)
{
_riggableSystem.Explode(uid, cause: null);
return;
}
var frac = args.Charge / args.MaxCharge;
var level = (byte)ContentHelpers.RoundToNearestLevels(frac, 1, PowerCellComponent.PowerCellVisualsLevels);
_sharedAppearanceSystem.SetData(uid, PowerCellVisuals.ChargeLevel, level);
// If this power cell is inside a cell-slot, inform that entity that the power has changed (for updating visuals n such).
if (_containerSystem.TryGetContainingContainer((uid, null, null), out var container)
&& TryComp(container.Owner, out PowerCellSlotComponent? slot)
&& _itemSlotsSystem.TryGetSlot(container.Owner, slot.CellSlotId, out var itemSlot))
{
if (itemSlot.Item == uid)
RaiseLocalEvent(container.Owner, new PowerCellChangedEvent(false));
}
}
protected override void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
{
base.OnCellRemoved(uid, component, args);
if (args.Container.ID != component.CellSlotId)
return;
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(uid, ref ev);
}
#region Activatable
/// <inheritdoc/>
public override bool HasActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
{
// Default to true if we don't have the components.
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return HasCharge(uid, battery.UseRate, cell, user);
}
/// <summary>
/// Tries to use the <see cref="PowerCellDrawComponent.UseRate"/> for this entity.
/// </summary>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public bool TryUseActivatableCharge(EntityUid uid, PowerCellDrawComponent? battery = null, PowerCellSlotComponent? cell = null, EntityUid? user = null)
{
// Default to true if we don't have the components.
if (!Resolve(uid, ref battery, ref cell, false))
return true;
if (TryUseCharge(uid, battery.UseRate, cell, user))
{
_sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, HasActivatableCharge(uid, battery, cell, user));
_activatable.CheckUsage(uid);
return true;
}
return false;
}
/// <inheritdoc/>
public override bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null)
{
if (!Resolve(uid, ref battery, ref cell, false))
return true;
return HasCharge(uid, battery.DrawRate, cell, user);
}
#endregion
/// <summary>
/// Returns whether the entity has a slotted battery and charge for the requested action.
/// </summary>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public bool HasCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
{
if (!TryGetBatteryFromSlot(uid, out var battery, component))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
return false;
}
if (battery.CurrentCharge < charge)
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
return false;
}
return true;
}
/// <summary>
/// Tries to use charge from a slotted battery.
/// </summary>
public bool TryUseCharge(EntityUid uid, float charge, PowerCellSlotComponent? component = null, EntityUid? user = null)
{
if (!TryGetBatteryFromSlot(uid, out var batteryEnt, out var battery, component))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), uid, user.Value);
return false;
}
if (!_battery.TryUseCharge((batteryEnt.Value, battery), charge))
{
if (user != null)
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), uid, user.Value);
return false;
}
_sharedAppearanceSystem.SetData(uid, PowerCellSlotVisuals.Enabled, battery.CurrentCharge > 0);
return true;
}
public bool TryGetBatteryFromSlot(EntityUid uid, [NotNullWhen(true)] out BatteryComponent? battery, PowerCellSlotComponent? component = null)
{
return TryGetBatteryFromSlot(uid, out _, out battery, component);
}
public bool TryGetBatteryFromSlot(EntityUid uid,
[NotNullWhen(true)] out EntityUid? batteryEnt,
[NotNullWhen(true)] out BatteryComponent? battery,
PowerCellSlotComponent? component = null)
{
if (!Resolve(uid, ref component, false))
{
batteryEnt = null;
battery = null;
return false;
}
if (_itemSlotsSystem.TryGetSlot(uid, component.CellSlotId, out ItemSlot? slot))
{
batteryEnt = slot.Item;
return TryComp(slot.Item, out battery);
}
batteryEnt = null;
battery = null;
return false;
}
private void OnCellExamined(EntityUid uid, PowerCellComponent component, ExaminedEvent args)
{
TryComp<BatteryComponent>(uid, out var battery);
OnBatteryExamined(uid, battery, args);
}
private void OnCellSlotExamined(EntityUid uid, PowerCellSlotComponent component, ExaminedEvent args)
{
TryGetBatteryFromSlot(uid, out var battery);
OnBatteryExamined(uid, battery, args);
}
private void OnBatteryExamined(EntityUid uid, BatteryComponent? component, ExaminedEvent args)
{
if (component != null)
{
var charge = component.CurrentCharge / component.MaxCharge * 100;
args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
}
else
{
args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
}
}
private void OnGetCharge(Entity<PowerCellSlotComponent> entity, ref GetChargeEvent args)
{
if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
return;
RaiseLocalEvent(batteryUid.Value, ref args);
}
private void OnChangeCharge(Entity<PowerCellSlotComponent> entity, ref ChangeChargeEvent args)
{
if (!TryGetBatteryFromSlot(entity, out var batteryUid, out _))
return;
RaiseLocalEvent(batteryUid.Value, ref args);
}
}

View File

@@ -1,8 +1,7 @@
using Content.Server.Power.EntitySystems;
using Content.Server.PowerCell;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.Interaction;
using Content.Shared.PowerCell.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Radio.EntitySystems;
using Content.Shared.Radio.Components;
using Content.Shared.DeviceNetwork.Systems;
@@ -12,7 +11,7 @@ namespace Content.Server.Radio.EntitySystems;
public sealed class JammerSystem : SharedJammerSystem
{
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedDeviceNetworkJammerSystem _jammer = default!;
@@ -25,6 +24,8 @@ public sealed class JammerSystem : SharedJammerSystem
SubscribeLocalEvent<RadioSendAttemptEvent>(OnRadioSendAttempt);
}
// TODO: Very important: Make this charge rate based instead of updating every single tick
// See PredictedBatteryComponent
public override void Update(float frameTime)
{
var query = EntityQueryEnumerator<ActiveRadioJammerComponent, RadioJammerComponent>();
@@ -32,9 +33,9 @@ public sealed class JammerSystem : SharedJammerSystem
while (query.MoveNext(out var uid, out var _, out var jam))
{
if (_powerCell.TryGetBatteryFromSlot(uid, out var batteryUid, out var battery))
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
if (!_battery.TryUseCharge((batteryUid.Value, battery), GetCurrentWattage((uid, jam)) * frameTime))
if (!_battery.TryUseCharge(battery.Value.AsNullable(), GetCurrentWattage((uid, jam)) * frameTime))
{
ChangeLEDState(uid, false);
RemComp<ActiveRadioJammerComponent>(uid);
@@ -42,7 +43,7 @@ public sealed class JammerSystem : SharedJammerSystem
}
else
{
var percentCharged = battery.CurrentCharge / battery.MaxCharge;
var percentCharged = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
var chargeLevel = percentCharged switch
{
> 0.50f => RadioJammerChargeLevel.High,
@@ -64,7 +65,7 @@ public sealed class JammerSystem : SharedJammerSystem
var activated = !HasComp<ActiveRadioJammerComponent>(ent) &&
_powerCell.TryGetBatteryFromSlot(ent.Owner, out var battery) &&
battery.CurrentCharge > GetCurrentWattage(ent);
_battery.GetCharge(battery.Value.AsNullable()) > GetCurrentWattage(ent);
if (activated)
{
ChangeLEDState(ent.Owner, true);

View File

@@ -27,10 +27,8 @@ public sealed partial class BorgSystem
SubscribeLocalEvent<BorgTransponderComponent, DeviceNetworkPacketEvent>(OnPacketReceived);
}
public override void Update(float frameTime)
public void UpdateTransponder(float frameTime)
{
base.Update(frameTime);
var now = _timing.CurTime;
var query = EntityQueryEnumerator<BorgTransponderComponent, BorgChassisComponent, DeviceNetworkComponent, MetaDataComponent>();
while (query.MoveNext(out var uid, out var comp, out var chassis, out var device, out var meta))
@@ -43,7 +41,7 @@ public sealed partial class BorgSystem
var charge = 0f;
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
charge = battery.CurrentCharge / battery.MaxCharge;
charge = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
var hpPercent = CalcHP(uid);

View File

@@ -91,6 +91,43 @@ public sealed partial class BorgSystem
UpdateUI(uid, component);
}
public void UpdateBattery(Entity<BorgChassisComponent> ent)
{
UpdateBatteryAlert(ent);
// if we aren't drawing and suddenly get enough power to draw again, reeanble.
if (_powerCell.HasDrawCharge(ent.Owner))
{
Toggle.TryActivate(ent.Owner);
}
UpdateUI(ent, ent);
}
// TODO: Move to client so we don't have to network this periodically.
private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
{
if (!_powerCell.TryGetBatteryFromSlot((ent.Owner, slotComponent), out var battery))
{
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
return;
}
var chargePercent = (short)MathF.Round(_battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargePercent == 0 && _powerCell.HasDrawCharge((ent.Owner, null, slotComponent)))
{
chargePercent = 1;
}
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
}
// TODO: Component states and update this on the client
public void UpdateUI(EntityUid uid, BorgChassisComponent? component = null)
{
if (!Resolve(uid, ref component))
@@ -101,10 +138,26 @@ public sealed partial class BorgSystem
if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
hasBattery = true;
chargePercent = battery.CurrentCharge / battery.MaxCharge;
chargePercent = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
}
var state = new BorgBuiState(chargePercent, hasBattery);
_ui.SetUiState(uid, BorgUiKey.Key, state);
}
// periodically update the charge indicator
// TODO: Move this to the client.
public void UpdateBattery(float frameTime)
{
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<BorgChassisComponent>();
while (query.MoveNext(out var uid, out var borgChassis))
{
if (curTime < borgChassis.NextBatteryUpdate)
continue;
UpdateBattery((uid, borgChassis));
borgChassis.NextBatteryUpdate = curTime + TimeSpan.FromSeconds(1);
}
}
}

View File

@@ -5,7 +5,6 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.Hands.Systems;
using Content.Server.PowerCell;
using Content.Shared.Alert;
using Content.Shared.Body.Events;
using Content.Shared.Database;
@@ -18,6 +17,8 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Content.Shared.Movement.Systems;
using Content.Shared.Pointing;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Roles;
@@ -61,6 +62,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
[Dependency] private readonly ISharedPlayerManager _player = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
public static readonly ProtoId<JobPrototype> BorgJobId = "Borg";
@@ -76,6 +78,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
SubscribeLocalEvent<BorgChassisComponent, MobStateChangedEvent>(OnMobStateChanged);
SubscribeLocalEvent<BorgChassisComponent, BeingGibbedEvent>(OnBeingGibbed);
SubscribeLocalEvent<BorgChassisComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<BorgChassisComponent, PredictedBatteryChargeChangedEvent>(OnBatteryChargeChanged);
SubscribeLocalEvent<BorgChassisComponent, PowerCellSlotEmptyEvent>(OnPowerCellSlotEmpty);
SubscribeLocalEvent<BorgChassisComponent, GetCharactedDeadIcEvent>(OnGetDeadIC);
SubscribeLocalEvent<BorgChassisComponent, GetCharacterUnrevivableIcEvent>(OnGetUnrevivableIC);
@@ -209,17 +212,14 @@ public sealed partial class BorgSystem : SharedBorgSystem
_container.EmptyContainer(component.ModuleContainer);
}
private void OnPowerCellChanged(EntityUid uid, BorgChassisComponent component, PowerCellChangedEvent args)
private void OnPowerCellChanged(Entity<BorgChassisComponent> ent, ref PowerCellChangedEvent args)
{
UpdateBatteryAlert((uid, component));
UpdateBattery(ent);
}
// if we aren't drawing and suddenly get enough power to draw again, reeanble.
if (_powerCell.HasDrawCharge(uid))
{
Toggle.TryActivate(uid);
}
UpdateUI(uid, component);
private void OnBatteryChargeChanged(Entity<BorgChassisComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
UpdateBattery(ent);
}
private void OnPowerCellSlotEmpty(EntityUid uid, BorgChassisComponent component, ref PowerCellSlotEmptyEvent args)
@@ -286,28 +286,6 @@ public sealed partial class BorgSystem : SharedBorgSystem
args.Cancel();
}
private void UpdateBatteryAlert(Entity<BorgChassisComponent> ent, PowerCellSlotComponent? slotComponent = null)
{
if (!_powerCell.TryGetBatteryFromSlot(ent, out var battery, slotComponent))
{
_alerts.ClearAlert(ent.Owner, ent.Comp.BatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.NoBatteryAlert);
return;
}
var chargePercent = (short) MathF.Round(battery.CurrentCharge / battery.MaxCharge * 10f);
// we make sure 0 only shows if they have absolutely no battery.
// also account for floating point imprecision
if (chargePercent == 0 && _powerCell.HasDrawCharge(ent, cell: slotComponent))
{
chargePercent = 1;
}
_alerts.ClearAlert(ent.Owner, ent.Comp.NoBatteryAlert);
_alerts.ShowAlert(ent.Owner, ent.Comp.BatteryAlert, chargePercent);
}
public bool TryEjectPowerCell(EntityUid uid, BorgChassisComponent component, [NotNullWhen(true)] out List<EntityUid>? ents)
{
ents = null;
@@ -357,4 +335,12 @@ public sealed partial class BorgSystem : SharedBorgSystem
return true;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
UpdateTransponder(frameTime);
UpdateBattery(frameTime);
}
}

View File

@@ -1,9 +1,10 @@
using System.Text;
using Content.Server.Destructible;
using Content.Server.PowerCell;
using Content.Shared.Speech.Components;
using Content.Shared.Damage.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Power.EntitySystems;
using Content.Shared.PowerCell;
using Content.Shared.Speech;
using Robust.Shared.Random;
@@ -12,6 +13,7 @@ namespace Content.Server.Speech.EntitySystems;
public sealed class DamagedSiliconAccentSystem : EntitySystem
{
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly DestructibleSystem _destructibleSystem = default!;
@@ -34,7 +36,7 @@ public sealed class DamagedSiliconAccentSystem : EntitySystem
}
else if (_powerCell.TryGetBatteryFromSlot(uid, out var battery))
{
currentChargeLevel = battery.CurrentCharge / battery.MaxCharge;
currentChargeLevel = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge;
}
currentChargeLevel = Math.Clamp(currentChargeLevel, 0.0f, 1.0f);
// Corrupt due to low power (drops characters on longer messages)

View File

@@ -1,6 +1,6 @@
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Server.Power.Events;
using Content.Server.Power.EntitySystems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Damage.Events;
using Content.Shared.Examine;
@@ -9,6 +9,7 @@ using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable.Systems
@@ -17,7 +18,7 @@ namespace Content.Server.Stunnable.Systems
{
[Dependency] private readonly RiggableSystem _riggableSystem = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly ItemToggleSystem _itemToggle = default!;
public override void Initialize()
@@ -27,13 +28,13 @@ namespace Content.Server.Stunnable.Systems
SubscribeLocalEvent<StunbatonComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<StunbatonComponent, SolutionContainerChangedEvent>(OnSolutionChange);
SubscribeLocalEvent<StunbatonComponent, StaminaDamageOnHitAttemptEvent>(OnStaminaHitAttempt);
SubscribeLocalEvent<StunbatonComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<StunbatonComponent, PredictedBatteryChargeChangedEvent>(OnChargeChanged);
}
private void OnStaminaHitAttempt(Entity<StunbatonComponent> entity, ref StaminaDamageOnHitAttemptEvent args)
{
if (!_itemToggle.IsActivated(entity.Owner) ||
!TryComp<BatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
!TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) || !_battery.TryUseCharge((entity.Owner, battery), entity.Comp.EnergyPerUse))
{
args.Cancelled = true;
}
@@ -46,9 +47,9 @@ namespace Content.Server.Stunnable.Systems
: Loc.GetString("comp-stunbaton-examined-off");
args.PushMarkup(onMsg);
if (TryComp<BatteryComponent>(entity.Owner, out var battery))
if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery))
{
var count = (int) (battery.CurrentCharge / entity.Comp.EnergyPerUse);
var count = _battery.GetRemainingUses((entity.Owner, battery), entity.Comp.EnergyPerUse);
args.PushMarkup(Loc.GetString("melee-battery-examine", ("color", "yellow"), ("count", count)));
}
}
@@ -57,7 +58,7 @@ namespace Content.Server.Stunnable.Systems
{
base.TryTurnOn(entity, ref args);
if (!TryComp<BatteryComponent>(entity, out var battery) || battery.CurrentCharge < entity.Comp.EnergyPerUse)
if (!TryComp<PredictedBatteryComponent>(entity, out var battery) || _battery.GetCharge((entity, battery)) < entity.Comp.EnergyPerUse)
{
args.Cancelled = true;
if (args.User != null)
@@ -69,7 +70,7 @@ namespace Content.Server.Stunnable.Systems
if (TryComp<RiggableComponent>(entity, out var rig) && rig.IsRigged)
{
_riggableSystem.Explode(entity.Owner, battery, args.User);
_riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)), args.User);
}
}
@@ -78,13 +79,14 @@ namespace Content.Server.Stunnable.Systems
{
// Explode if baton is activated and rigged.
if (!TryComp<RiggableComponent>(entity, out var riggable) ||
!TryComp<BatteryComponent>(entity, out var battery))
!TryComp<PredictedBatteryComponent>(entity, out var battery))
return;
if (_itemToggle.IsActivated(entity.Owner) && riggable.IsRigged)
_riggableSystem.Explode(entity.Owner, battery);
_riggableSystem.Explode(entity.Owner, _battery.GetCharge((entity, battery)));
}
// TODO: Not used anywhere?
private void SendPowerPulse(EntityUid target, EntityUid? user, EntityUid used)
{
RaiseLocalEvent(target, new PowerPulseEvent()
@@ -94,10 +96,10 @@ namespace Content.Server.Stunnable.Systems
});
}
private void OnChargeChanged(Entity<StunbatonComponent> entity, ref ChargeChangedEvent args)
private void OnChargeChanged(Entity<StunbatonComponent> entity, ref PredictedBatteryChargeChangedEvent args)
{
if (TryComp<BatteryComponent>(entity.Owner, out var battery) &&
battery.CurrentCharge < entity.Comp.EnergyPerUse)
if (TryComp<PredictedBatteryComponent>(entity.Owner, out var battery) &&
_battery.GetCharge((entity.Owner, battery)) < entity.Comp.EnergyPerUse)
{
_itemToggle.TryDeactivate(entity.Owner, predicted: false);
}

View File

@@ -1,4 +1,3 @@
using Content.Server.PowerCell;
using Content.Shared.Item.ItemToggle;
using Content.Shared.PowerCell;
using Content.Shared.Weapons.Misc;

View File

@@ -1,75 +0,0 @@
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
namespace Content.Server.Weapons.Ranged.Systems;
public sealed partial class GunSystem
{
protected override void InitializeBattery()
{
base.InitializeBattery();
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ChargeChangedEvent>(OnBatteryChargeChange);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
}
private void OnBatteryStartup<T>(Entity<T> entity, ref ComponentStartup args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp);
}
private void OnBatteryChargeChange<T>(Entity<T> entity, ref ChargeChangedEvent args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp, args.Charge, args.MaxCharge);
}
private void OnPowerCellChanged<T>(Entity<T> entity, ref PowerCellChangedEvent args) where T : BatteryAmmoProviderComponent
{
UpdateShots(entity, entity.Comp);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component)
{
var ev = new GetChargeEvent();
RaiseLocalEvent(uid, ref ev);
UpdateShots(uid, component, ev.CurrentCharge, ev.MaxCharge);
}
private void UpdateShots(EntityUid uid, BatteryAmmoProviderComponent component, float charge, float maxCharge)
{
var shots = (int) (charge / component.FireCost);
var maxShots = (int) (maxCharge / component.FireCost);
if (component.Shots != shots || component.Capacity != maxShots)
{
Dirty(uid, component);
}
component.Shots = shots;
if (maxShots > 0)
component.Capacity = maxShots;
UpdateBatteryAppearance(uid, component);
var updateAmmoEv = new UpdateClientAmmoEvent();
RaiseLocalEvent(uid, ref updateAmmoEv);
}
protected override void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
{
// Take charge from either the BatteryComponent or PowerCellSlotComponent.
var ev = new ChangeChargeEvent(-entity.Comp.FireCost);
RaiseLocalEvent(entity, ref ev);
}
}

View File

@@ -1,6 +1,7 @@
using Content.Server.Power.EntitySystems;
using Content.Server.Xenoarchaeology.Artifact.XAE.Components;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Xenoarchaeology.Artifact;
using Content.Shared.Xenoarchaeology.Artifact.XAE;
@@ -12,20 +13,29 @@ namespace Content.Server.Xenoarchaeology.Artifact.XAE;
public sealed class XAEChargeBatterySystem : BaseXAESystem<XAEChargeBatteryComponent>
{
[Dependency] private readonly BatterySystem _battery = default!;
[Dependency] private readonly PredictedBatterySystem _predictedBattery = default!;
[Dependency] private readonly EntityLookupSystem _lookup = default!;
/// <summary> Pre-allocated and re-used collection.</summary>
private readonly HashSet<Entity<BatteryComponent>> _batteryEntities = new();
private readonly HashSet<Entity<PredictedBatteryComponent>> _pBatteryEntities = new();
/// <inheritdoc />
protected override void OnActivated(Entity<XAEChargeBatteryComponent> ent, ref XenoArtifactNodeActivatedEvent args)
{
var chargeBatteryComponent = ent.Comp;
_batteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, chargeBatteryComponent.Radius, _batteryEntities);
_pBatteryEntities.Clear();
_lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _batteryEntities);
foreach (var battery in _batteryEntities)
{
_battery.SetCharge(battery.AsNullable(), battery.Comp.MaxCharge);
}
_lookup.GetEntitiesInRange(args.Coordinates, ent.Comp.Radius, _pBatteryEntities);
foreach (var pBattery in _pBatteryEntities)
{
_predictedBattery.SetCharge(pBattery.AsNullable(), pBattery.Comp.MaxCharge);
}
}
}

View File

@@ -0,0 +1,10 @@
namespace Content.Shared.Kitchen;
/// <summary>
/// Raised on an entity when it is inside a microwave and it starts cooking.
/// </summary>
public sealed class BeingMicrowavedEvent(EntityUid microwave, EntityUid? user) : HandledEntityEventArgs
{
public EntityUid Microwave = microwave;
public EntityUid? User = user;
}

View File

@@ -283,6 +283,7 @@ public abstract partial class SharedMechSystem : EntitySystem
/// <summary>
/// Attempts to change the amount of energy in the mech.
/// TODO: Power cells are predicted now, so no need to duplicate the charge level
/// </summary>
/// <param name="uid">The mech itself</param>
/// <param name="delta">The change in energy</param>

View File

@@ -5,14 +5,44 @@ namespace Content.Shared.Power;
/// <summary>
/// Raised when a battery's charge or capacity changes (capacity affects relative charge percentage).
/// Only raised for entities with <see cref="BatteryComponent"/>.
/// </summary>
[ByRefEvent]
public readonly record struct ChargeChangedEvent(float Charge, float MaxCharge);
/// <summary>
/// Raised when a predicted battery's charge or capacity changes (capacity affects relative charge percentage).
/// Unlike <see cref="ChargeChangedEvent"/> this is not raised repeatedly each time the charge changes, but only when the charge rate is changed
/// or a charge amount was added or removed instantaneously. The current charge can be inferred from the time of the last update and the charge and
/// charge rate at that time.
/// Only raised for entities with <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public readonly record struct PredictedBatteryChargeChangedEvent(float CurrentCharge, float CurrentChargeRate, TimeSpan CurrentTime, float MaxCharge);
/// <summary>
/// Raised when a battery changes its state between full, empty, or neither.
/// Used only for <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public record struct PredictedBatteryStateChangedEvent(BatteryState OldState, BatteryState NewState);
/// <summary>
/// Raised to calculate a predicted battery's recharge rate.
/// Subscribe to this to offset its current charge rate.
/// Used only for <see cref="PredictedBatteryComponent"/>.
/// </summary>
[ByRefEvent]
public record struct RefreshChargeRateEvent(float MaxCharge)
{
public readonly float MaxCharge = MaxCharge;
public float NewChargeRate;
}
/// <summary>
/// Event that supports multiple battery types.
/// Raised when it is necessary to get information about battery charges.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
/// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then the results will be summed up.
/// </summary>
[ByRefEvent]
@@ -25,7 +55,7 @@ public record struct GetChargeEvent
/// <summary>
/// Method event that supports multiple battery types.
/// Raised when it is necessary to change the current battery charge by some value.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PowerCellSlotComponent"/>.
/// Works with either <see cref="BatteryComponent"/>, <see cref="PredictedBatteryComponent"/>, or <see cref="PowerCellSlotComponent"/>.
/// If there are multiple batteries then they will be changed in order of subscription until the total value was reached.
/// </summary>
[ByRefEvent]

View File

@@ -5,6 +5,8 @@ namespace Content.Shared.Power.Components;
/// <summary>
/// Battery node on the pow3r network. Needs other components to connect to actual networks.
/// Use this for batteries that cannot be predicted.
/// Use <see cref="PredictedBatteryComponent"/> otherwise.
/// </summary>
[RegisterComponent]
[Virtual]

View File

@@ -5,6 +5,7 @@ namespace Content.Shared.Power.Components;
/// <summary>
/// Self-recharging battery.
/// To be used in combination with <see cref="BatteryComponent"/>.
/// For <see cref="PredictedBatteryComponent"/> use <see cref="PredictedBatterySelfRechargerComponent"/> instead.
/// </summary>
[RegisterComponent, AutoGenerateComponentPause]
public sealed partial class BatterySelfRechargerComponent : Component
@@ -16,7 +17,7 @@ public sealed partial class BatterySelfRechargerComponent : Component
public bool AutoRecharge = true;
/// <summary>
/// At what rate does the entity automatically recharge?
/// At what rate does the entity automatically recharge? In watts.
/// </summary>
[DataField]
public float AutoRechargeRate;

View File

@@ -1,19 +1,24 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Components;
[RegisterComponent, NetworkedComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ChargerComponent : Component
{
[ViewVariables]
public CellChargerStatus Status;
/// <summary>
/// The charge rate of the charger, in watts.
/// </summary>
[DataField, AutoNetworkedField]
public float ChargeRate = 20.0f;
/// <summary>
/// The charge rate of the charger, in watts
/// Passive draw when no power cell is inserted, in watts.
/// This should be larger than 0 or the charger will be considered as powered even without a LV supply.
/// </summary>
[DataField]
public float ChargeRate = 20.0f;
[DataField, AutoNetworkedField]
public float PassiveDraw = 1f;
/// <summary>
/// The container ID that is holds the entities being charged.
@@ -24,13 +29,29 @@ public sealed partial class ChargerComponent : Component
/// <summary>
/// A whitelist for what entities can be charged by this Charger.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public EntityWhitelist? Whitelist;
/// <summary>
/// Indicates whether the charger is portable and thus subject to EMP effects
/// and bypasses checks for transform, anchored, and ApcPowerReceiverComponent.
/// </summary>
[DataField]
[DataField, AutoNetworkedField]
public bool Portable = false;
}
[Serializable, NetSerializable]
public enum CellChargerStatus
{
Off,
Empty,
Charging,
Charged,
}
[Serializable, NetSerializable]
public enum CellVisual
{
Occupied, // If there's an item in it
Light,
}

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
/// <summary>
/// Allows the charge of a battery to be seen by examination.
/// Works with either <see cref="BatteryComponent"/> or <see cref="PredictedBatteryComponent"/>.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ExaminableBatteryComponent : Component;

View File

@@ -0,0 +1,10 @@
using Robust.Shared.GameStates;
namespace Content.Shared.Power.Components;
/// <summary>
/// This entity is currently inside the charging slot of an entity with <see cref="ChargerComponent"/>.
/// Added regardless whether or not the charger is powered.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class InsideChargerComponent : Component;

View File

@@ -0,0 +1,94 @@
using Content.Shared.Power.EntitySystems;
using Content.Shared.Guidebook;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Power.Components;
/// <summary>
/// Predicted equivalent to <see cref="BatteryComponent"/>.
/// Use this for electrical power storages that only have a constant charge rate or instantaneous power draw.
/// Devices being directly charged by the power network do not fulfill that requirement as their power supply ramps up over time.
/// </summary>
/// <remarks>
/// We cannot simply network <see cref="BatteryComponent"/> since it would get dirtied every single tick when it updates.
/// This component solves this by requiring a constant charge rate and having the client infer the current charge from the rate
/// and the timestamp the charge was last networked at. This can possibly be expanded in the future by adding a second time derivative.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
[Access(typeof(PredictedBatterySystem))]
public sealed partial class PredictedBatteryComponent : Component
{
/// <summary>
/// Maximum charge of the battery in joules (ie. watt seconds)
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
[GuidebookData]
public float MaxCharge;
/// <summary>
/// The price per one joule. Default is 1 speso for 10kJ.
/// </summary>
[DataField]
public float PricePerJoule = 0.0001f;
/// <summary>
/// Time stamp of the last networked update.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField, ViewVariables]
public TimeSpan LastUpdate = TimeSpan.Zero;
/// <summary>
/// The intial charge to be set on map init.
/// </summary>
[DataField]
public float StartingCharge;
/// <summary>
/// The charge at the last update in joules (i.e. watt seconds).
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float LastCharge;
/// <summary>
/// The current charge rate in watt.
/// </summary>
/// <remarks>
/// Not a datafield as this is only cached and recalculated on component startup.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public float ChargeRate;
/// <summary>
/// The current charge state of the battery.
/// Used to track state changes for raising <see cref="PredictedBatteryStateChangedEvent"/>.
/// </summary>
/// <remarks>
/// Not a datafield as this is only cached and recalculated in an update loop.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public BatteryState State = BatteryState.Neither;
}
/// <summary>
/// Charge level status of the battery.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryState : byte
{
/// <summary>
/// Full charge.
/// </summary>
Full,
/// <summary>
/// No charge.
/// </summary>
Empty,
/// <summary>
/// Neither full nor empty.
/// </summary>
Neither,
}

View File

@@ -0,0 +1,33 @@
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Power.Components;
/// <summary>
/// Self-recharging battery.
/// To be used in combination with <see cref="PredictedBatteryComponent"/>.
/// For <see cref="BatteryComponent"/> use <see cref="BatterySelfRechargerComponent"/> instead.
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class PredictedBatterySelfRechargerComponent : Component
{
/// <summary>
/// At what rate does the entity automatically recharge? In watts.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float AutoRechargeRate;
/// <summary>
/// How long should the entity stop automatically recharging if a charge is used?
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan AutoRechargePauseTime = TimeSpan.Zero;
/// <summary>
/// Do not auto recharge if this timestamp has yet to happen, set for the auto recharge pause system.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField, ViewVariables]
public TimeSpan? NextAutoRecharge = TimeSpan.FromSeconds(0);
}

View File

@@ -0,0 +1,51 @@
using Content.Shared.PowerCell.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Power.Components;
/// <summary>
/// Marker component that makes an entity with <see cref="PredictedBatteryComponent"/> update its appearance data for use with visualizers.
/// Also works with an entity with <see cref="PowerCellSlotComponent"/> and will relay the state of the inserted powercell.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class PredictedBatteryVisualsComponent : Component;
/// <summary>
/// Keys for the appearance data.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryVisuals : byte
{
/// <summary>
/// The current charge state of the battery.
/// Either full, empty, or neither.
/// Uses a <see cref="BatteryState"/>.
/// </summary>
State,
/// <summary>
/// Is the battery currently charging or discharging?
/// Uses a <see cref="BatteryChargingState"/>.
/// </summary>
Charging,
}
/// <summary>
/// Charge level status of the battery.
/// </summary>
[Serializable, NetSerializable]
public enum BatteryChargingState : byte
{
/// <summary>
/// PredictedBatteryComponent.ChargeRate &gt; 0
/// </summary>
Charging,
/// <summary>
/// PredictedBatteryComponent.ChargeRate &lt; 0
/// </summary>
Decharging,
/// <summary>
/// PredictedBatteryComponent.ChargeRate == 0
/// </summary>
Constant,
}

View File

@@ -0,0 +1,270 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Content.Shared.Storage.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.Power.EntitySystems;
public sealed class ChargerSystem : EntitySystem
{
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly PowerCellSystem _powerCell = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<ChargerComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<ChargerComponent, EntInsertedIntoContainerMessage>(OnInserted);
SubscribeLocalEvent<ChargerComponent, EntRemovedFromContainerMessage>(OnRemoved);
SubscribeLocalEvent<ChargerComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
SubscribeLocalEvent<ChargerComponent, InsertIntoEntityStorageAttemptEvent>(OnEntityStorageInsertAttempt);
SubscribeLocalEvent<ChargerComponent, ExaminedEvent>(OnChargerExamine);
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<ChargerComponent, EmpDisabledRemovedEvent>(OnEmpRemoved);
SubscribeLocalEvent<InsideChargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
SubscribeLocalEvent<InsideChargerComponent, PredictedBatteryStateChangedEvent>(OnStatusChanged);
}
private void OnStartup(Entity<ChargerComponent> ent, ref ComponentStartup args)
{
UpdateStatus(ent);
}
private void OnChargerExamine(EntityUid uid, ChargerComponent component, ExaminedEvent args)
{
using (args.PushGroup(nameof(ChargerComponent)))
{
// rate at which the charger charges
args.PushMarkup(Loc.GetString("charger-examine", ("color", "yellow"), ("chargeRate", (int)component.ChargeRate)));
// try to get contents of the charger
if (!_container.TryGetContainer(uid, component.SlotId, out var container))
return;
if (HasComp<PowerCellSlotComponent>(uid))
return;
// if charger is empty and not a power cell type charger, add empty message
// power cells have their own empty message by default, for things like flash lights
if (container.ContainedEntities.Count == 0)
{
args.PushMarkup(Loc.GetString("charger-empty"));
}
else
{
// add how much each item is charged it
foreach (var contained in container.ContainedEntities)
{
if (!SearchForBattery(contained, out var battery))
continue;
var chargePercentage = _battery.GetCharge(battery.Value.AsNullable()) / battery.Value.Comp.MaxCharge * 100;
args.PushMarkup(Loc.GetString("charger-content", ("chargePercentage", (int)chargePercentage)));
}
}
}
}
private void OnPowerChanged(Entity<ChargerComponent> ent, ref PowerChangedEvent args)
{
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnInserted(Entity<ChargerComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (_timing.ApplyingState)
return; // Already networked in the same gamestate
if (args.Container.ID != ent.Comp.SlotId)
return;
AddComp<InsideChargerComponent>(args.Entity);
if (SearchForBattery(args.Entity, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
UpdateStatus(ent);
}
private void OnRemoved(Entity<ChargerComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (_timing.ApplyingState)
return; // Already networked in the same gamestate
if (args.Container.ID != ent.Comp.SlotId)
return;
RemComp<InsideChargerComponent>(args.Entity);
if (SearchForBattery(args.Entity, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
UpdateStatus(ent);
}
/// <summary>
/// Verify that the entity being inserted is actually rechargeable.
/// </summary>
private void OnInsertAttempt(EntityUid uid, ChargerComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(args.EntityUid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancel();
}
private void OnEntityStorageInsertAttempt(EntityUid uid, ChargerComponent component, ref InsertIntoEntityStorageAttemptEvent args)
{
if (!component.Initialized || args.Cancelled)
return;
if (args.Container.ID != component.SlotId)
return;
if (!TryComp<PowerCellSlotComponent>(uid, out var cellSlot))
return;
if (!cellSlot.FitsInCharger)
args.Cancelled = true;
}
private void OnEmpPulse(Entity<ChargerComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnEmpRemoved(Entity<ChargerComponent> ent, ref EmpDisabledRemovedEvent args)
{
RefreshAllBatteries(ent);
UpdateStatus(ent);
}
private void OnRefreshChargeRate(Entity<InsideChargerComponent> ent, ref RefreshChargeRateEvent args)
{
var chargerUid = Transform(ent).ParentUid;
if (HasComp<EmpDisabledComponent>(chargerUid))
return;
if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
return;
if (!chargerComp.Portable && !_receiver.IsPowered(chargerUid))
return;
if (_whitelist.IsWhitelistFail(chargerComp.Whitelist, ent.Owner))
return;
args.NewChargeRate += chargerComp.ChargeRate;
}
private void OnStatusChanged(Entity<InsideChargerComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
// If the battery is full update the visuals and power draw of the charger.
var chargerUid = Transform(ent).ParentUid;
if (!TryComp<ChargerComponent>(chargerUid, out var chargerComp))
return;
UpdateStatus((chargerUid, chargerComp));
}
private bool SearchForBattery(EntityUid uid, [NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
{
// try get a battery directly on the inserted entity
if (TryComp<PredictedBatteryComponent>(uid, out var batteryComp))
{
battery = (uid, batteryComp);
return true;
}
// or by checking for a power cell slot on the inserted entity
if (_powerCell.TryGetBatteryFromSlot(uid, out battery))
return true;
battery = null;
return false;
}
private void RefreshAllBatteries(Entity<ChargerComponent> ent)
{
// try to get contents of the charger
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return;
foreach (var item in container.ContainedEntities)
{
if (SearchForBattery(item, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
}
}
private void UpdateStatus(Entity<ChargerComponent> ent)
{
TryComp<AppearanceComponent>(ent, out var appearance);
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return;
_appearance.SetData(ent.Owner, CellVisual.Occupied, container.ContainedEntities.Count != 0, appearance);
var status = GetStatus(ent);
switch (status)
{
case CellChargerStatus.Charging:
// TODO: If someone ever adds chargers that can charge multiple batteries at once then set this to the total draw rate.
_receiver.SetLoad(ent.Owner, ent.Comp.ChargeRate);
break;
default:
// Don't set the load to 0 or the charger will be considered as powered even if the LV connection is unpowered.
// TODO: Fix this on an ApcPowerReceiver level.
_receiver.SetLoad(ent.Owner, ent.Comp.PassiveDraw);
break;
}
_appearance.SetData(ent.Owner, CellVisual.Light, status, appearance);
}
private CellChargerStatus GetStatus(Entity<ChargerComponent> ent)
{
if (!ent.Comp.Portable && !Transform(ent).Anchored)
return CellChargerStatus.Off;
if (!ent.Comp.Portable && !_receiver.IsPowered(ent.Owner))
return CellChargerStatus.Off;
if (HasComp<EmpDisabledComponent>(ent))
return CellChargerStatus.Off;
if (!_container.TryGetContainer(ent.Owner, ent.Comp.SlotId, out var container))
return CellChargerStatus.Off;
if (container.ContainedEntities.Count == 0)
return CellChargerStatus.Empty;
// Use the first stored battery for visuals. If someone ever makes a multi-slot charger then this will need to be changed.
if (!SearchForBattery(container.ContainedEntities[0], out var battery))
return CellChargerStatus.Off;
if (_battery.IsFull(battery.Value.AsNullable()))
return CellChargerStatus.Charged;
return CellChargerStatus.Charging;
}
}

View File

@@ -0,0 +1,278 @@
using Content.Shared.Power.Components;
using JetBrains.Annotations;
namespace Content.Shared.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="PredictedBatteryComponent"/>.
/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class PredictedBatterySystem
{
/// <summary>
/// Changes the battery's charge by the given amount
/// and resets the self-recharge cooldown if it exists.
/// A positive value will add charge, a negative value will remove charge.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public float ChangeCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp))
return 0;
var oldValue = GetCharge(ent);
var newValue = Math.Clamp(oldValue + amount, 0, ent.Comp.MaxCharge);
var delta = newValue - oldValue;
if (delta == 0f)
return 0f;
var curTime = _timing.CurTime;
ent.Comp.LastCharge = newValue;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
TrySetChargeCooldown(ent.Owner);
var changedEv = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref changedEv);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
return delta;
}
/// <summary>
/// Removes the given amount of charge from the battery
/// and resets the self-recharge cooldown if it exists.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public float UseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (amount <= 0f)
return 0f;
return ChangeCharge(ent, -amount);
}
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// Resets the self-recharge cooldown if it exists.
/// Always returns false on the client.
/// </summary>
/// <returns>If the full amount was able to be removed.</returns>
[PublicAPI]
public bool TryUseCharge(Entity<PredictedBatteryComponent?> ent, float amount)
{
if (!Resolve(ent, ref ent.Comp, false) || amount > GetCharge(ent))
return false;
UseCharge(ent, amount);
return true;
}
/// <summary>
/// Sets the battery's charge.
/// </summary>
[PublicAPI]
public void SetCharge(Entity<PredictedBatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
var oldValue = GetCharge(ent);
var newValue = Math.Clamp(value, 0, ent.Comp.MaxCharge);
var delta = newValue - oldValue;
if (delta == 0f)
return;
var curTime = _timing.CurTime;
ent.Comp.LastCharge = newValue;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
var ev = new PredictedBatteryChargeChangedEvent(newValue, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
}
/// <summary>
/// Sets the battery's maximum charge.
/// </summary>
[PublicAPI]
public void SetMaxCharge(Entity<PredictedBatteryComponent?> ent, float value)
{
if (!Resolve(ent, ref ent.Comp))
return;
// ReSharper disable once CompareOfFloatsByEqualityOperator
if (value == ent.Comp.MaxCharge)
return;
ent.Comp.MaxCharge = Math.Max(value, 0);
ent.Comp.LastCharge = GetCharge(ent); // This clamps it using the new max.
var curTime = _timing.CurTime;
ent.Comp.LastUpdate = curTime;
Dirty(ent);
var ev = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref ev);
// Raise events if the battery status changed between full, empty, or neither.
UpdateState(ent);
}
/// <summary>
/// Updates the battery's charge state and sends an event if it changed.
/// </summary>
[PublicAPI]
public void UpdateState(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return;
var oldState = ent.Comp.State;
var newState = BatteryState.Neither;
var charge = GetCharge(ent);
if (charge == ent.Comp.MaxCharge)
newState = BatteryState.Full;
else if (charge == 0f)
newState = BatteryState.Empty;
if (oldState == newState)
return;
ent.Comp.State = newState;
Dirty(ent);
var changedEv = new PredictedBatteryStateChangedEvent(oldState, newState);
RaiseLocalEvent(ent, ref changedEv);
}
/// <summary>
/// Gets the battery's current charge.
/// </summary>
[PublicAPI]
public float GetCharge(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
var curTime = _timing.CurTime;
// We have a constant charge rate, so the charge changes linearly over time.
var dt = (curTime - ent.Comp.LastUpdate).TotalSeconds;
var charge = Math.Clamp(ent.Comp.LastCharge + (float)(dt * ent.Comp.ChargeRate), 0f, ent.Comp.MaxCharge);
return charge;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
[PublicAPI]
public int GetRemainingUses(Entity<PredictedBatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(GetCharge(ent) / cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
[PublicAPI]
public int GetMaxUses(Entity<PredictedBatteryComponent?> ent, float cost)
{
if (cost <= 0)
return 0;
if (!Resolve(ent, ref ent.Comp))
return 0;
return (int)(ent.Comp.MaxCharge / cost);
}
/// <summary>
/// Refreshes the battery's current charge rate by raising a <see cref="RefreshChargeRateEvent"/>.
/// </summary>
/// <returns>The new charge rate.</returns>
[PublicAPI]
public float RefreshChargeRate(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return 0f;
ent.Comp.LastCharge = GetCharge(ent); // Prevent the new rate from modifying the current charge.
var curTime = _timing.CurTime;
ent.Comp.LastUpdate = curTime;
var refreshEv = new RefreshChargeRateEvent(ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref refreshEv);
ent.Comp.ChargeRate = refreshEv.NewChargeRate;
Dirty(ent);
// Inform other systems about the new rate;
var changedEv = new PredictedBatteryChargeChangedEvent(ent.Comp.LastCharge, ent.Comp.ChargeRate, curTime, ent.Comp.MaxCharge);
RaiseLocalEvent(ent, ref changedEv);
return refreshEv.NewChargeRate;
}
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component.
/// </summary>
[PublicAPI]
public void TrySetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return;
if (ent.Comp.AutoRechargePauseTime == TimeSpan.Zero)
return; // no recharge pause
if (_timing.CurTime + ent.Comp.AutoRechargePauseTime <= ent.Comp.NextAutoRecharge)
return; // the current pause is already longer
SetChargeCooldown(ent, ent.Comp.AutoRechargePauseTime);
}
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
[PublicAPI]
public void SetChargeCooldown(Entity<PredictedBatterySelfRechargerComponent?> ent, TimeSpan cooldown)
{
if (!Resolve(ent, ref ent.Comp))
return;
ent.Comp.NextAutoRecharge = _timing.CurTime + cooldown;
Dirty(ent);
RefreshChargeRate(ent.Owner); // Apply the new charge rate.
}
/// <summary>
/// Returns whether the battery is full.
/// </summary>
[PublicAPI]
public bool IsFull(Entity<PredictedBatteryComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
return GetCharge(ent) >= ent.Comp.MaxCharge;
}
}

View File

@@ -0,0 +1,182 @@
using Content.Shared.Cargo;
using Content.Shared.Emp;
using Content.Shared.Examine;
using Content.Shared.Power.Components;
using Content.Shared.Rejuvenate;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Shared.Power.EntitySystems;
/// <summary>
/// Responsible for <see cref="PredictedBatteryComponent"/>.
/// Predicted equivalent of <see cref="Content.Server.Power.EntitySystems.BatterySystem"/>.
/// If you make changes to this make sure to keep the two consistent.
/// </summary>
public sealed partial class PredictedBatterySystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PredictedBatteryComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<PredictedBatteryComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<PredictedBatteryComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PredictedBatteryComponent, EmpPulseEvent>(OnEmpPulse);
SubscribeLocalEvent<PredictedBatteryComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<PredictedBatteryComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<PredictedBatteryComponent, PriceCalculationEvent>(CalculateBatteryPrice);
SubscribeLocalEvent<PredictedBatteryComponent, ChangeChargeEvent>(OnChangeCharge);
SubscribeLocalEvent<PredictedBatteryComponent, GetChargeEvent>(OnGetCharge);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, RefreshChargeRateEvent>(OnRefreshChargeRate);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentStartup>(OnRechargerStartup);
SubscribeLocalEvent<PredictedBatterySelfRechargerComponent, ComponentRemove>(OnRechargerRemove);
SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryChargeChangedEvent>(OnVisualsChargeChanged);
SubscribeLocalEvent<PredictedBatteryVisualsComponent, PredictedBatteryStateChangedEvent>(OnVisualsStateChanged);
}
private void OnInit(Entity<PredictedBatteryComponent> ent, ref ComponentInit args)
{
DebugTools.Assert(!HasComp<BatteryComponent>(ent), $"{ent} has both BatteryComponent and PredictedBatteryComponent");
}
private void OnStartup(Entity<PredictedBatteryComponent> ent, ref ComponentStartup args)
{
// In case a recharging component was added before the battery component itself.
// Doing this only on map init is not enough because the charge rate is not a datafield, but cached, so it would get lost when reloading the game.
// If we would make it a datafield then the integration tests would complain about modifying it before map init.
RefreshChargeRate(ent.AsNullable());
}
private void OnMapInit(Entity<PredictedBatteryComponent> ent, ref MapInitEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.StartingCharge);
RefreshChargeRate(ent.AsNullable());
}
private void OnRejuvenate(Entity<PredictedBatteryComponent> ent, ref RejuvenateEvent args)
{
SetCharge(ent.AsNullable(), ent.Comp.MaxCharge);
}
private void OnEmpPulse(Entity<PredictedBatteryComponent> ent, ref EmpPulseEvent args)
{
args.Affected = true;
UseCharge(ent.AsNullable(), args.EnergyConsumption);
}
private void OnExamine(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
{
if (!args.IsInDetailsRange)
return;
if (!HasComp<ExaminableBatteryComponent>(ent))
return;
var chargePercentRounded = 0;
var currentCharge = GetCharge(ent.AsNullable());
if (ent.Comp.MaxCharge != 0)
chargePercentRounded = (int)(100 * currentCharge / ent.Comp.MaxCharge);
args.PushMarkup(
Loc.GetString(
"examinable-battery-component-examine-detail",
("percent", chargePercentRounded),
("markupPercentColor", "green")
)
);
}
/// <summary>
/// Gets the price for the power contained in an entity's battery.
/// </summary>
private void CalculateBatteryPrice(Entity<PredictedBatteryComponent> ent, ref PriceCalculationEvent args)
{
args.Price += GetCharge(ent.AsNullable()) * ent.Comp.PricePerJoule;
}
private void OnChangeCharge(Entity<PredictedBatteryComponent> ent, ref ChangeChargeEvent args)
{
if (args.ResidualValue == 0)
return;
args.ResidualValue -= ChangeCharge(ent.AsNullable(), args.ResidualValue);
}
private void OnGetCharge(Entity<PredictedBatteryComponent> ent, ref GetChargeEvent args)
{
args.CurrentCharge += GetCharge(ent.AsNullable());
args.MaxCharge += ent.Comp.MaxCharge;
}
private void OnRefreshChargeRate(Entity<PredictedBatterySelfRechargerComponent> ent, ref RefreshChargeRateEvent args)
{
if (_timing.CurTime < ent.Comp.NextAutoRecharge)
return; // Still on cooldown
args.NewChargeRate += ent.Comp.AutoRechargeRate;
}
public override void Update(float frameTime)
{
var curTime = _timing.CurTime;
// Update self-recharging cooldowns.
var rechargerQuery = EntityQueryEnumerator<PredictedBatterySelfRechargerComponent, PredictedBatteryComponent>();
while (rechargerQuery.MoveNext(out var uid, out var recharger, out var battery))
{
if (recharger.NextAutoRecharge == null || curTime < recharger.NextAutoRecharge)
continue;
recharger.NextAutoRecharge = null; // Don't refresh every tick.
Dirty(uid, recharger);
RefreshChargeRate((uid, battery)); // Cooldown is over, apply the new recharge rate.
}
// Raise events when the battery is full or empty so that other systems can react and visuals can get updated.
// This is not doing that many calculations, it only has to get the current charge and only raises events if something did change.
// If this turns out to be too expensive and shows up on grafana consider updating it less often.
var batteryQuery = EntityQueryEnumerator<PredictedBatteryComponent>();
while (batteryQuery.MoveNext(out var uid, out var battery))
{
if (battery.ChargeRate == 0f)
continue; // No need to check if it's constant.
UpdateState((uid, battery));
}
}
private void OnRechargerStartup(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentStartup args)
{
// In case this component is added after the battery component.
RefreshChargeRate(ent.Owner);
}
private void OnRechargerRemove(Entity<PredictedBatterySelfRechargerComponent> ent, ref ComponentRemove args)
{
// We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
RefreshChargeRate(ent.Owner);
}
private void OnVisualsChargeChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
// Update the appearance data for the charge rate.
// We have a separate component for this to not duplicate the networking cost unless we actually use it.
var state = BatteryChargingState.Constant;
if (args.CurrentChargeRate > 0f)
state = BatteryChargingState.Charging;
else if (args.CurrentChargeRate < 0f)
state = BatteryChargingState.Decharging;
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, state);
}
private void OnVisualsStateChanged(Entity<PredictedBatteryVisualsComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
// Update the appearance data for the fill level (empty, full, in-between).
// We have a separate component for this to not duplicate the networking cost unless we actually use it.
_appearance.SetData(ent.Owner, BatteryVisuals.State, args.NewState);
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Emp;
using Content.Shared.Power.Components;
using JetBrains.Annotations;
namespace Content.Shared.Power.EntitySystems;
@@ -21,19 +22,23 @@ public abstract class SharedBatterySystem : EntitySystem
}
/// <summary>
/// Changes the battery's charge by the given amount.
/// Changes the battery's charge by the given amount
/// and resets the self-recharge cooldown if it exists.
/// A positive value will add charge, a negative value will remove charge.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public virtual float ChangeCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
}
/// <summary>
/// Removes the given amount of charge from the battery.
/// Removes the given amount of charge from the battery
/// and resets the self-recharge cooldown if it exists.
/// </summary>
/// <returns>The actually changed amount.</returns>
[PublicAPI]
public virtual float UseCharge(Entity<BatteryComponent?> ent, float amount)
{
return 0f;
@@ -41,9 +46,11 @@ public abstract class SharedBatterySystem : EntitySystem
/// <summary>
/// If sufficient charge is available on the battery, use it. Otherwise, don't.
/// Resets the self-recharge cooldown if it exists.
/// Always returns false on the client.
/// </summary>
/// <returns>If the full amount was able to be removed.</returns>
[PublicAPI]
public virtual bool TryUseCharge(Entity<BatteryComponent?> ent, float amount)
{
return false;
@@ -52,21 +59,25 @@ public abstract class SharedBatterySystem : EntitySystem
/// <summary>
/// Sets the battery's charge.
/// </summary>
[PublicAPI]
public virtual void SetCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Sets the battery's maximum charge.
/// </summary>
[PublicAPI]
public virtual void SetMaxCharge(Entity<BatteryComponent?> ent, float value) { }
/// <summary>
/// Checks if the entity has a self recharge and puts it on cooldown if applicable.
/// Uses the cooldown time given in the component.
/// </summary>
[PublicAPI]
public virtual void TrySetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent) { }
/// <summary>
/// Puts the entity's self recharge on cooldown for the specified time.
/// </summary>
[PublicAPI]
public virtual void SetChargeCooldown(Entity<BatterySelfRechargerComponent?> ent, TimeSpan cooldown) { }
}

View File

@@ -1,20 +0,0 @@
using Content.Shared.Emp;
using Content.Shared.Power.Components;
namespace Content.Shared.Power.EntitySystems;
public abstract class SharedChargerSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ChargerComponent, EmpPulseEvent>(OnEmpPulse);
}
private void OnEmpPulse(EntityUid uid, ChargerComponent component, ref EmpPulseEvent args)
{
args.Affected = true;
args.Disabled = true;
}
}

View File

@@ -92,8 +92,19 @@ public abstract class SharedPowerReceiverSystem : EntitySystem
// NOOP on server because client has 0 idea of load so we can't raise it properly in shared.
}
/// <summary>
/// Checks if entity is APC-powered device, and if it have power.
/// <summary>
/// Sets the power load of this power receiver.
/// </summary>
public void SetLoad(Entity<SharedApcPowerReceiverComponent?> entity, float load)
{
if (!ResolveApc(entity.Owner, ref entity.Comp))
return;
entity.Comp.Load = load;
}
/// <summary>
/// Checks if entity is APC-powered device, and if it have power.
/// </summary>
public bool IsPowered(Entity<SharedApcPowerReceiverComponent?> entity)
{

View File

@@ -1,20 +0,0 @@
using Robust.Shared.Serialization;
namespace Content.Shared.Power
{
[Serializable, NetSerializable]
public enum CellChargerStatus
{
Off,
Empty,
Charging,
Charged,
}
[Serializable, NetSerializable]
public enum CellVisual
{
Occupied, // If there's an item in it
Light,
}
}

View File

@@ -1,26 +1,11 @@
using Content.Shared.Power.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.PowerCell;
namespace Content.Shared.PowerCell.Components;
/// <summary>
/// This component enables power-cell related interactions (e.g., entity white-lists, cell sizes, examine, rigging).
/// The actual power functionality is provided by the server-side BatteryComponent.
/// This component enables power-cell related interactions (e.g. EntityWhitelists, cell sizes, examine, rigging).
/// The actual power functionality is provided by the <see cref="PredictedBatteryComponent"/>.
/// </summary>
[NetworkedComponent]
[RegisterComponent]
public sealed partial class PowerCellComponent : Component
{
public const int PowerCellVisualsLevels = 2;
}
[Serializable, NetSerializable]
public enum PowerCellVisuals : byte
{
ChargeLevel
}
[Serializable, NetSerializable]
public enum PowerCellSlotVisuals : byte
{
Enabled
}
[RegisterComponent, NetworkedComponent]
public sealed partial class PowerCellComponent : Component;

View File

@@ -0,0 +1,35 @@
using Robust.Shared.GameStates;
namespace Content.Shared.PowerCell.Components;
/// <summary>
/// Indicates that the entity's ActivatableUI requires power or else it closes.
/// </summary>
/// <remarks>
/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[Access(typeof(PowerCellSystem))]
public sealed partial class PowerCellDrawComponent : Component
{
/// <summary>
/// Whether drawing is enabled.
/// Having no cell will still disable it.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public bool Enabled = true;
/// <summary>
/// How much the entity draws while the UI is open (in Watts).
/// Set to 0 if you just wish to check for power upon opening the UI.
/// </summary>
[DataField, AutoNetworkedField, ViewVariables]
public float DrawRate = 1f;
/// <summary>
/// How much power is used whenever the entity is "used" (in Joules).
/// This is used to ensure the UI won't open again without a minimum use power.
/// </summary>
[DataField, AutoNetworkedField]
public float UseCharge;
}

View File

@@ -1,8 +1,9 @@
using Content.Shared.Containers.ItemSlots;
using Robust.Shared.GameStates;
namespace Content.Shared.PowerCell.Components;
[RegisterComponent]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class PowerCellSlotComponent : Component
{
/// <summary>
@@ -10,29 +11,17 @@ public sealed partial class PowerCellSlotComponent : Component
/// </summary>
/// <remarks>
/// Given that <see cref="PowerCellSystem"/> needs to verify that a given cell has the correct cell-size before
/// inserting anyways, there is no need to specify a separate entity whitelist. In this slot's yaml definition.
/// inserting anyways, there is no need to specify a separate entity whitelist in this slot's yaml definition.
/// </remarks>
[DataField("cellSlotId", required: true)]
[DataField(required: true)]
public string CellSlotId = string.Empty;
/// <summary>
/// Can this entity be inserted directly into a charging station? If false, you need to manually remove the power
/// cell and recharge it separately.
/// </summary>
[DataField("fitsInCharger")]
[DataField, AutoNetworkedField]
public bool FitsInCharger = true;
}
/// <summary>
/// Raised directed at an entity with a power cell slot when the power cell inside has its charge updated or is ejected/inserted.
/// </summary>
public sealed class PowerCellChangedEvent : EntityEventArgs
{
public readonly bool Ejected;
public PowerCellChangedEvent(bool ejected)
{
Ejected = ejected;
}
}

View File

@@ -1,68 +0,0 @@
using Content.Shared.Item.ItemToggle.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.PowerCell;
/// <summary>
/// Indicates that the entity's ActivatableUI requires power or else it closes.
/// </summary>
/// <remarks>
/// With ActivatableUI it will activate and deactivate when the ui is opened and closed, drawing power inbetween.
/// </remarks>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class PowerCellDrawComponent : Component
{
#region Prediction
/// <summary>
/// Whether there is any charge available to draw.
/// </summary>
[DataField, AutoNetworkedField]
public bool CanDraw;
/// <summary>
/// Whether there is sufficient charge to use.
/// </summary>
[DataField, AutoNetworkedField]
public bool CanUse;
#endregion
/// <summary>
/// Whether drawing is enabled.
/// Having no cell will still disable it.
/// </summary>
[DataField, AutoNetworkedField]
public bool Enabled = true;
/// <summary>
/// How much the entity draws while the UI is open (in Watts).
/// Set to 0 if you just wish to check for power upon opening the UI.
/// </summary>
[DataField]
public float DrawRate = 1f;
/// <summary>
/// How much power is used whenever the entity is "used" (in Joules).
/// This is used to ensure the UI won't open again without a minimum use power.
/// </summary>
/// <remarks>
/// This is not a rate how the datafield name implies, but a one-time cost.
/// </remarks>
[DataField]
public float UseRate;
/// <summary>
/// When the next automatic power draw will occur
/// </summary>
[DataField("nextUpdate", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextUpdateTime;
/// <summary>
/// How long to wait between power drawing.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1);
}

View File

@@ -5,3 +5,9 @@ namespace Content.Shared.PowerCell;
/// </summary>
[ByRefEvent]
public readonly record struct PowerCellSlotEmptyEvent;
/// <summary>
/// Raised directed at an entity with a power cell slot when a power cell is ejected/inserted.
/// </summary>
[ByRefEvent]
public record struct PowerCellChangedEvent(bool Ejected);

View File

@@ -0,0 +1,145 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Power.Components;
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
/// <summary>
/// Gets the power cell battery inside a power cell slot.
/// </summary>
[PublicAPI]
public bool TryGetBatteryFromSlot(
Entity<PowerCellSlotComponent?> ent,
[NotNullWhen(true)] out Entity<PredictedBatteryComponent>? battery)
{
if (!Resolve(ent, ref ent.Comp, false))
{
battery = null;
return false;
}
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out ItemSlot? slot))
{
battery = null;
return false;
}
if (!TryComp<PredictedBatteryComponent>(slot.Item, out var batteryComp))
{
battery = null;
return false;
}
battery = (slot.Item.Value, batteryComp);
return true;
}
/// <summary>
/// Returns whether the entity has a slotted battery and charge for the requested action.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
return false;
}
if (_battery.GetCharge(battery.Value.AsNullable()) < charge)
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
return false;
}
return true;
}
/// <summary>
/// Tries to use charge from a slotted battery.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="charge">The charge that is needed.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="predicted">Whether to predict the popup or not.</param>
[PublicAPI]
public bool TryUseCharge(Entity<PowerCellSlotComponent?> ent, float charge, EntityUid? user = null, bool predicted = false)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-no-battery"), ent.Owner, user.Value);
return false;
}
if (!_battery.TryUseCharge((battery.Value, battery), charge))
{
if (user == null)
return false;
if (predicted)
_popup.PopupClient(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
else
_popup.PopupEntity(Loc.GetString("power-cell-insufficient"), ent.Owner, user.Value);
return false;
}
return true;
}
/// <summary>
/// Gets number of remaining uses for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetRemainingUses(Entity<PowerCellSlotComponent?> ent, float cost)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
return 0;
return _battery.GetRemainingUses(battery.Value.AsNullable(), cost);
}
/// <summary>
/// Gets number of maximum uses at full charge for the given charge cost.
/// </summary>
/// <param name="ent">The power cell.</param>
/// <param name="cost">The cost per use.</param>
[PublicAPI]
public int GetMaxUses(Entity<PowerCellSlotComponent?> ent, float cost)
{
if (!TryGetBatteryFromSlot(ent, out var battery))
return 0;
return _battery.GetMaxUses(battery.Value.AsNullable(), cost);
}
}

View File

@@ -0,0 +1,76 @@
using Content.Shared.PowerCell.Components;
using JetBrains.Annotations;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
/// <summary>
/// Enables or disables the power cell draw.
/// </summary>
[PublicAPI]
public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
{
if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
return;
ent.Comp.Enabled = enabled;
Dirty(ent, ent.Comp);
if (TryGetBatteryFromSlot(ent.Owner, out var battery))
_battery.RefreshChargeRate(battery.Value.AsNullable());
}
/// <summary>
/// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseCharge"/> charge.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
return HasCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted);
}
/// <summary>
/// Tries to use the <see cref="PowerCellDrawComponent.UseCharge"/> for this entity.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool TryUseActivatableCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
if (TryUseCharge((ent, ent.Comp2), ent.Comp1.UseCharge, user, predicted))
return true;
return false;
}
/// <summary>
/// Whether the power cell has any power at all for the draw rate.
/// </summary>
/// <param name="ent">The device with the power cell slot.</param>
/// <param name="user">Show a popup to this user with the relevant details if specified.</param>
/// <param name="user">Whether to predict the popup or not.</param>
[PublicAPI]
public bool HasDrawCharge(Entity<PowerCellDrawComponent?, PowerCellSlotComponent?> ent, EntityUid? user = null, bool predicted = false)
{
// Default to true if we don't have the components.
if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2, false))
return true;
// 1 second of charge at the required draw rate.
return HasCharge((ent, ent.Comp2), ent.Comp1.DrawRate, user, predicted);
}
}

View File

@@ -0,0 +1,40 @@
using Content.Shared.Emp;
using Content.Shared.Kitchen;
using Content.Shared.Power;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rejuvenate;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem
{
public void InitializeRelay()
{
SubscribeLocalEvent<PowerCellSlotComponent, BeingMicrowavedEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, GetChargeEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellSlotComponent, ChangeChargeEvent>(RelayToCell);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(RelayToCellSlot); // Prevent the ninja from EMPing its own battery
SubscribeLocalEvent<PowerCellComponent, PredictedBatteryChargeChangedEvent>(RelayToCellSlot);
SubscribeLocalEvent<PowerCellComponent, PredictedBatteryStateChangedEvent>(RelayToCellSlot); // For shutting down devices if the battery is empty
SubscribeLocalEvent<PowerCellComponent, RefreshChargeRateEvent>(RelayToCellSlot); // Allow devices to charge/drain inserted batteries
}
private void RelayToCell<T>(Entity<PowerCellSlotComponent> ent, ref T args) where T : notnull
{
if (!_itemSlots.TryGetSlot(ent.Owner, ent.Comp.CellSlotId, out var slot) || !slot.Item.HasValue)
return;
// Relay the event to the power cell.
RaiseLocalEvent(slot.Item.Value, ref args);
}
private void RelayToCellSlot<T>(Entity<PowerCellComponent> ent, ref T args) where T : notnull
{
var parent = Transform(ent).ParentUid;
// Relay the event to the slot entity.
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, ref args);
}
}

View File

@@ -0,0 +1,154 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.PowerCell.Components;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.PowerCell;
public sealed partial class PowerCellSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly PredictedBatterySystem _battery = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
public override void Initialize()
{
base.Initialize();
InitializeRelay();
SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellSlotInsertAttempt);
SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellSlotInserted);
SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellSlotRemoved);
SubscribeLocalEvent<PowerCellSlotComponent, ExaminedEvent>(OnCellSlotExamined);
SubscribeLocalEvent<PowerCellSlotComponent, PredictedBatteryStateChangedEvent>(OnCellSlotStateChanged);
SubscribeLocalEvent<PowerCellComponent, ExaminedEvent>(OnCellExamined);
SubscribeLocalEvent<PowerCellDrawComponent, RefreshChargeRateEvent>(OnDrawRefreshChargeRate);
SubscribeLocalEvent<PowerCellDrawComponent, ComponentStartup>(OnDrawStartup);
SubscribeLocalEvent<PowerCellDrawComponent, ComponentRemove>(OnDrawRemove);
}
private void OnCellSlotInsertAttempt(Entity<PowerCellSlotComponent> ent, ref ContainerIsInsertingAttemptEvent args)
{
if (!ent.Comp.Initialized)
return;
if (args.Container.ID != ent.Comp.CellSlotId)
return;
// TODO: Can't this just use the ItemSlot's whitelist?
if (!HasComp<PowerCellComponent>(args.EntityUid))
args.Cancel();
}
private void OnCellSlotInserted(Entity<PowerCellSlotComponent> ent, ref EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ent.Comp.CellSlotId)
return;
if (_timing.ApplyingState)
return; // The change in appearance data is already networked separately.
var ev = new PowerCellChangedEvent(false);
RaiseLocalEvent(ent, ref ev);
_battery.RefreshChargeRate(args.Entity);
// Only update the visuals if we actually use them.
if (!HasComp<PredictedBatteryVisualsComponent>(ent))
return;
// Set the data to that of the power cell
if (_appearance.TryGetData(args.Entity, BatteryVisuals.State, out BatteryState state))
_appearance.SetData(ent.Owner, BatteryVisuals.State, state);
// Set the data to that of the power cell
if (_appearance.TryGetData(args.Entity, BatteryVisuals.Charging, out BatteryChargingState charging))
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, charging);
}
private void OnCellSlotRemoved(Entity<PowerCellSlotComponent> ent, ref EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ent.Comp.CellSlotId)
return;
if (_timing.ApplyingState)
return; // The change in appearance data is already networked separately.
var ev = new PowerCellChangedEvent(true);
RaiseLocalEvent(ent, ref ev);
var emptyEv = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref emptyEv);
_battery.RefreshChargeRate(args.Entity);
// Only update the visuals if we actually use them.
if (!HasComp<PredictedBatteryVisualsComponent>(ent))
return;
// Set the appearance to empty.
_appearance.SetData(ent.Owner, BatteryVisuals.State, BatteryState.Empty);
_appearance.SetData(ent.Owner, BatteryVisuals.Charging, BatteryChargingState.Constant);
}
private void OnCellSlotStateChanged(Entity<PowerCellSlotComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
if (args.NewState != BatteryState.Empty)
return;
// Inform the device that the battery is empty.
var ev = new PowerCellSlotEmptyEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnCellSlotExamined(Entity<PowerCellSlotComponent> ent, ref ExaminedEvent args)
{
if (TryGetBatteryFromSlot(ent.AsNullable(), out var battery))
OnBatteryExamined(battery.Value, ref args);
else
args.PushMarkup(Loc.GetString("power-cell-component-examine-details-no-battery"));
}
private void OnCellExamined(Entity<PowerCellComponent> ent, ref ExaminedEvent args)
{
if (TryComp<PredictedBatteryComponent>(ent, out var battery))
OnBatteryExamined((ent.Owner, battery), ref args);
}
private void OnBatteryExamined(Entity<PredictedBatteryComponent> ent, ref ExaminedEvent args)
{
var charge = _battery.GetCharge(ent.AsNullable()) / ent.Comp.MaxCharge * 100;
args.PushMarkup(Loc.GetString("power-cell-component-examine-details", ("currentCharge", $"{charge:F0}")));
}
private void OnDrawRefreshChargeRate(Entity<PowerCellDrawComponent> ent, ref RefreshChargeRateEvent args)
{
if (ent.Comp.Enabled)
args.NewChargeRate -= ent.Comp.DrawRate;
}
private void OnDrawStartup(Entity<PowerCellDrawComponent> ent, ref ComponentStartup args)
{
if (ent.Comp.Enabled)
_battery.RefreshChargeRate(ent.Owner);
}
private void OnDrawRemove(Entity<PowerCellDrawComponent> ent, ref ComponentRemove args)
{
// We use ComponentRemove to make sure this component no longer subscribes to the refresh event.
if (ent.Comp.Enabled)
_battery.RefreshChargeRate(ent.Owner);
}
}

View File

@@ -1,118 +0,0 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Emp;
using Content.Shared.PowerCell.Components;
using Content.Shared.Rejuvenate;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
namespace Content.Shared.PowerCell;
public abstract class SharedPowerCellSystem : EntitySystem
{
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlots = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PowerCellDrawComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PowerCellSlotComponent, RejuvenateEvent>(OnRejuvenate);
SubscribeLocalEvent<PowerCellSlotComponent, EntInsertedIntoContainerMessage>(OnCellInserted);
SubscribeLocalEvent<PowerCellSlotComponent, EntRemovedFromContainerMessage>(OnCellRemoved);
SubscribeLocalEvent<PowerCellSlotComponent, ContainerIsInsertingAttemptEvent>(OnCellInsertAttempt);
SubscribeLocalEvent<PowerCellComponent, EmpAttemptEvent>(OnCellEmpAttempt);
}
private void OnMapInit(Entity<PowerCellDrawComponent> ent, ref MapInitEvent args)
{
ent.Comp.NextUpdateTime = Timing.CurTime + ent.Comp.Delay;
}
private void OnRejuvenate(EntityUid uid, PowerCellSlotComponent component, RejuvenateEvent args)
{
if (!_itemSlots.TryGetSlot(uid, component.CellSlotId, out var itemSlot) || !itemSlot.Item.HasValue)
return;
// charge entity batteries and remove booby traps.
RaiseLocalEvent(itemSlot.Item.Value, args);
}
private void OnCellInsertAttempt(EntityUid uid, PowerCellSlotComponent component, ContainerIsInsertingAttemptEvent args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.CellSlotId)
return;
if (!HasComp<PowerCellComponent>(args.EntityUid))
{
args.Cancel();
}
}
private void OnCellInserted(EntityUid uid, PowerCellSlotComponent component, EntInsertedIntoContainerMessage args)
{
if (!component.Initialized)
return;
if (args.Container.ID != component.CellSlotId)
return;
_appearance.SetData(uid, PowerCellSlotVisuals.Enabled, true);
RaiseLocalEvent(uid, new PowerCellChangedEvent(false), false);
}
protected virtual void OnCellRemoved(EntityUid uid, PowerCellSlotComponent component, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != component.CellSlotId)
return;
_appearance.SetData(uid, PowerCellSlotVisuals.Enabled, false);
RaiseLocalEvent(uid, new PowerCellChangedEvent(true), false);
}
private void OnCellEmpAttempt(Entity<PowerCellComponent> entity, ref EmpAttemptEvent args)
{
var parent = Transform(entity).ParentUid;
// relay the attempt event to the slot so it can cancel it
if (HasComp<PowerCellSlotComponent>(parent))
RaiseLocalEvent(parent, ref args);
}
public void SetDrawEnabled(Entity<PowerCellDrawComponent?> ent, bool enabled)
{
if (!Resolve(ent, ref ent.Comp, false) || ent.Comp.Enabled == enabled)
return;
if (enabled)
ent.Comp.NextUpdateTime = Timing.CurTime;
ent.Comp.Enabled = enabled;
Dirty(ent, ent.Comp);
}
/// <summary>
/// Returns whether the entity has a slotted battery and <see cref="PowerCellDrawComponent.UseRate"/> charge.
/// </summary>
/// <param name="uid"></param>
/// <param name="battery"></param>
/// <param name="cell"></param>
/// <param name="user">Popup to this user with the relevant detail if specified.</param>
public abstract bool HasActivatableCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null);
/// <summary>
/// Whether the power cell has any power at all for the draw rate.
/// </summary>
public abstract bool HasDrawCharge(
EntityUid uid,
PowerCellDrawComponent? battery = null,
PowerCellSlotComponent? cell = null,
EntityUid? user = null);
}

View File

@@ -10,7 +10,7 @@ namespace Content.Shared.PowerCell;
public sealed class ToggleCellDrawSystem : EntitySystem
{
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedPowerCellSystem _cell = default!;
[Dependency] private readonly PowerCellSystem _cell = default!;
public override void Initialize()
{
@@ -29,8 +29,8 @@ public sealed class ToggleCellDrawSystem : EntitySystem
private void OnActivateAttempt(Entity<ToggleCellDrawComponent> ent, ref ItemToggleActivateAttemptEvent args)
{
if (!_cell.HasDrawCharge(ent, user: args.User)
|| !_cell.HasActivatableCharge(ent, user: args.User))
if (!_cell.HasDrawCharge(ent.Owner, user: args.User, predicted: true)
|| !_cell.HasActivatableCharge(ent.Owner, user: args.User, predicted: true))
args.Cancelled = true;
}

View File

@@ -4,6 +4,7 @@ using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Silicons.Borgs.Components;
@@ -12,7 +13,8 @@ namespace Content.Shared.Silicons.Borgs.Components;
/// "brain", legs, modules, and battery. Essentially the master component
/// for borg logic.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem)), AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[AutoGenerateComponentState, AutoGenerateComponentPause]
public sealed partial class BorgChassisComponent : Component
{
#region Brain
@@ -79,6 +81,14 @@ public sealed partial class BorgChassisComponent : Component
[DataField]
public ProtoId<AlertPrototype> NoBatteryAlert = "BorgBatteryNone";
/// <summary>
/// The next update time for the battery charge level.
/// Used for the alert and borg UI.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
public TimeSpan NextBatteryUpdate = TimeSpan.Zero;
/// <summary>
/// If the entity can open own UI.
/// </summary>

View File

@@ -158,13 +158,13 @@ public sealed class EntityStorageComponentState : ComponentState
/// Raised on the entity being inserted whenever checking if an entity can be inserted into an entity storage.
/// </summary>
[ByRefEvent]
public record struct InsertIntoEntityStorageAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
public record struct InsertIntoEntityStorageAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary>
/// Raised on the entity storage whenever checking if an entity can be inserted into it.
/// </summary>
[ByRefEvent]
public record struct EntityStorageInsertedIntoAttemptEvent(EntityUid ItemToInsert, bool Cancelled = false);
public record struct EntityStorageInsertedIntoAttemptEvent(BaseContainer Container, EntityUid ItemToInsert, bool Cancelled = false);
/// <summary>
/// Raised on the Container's owner whenever an entity storage tries to dump its

View File

@@ -341,14 +341,14 @@ public abstract class SharedEntityStorageSystem : EntitySystem
return false;
// Allow other systems to prevent inserting the item: e.g. the item is actually a ghost.
var attemptEvent = new InsertIntoEntityStorageAttemptEvent(toInsert);
var attemptEvent = new InsertIntoEntityStorageAttemptEvent(component.Contents, toInsert);
RaiseLocalEvent(toInsert, ref attemptEvent);
if (attemptEvent.Cancelled)
return false;
// Allow other components on the container to prevent inserting the item: e.g. the container is folded
var containerAttemptEvent = new EntityStorageInsertedIntoAttemptEvent(toInsert);
var containerAttemptEvent = new EntityStorageInsertedIntoAttemptEvent(component.Contents, toInsert);
RaiseLocalEvent(container, ref containerAttemptEvent);
if (containerAttemptEvent.Cancelled)

View File

@@ -1,13 +1,10 @@
using Content.Shared.PowerCell;
using Content.Shared.PowerCell.Components;
using Robust.Shared.GameStates;
namespace Content.Shared.UserInterface;
/// <summary>
/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power.
/// Specifies that the attached entity requires <see cref="PowerCellDrawComponent"/> power to open the activatable UI.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActivatableUIRequiresPowerCellComponent : Component
{
}
public sealed partial class ActivatableUIRequiresPowerCellComponent : Component;

View File

@@ -1,27 +1,29 @@
using Content.Shared.Item.ItemToggle;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.PowerCell;
using Robust.Shared.Containers;
using Content.Shared.Power;
using Content.Shared.Power.Components;
namespace Content.Shared.UserInterface;
public sealed partial class ActivatableUISystem
{
[Dependency] private readonly ItemToggleSystem _toggle = default!;
[Dependency] private readonly SharedPowerCellSystem _cell = default!;
[Dependency] private readonly PowerCellSystem _cell = default!;
private void InitializePower()
{
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIOpenedEvent>(OnBatteryOpened);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, BoundUIClosedEvent>(OnBatteryClosed);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ItemToggledEvent>(OnToggled);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, PredictedBatteryStateChangedEvent>(OnBatteryStateChanged);
SubscribeLocalEvent<ActivatableUIRequiresPowerCellComponent, ActivatableUIOpenAttemptEvent>(OnBatteryOpenAttempt);
}
private void OnToggled(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref ItemToggledEvent args)
{
// only close ui when losing power
if (!TryComp<ActivatableUIComponent>(ent, out var activatable) || args.Activated)
if (args.Activated || !TryComp<ActivatableUIComponent>(ent, out var activatable))
return;
if (activatable.Key == null)
@@ -55,35 +57,25 @@ public sealed partial class ActivatableUISystem
_toggle.TryDeactivate(uid);
}
/// <summary>
/// Call if you want to check if the UI should close due to a recent battery usage.
/// </summary>
public void CheckUsage(EntityUid uid, ActivatableUIComponent? active = null, ActivatableUIRequiresPowerCellComponent? component = null, PowerCellDrawComponent? draw = null)
private void OnBatteryStateChanged(Entity<ActivatableUIRequiresPowerCellComponent> ent, ref PredictedBatteryStateChangedEvent args)
{
if (!Resolve(uid, ref component, ref draw, ref active, false))
// Deactivate when empty.
if (args.NewState != BatteryState.Empty)
return;
if (active.Key == null)
{
Log.Error($"Encountered null key in activatable ui on entity {ToPrettyString(uid)}");
return;
}
if (_cell.HasActivatableCharge(uid))
return;
_uiSystem.CloseUi(uid, active.Key);
var activatable = Comp<ActivatableUIComponent>(ent);
if (activatable.Key != null)
_uiSystem.CloseUi(ent.Owner, activatable.Key);
}
private void OnBatteryOpenAttempt(EntityUid uid, ActivatableUIRequiresPowerCellComponent component, ActivatableUIOpenAttemptEvent args)
{
if (!TryComp<PowerCellDrawComponent>(uid, out var draw))
if (args.Cancelled)
return;
// Check if we have the appropriate drawrate / userate to even open it.
if (args.Cancelled ||
!_cell.HasActivatableCharge(uid, draw, user: args.User) ||
!_cell.HasDrawCharge(uid, draw, user: args.User))
if (!_cell.HasActivatableCharge(uid, user: args.User, predicted: true) ||
!_cell.HasDrawCharge(uid, user: args.User, predicted: true))
{
args.Cancel();
}

View File

@@ -1,18 +1,70 @@
using Content.Shared.Power.Components;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components;
public abstract partial class BatteryAmmoProviderComponent : AmmoProviderComponent
/// <summary>
/// Ammo provider that uses electric charge from a battery to provide ammunition to a weapon.
/// This works with both <see cref="BatteryComponent"/> and <see cref="PredictedBatteryComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause]
public sealed partial class BatteryAmmoProviderComponent : AmmoProviderComponent
{
/// <summary>
/// How much battery it costs to fire once.
/// The projectile or hitscan entity to spawn when firing.
/// </summary>
[DataField("fireCost"), ViewVariables(VVAccess.ReadWrite)]
[DataField("proto", required: true)]
public EntProtoId Prototype;
/// <summary>
/// How much charge it costs to fire once, in watts.
/// </summary>
[DataField, AutoNetworkedField]
public float FireCost = 100;
// Batteries aren't predicted which means we need to track the battery and manually count it ourselves woo!
/// <summary>
/// Timestamp for the next update for the shot counter and visuals.
/// This is the expected time at which the next integer will be reached.
/// Null if the charge rate is 0, meaning the shot amount is constant.
/// Only used for predicted batteries.
/// </summary>
/// <remarks>
/// Not a datafield since this is refreshed along with the battery's charge rate anyways.
/// </remarks>
[ViewVariables, AutoNetworkedField, AutoPausedField]
public TimeSpan? NextUpdate;
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// The time between reaching full charges at the current charge rate.
/// Only used for predicted batteries.
/// </summary>
/// <remarks>
/// Not a datafield since this is refreshed along with the battery's charge rate anyways.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public TimeSpan ChargeTime = TimeSpan.Zero;
/// <summary>
/// The current amount of available shots.
/// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
/// </summary>
/// <remarks>
/// Not a datafield since this is only cached and refreshed on component startup.
/// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public int Shots;
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// The maximum amount of available shots.
/// BatteryComponent is not predicted, so we need to manually network this for the ammo indicator and examination text.
/// </summary>
/// <remarks>
/// Not a datafield since this is only cached and refreshed on component startup.
/// TODO: If we ever fully predict all batteries then remove this and just read the charge on the client.
/// </remarks>
[ViewVariables, AutoNetworkedField]
public int Capacity;
}

View File

@@ -1,11 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class HitscanBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
{
[DataField("proto", required: true)]
public EntProtoId HitscanEntityProto;
}

View File

@@ -1,12 +0,0 @@
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Shared.Weapons.Ranged.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class ProjectileBatteryAmmoProviderComponent : BatteryAmmoProviderComponent
{
[ViewVariables(VVAccess.ReadWrite), DataField("proto", required: true, customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Prototype = default!;
}

View File

@@ -1,4 +0,0 @@
namespace Content.Shared.Weapons.Ranged.Events;
[ByRefEvent]
public readonly record struct UpdateClientAmmoEvent();

View File

@@ -17,6 +17,7 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly AccessReaderSystem _accessReaderSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedGunSystem _gun = default!;
public override void Initialize()
{
@@ -126,21 +127,14 @@ public sealed class BatteryWeaponFireModesSystem : EntitySystem
_popupSystem.PopupClient(Loc.GetString("gun-set-fire-mode", ("mode", prototype.Name)), uid, user.Value);
}
if (TryComp(uid, out ProjectileBatteryAmmoProviderComponent? projectileBatteryAmmoProviderComponent))
if (TryComp(uid, out BatteryAmmoProviderComponent? batteryAmmoProviderComponent))
{
// TODO: Have this get the info directly from the batteryComponent when power is moved to shared.
var OldFireCost = projectileBatteryAmmoProviderComponent.FireCost;
projectileBatteryAmmoProviderComponent.Prototype = fireMode.Prototype;
projectileBatteryAmmoProviderComponent.FireCost = fireMode.FireCost;
batteryAmmoProviderComponent.Prototype = fireMode.Prototype;
batteryAmmoProviderComponent.FireCost = fireMode.FireCost;
float FireCostDiff = (float)fireMode.FireCost / (float)OldFireCost;
projectileBatteryAmmoProviderComponent.Shots = (int)Math.Round(projectileBatteryAmmoProviderComponent.Shots / FireCostDiff);
projectileBatteryAmmoProviderComponent.Capacity = (int)Math.Round(projectileBatteryAmmoProviderComponent.Capacity / FireCostDiff);
Dirty(uid, batteryAmmoProviderComponent);
Dirty(uid, projectileBatteryAmmoProviderComponent);
var updateClientAmmoEvent = new UpdateClientAmmoEvent();
RaiseLocalEvent(uid, ref updateClientAmmoEvent);
_gun.UpdateShots((uid, batteryAmmoProviderComponent));
}
}
}

View File

@@ -2,13 +2,13 @@ using Content.Shared.Damage;
using Content.Shared.Damage.Events;
using Content.Shared.Examine;
using Content.Shared.Projectiles;
using Content.Shared.Power;
using Content.Shared.PowerCell;
using Content.Shared.Weapons.Hitscan.Components;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.GameStates;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Weapons.Ranged.Systems;
@@ -16,160 +16,202 @@ public abstract partial class SharedGunSystem
{
protected virtual void InitializeBattery()
{
// Trying to dump comp references hence the below
// Hitscan
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
SubscribeLocalEvent<HitscanBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
// Projectile
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentGetState>(OnBatteryGetState);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ComponentHandleState>(OnBatteryHandleState);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
SubscribeLocalEvent<ProjectileBatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
SubscribeLocalEvent<BatteryAmmoProviderComponent, ComponentStartup>(OnBatteryStartup);
SubscribeLocalEvent<BatteryAmmoProviderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
SubscribeLocalEvent<BatteryAmmoProviderComponent, TakeAmmoEvent>(OnBatteryTakeAmmo);
SubscribeLocalEvent<BatteryAmmoProviderComponent, GetAmmoCountEvent>(OnBatteryAmmoCount);
SubscribeLocalEvent<BatteryAmmoProviderComponent, ExaminedEvent>(OnBatteryExamine);
SubscribeLocalEvent<BatteryAmmoProviderComponent, DamageExamineEvent>(OnBatteryDamageExamine);
SubscribeLocalEvent<BatteryAmmoProviderComponent, PowerCellChangedEvent>(OnPowerCellChanged);
SubscribeLocalEvent<BatteryAmmoProviderComponent, PredictedBatteryChargeChangedEvent>(OnPredictedChargeChanged);
SubscribeLocalEvent<BatteryAmmoProviderComponent, ChargeChangedEvent>(OnChargeChanged);
}
private void OnBatteryHandleState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentHandleState args)
private void OnBatteryExamine(Entity<BatteryAmmoProviderComponent> ent, ref ExaminedEvent args)
{
if (args.Current is not BatteryAmmoProviderComponentState state)
return;
component.Shots = state.Shots;
component.Capacity = state.MaxShots;
component.FireCost = state.FireCost;
UpdateAmmoCount(uid, prediction: false);
args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", ent.Comp.Shots)));
}
private void OnBatteryGetState(EntityUid uid, BatteryAmmoProviderComponent component, ref ComponentGetState args)
private void OnBatteryDamageExamine(Entity<BatteryAmmoProviderComponent> ent, ref DamageExamineEvent args)
{
args.State = new BatteryAmmoProviderComponentState()
var proto = ProtoManager.Index<EntityPrototype>(ent.Comp.Prototype);
DamageSpecifier? damageSpec = null;
var damageType = string.Empty;
if (proto.TryGetComponent<ProjectileComponent>(out var projectileComp, Factory))
{
Shots = component.Shots,
MaxShots = component.Capacity,
FireCost = component.FireCost,
};
}
private void OnBatteryExamine(EntityUid uid, BatteryAmmoProviderComponent component, ExaminedEvent args)
{
args.PushMarkup(Loc.GetString("gun-battery-examine", ("color", AmmoExamineColor), ("count", component.Shots)));
}
private void OnBatteryDamageExamine<T>(Entity<T> entity, ref DamageExamineEvent args) where T : BatteryAmmoProviderComponent
{
var damageSpec = GetDamage(entity.Comp);
if (!projectileComp.Damage.Empty)
{
damageType = Loc.GetString("damage-projectile");
damageSpec = projectileComp.Damage * Damageable.UniversalProjectileDamageModifier;
}
}
else if (proto.TryGetComponent<HitscanBasicDamageComponent>(out var hitscanComp, Factory))
{
if (!hitscanComp.Damage.Empty)
{
damageType = Loc.GetString("damage-hitscan");
damageSpec = hitscanComp.Damage * Damageable.UniversalHitscanDamageModifier;
}
}
if (damageSpec == null)
return;
var damageType = entity.Comp switch
{
HitscanBatteryAmmoProviderComponent => Loc.GetString("damage-hitscan"),
ProjectileBatteryAmmoProviderComponent => Loc.GetString("damage-projectile"),
_ => throw new ArgumentOutOfRangeException(),
};
_damageExamine.AddDamageExamine(args.Message, Damageable.ApplyUniversalAllModifiers(damageSpec), damageType);
}
private DamageSpecifier? GetDamage(BatteryAmmoProviderComponent component)
private void OnBatteryTakeAmmo(Entity<BatteryAmmoProviderComponent> ent, ref TakeAmmoEvent args)
{
if (component is ProjectileBatteryAmmoProviderComponent battery)
{
if (ProtoManager.Index<EntityPrototype>(battery.Prototype).Components
.TryGetValue(Factory.GetComponentName<ProjectileComponent>(), out var projectile))
{
var p = (ProjectileComponent) projectile.Component;
var shots = Math.Min(args.Shots, ent.Comp.Shots);
if (!p.Damage.Empty)
{
return p.Damage * Damageable.UniversalProjectileDamageModifier;
}
}
return null;
}
if (component is HitscanBatteryAmmoProviderComponent hitscan)
{
var dmg = ProtoManager.Index(hitscan.HitscanEntityProto);
if (!dmg.TryGetComponent<HitscanBasicDamageComponent>(out var basicDamageComp, Factory))
return null;
return basicDamageComp.Damage * Damageable.UniversalHitscanDamageModifier;
}
return null;
}
private void OnBatteryTakeAmmo(EntityUid uid, BatteryAmmoProviderComponent component, TakeAmmoEvent args)
{
var shots = Math.Min(args.Shots, component.Shots);
// Don't dirty if it's an empty fire.
if (shots == 0)
return;
for (var i = 0; i < shots; i++)
{
args.Ammo.Add(GetShootable(component, args.Coordinates));
component.Shots--;
args.Ammo.Add(GetShootable(ent, args.Coordinates));
}
TakeCharge((uid, component));
UpdateBatteryAppearance(uid, component);
Dirty(uid, component);
TakeCharge(ent, shots);
}
private void OnBatteryAmmoCount(EntityUid uid, BatteryAmmoProviderComponent component, ref GetAmmoCountEvent args)
private void OnBatteryAmmoCount(Entity<BatteryAmmoProviderComponent> ent, ref GetAmmoCountEvent args)
{
args.Count = component.Shots;
args.Capacity = component.Capacity;
args.Count = ent.Comp.Shots;
args.Capacity = ent.Comp.Capacity;
}
/// <summary>
/// Update the battery (server-only) whenever fired.
/// Use up the required amount of battery charge for firing.
/// </summary>
protected virtual void TakeCharge(Entity<BatteryAmmoProviderComponent> entity)
public void TakeCharge(Entity<BatteryAmmoProviderComponent> ent, int shots = 1)
{
UpdateAmmoCount(entity, prediction: false);
}
protected void UpdateBatteryAppearance(EntityUid uid, BatteryAmmoProviderComponent component)
{
if (!TryComp<AppearanceComponent>(uid, out var appearance))
return;
Appearance.SetData(uid, AmmoVisuals.HasAmmo, component.Shots != 0, appearance);
Appearance.SetData(uid, AmmoVisuals.AmmoCount, component.Shots, appearance);
Appearance.SetData(uid, AmmoVisuals.AmmoMax, component.Capacity, appearance);
// Take charge from either the BatteryComponent, PredictedBatteryComponent or PowerCellSlotComponent.
var ev = new ChangeChargeEvent(-ent.Comp.FireCost * shots);
RaiseLocalEvent(ent, ref ev);
// UpdateShots is already called by the resulting PredictedBatteryChargeChangedEvent or ChargeChangedEvent
}
private (EntityUid? Entity, IShootable) GetShootable(BatteryAmmoProviderComponent component, EntityCoordinates coordinates)
{
switch (component)
{
case ProjectileBatteryAmmoProviderComponent proj:
var ent = Spawn(proj.Prototype, coordinates);
return (ent, EnsureShootable(ent));
case HitscanBatteryAmmoProviderComponent hitscan:
var hitscanEnt = Spawn(hitscan.HitscanEntityProto);
return (hitscanEnt, EnsureShootable(hitscanEnt));
default:
throw new ArgumentOutOfRangeException();
}
var ent = Spawn(component.Prototype, coordinates);
return (ent, EnsureShootable(ent));
}
[Serializable, NetSerializable]
private sealed class BatteryAmmoProviderComponentState : ComponentState
public void UpdateShots(Entity<BatteryAmmoProviderComponent> ent)
{
public int Shots;
public int MaxShots;
public float FireCost;
var oldShots = ent.Comp.Shots;
var oldCapacity = ent.Comp.Capacity;
(var newShots, var newCapacity) = GetShots(ent);
// Only dirty if necessary.
if (oldShots == newShots && oldCapacity == newCapacity)
return;
ent.Comp.Shots = newShots;
if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
ent.Comp.Capacity = newCapacity;
// Update the ammo counter predictively if the change was predicted. On the server this does nothing.
UpdateAmmoCount(ent.Owner);
Dirty(ent); // Dirtying will update the client's ammo counter in an AfterAutoHandleStateEvent subscription in case it was not predicted.
if (!TryComp<AppearanceComponent>(ent, out var appearance))
return;
// Update the visuals.
Appearance.SetData(ent.Owner, AmmoVisuals.HasAmmo, newShots != 0, appearance);
Appearance.SetData(ent.Owner, AmmoVisuals.AmmoCount, newShots, appearance);
if (newCapacity > 0) // Don't make the capacity 0 when removing a power cell as this will make it be visualized as full instead of empty.
Appearance.SetData(ent.Owner, AmmoVisuals.AmmoMax, newCapacity, appearance);
}
// For server side changes the client's ammo counter needs to be updated as well.
private void OnAfterAutoHandleState(Entity<BatteryAmmoProviderComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateAmmoCount(ent); // Need to have prediction set to true because the state is applied repeatedly while prediction is running.
}
// For when a power cell gets inserted or removed.
private void OnPowerCellChanged(Entity<BatteryAmmoProviderComponent> ent, ref PowerCellChangedEvent args)
{
UpdateShots(ent);
}
// For predicted batteries.
// If the entity is has a PowerCellSlotComponent then this event is relayed from the power cell to the slot entity.
private void OnPredictedChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref PredictedBatteryChargeChangedEvent args)
{
// Update the visuals and charge counter UI.
UpdateShots(ent);
// Queue the update for when the autorecharge reaches enough charge for another shot.
UpdateNextUpdate(ent, args.CurrentCharge, args.MaxCharge, args.CurrentChargeRate);
}
// For unpredicted batteries.
private void OnChargeChanged(Entity<BatteryAmmoProviderComponent> ent, ref ChargeChangedEvent args)
{
// Update the visuals and charge counter UI.
UpdateShots(ent);
// No need to queue an update here since unpredicted batteries already update periodically as they charge/discharge.
}
private void UpdateNextUpdate(Entity<BatteryAmmoProviderComponent> ent, float currentCharge, float maxCharge, float currentChargeRate)
{
// Don't queue any updates if charge is constant.
ent.Comp.NextUpdate = null;
// ETA of the next full charge.
if (currentChargeRate > 0f && currentCharge != maxCharge)
{
ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds((ent.Comp.FireCost - (currentCharge % ent.Comp.FireCost)) / currentChargeRate);
ent.Comp.ChargeTime = TimeSpan.FromSeconds(ent.Comp.FireCost / currentChargeRate);
}
else if (currentChargeRate < 0f && currentCharge != 0f)
{
ent.Comp.NextUpdate = Timing.CurTime + TimeSpan.FromSeconds(-(currentCharge % ent.Comp.FireCost) / currentChargeRate);
ent.Comp.ChargeTime = TimeSpan.FromSeconds(-ent.Comp.FireCost / currentChargeRate);
}
Dirty(ent);
}
// Shots are only chached, not a DataField, so we need to refresh this when the game is loaded.
private void OnBatteryStartup(Entity<BatteryAmmoProviderComponent> ent, ref ComponentStartup args)
{
UpdateShots(ent);
}
/// <summary>
/// Gets the current and maximum amount of shots from this entity's battery.
/// This works for BatteryComponent, PredictedBatteryComponent and PowercellSlotComponent.
/// </summary>
public (int, int) GetShots(Entity<BatteryAmmoProviderComponent> ent)
{
var ev = new GetChargeEvent();
RaiseLocalEvent(ent, ref ev);
var currentShots = (int)(ev.CurrentCharge / ent.Comp.FireCost);
var maxShots = (int)(ev.MaxCharge / ent.Comp.FireCost);
return (currentShots, maxShots);
}
/// <summary>
/// Update loop for refreshing the ammo counter for charging/draining predicted batteries.
/// This is not needed for unpredicted batteries since those already raise ChargeChangedEvent periodically.
/// </summary>
private void UpdateBattery(float frameTime)
{
var curTime = Timing.CurTime;
var hitscanQuery = EntityQueryEnumerator<BatteryAmmoProviderComponent>();
while (hitscanQuery.MoveNext(out var uid, out var provider))
{
if (provider.NextUpdate == null || curTime < provider.NextUpdate)
continue;
UpdateShots((uid, provider));
provider.NextUpdate += provider.ChargeTime; // Queue another update for when we reach the next full charge.
Dirty(uid, provider);
// TODO: Stop updating when full or empty.
}
}
}

View File

@@ -649,6 +649,11 @@ public abstract partial class SharedGunSystem : EntitySystem
RaiseLocalEvent(uid, ref ammoEv);
return ammoEv.Capacity;
}
public override void Update(float frameTime)
{
UpdateBattery(frameTime);
}
}
/// <summary>

View File

@@ -2593,9 +2593,6 @@ entities:
- type: Transform
pos: 6.5,-7.5
parent: 1
- type: PowerCellDraw
canUse: True
canDraw: True
- type: Physics
canCollide: False
- type: ContainerContainer

View File

@@ -530,7 +530,7 @@
amount: 6
whitelist:
components:
- HitscanBatteryAmmoProvider
- BatteryAmmoProvider
blacklist:
components:
- PowerCell

View File

@@ -619,7 +619,7 @@
damage: 35
sound: /Audio/Weapons/egloves.ogg
- type: LandAtCursor # it deals stamina damage when thrown
- type: Battery
- type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: GuideHelp

View File

@@ -243,10 +243,10 @@
startValue: 0.1
endValue: 2.0
isLooped: true
- type: Battery
- type: PredictedBattery
maxCharge: 600 #lights drain 3/s but recharge of 2 makes this 1/s. Therefore 600 is 10 minutes of light.
startingCharge: 600
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 2 #recharge of 2 makes total drain 1w / s so max charge is 1:1 with time. Time to fully charge should be 5 minutes. Having recharge gives light an extended flicker period which gives you some warning to return to light area.
- type: entity

View File

@@ -296,10 +296,10 @@
startValue: 0.1
endValue: 2.0
isLooped: true
- type: Battery
- type: PredictedBattery
maxCharge: 600
startingCharge: 600
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 2
- type: Item
size: Normal

View File

@@ -19,13 +19,13 @@
- type: HTN
rootTask:
task: SimpleRangedHostileCompound
- type: HitscanBatteryAmmoProvider
- type: BatteryAmmoProvider
proto: RedLaser
fireCost: 62.5
- type: Battery
- type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 40
- type: Gun
fireRate: 0.75

View File

@@ -50,12 +50,12 @@
- type: MovementSpeedModifier
baseWalkSpeed: 5
baseSprintSpeed: 7
- type: ProjectileBatteryAmmoProvider
- type: BatteryAmmoProvider
proto: WatcherBolt
fireCost: 50
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 50
- type: Battery
- type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: Gun
@@ -125,7 +125,7 @@
radius: 1
energy: 3
color: orangered
- type: ProjectileBatteryAmmoProvider
- type: BatteryAmmoProvider
proto: WatcherBoltMagmawing
fireCost: 50

View File

@@ -168,13 +168,13 @@
damage:
types:
Heat: 5
- type: HitscanBatteryAmmoProvider
- type: BatteryAmmoProvider
proto: RedLaser
fireCost: 140
- type: Battery
- type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 50
- type: Gun
fireRate: 0.3

View File

@@ -47,12 +47,12 @@
- type: Tag
tags:
- FootstepSound
- type: HitscanBatteryAmmoProvider
- type: BatteryAmmoProvider
proto: RedLightLaser
fireCost: 50
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 50
- type: Battery
- type: PredictedBattery
maxCharge: 1000
startingCharge: 1000
- type: Gun

View File

@@ -8,5 +8,5 @@
onUse: false # above component does the toggling
- type: PowerCellDraw
drawRate: 0
useRate: 20
useCharge: 20
- type: ToggleCellDraw

View File

@@ -43,7 +43,7 @@
components:
- type: PowerCellDraw
drawRate: 1
useRate: 0
useCharge: 0
- type: ToggleCellDraw
- type: entity

View File

@@ -8,7 +8,12 @@
size: Huge
- type: Sprite
sprite: Objects/Power/portable_recharger.rsi
state: charging
layers:
- map: ["enum.PowerChargerVisualLayers.Base"]
state: charging
- map: ["enum.PowerChargerVisualLayers.Light"]
state: charging-unlit
shader: unshaded
- type: Clothing
equippedPrefix: charging
quickEquip: false
@@ -18,9 +23,6 @@
slotId: charger_slot
portable: true
- type: PowerChargerVisuals
- type: ApcPowerReceiver
needsPower: false
powerLoad: 0
- type: StaticPrice
price: 500
- type: Tag
@@ -34,5 +36,4 @@
ejectOnInteract: true
whitelist:
components:
- HitscanBatteryAmmoProvider
- ProjectileBatteryAmmoProvider
- BatteryAmmoProvider

View File

@@ -5,7 +5,7 @@
components:
- type: Item
storedRotation: -90
- type: Battery
- type: PredictedBattery
pricePerJoule: 0.15
- type: PowerCell
- type: Explosive
@@ -30,9 +30,16 @@
- type: Tag
tags:
- PowerCell
- type: Appearance
- type: PowerCellVisuals
- type: Riggable
- type: Appearance
- type: PredictedBatteryVisuals
- type: GenericVisualizer
visuals:
enum.BatteryVisuals.State:
enum.PowerCellVisualLayers.Unshaded:
Full: {visible: true, state: o2}
Neither: {visible: true, state: o1}
Empty: {visible: false}
- type: entity
name: potato battery
@@ -43,7 +50,7 @@
- type: Sprite
layers:
- state: potato
- type: Battery
- type: PredictedBattery
maxCharge: 70
startingCharge: 70
- type: Tag
@@ -67,7 +74,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 360
startingCharge: 360
- type: Tag
@@ -87,7 +94,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
maxCharge: 360
startingCharge: 0
@@ -97,7 +104,7 @@
name: small-capacity nuclear power cell
description: A self rechargeable power cell, designed for fast recharge rate at the expense of capacity.
components:
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 36 # 10 seconds to recharge
autoRechargePauseTime: 30
@@ -115,7 +122,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 720
startingCharge: 720
@@ -132,7 +139,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
maxCharge: 720
startingCharge: 0
@@ -150,7 +157,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 1080
startingCharge: 1080
@@ -167,7 +174,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
maxCharge: 1080
startingCharge: 0
@@ -185,7 +192,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 1800
startingCharge: 1800
@@ -202,7 +209,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
maxCharge: 1800
startingCharge: 0
@@ -220,10 +227,10 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 720
startingCharge: 720
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 12 # takes 1 minute to charge itself back to full
- type: entity
@@ -239,7 +246,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
startingCharge: 0
- type: entity
@@ -255,10 +262,10 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 1200
startingCharge: 1200
- type: BatterySelfRecharger
- type: PredictedBatterySelfRecharger
autoRechargeRate: 40
# Power cage (big heavy power cell for big devices)
@@ -302,7 +309,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 1400
startingCharge: 1400
@@ -320,7 +327,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 2700
startingCharge: 2700
@@ -338,7 +345,7 @@
- map: [ "enum.PowerCellVisualLayers.Unshaded" ]
state: o2
shader: unshaded
- type: Battery
- type: PredictedBattery
maxCharge: 6200
startingCharge: 6200
@@ -356,7 +363,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
maxCharge: 1400
startingCharge: 0
@@ -374,7 +381,7 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
startingCharge: 0
- type: entity
@@ -391,5 +398,5 @@
state: o2
shader: unshaded
visible: false
- type: Battery
- type: PredictedBattery
startingCharge: 0

View File

@@ -48,7 +48,7 @@
- type: MultiHandedItem
- type: ToggleCellDraw
- type: PowerCellDraw
useRate: 100
useCharge: 100
- type: entity
id: DefibrillatorEmpty
@@ -86,7 +86,7 @@
size: Normal
- type: ToggleCellDraw
- type: PowerCellDraw
useRate: 100
useCharge: 100
- type: Defibrillator
zapHeal:
types:

Some files were not shown because too many files have changed in this diff Show More