mirror of
https://github.com/wega-team/ss14-wega.git
synced 2026-02-14 19:30:01 +01:00
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:
@@ -1,5 +0,0 @@
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
public sealed class ChargerSystem : SharedChargerSystem;
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
11
Content.Client/PowerCell/PowerCellVisualLayers.cs
Normal file
11
Content.Client/PowerCell/PowerCellVisualLayers.cs
Normal 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,
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class PowerCellVisualsComponent : Component {}
|
||||
@@ -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",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Shared.Power;
|
||||
using Content.Shared.Power.Components;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PowerCell;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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!;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Pinpointer;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class ExaminableBatteryComponent : Component
|
||||
{}
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Content.Server.PowerCell;
|
||||
using Content.Shared.Item.ItemToggle;
|
||||
using Content.Shared.PowerCell;
|
||||
using Content.Shared.Weapons.Misc;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
Content.Shared/Kitchen/BeingMicrowavedEvent.cs
Normal file
10
Content.Shared/Kitchen/BeingMicrowavedEvent.cs
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
10
Content.Shared/Power/Components/InsideChargerComponent.cs
Normal file
10
Content.Shared/Power/Components/InsideChargerComponent.cs
Normal 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;
|
||||
94
Content.Shared/Power/Components/PredictedBatteryComponent.cs
Normal file
94
Content.Shared/Power/Components/PredictedBatteryComponent.cs
Normal 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,
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 > 0
|
||||
/// </summary>
|
||||
Charging,
|
||||
/// <summary>
|
||||
/// PredictedBatteryComponent.ChargeRate < 0
|
||||
/// </summary>
|
||||
Decharging,
|
||||
/// <summary>
|
||||
/// PredictedBatteryComponent.ChargeRate == 0
|
||||
/// </summary>
|
||||
Constant,
|
||||
}
|
||||
270
Content.Shared/Power/EntitySystems/ChargerSystem.cs
Normal file
270
Content.Shared/Power/EntitySystems/ChargerSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
278
Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs
Normal file
278
Content.Shared/Power/EntitySystems/PredictedBatterySystem.API.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
182
Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs
Normal file
182
Content.Shared/Power/EntitySystems/PredictedBatterySystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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) { }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
145
Content.Shared/PowerCell/PowerCellSystem.API.cs
Normal file
145
Content.Shared/PowerCell/PowerCellSystem.API.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
76
Content.Shared/PowerCell/PowerCellSystem.Draw.cs
Normal file
76
Content.Shared/PowerCell/PowerCellSystem.Draw.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
40
Content.Shared/PowerCell/PowerCellSystem.Relay.cs
Normal file
40
Content.Shared/PowerCell/PowerCellSystem.Relay.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
154
Content.Shared/PowerCell/PowerCellSystem.cs
Normal file
154
Content.Shared/PowerCell/PowerCellSystem.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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!;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Content.Shared.Weapons.Ranged.Events;
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly record struct UpdateClientAmmoEvent();
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -530,7 +530,7 @@
|
||||
amount: 6
|
||||
whitelist:
|
||||
components:
|
||||
- HitscanBatteryAmmoProvider
|
||||
- BatteryAmmoProvider
|
||||
blacklist:
|
||||
components:
|
||||
- PowerCell
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
onUse: false # above component does the toggling
|
||||
- type: PowerCellDraw
|
||||
drawRate: 0
|
||||
useRate: 20
|
||||
useCharge: 20
|
||||
- type: ToggleCellDraw
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
components:
|
||||
- type: PowerCellDraw
|
||||
drawRate: 1
|
||||
useRate: 0
|
||||
useCharge: 0
|
||||
- type: ToggleCellDraw
|
||||
|
||||
- type: entity
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user