mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-02-15 04:30:57 +01:00
Merge remote-tracking branch 'upstream/master' into upstream-sync
# Conflicts: # Resources/Prototypes/Recipes/Lathes/medical.yml # Resources/Prototypes/Recipes/Lathes/sheet.yml
This commit is contained in:
@@ -149,7 +149,7 @@ namespace Content.Client.Actions
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action)
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action, bool predicted)
|
||||
{
|
||||
var performedAction = action.Sound != null
|
||||
|| !string.IsNullOrWhiteSpace(action.UserPopup)
|
||||
@@ -233,7 +233,7 @@ namespace Content.Client.Actions
|
||||
if (instantAction.Event != null)
|
||||
instantAction.Event.Performer = user;
|
||||
|
||||
PerformAction(PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
PerformAction(user, PlayerActions, instantAction, instantAction.Event, GameTiming.CurTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -77,10 +77,6 @@ public sealed partial class SensorInfo : BoxContainer
|
||||
{
|
||||
OnThresholdUpdate!(_address, type, threshold, arg3);
|
||||
};
|
||||
|
||||
foreach (var (gas, threshold) in data.GasThresholds)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public void ChangeData(AtmosSensorData data)
|
||||
|
||||
@@ -86,6 +86,7 @@ namespace Content.Client.Audio
|
||||
{
|
||||
base.Initialize();
|
||||
UpdatesOutsidePrediction = true;
|
||||
UpdatesAfter.Add(typeof(AmbientSoundTreeSystem));
|
||||
|
||||
_cfg.OnValueChanged(CCVars.AmbientCooldown, SetCooldown, true);
|
||||
_cfg.OnValueChanged(CCVars.MaxAmbientSources, SetAmbientCount, true);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
using Content.Shared.Construction;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.Construction
|
||||
{
|
||||
[UsedImplicitly]
|
||||
public sealed class MachineFrameVisualizer : AppearanceVisualizer
|
||||
{
|
||||
[Obsolete("Subscribe to AppearanceChangeEvent instead.")]
|
||||
public override void OnChangeData(AppearanceComponent component)
|
||||
{
|
||||
base.OnChangeData(component);
|
||||
|
||||
if (component.TryGetData<int>(MachineFrameVisuals.State, out var data))
|
||||
{
|
||||
var sprite = IoCManager.Resolve<IEntityManager>().GetComponent<ISpriteComponent>(component.Owner);
|
||||
|
||||
sprite.LayerSetState(0, $"box_{data}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ namespace Content.Client.Lathe.UI
|
||||
base.Open();
|
||||
|
||||
_menu = new LatheMenu(this);
|
||||
_queueMenu = new LatheQueueMenu(this);
|
||||
_queueMenu = new LatheQueueMenu();
|
||||
|
||||
_menu.OnClose += Close;
|
||||
|
||||
|
||||
@@ -140,7 +140,9 @@ public sealed partial class LatheMenu : DefaultWindow
|
||||
sb.Append(Loc.GetString(proto.Name));
|
||||
}
|
||||
|
||||
var icon = _spriteSystem.Frame0(prototype.Icon);
|
||||
var icon = prototype.Icon == null
|
||||
? _spriteSystem.GetPrototypeIcon(prototype.Result).Default
|
||||
: _spriteSystem.Frame0(prototype.Icon);
|
||||
var canProduce = _lathe.CanProduce(lathe, prototype, quantity);
|
||||
|
||||
var control = new RecipeControl(prototype, sb.ToString(), canProduce, icon);
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Content.Client.Lathe.UI
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
public LatheQueueMenu(LatheBoundUserInterface owner)
|
||||
public LatheQueueMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
@@ -26,7 +26,9 @@ namespace Content.Client.Lathe.UI
|
||||
{
|
||||
if (recipe != null)
|
||||
{
|
||||
Icon.Texture = _spriteSystem.Frame0(recipe.Icon);
|
||||
Icon.Texture = recipe.Icon == null
|
||||
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
|
||||
: _spriteSystem.Frame0(recipe.Icon);
|
||||
NameLabel.Text = recipe.Name;
|
||||
Description.Text = recipe.Description;
|
||||
}
|
||||
@@ -44,7 +46,10 @@ namespace Content.Client.Lathe.UI
|
||||
var idx = 1;
|
||||
foreach (var recipe in queue)
|
||||
{
|
||||
QueueList.AddItem($"{idx}. {recipe.Name}", _spriteSystem.Frame0(recipe.Icon));
|
||||
var icon =recipe.Icon == null
|
||||
? _spriteSystem.GetPrototypeIcon(recipe.Result).Default
|
||||
: _spriteSystem.Frame0(recipe.Icon);
|
||||
QueueList.AddItem($"{idx}. {recipe.Name}", icon);
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using Content.Client.Examine;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
@@ -6,6 +9,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Popups;
|
||||
@@ -70,6 +74,7 @@ public sealed class PopupOverlay : Overlay
|
||||
return;
|
||||
|
||||
var matrix = args.ViewportControl!.GetWorldToScreenMatrix();
|
||||
var viewPos = new MapCoordinates(args.WorldAABB.Center, args.MapId);
|
||||
|
||||
foreach (var popup in _popup.WorldLabels)
|
||||
{
|
||||
@@ -78,7 +83,11 @@ public sealed class PopupOverlay : Overlay
|
||||
if (mapPos.MapId != args.MapId)
|
||||
continue;
|
||||
|
||||
if (!args.WorldAABB.Contains(mapPos.Position))
|
||||
var distance = (mapPos.Position - args.WorldBounds.Centre).Length;
|
||||
|
||||
// Should handle fade here too wyci.
|
||||
if (!args.WorldAABB.Contains(mapPos.Position) || !ExamineSystemShared.InRangeUnOccluded(viewPos, mapPos, distance,
|
||||
e => e == popup.InitialPos.EntityId, entMan: _entManager))
|
||||
continue;
|
||||
|
||||
var pos = matrix.Transform(mapPos.Position);
|
||||
|
||||
@@ -9,6 +9,7 @@ using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Client.Suspicion
|
||||
{
|
||||
@@ -52,7 +53,7 @@ namespace Content.Client.Suspicion
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_entityManager.TryGetComponent(ally, out IPhysBody? physics))
|
||||
if (!_entityManager.TryGetComponent(ally, out PhysicsComponent? physics))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Client.Actions;
|
||||
using Content.Client.Construction;
|
||||
@@ -224,7 +224,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
_actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime);
|
||||
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
|
||||
}
|
||||
else
|
||||
_entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, coords));
|
||||
@@ -256,7 +256,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
|
||||
action.Event.Performer = user;
|
||||
}
|
||||
|
||||
_actionsSystem.PerformAction(actionComp, action, action.Event, _timing.CurTime);
|
||||
_actionsSystem.PerformAction(user, actionComp, action, action.Event, _timing.CurTime);
|
||||
}
|
||||
else
|
||||
_entities.RaisePredictiveEvent(new RequestPerformActionEvent(action, args.EntityUid));
|
||||
|
||||
@@ -20,6 +20,8 @@ public sealed class CargoTest
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings() {NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
@@ -27,7 +29,7 @@ public sealed class CargoTest
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var mapId = mapManager.CreateMap();
|
||||
var mapId = testMap.MapId;
|
||||
|
||||
foreach (var proto in protoManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
@@ -50,13 +52,15 @@ public sealed class CargoTest
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
var entManager = server.ResolveDependency<IEntityManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var protoManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var mapId = mapManager.CreateMap();
|
||||
var mapId = testMap.MapId;
|
||||
var grid = mapManager.CreateGrid(mapId);
|
||||
var coord = new EntityCoordinates(grid.Owner, 0, 0);
|
||||
|
||||
@@ -73,8 +77,10 @@ public sealed class CargoTest
|
||||
&& stackpricecomp.Price > 0)
|
||||
{
|
||||
if (entManager.TryGetComponent<StaticPriceComponent>(ent, out var staticpricecomp))
|
||||
{
|
||||
Assert.That(staticpricecomp.Price, Is.EqualTo(0),
|
||||
$"The prototype {proto} have a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
|
||||
$"The prototype {proto} have a StackPriceComponent and StaticPriceComponent whose values are not compatible with each other.");
|
||||
}
|
||||
}
|
||||
entManager.DeleteEntity(ent);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Doors
|
||||
{
|
||||
@@ -114,7 +115,7 @@ namespace Content.IntegrationTests.Tests.Doors
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
IPhysBody physBody = null;
|
||||
PhysicsComponent physBody = null;
|
||||
EntityUid physicsDummy = default;
|
||||
EntityUid airlock = default;
|
||||
DoorComponent doorComponent = null;
|
||||
@@ -145,7 +146,7 @@ namespace Content.IntegrationTests.Tests.Doors
|
||||
for (var i = 0; i < 240; i += 10)
|
||||
{
|
||||
// Keep the airlock awake so they collide
|
||||
await server.WaitPost(() => entityManager.GetComponent<IPhysBody>(airlock).WakeBody());
|
||||
await server.WaitPost(() => entityManager.GetComponent<PhysicsComponent>(airlock).WakeBody());
|
||||
|
||||
await server.WaitRunTicks(10);
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
@@ -59,7 +59,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Movement
|
||||
// // Now let's make the player enter a climbing transitioning state.
|
||||
// climbing.IsClimbing = true;
|
||||
// EntitySystem.Get<ClimbSystem>().MoveEntityToward(human, table, climbing:climbing);
|
||||
// var body = entityManager.GetComponent<IPhysBody>(human);
|
||||
// var body = entityManager.GetComponent<PhysicsComponent>(human);
|
||||
// // TODO: Check it's climbing
|
||||
//
|
||||
// // Force the player out of climb state. It should immediately remove the ClimbController.
|
||||
|
||||
360
Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
Normal file
360
Content.IntegrationTests/Tests/VendingMachineRestockTest.cs
Normal file
@@ -0,0 +1,360 @@
|
||||
#nullable enable
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.VendingMachines;
|
||||
using Content.Server.VendingMachines.Restock;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Prototypes;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.IntegrationTests.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
[TestOf(typeof(VendingMachineRestockComponent))]
|
||||
[TestOf(typeof(VendingMachineRestockSystem))]
|
||||
public sealed class VendingMachineRestockTest : EntitySystem
|
||||
{
|
||||
private const string Prototypes = @"
|
||||
- type: entity
|
||||
name: HumanDummy
|
||||
id: HumanDummy
|
||||
components:
|
||||
- type: Hands
|
||||
- type: Body
|
||||
prototype: Human
|
||||
|
||||
- type: entity
|
||||
parent: FoodSnackBase
|
||||
id: TestRamen
|
||||
name: TestRamen
|
||||
|
||||
- type: vendingMachineInventory
|
||||
id: TestInventory
|
||||
startingInventory:
|
||||
TestRamen: 1
|
||||
|
||||
- type: vendingMachineInventory
|
||||
id: OtherTestInventory
|
||||
startingInventory:
|
||||
TestRamen: 3
|
||||
|
||||
- type: vendingMachineInventory
|
||||
id: BigTestInventory
|
||||
startingInventory:
|
||||
TestRamen: 4
|
||||
|
||||
- type: entity
|
||||
parent: BaseVendingMachineRestock
|
||||
id: TestRestockWrong
|
||||
name: TestRestockWrong
|
||||
components:
|
||||
- type: VendingMachineRestock
|
||||
canRestock:
|
||||
- OtherTestInventory
|
||||
|
||||
- type: entity
|
||||
parent: BaseVendingMachineRestock
|
||||
id: TestRestockCorrect
|
||||
name: TestRestockCorrect
|
||||
components:
|
||||
- type: VendingMachineRestock
|
||||
canRestock:
|
||||
- TestInventory
|
||||
|
||||
- type: entity
|
||||
parent: BaseVendingMachineRestock
|
||||
id: TestRestockExplode
|
||||
name: TestRestockExplode
|
||||
components:
|
||||
- type: Damageable
|
||||
damageContainer: Inorganic
|
||||
damageModifierSet: Metallic
|
||||
- type: Destructible
|
||||
thresholds:
|
||||
- trigger:
|
||||
!type:DamageTrigger
|
||||
damage: 20
|
||||
behaviors:
|
||||
- !type:DumpRestockInventory
|
||||
- !type:DoActsBehavior
|
||||
acts: [ 'Destruction' ]
|
||||
- type: VendingMachineRestock
|
||||
canRestock:
|
||||
- BigTestInventory
|
||||
|
||||
- type: entity
|
||||
parent: VendingMachine
|
||||
id: VendingMachineTest
|
||||
name: Test Ramen
|
||||
components:
|
||||
- type: Wires
|
||||
LayoutId: Vending
|
||||
- type: VendingMachine
|
||||
pack: TestInventory
|
||||
- type: Sprite
|
||||
sprite: error.rsi
|
||||
";
|
||||
|
||||
[Test]
|
||||
public async Task TestAllRestocksAreAvailableToBuy()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true});
|
||||
var server = pairTracker.Pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
HashSet<string> restocks = new();
|
||||
Dictionary<string, List<string>> restockStores = new();
|
||||
|
||||
// Collect all the prototypes with restock components.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract ||
|
||||
!proto.TryGetComponent<VendingMachineRestockComponent>(out var restock))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
restocks.Add(proto.ID);
|
||||
}
|
||||
|
||||
// Collect all the prototypes with StorageFills referencing those entities.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (!proto.TryGetComponent<StorageFillComponent>(out var storage))
|
||||
continue;
|
||||
|
||||
List<string> restockStore = new();
|
||||
foreach (var spawnEntry in storage.Contents)
|
||||
{
|
||||
if (spawnEntry.PrototypeId != null && restocks.Contains(spawnEntry.PrototypeId))
|
||||
restockStore.Add(spawnEntry.PrototypeId);
|
||||
}
|
||||
|
||||
if (restockStore.Count > 0)
|
||||
restockStores.Add(proto.ID, restockStore);
|
||||
}
|
||||
|
||||
// Iterate through every CargoProduct and make sure each
|
||||
// prototype with a restock component is referenced in a
|
||||
// purchaseable entity with a StorageFill.
|
||||
foreach (var proto in prototypeManager.EnumeratePrototypes<CargoProductPrototype>())
|
||||
{
|
||||
if (restockStores.ContainsKey(proto.Product))
|
||||
{
|
||||
foreach (var entry in restockStores[proto.Product])
|
||||
restocks.Remove(entry);
|
||||
|
||||
restockStores.Remove(proto.Product);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.That(restockStores.Count, Is.EqualTo(0),
|
||||
$"Some entities containing entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restockStores.Keys)}");
|
||||
|
||||
Assert.That(restocks.Count, Is.EqualTo(0),
|
||||
$"Some entities with VendingMachineRestock components are unavailable for purchase: \n - {string.Join("\n - ", restocks)}");
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestCompleteRestockProcess()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
var server = pairTracker.Pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
|
||||
EntityUid packageRight;
|
||||
EntityUid packageWrong;
|
||||
EntityUid machine;
|
||||
EntityUid user;
|
||||
VendingMachineComponent machineComponent;
|
||||
VendingMachineRestockComponent restockRightComponent;
|
||||
VendingMachineRestockComponent restockWrongComponent;
|
||||
WiresComponent machineWires;
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
// Spawn the entities.
|
||||
user = entityManager.SpawnEntity("HumanDummy", coordinates);
|
||||
machine = entityManager.SpawnEntity("VendingMachineTest", coordinates);
|
||||
packageRight = entityManager.SpawnEntity("TestRestockCorrect", coordinates);
|
||||
packageWrong = entityManager.SpawnEntity("TestRestockWrong", coordinates);
|
||||
|
||||
// Sanity test for components existing.
|
||||
Assert.True(entityManager.TryGetComponent(machine, out machineComponent!), $"Machine has no {nameof(VendingMachineComponent)}");
|
||||
Assert.True(entityManager.TryGetComponent(packageRight, out restockRightComponent!), $"Correct package has no {nameof(VendingMachineRestockComponent)}");
|
||||
Assert.True(entityManager.TryGetComponent(packageWrong, out restockWrongComponent!), $"Wrong package has no {nameof(VendingMachineRestockComponent)}");
|
||||
Assert.True(entityManager.TryGetComponent(machine, out machineWires!), $"Machine has no {nameof(WiresComponent)}");
|
||||
|
||||
var systemRestock = entitySystemManager.GetEntitySystem<VendingMachineRestockSystem>();
|
||||
var systemMachine = entitySystemManager.GetEntitySystem<VendingMachineSystem>();
|
||||
|
||||
// Test that the panel needs to be opened first.
|
||||
Assert.That(systemRestock.TryAccessMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.False, "Right package is able to restock without opened access panel");
|
||||
Assert.That(systemRestock.TryAccessMachine(packageWrong, restockWrongComponent, machineComponent, user, machine), Is.False, "Wrong package is able to restock without opened access panel");
|
||||
|
||||
// Open the panel.
|
||||
machineWires.IsPanelOpen = true;
|
||||
|
||||
// Test that the right package works for the right machine.
|
||||
Assert.That(systemRestock.TryAccessMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.True, "Correct package is unable to restock with access panel opened");
|
||||
|
||||
// Test that the wrong package does not work.
|
||||
Assert.That(systemRestock.TryMatchPackageToMachine(packageWrong, restockWrongComponent, machineComponent, user, machine), Is.False, "Package with invalid canRestock is able to restock machine");
|
||||
|
||||
// Test that the right package does work.
|
||||
Assert.That(systemRestock.TryMatchPackageToMachine(packageRight, restockRightComponent, machineComponent, user, machine), Is.True, "Package with valid canRestock is unable to restock machine");
|
||||
|
||||
// Make sure there's something in there to begin with.
|
||||
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.GreaterThan(0),
|
||||
"Machine inventory is empty before emptying.");
|
||||
|
||||
// Empty the inventory.
|
||||
systemMachine.EjectRandom(machine, false, true, machineComponent);
|
||||
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.EqualTo(0),
|
||||
"Machine inventory is not empty after ejecting.");
|
||||
|
||||
// Test that the inventory is actually restocked.
|
||||
systemMachine.TryRestockInventory(machine, machineComponent);
|
||||
Assert.That(systemMachine.GetAvailableInventory(machine, machineComponent).Count, Is.GreaterThan(0),
|
||||
"Machine available inventory count is not greater than zero after restock.");
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestRestockBreaksOpen()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
var server = pairTracker.Pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var prototypeManager = server.ResolveDependency<IPrototypeManager>();
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
|
||||
var damageableSystem = entitySystemManager.GetEntitySystem<DamageableSystem>();
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
EntityUid restock = default;
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
var totalStartingRamen = 0;
|
||||
|
||||
foreach (var meta in entityManager.EntityQuery<MetaDataComponent>())
|
||||
if (!meta.Deleted && meta.EntityPrototype?.ID == "TestRamen")
|
||||
totalStartingRamen++;
|
||||
|
||||
Assert.That(totalStartingRamen, Is.EqualTo(0),
|
||||
"Did not start with zero ramen.");
|
||||
|
||||
restock = entityManager.SpawnEntity("TestRestockExplode", coordinates);
|
||||
var damageSpec = new DamageSpecifier(prototypeManager.Index<DamageTypePrototype>("Blunt"), 100);
|
||||
var damageResult = damageableSystem.TryChangeDamage(restock, damageSpec);
|
||||
|
||||
Assert.IsNotNull(damageResult,
|
||||
"Received null damageResult when attempting to damage restock box.");
|
||||
|
||||
Assert.That((int) damageResult!.Total, Is.GreaterThan(0),
|
||||
"Box damage result was not greater than 0.");
|
||||
});
|
||||
await server.WaitRunTicks(15);
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.That(entityManager.Deleted(restock),
|
||||
"Restock box was not deleted after being damaged.");
|
||||
|
||||
var totalRamen = 0;
|
||||
|
||||
foreach (var meta in entityManager.EntityQuery<MetaDataComponent>())
|
||||
if (!meta.Deleted && meta.EntityPrototype?.ID == "TestRamen")
|
||||
totalRamen++;
|
||||
|
||||
Assert.That(totalRamen, Is.EqualTo(2),
|
||||
"Did not find enough ramen after destroying restock box.");
|
||||
|
||||
mapManager.DeleteMap(testMap.MapId);
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task TestRestockInventoryBounds()
|
||||
{
|
||||
await using var pairTracker = await PoolManager.GetServerClient(new PoolSettings{NoClient = true, ExtraPrototypes = Prototypes});
|
||||
var server = pairTracker.Pair.Server;
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
var mapManager = server.ResolveDependency<IMapManager>();
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var entitySystemManager = server.ResolveDependency<IEntitySystemManager>();
|
||||
|
||||
var vendingMachineSystem = entitySystemManager.GetEntitySystem<SharedVendingMachineSystem>();
|
||||
|
||||
var testMap = await PoolManager.CreateTestMap(pairTracker);
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var coordinates = testMap.GridCoords;
|
||||
|
||||
EntityUid machine = entityManager.SpawnEntity("VendingMachineTest", coordinates);
|
||||
|
||||
Assert.That(vendingMachineSystem.GetAvailableInventory(machine).Count, Is.EqualTo(1),
|
||||
"Machine's available inventory did not contain one entry.");
|
||||
|
||||
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(1),
|
||||
"Machine's available inventory is not the expected amount.");
|
||||
|
||||
vendingMachineSystem.RestockInventoryFromPrototype(machine);
|
||||
|
||||
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(2),
|
||||
"Machine's available inventory is not double its starting amount after a restock.");
|
||||
|
||||
vendingMachineSystem.RestockInventoryFromPrototype(machine);
|
||||
|
||||
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3),
|
||||
"Machine's available inventory is not triple its starting amount after two restocks.");
|
||||
|
||||
vendingMachineSystem.RestockInventoryFromPrototype(machine);
|
||||
|
||||
Assert.That(vendingMachineSystem.GetAvailableInventory(machine)[0].Amount, Is.EqualTo(3),
|
||||
"Machine's available inventory did not stay the same after a third restock.");
|
||||
});
|
||||
|
||||
await pairTracker.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#nullable disable
|
||||
@@ -9,6 +9,7 @@ using Content.Shared.Maps;
|
||||
using Content.Shared.MobState.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Abilities.Mime
|
||||
@@ -74,7 +75,7 @@ namespace Content.Server.Abilities.Mime
|
||||
// Check there are no walls or mobs there
|
||||
foreach (var entity in coords.GetEntitiesInTile())
|
||||
{
|
||||
IPhysBody? physics = null; // We use this to check if it's impassable
|
||||
PhysicsComponent? physics = null; // We use this to check if it's impassable
|
||||
if ((HasComp<MobStateComponent>(entity) && entity != uid) || // Is it a mob?
|
||||
((Resolve(entity, ref physics, false) && (physics.CollisionLayer & (int) CollisionGroup.Impassable) != 0) // Is it impassable?
|
||||
&& !(TryComp<DoorComponent>(entity, out var door) && door.State != DoorState.Closed))) // Is it a door that's open and so not actually impassable?
|
||||
|
||||
30
Content.Server/Actions/ActionOnInteractComponent.cs
Normal file
30
Content.Server/Actions/ActionOnInteractComponent.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Interaction;
|
||||
|
||||
namespace Content.Server.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This component enables an entity to perform actions when used to interact with the world, without actually
|
||||
/// granting that action to the entity that is using the item.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the entity is used in hand (<see cref="ActivateInWorldEvent"/>), it will perform a random available instant
|
||||
/// action. If the entity is used to interact with another entity (<see cref="InteractUsingEvent"/>), it will
|
||||
/// attempt to perform a random entity target action. Finally, if the entity is used to click somewhere in the world
|
||||
/// and no other interaction takes place (<see cref="AfterInteractEvent"/>), then it will try to perform a random
|
||||
/// available entity or world target action. This component does not bypass standard interaction checks.
|
||||
///
|
||||
/// This component mainly exists as a lazy way to add utility entities that can do things like cast "spells".
|
||||
/// </remarks>
|
||||
[RegisterComponent]
|
||||
public sealed class ActionOnInteractComponent : Component
|
||||
{
|
||||
[DataField("activateActions")]
|
||||
public List<InstantAction>? ActivateActions;
|
||||
|
||||
[DataField("entityActions")]
|
||||
public List<EntityTargetAction>? EntityActions;
|
||||
|
||||
[DataField("worldActions")]
|
||||
public List<WorldTargetAction>? WorldActions;
|
||||
}
|
||||
131
Content.Server/Actions/ActionOnInteractSystem.cs
Normal file
131
Content.Server/Actions/ActionOnInteractSystem.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using Content.Shared.Actions;
|
||||
using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Interaction;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Actions;
|
||||
|
||||
/// <summary>
|
||||
/// This System handled interactions for the <see cref="ActionOnInteractComponent"/>.
|
||||
/// </summary>
|
||||
public sealed class ActionOnInteractSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SharedActionsSystem _actions = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<ActionOnInteractComponent, ActivateInWorldEvent>(OnActivate);
|
||||
SubscribeLocalEvent<ActionOnInteractComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, ActionOnInteractComponent component, ActivateInWorldEvent args)
|
||||
{
|
||||
if (args.Handled || component.ActivateActions == null)
|
||||
return;
|
||||
|
||||
var options = new List<InstantAction>();
|
||||
foreach (var action in component.ActivateActions)
|
||||
{
|
||||
if (ValidAction(action))
|
||||
options.Add(action);
|
||||
}
|
||||
|
||||
if (options.Count == 0)
|
||||
return;
|
||||
|
||||
var act = _random.Pick(options);
|
||||
if (act.Event != null)
|
||||
act.Event.Performer = args.User;
|
||||
|
||||
act.Provider = uid;
|
||||
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, ActionOnInteractComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
// First, try entity target actions
|
||||
if (args.Target != null && component.EntityActions != null)
|
||||
{
|
||||
var entOptions = new List<EntityTargetAction>();
|
||||
foreach (var action in component.EntityActions)
|
||||
{
|
||||
if (!ValidAction(action, args.CanReach))
|
||||
continue;
|
||||
|
||||
if (!_actions.ValidateEntityTarget(args.User, args.Target.Value, action))
|
||||
continue;
|
||||
|
||||
entOptions.Add(action);
|
||||
}
|
||||
|
||||
if (entOptions.Count > 0)
|
||||
{
|
||||
var entAct = _random.Pick(entOptions);
|
||||
if (entAct.Event != null)
|
||||
{
|
||||
entAct.Event.Performer = args.User;
|
||||
entAct.Event.Target = args.Target.Value;
|
||||
}
|
||||
|
||||
entAct.Provider = uid;
|
||||
_actions.PerformAction(args.User, null, entAct, entAct.Event, _timing.CurTime, false);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// else: try world target actions
|
||||
if (component.WorldActions == null)
|
||||
return;
|
||||
|
||||
var options = new List<WorldTargetAction>();
|
||||
foreach (var action in component.WorldActions)
|
||||
{
|
||||
if (!ValidAction(action, args.CanReach))
|
||||
continue;
|
||||
|
||||
if (!_actions.ValidateWorldTarget(args.User, args.ClickLocation, action))
|
||||
continue;
|
||||
|
||||
options.Add(action);
|
||||
}
|
||||
|
||||
if (options.Count == 0)
|
||||
return;
|
||||
|
||||
var act = _random.Pick(options);
|
||||
if (act.Event != null)
|
||||
{
|
||||
act.Event.Performer = args.User;
|
||||
act.Event.Target = args.ClickLocation;
|
||||
}
|
||||
|
||||
act.Provider = uid;
|
||||
_actions.PerformAction(args.User, null, act, act.Event, _timing.CurTime, false);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private bool ValidAction(ActionType act, bool canReach = true)
|
||||
{
|
||||
if (!act.Enabled)
|
||||
return false;
|
||||
|
||||
if (act.Charges.HasValue && act.Charges <= 0)
|
||||
return false;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
if (act.Cooldown.HasValue && act.Cooldown.Value.End > curTime)
|
||||
return false;
|
||||
|
||||
return canReach || act is TargetedAction { CheckCanAccess: false };
|
||||
}
|
||||
}
|
||||
@@ -18,9 +18,9 @@ namespace Content.Server.Actions
|
||||
base.Initialize();
|
||||
}
|
||||
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action)
|
||||
protected override bool PerformBasicActions(EntityUid user, ActionType action, bool predicted)
|
||||
{
|
||||
var result = base.PerformBasicActions(user, action);
|
||||
var result = base.PerformBasicActions(user, action, predicted);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(action.Speech))
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@ using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Server.Administration.Commands
|
||||
{
|
||||
@@ -121,7 +122,7 @@ namespace Content.Server.Administration.Commands
|
||||
var xform = entMan.GetComponent<TransformComponent>(playerEntity);
|
||||
xform.Coordinates = coords;
|
||||
xform.AttachToGridOrMap();
|
||||
if (entMan.TryGetComponent(playerEntity, out IPhysBody? physics))
|
||||
if (entMan.TryGetComponent(playerEntity, out PhysicsComponent? physics))
|
||||
{
|
||||
physics.LinearVelocity = Vector2.Zero;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Shared.AirlockPainter;
|
||||
using Content.Shared.AirlockPainter.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Interaction;
|
||||
using JetBrains.Annotations;
|
||||
@@ -18,6 +20,7 @@ namespace Content.Server.AirlockPainter
|
||||
[UsedImplicitly]
|
||||
public sealed class AirlockPainterSystem : SharedAirlockPainterSystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
@@ -39,8 +42,11 @@ namespace Content.Server.AirlockPainter
|
||||
if (TryComp<AppearanceComponent>(ev.Target, out var appearance) &&
|
||||
TryComp<PaintableAirlockComponent>(ev.Target, out PaintableAirlockComponent? airlock))
|
||||
{
|
||||
SoundSystem.Play(ev.Component.SpraySound.GetSound(), Filter.Pvs(ev.User, entityManager:EntityManager), ev.User);
|
||||
SoundSystem.Play(ev.Component.SpraySound.GetSound(), Filter.Pvs(ev.UsedTool, entityManager:EntityManager), ev.UsedTool);
|
||||
appearance.SetData(DoorVisuals.BaseRSI, ev.Sprite);
|
||||
|
||||
// Log success
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(ev.User):user} painted {ToPrettyString(ev.Target):target}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,22 +93,28 @@ namespace Content.Server.AirlockPainter
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
NeedHand = true,
|
||||
BroadcastFinishedEvent = new AirlockPainterDoAfterComplete(uid, target, sprite, component),
|
||||
BroadcastFinishedEvent = new AirlockPainterDoAfterComplete(uid, target, sprite, component, args.User),
|
||||
BroadcastCancelledEvent = new AirlockPainterDoAfterCancelled(component),
|
||||
};
|
||||
_doAfterSystem.DoAfter(doAfterEventArgs);
|
||||
|
||||
// Log attempt
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is painting {ToPrettyString(uid):target} to '{style}' at {Transform(uid).Coordinates:targetlocation}");
|
||||
}
|
||||
|
||||
private sealed class AirlockPainterDoAfterComplete : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid User;
|
||||
public readonly EntityUid UsedTool;
|
||||
public readonly EntityUid Target;
|
||||
public readonly string Sprite;
|
||||
public readonly AirlockPainterComponent Component;
|
||||
|
||||
public AirlockPainterDoAfterComplete(EntityUid user, EntityUid target, string sprite, AirlockPainterComponent component)
|
||||
public AirlockPainterDoAfterComplete(EntityUid usedTool, EntityUid target, string sprite,
|
||||
AirlockPainterComponent component, EntityUid user)
|
||||
{
|
||||
User = user;
|
||||
UsedTool = usedTool;
|
||||
Target = target;
|
||||
Sprite = sprite;
|
||||
Component = component;
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
using Content.Server.Atmos.EntitySystems;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Server.Body.Systems;
|
||||
using Content.Server.Popups;
|
||||
using Content.Server.Shuttles.Systems;
|
||||
using Content.Shared.Alert;
|
||||
using Content.Shared.Shuttles.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Alert.Click;
|
||||
|
||||
|
||||
@@ -274,7 +274,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
_timer -= UpdateTime;
|
||||
|
||||
// TODO: This needs cleanup to take off the crust from TemperatureComponent and shit.
|
||||
foreach (var (flammable, physics, transform) in EntityManager.EntityQuery<FlammableComponent, IPhysBody, TransformComponent>())
|
||||
foreach (var (flammable, physics, transform) in EntityManager.EntityQuery<FlammableComponent, PhysicsComponent, TransformComponent>())
|
||||
{
|
||||
var uid = flammable.Owner;
|
||||
|
||||
@@ -335,7 +335,7 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
continue;
|
||||
}
|
||||
|
||||
var otherPhysics = EntityManager.GetComponent<IPhysBody>(uid);
|
||||
var otherPhysics = EntityManager.GetComponent<PhysicsComponent>(uid);
|
||||
|
||||
// TODO: Sloth, please save our souls!
|
||||
if (!physics.GetWorldAABB().Intersects(otherPhysics.GetWorldAABB()))
|
||||
|
||||
@@ -188,9 +188,12 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
|
||||
public void ConnectToInternals(GasTankComponent component)
|
||||
{
|
||||
if (component.IsConnected || !CanConnectToInternals(component)) return;
|
||||
if (component.IsConnected || !CanConnectToInternals(component))
|
||||
return;
|
||||
|
||||
var internals = GetInternalsComponent(component);
|
||||
if (internals == null) return;
|
||||
if (internals == null)
|
||||
return;
|
||||
|
||||
if (_internals.TryConnectTank(internals, component.Owner))
|
||||
component.User = internals.Owner;
|
||||
@@ -198,7 +201,8 @@ namespace Content.Server.Atmos.EntitySystems
|
||||
_actions.SetToggled(component.ToggleAction, component.IsConnected);
|
||||
|
||||
// Couldn't toggle!
|
||||
if (!component.IsConnected) return;
|
||||
if (!component.IsConnected)
|
||||
return;
|
||||
|
||||
component.ConnectStream?.Stop();
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
|
||||
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
|
||||
@@ -206,19 +207,19 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
|
||||
if (canister.Air.Pressure < 10)
|
||||
{
|
||||
appearance.SetData(GasCanisterVisuals.PressureState, 0);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 0, appearance);
|
||||
}
|
||||
else if (canister.Air.Pressure < Atmospherics.OneAtmosphere)
|
||||
{
|
||||
appearance.SetData(GasCanisterVisuals.PressureState, 1);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 1, appearance);
|
||||
}
|
||||
else if (canister.Air.Pressure < (15 * Atmospherics.OneAtmosphere))
|
||||
{
|
||||
appearance.SetData(GasCanisterVisuals.PressureState, 2);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 2, appearance);
|
||||
}
|
||||
else
|
||||
{
|
||||
appearance.SetData(GasCanisterVisuals.PressureState, 3);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.PressureState, 3, appearance);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
return;
|
||||
}
|
||||
|
||||
_userInterfaceSystem.GetUiOrNull(uid, GasCanisterUiKey.Key)?.Open(actor.PlayerSession);
|
||||
_userInterfaceSystem.TryOpen(uid, GasCanisterUiKey.Key, actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -246,13 +247,13 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
if (TryComp<LockComponent>(uid, out var lockComponent) && lockComponent.Locked)
|
||||
return;
|
||||
|
||||
_userInterfaceSystem.GetUiOrNull(uid, GasCanisterUiKey.Key)?.Open(actor.PlayerSession);
|
||||
_userInterfaceSystem.TryOpen(uid, GasCanisterUiKey.Key, actor.PlayerSession);
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void OnCanisterInteractUsing(EntityUid canister, GasCanisterComponent component, InteractUsingEvent args)
|
||||
{
|
||||
var container = canister.EnsureContainer<ContainerSlot>(component.ContainerName);
|
||||
var container = _containerSystem.EnsureContainer<ContainerSlot>(canister, component.ContainerName);
|
||||
|
||||
// Container full.
|
||||
if (container.ContainedEntity != null)
|
||||
@@ -277,10 +278,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
|
||||
DirtyUI(uid, component);
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
appearance.SetData(GasCanisterVisuals.TankInserted, true);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.TankInserted, true);
|
||||
}
|
||||
|
||||
private void OnCanisterContainerRemoved(EntityUid uid, GasCanisterComponent component, EntRemovedFromContainerMessage args)
|
||||
@@ -290,10 +288,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
|
||||
|
||||
DirtyUI(uid, component);
|
||||
|
||||
if (!EntityManager.TryGetComponent(uid, out AppearanceComponent? appearance))
|
||||
return;
|
||||
|
||||
appearance.SetData(GasCanisterVisuals.TankInserted, false);
|
||||
_appearanceSystem.SetData(uid, GasCanisterVisuals.TankInserted, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -15,18 +15,12 @@ namespace Content.Server.Audio
|
||||
|
||||
private void HandlePowerSupply(EntityUid uid, AmbientOnPoweredComponent component, ref PowerNetBatterySupplyEvent args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
|
||||
if (ambientSound.Enabled == args.Supply) return;
|
||||
ambientSound.Enabled = args.Supply;
|
||||
Dirty(ambientSound);
|
||||
SetAmbience(uid, args.Supply);
|
||||
}
|
||||
|
||||
private void HandlePowerChange(EntityUid uid, AmbientOnPoweredComponent component, ref PowerChangedEvent args)
|
||||
{
|
||||
if (!EntityManager.TryGetComponent<AmbientSoundComponent>(uid, out var ambientSound)) return;
|
||||
if (ambientSound.Enabled == args.Powered) return;
|
||||
ambientSound.Enabled = args.Powered;
|
||||
Dirty(ambientSound);
|
||||
SetAmbience(uid, args.Powered);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,13 +53,13 @@ namespace Content.Server.Body.Components
|
||||
/// The base bloodloss damage to be incurred if below <see cref="BloodlossThreshold"/>
|
||||
/// </summary>
|
||||
[DataField("bloodlossDamage", required: true)]
|
||||
public DamageSpecifier BloodlossDamage = default!;
|
||||
public DamageSpecifier BloodlossDamage = new();
|
||||
|
||||
/// <summary>
|
||||
/// The base bloodloss damage to be healed if above <see cref="BloodlossThreshold"/>
|
||||
/// </summary>
|
||||
[DataField("bloodlossHealDamage", required: true)]
|
||||
public DamageSpecifier BloodlossHealDamage = default!;
|
||||
public DamageSpecifier BloodlossHealDamage = new();
|
||||
|
||||
/// <summary>
|
||||
/// How frequently should this bloodstream update, in seconds?
|
||||
|
||||
@@ -9,7 +9,6 @@ using Robust.Shared.Containers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Verbs;
|
||||
using Content.Server.Popups;
|
||||
using Robust.Shared.Player;
|
||||
using Content.Server.DoAfter;
|
||||
using System.Threading;
|
||||
|
||||
@@ -86,12 +85,15 @@ public sealed class InternalsSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
// Is the target not you? If yes, use a do-after to give them time to respond.
|
||||
if (!force && uid != user)
|
||||
if (!force)
|
||||
{
|
||||
// Is the target not you? If yes, use a do-after to give them time to respond.
|
||||
//If no, do a short delay. There's no reason it should be beyond 1 second.
|
||||
var delay = uid != user ? internals.Delay : 1.0f;
|
||||
|
||||
internals.CancelToken?.Cancel();
|
||||
internals.CancelToken = new CancellationTokenSource();
|
||||
_doAfter.DoAfter(new DoAfterEventArgs(user, internals.Delay, internals.CancelToken.Token, uid)
|
||||
_doAfter.DoAfter(new DoAfterEventArgs(user, delay, internals.CancelToken.Token, uid)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnDamage = true,
|
||||
|
||||
@@ -534,7 +534,7 @@ public sealed partial class CargoSystem
|
||||
return FoundOrganics(component.Owner, mobQuery, xformQuery);
|
||||
}
|
||||
|
||||
private bool FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
|
||||
public bool FoundOrganics(EntityUid uid, EntityQuery<MobStateComponent> mobQuery, EntityQuery<TransformComponent> xformQuery)
|
||||
{
|
||||
var xform = xformQuery.GetComponent(uid);
|
||||
var childEnumerator = xform.ChildEnumerator;
|
||||
|
||||
@@ -33,6 +33,8 @@ public sealed partial class ChatSystem
|
||||
{
|
||||
_keyCodes.Add(proto.KeyCode, proto);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void ShutdownRadio()
|
||||
@@ -45,6 +47,10 @@ public sealed partial class ChatSystem
|
||||
// TODO: Turn common into a true frequency and support multiple aliases.
|
||||
var isRadioMessage = false;
|
||||
RadioChannelPrototype? channel = null;
|
||||
|
||||
// Check if have headset and grab headset UID for later
|
||||
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) & TryComp<HeadsetComponent>(entityUid, out var _headsetComponent);
|
||||
|
||||
// First check if this is a message to the base radio frequency
|
||||
if (message.StartsWith(';'))
|
||||
{
|
||||
@@ -54,11 +60,25 @@ public sealed partial class ChatSystem
|
||||
isRadioMessage = true;
|
||||
}
|
||||
|
||||
// Check now if the remaining message is a targeted radio message
|
||||
|
||||
// Check now if the remaining message is a radio message
|
||||
if ((message.StartsWith(':') || message.StartsWith('.')) && message.Length >= 2)
|
||||
{
|
||||
// Redirect to defaultChannel of headsetComp if it goes to "h" channel code after making sure defaultChannel exists
|
||||
if (message[1] == 'h'
|
||||
&& _headsetComponent != null
|
||||
&& _headsetComponent.defaultChannel != null
|
||||
&& _prototypeManager.TryIndex(_headsetComponent.defaultChannel, out RadioChannelPrototype? protoDefaultChannel))
|
||||
{
|
||||
// Set Channel to headset defaultChannel
|
||||
channel = protoDefaultChannel;
|
||||
}
|
||||
else // otherwise it's a normal, targeted channel keycode
|
||||
{
|
||||
_keyCodes.TryGetValue(message[1], out channel);
|
||||
}
|
||||
|
||||
// Strip remaining message prefix.
|
||||
_keyCodes.TryGetValue(message[1], out channel);
|
||||
message = message[2..].TrimStart();
|
||||
isRadioMessage = true;
|
||||
}
|
||||
@@ -70,7 +90,8 @@ public sealed partial class ChatSystem
|
||||
if (message.Length <= 1)
|
||||
return (string.Empty, null);
|
||||
|
||||
if (channel == null)
|
||||
// Check for headset before no-such-channel, otherwise you can get two PopupEntities if no headset and no channel
|
||||
if (hasHeadset & channel == null )
|
||||
{
|
||||
_popup.PopupEntity(Loc.GetString("chat-manager-no-such-channel"), source, source);
|
||||
channel = null;
|
||||
@@ -79,7 +100,7 @@ public sealed partial class ChatSystem
|
||||
// Re-capitalize message since we removed the prefix.
|
||||
message = SanitizeMessageCapital(message);
|
||||
|
||||
var hasHeadset = _inventory.TryGetSlotEntity(source, "ears", out var entityUid) && HasComp<HeadsetComponent>(entityUid);
|
||||
|
||||
|
||||
if (!hasHeadset && !HasComp<IntrinsicRadioTransmitterComponent>(source))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,12 @@ namespace Content.Server.Chemistry.EntitySystems;
|
||||
/// </summary>
|
||||
public sealed class SolutionChangedEvent : EntityEventArgs
|
||||
{
|
||||
public readonly Solution Solution;
|
||||
|
||||
public SolutionChangedEvent(Solution solution)
|
||||
{
|
||||
Solution = solution;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -119,6 +125,8 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
|
||||
public void UpdateChemicals(EntityUid uid, Solution solutionHolder, bool needsReactionsProcessing = false, ReactionMixerComponent? mixerComponent = null)
|
||||
{
|
||||
DebugTools.Assert(solutionHolder.Name != null && TryGetSolution(uid, solutionHolder.Name, out var tmp) && tmp == solutionHolder);
|
||||
|
||||
// Process reactions
|
||||
if (needsReactionsProcessing && solutionHolder.CanReact)
|
||||
{
|
||||
@@ -126,7 +134,7 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
}
|
||||
|
||||
UpdateAppearance(uid, solutionHolder);
|
||||
RaiseLocalEvent(uid, new SolutionChangedEvent(), true);
|
||||
RaiseLocalEvent(uid, new SolutionChangedEvent(solutionHolder));
|
||||
}
|
||||
|
||||
public void RemoveAllSolution(EntityUid uid, Solution solutionHolder)
|
||||
@@ -224,6 +232,51 @@ public sealed partial class SolutionContainerSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves some quantity of a solution from one solution to another.
|
||||
/// </summary>
|
||||
/// <param name="sourceUid">entity holding the source solution</param>
|
||||
/// <param name="targetUid">entity holding the target solution</param>
|
||||
/// <param name="source">source solution</param>
|
||||
/// <param name="target">target solution</param>
|
||||
/// <param name="quantity">quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed.</param>
|
||||
public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, Solution source, Solution target, FixedPoint2 quantity)
|
||||
{
|
||||
if (quantity < 0)
|
||||
return TryTransferSolution(targetUid, sourceUid, target, source, -quantity);
|
||||
|
||||
quantity = FixedPoint2.Min(quantity, target.AvailableVolume, source.CurrentVolume);
|
||||
if (quantity == 0)
|
||||
return false;
|
||||
|
||||
// TODO after #12428 is merged, this should be made into a function that directly transfers reagents.
|
||||
// currently this is quite inefficient.
|
||||
target.AddSolution(source.SplitSolution(quantity));
|
||||
|
||||
UpdateChemicals(sourceUid, source, false);
|
||||
UpdateChemicals(targetUid, target, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves some quantity of a solution from one solution to another.
|
||||
/// </summary>
|
||||
/// <param name="sourceUid">entity holding the source solution</param>
|
||||
/// <param name="targetUid">entity holding the target solution</param>
|
||||
/// <param name="source">source solution</param>
|
||||
/// <param name="target">target solution</param>
|
||||
/// <param name="quantity">quantity of solution to move from source to target. If this is a negative number, the source & target roles are reversed.</param>
|
||||
public bool TryTransferSolution(EntityUid sourceUid, EntityUid targetUid, string source, string target, FixedPoint2 quantity)
|
||||
{
|
||||
if (!TryGetSolution(sourceUid, source, out var sourceSoln))
|
||||
return false;
|
||||
|
||||
if (!TryGetSolution(targetUid, target, out var targetSoln))
|
||||
return false;
|
||||
|
||||
return TryTransferSolution(sourceUid, targetUid, sourceSoln, targetSoln, quantity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a solution to the container, overflowing the rest.
|
||||
/// It will
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
using Content.Server.Construction.Components;
|
||||
using Content.Server.Stack;
|
||||
using Content.Shared.Construction;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Construction;
|
||||
|
||||
public sealed class MachineFrameSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
[Dependency] private readonly SharedContainerSystem _container = default!;
|
||||
[Dependency] private readonly TagSystem _tag = default!;
|
||||
@@ -58,8 +55,6 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
// Setup requirements and progress...
|
||||
ResetProgressAndRequirements(component, machineBoard);
|
||||
|
||||
_appearance.SetData(uid, MachineFrameVisuals.State, 2);
|
||||
|
||||
if (TryComp(uid, out ConstructionComponent? construction))
|
||||
{
|
||||
// So prying the components off works correctly.
|
||||
@@ -224,8 +219,6 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
{
|
||||
if (!component.HasBoard)
|
||||
{
|
||||
_appearance.SetData(component.Owner, MachineFrameVisuals.State, 1);
|
||||
|
||||
component.TagRequirements.Clear();
|
||||
component.MaterialRequirements.Clear();
|
||||
component.ComponentRequirements.Clear();
|
||||
@@ -243,8 +236,6 @@ public sealed class MachineFrameSystem : EntitySystem
|
||||
if (!TryComp<MachineBoardComponent>(board, out var machineBoard))
|
||||
return;
|
||||
|
||||
_appearance.SetData(component.Owner, MachineFrameVisuals.State, 2);
|
||||
|
||||
ResetProgressAndRequirements(component, machineBoard);
|
||||
|
||||
foreach (var part in component.PartContainer.ContainedEntities)
|
||||
|
||||
@@ -88,13 +88,13 @@ public sealed class CrewManifestSystem : EntitySystem
|
||||
|
||||
private void OnBoundUiClose(EntityUid uid, CrewManifestViewerComponent component, BoundUIClosedEvent ev)
|
||||
{
|
||||
var owningStation = _stationSystem.GetOwningStation(uid);
|
||||
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var owningStation = _stationSystem.GetOwningStation(uid);
|
||||
if (owningStation == null || ev.Session is not IPlayerSession sessionCast)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CloseEui(owningStation.Value, sessionCast, uid);
|
||||
CloseEui(owningStation.Value, sessionCast, uid);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -215,6 +215,8 @@ public sealed class CrewManifestSystem : EntitySystem
|
||||
entries.Entries.Add(entry);
|
||||
}
|
||||
|
||||
entries.Entries = entries.Entries.OrderBy(e => e.JobTitle).ThenBy(e => e.Name).ToList();
|
||||
|
||||
if (_cachedEntries.ContainsKey(station))
|
||||
{
|
||||
_cachedEntries[station] = entries;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using Robust.Shared.Random;
|
||||
using Content.Shared.Stacks;
|
||||
using Content.Server.VendingMachines.Restock;
|
||||
using Content.Shared.Prototypes;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.Server.Destructible.Thresholds.Behaviors
|
||||
{
|
||||
/// <summary>
|
||||
/// Spawns a portion of the total items from one of the canRestock
|
||||
/// inventory entries on a VendingMachineRestock component.
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
[DataDefinition]
|
||||
public sealed class DumpRestockInventory: IThresholdBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// The percent of each inventory entry that will be salvaged
|
||||
/// upon destruction of the package.
|
||||
/// </summary>
|
||||
[DataField("percent", required: true)]
|
||||
public float Percent = 0.5f;
|
||||
|
||||
[DataField("offset")]
|
||||
public float Offset { get; set; } = 0.5f;
|
||||
|
||||
public void Execute(EntityUid owner, DestructibleSystem system)
|
||||
{
|
||||
if (!system.EntityManager.TryGetComponent<VendingMachineRestockComponent>(owner, out var packagecomp) ||
|
||||
!system.EntityManager.TryGetComponent<TransformComponent>(owner, out var xform))
|
||||
return;
|
||||
|
||||
var randomInventory = system.Random.Pick(packagecomp.CanRestock);
|
||||
|
||||
if (!system.PrototypeManager.TryIndex(randomInventory, out VendingMachineInventoryPrototype? packPrototype))
|
||||
return;
|
||||
|
||||
foreach (var (entityId, count) in packPrototype.StartingInventory)
|
||||
{
|
||||
var toSpawn = (int) Math.Round(count * Percent);
|
||||
|
||||
if (toSpawn == 0) continue;
|
||||
|
||||
if (EntityPrototypeHelpers.HasComponent<StackComponent>(entityId, system.PrototypeManager, system.ComponentFactory))
|
||||
{
|
||||
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
|
||||
system.StackSystem.SetCount(spawned, toSpawn);
|
||||
system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < toSpawn; i++)
|
||||
{
|
||||
var spawned = system.EntityManager.SpawnEntity(entityId, xform.Coordinates.Offset(system.Random.NextVector2(-Offset, Offset)));
|
||||
system.EntityManager.GetComponent<TransformComponent>(spawned).LocalRotation = system.Random.NextAngle();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ using Content.Server.UserInterface;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
|
||||
|
||||
@@ -23,7 +24,7 @@ namespace Content.Server.Disposal.Tube.Components
|
||||
|
||||
[ViewVariables]
|
||||
public bool Anchored =>
|
||||
!_entMan.TryGetComponent(Owner, out IPhysBody? physics) ||
|
||||
!_entMan.TryGetComponent(Owner, out PhysicsComponent? physics) ||
|
||||
physics.BodyType == BodyType.Static;
|
||||
|
||||
[ViewVariables] public BoundUserInterface? UserInterface => Owner.GetUIOrNull(DisposalRouterUiKey.Key);
|
||||
|
||||
@@ -4,6 +4,7 @@ using Content.Shared.Body.Components;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Server.Disposal.Unit.Components
|
||||
{
|
||||
@@ -82,7 +83,7 @@ namespace Content.Server.Disposal.Unit.Components
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_entMan.TryGetComponent(entity, out IPhysBody? physics))
|
||||
if (_entMan.TryGetComponent(entity, out PhysicsComponent? physics))
|
||||
{
|
||||
physics.CanCollide = false;
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
var modEv = new DoorGetPryTimeModifierEvent();
|
||||
var modEv = new DoorGetPryTimeModifierEvent(user);
|
||||
RaiseLocalEvent(target, modEv, false);
|
||||
|
||||
door.BeingPried = true;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Content.Shared.Atmos.Monitor;
|
||||
using Content.Shared.Doors;
|
||||
using Content.Shared.Doors.Components;
|
||||
using Content.Shared.Doors.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Map.Components;
|
||||
@@ -144,12 +145,12 @@ namespace Content.Server.Doors.Systems
|
||||
if (state.Fire)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-fire-message"),
|
||||
uid);
|
||||
uid, args.User, PopupType.MediumCaution);
|
||||
}
|
||||
else if (state.Pressure)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("firelock-component-is-holding-pressure-message"),
|
||||
uid);
|
||||
uid, args.User, PopupType.MediumCaution);
|
||||
}
|
||||
|
||||
if (state.Fire || state.Pressure)
|
||||
|
||||
@@ -22,10 +22,11 @@ public sealed class AbsorbentComponent : Component
|
||||
public FixedPoint2 ResidueAmount = FixedPoint2.New(10); // Should be higher than MopLowerLimit
|
||||
|
||||
/// <summary>
|
||||
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this amount.
|
||||
/// To leave behind a wet floor, this tool will be unable to take from puddles with a volume less than this
|
||||
/// amount. This limit is ignored if the target puddle does not evaporate.
|
||||
/// </summary>
|
||||
[DataField("mopLowerLimit")]
|
||||
public FixedPoint2 MopLowerLimit = FixedPoint2.New(5);
|
||||
[DataField("lowerLimit")]
|
||||
public FixedPoint2 LowerLimit = FixedPoint2.New(5);
|
||||
|
||||
[DataField("pickupSound")]
|
||||
public SoundSpecifier PickupSound = new SoundPathSpecifier("/Audio/Effects/Fluids/slosh.ogg");
|
||||
@@ -34,9 +35,9 @@ public sealed class AbsorbentComponent : Component
|
||||
public SoundSpecifier TransferSound = new SoundPathSpecifier("/Audio/Effects/Fluids/watersplash.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for the do_after delay for how quickly the mopping happens.
|
||||
/// Quantity of reagent that this mop can pick up per second. Determines the length of the do-after.
|
||||
/// </summary>
|
||||
[DataField("mopSpeed")] public float MopSpeed = 1;
|
||||
[DataField("speed")] public float Speed = 10;
|
||||
|
||||
/// <summary>
|
||||
/// How many entities can this tool interact with at once?
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems
|
||||
{
|
||||
@@ -14,7 +13,6 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
var queueDelete = new RemQueue<EvaporationComponent>();
|
||||
foreach (var evaporationComponent in EntityManager.EntityQuery<EvaporationComponent>())
|
||||
{
|
||||
var uid = evaporationComponent.Owner;
|
||||
@@ -38,21 +36,9 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
FixedPoint2.Min(FixedPoint2.New(1), solution.CurrentVolume)); // removes 1 unit, or solution current volume, whichever is lower.
|
||||
}
|
||||
|
||||
if (solution.CurrentVolume <= 0)
|
||||
{
|
||||
EntityManager.QueueDeleteEntity(uid);
|
||||
}
|
||||
else if (solution.CurrentVolume <= evaporationComponent.LowerLimit // if puddle is too big or too small to evaporate.
|
||||
|| solution.CurrentVolume >= evaporationComponent.UpperLimit)
|
||||
{
|
||||
evaporationComponent.EvaporationToggle = false; // pause evaporation
|
||||
}
|
||||
else evaporationComponent.EvaporationToggle = true; // unpause evaporation, e.g. if a puddle previously above evaporation UpperLimit was brought down below evaporation UpperLimit via mopping.
|
||||
}
|
||||
|
||||
foreach (var evaporationComponent in queueDelete)
|
||||
{
|
||||
EntityManager.RemoveComponent(evaporationComponent.Owner, evaporationComponent);
|
||||
evaporationComponent.EvaporationToggle =
|
||||
solution.CurrentVolume > evaporationComponent.LowerLimit
|
||||
&& solution.CurrentVolume < evaporationComponent.UpperLimit;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@ using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Chemistry.Components;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Tag;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Map;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems;
|
||||
|
||||
@@ -22,8 +22,10 @@ public sealed class MoppingSystem : EntitySystem
|
||||
[Dependency] private readonly TagSystem _tagSystem = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popups = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
|
||||
const string puddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
|
||||
const string PuddlePrototypeId = "PuddleSmear"; // The puddle prototype to use when releasing liquid to the floor, making a new puddle
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -35,264 +37,189 @@ public sealed class MoppingSystem : EntitySystem
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, AbsorbentComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (!args.CanReach) // if user cannot reach the target
|
||||
if (!args.CanReach || args.Handled)
|
||||
return;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorberSoln))
|
||||
return;
|
||||
|
||||
if (args.Target is not { Valid: true } target)
|
||||
{
|
||||
// Add liquid to an empty floor tile
|
||||
args.Handled = TryCreatePuddle(args.User, args.ClickLocation, component, absorberSoln);
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Handled) // if the event was already handled
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_solutionSystem.TryGetSolution(args.Used, AbsorbentComponent.SolutionName, out var absorbedSolution);
|
||||
|
||||
if (absorbedSolution is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var toolAvailableVolume = absorbedSolution.AvailableVolume;
|
||||
var toolCurrentVolume = absorbedSolution.CurrentVolume;
|
||||
|
||||
// For adding liquid to an empty floor tile
|
||||
if (args.Target is null) // if a tile is clicked
|
||||
{
|
||||
ReleaseToFloor(args.ClickLocation, component, absorbedSolution);
|
||||
args.Handled = true;
|
||||
args.User.PopupMessage(args.User, Loc.GetString("mopping-system-release-to-floor"));
|
||||
return;
|
||||
}
|
||||
else if (args.Target is not null)
|
||||
{
|
||||
// Handle our do_after logic
|
||||
HandleDoAfter(args.User, args.Used, args.Target.Value, component, toolCurrentVolume, toolAvailableVolume);
|
||||
}
|
||||
|
||||
args.Handled = true;
|
||||
return;
|
||||
args.Handled = TryPuddleInteract(args.User, uid, target, component, absorberSoln)
|
||||
|| TryEmptyAbsorber(args.User, uid, target, component, absorberSoln)
|
||||
|| TryFillAbsorber(args.User, uid, target, component, absorberSoln);
|
||||
}
|
||||
|
||||
private void ReleaseToFloor(EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution? absorbedSolution)
|
||||
/// <summary>
|
||||
/// Tries to create a puddle using solutions stored in the absorber entity.
|
||||
/// </summary>
|
||||
private bool TryCreatePuddle(EntityUid user, EntityCoordinates clickLocation, AbsorbentComponent absorbent, Solution absorberSoln)
|
||||
{
|
||||
if ((_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid)) // needs valid grid
|
||||
&& absorbedSolution is not null) // needs a solution to place on the tile
|
||||
{
|
||||
TileRef tile = mapGrid.GetTileRef(clickLocation);
|
||||
if (absorberSoln.CurrentVolume <= 0)
|
||||
return false;
|
||||
|
||||
// Drop some of the absorbed liquid onto the ground
|
||||
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorbedSolution.CurrentVolume); // The release amount specified on the absorbent component, or the amount currently absorbed (whichever is less).
|
||||
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorbedSolution, releaseAmount); // Remove releaseAmount of solution from the absorbent component
|
||||
_spillableSystem.SpillAt(tile, releasedSolution, puddlePrototypeId); // And spill it onto the tile.
|
||||
}
|
||||
if (!_mapManager.TryGetGrid(clickLocation.GetGridUid(EntityManager), out var mapGrid))
|
||||
return false;
|
||||
|
||||
var releaseAmount = FixedPoint2.Min(absorbent.ResidueAmount, absorberSoln.CurrentVolume);
|
||||
var releasedSolution = _solutionSystem.SplitSolution(absorbent.Owner, absorberSoln, releaseAmount);
|
||||
_spillableSystem.SpillAt(mapGrid.GetTileRef(clickLocation), releasedSolution, PuddlePrototypeId);
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-release-to-floor"), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Handles logic for our different types of valid target.
|
||||
// Checks for conditions that would prevent a doAfter from starting.
|
||||
private void HandleDoAfter(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, FixedPoint2 currentVolume, FixedPoint2 availableVolume)
|
||||
/// <summary>
|
||||
/// Attempt to fill an absorber from some drainable solution.
|
||||
/// </summary>
|
||||
private bool TryFillAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||
{
|
||||
// Below variables will be set within this function depending on what kind of target was clicked.
|
||||
// They will be passed to the OnTransferComplete if the doAfter succeeds.
|
||||
if (absorberSoln.AvailableVolume <= 0 || !TryComp(target, out DrainableSolutionComponent? drainable))
|
||||
return false;
|
||||
|
||||
EntityUid donor;
|
||||
EntityUid acceptor;
|
||||
string donorSolutionName;
|
||||
string acceptorSolutionName;
|
||||
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
|
||||
return false;
|
||||
|
||||
FixedPoint2 transferAmount;
|
||||
if (drainableSolution.CurrentVolume <= 0)
|
||||
{
|
||||
var msg = Loc.GetString("mopping-system-target-container-empty", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Let's transfer up to to half the tool's available capacity to the tool.
|
||||
var quantity = FixedPoint2.Max(component.PickupAmount, absorberSoln.AvailableVolume / 2);
|
||||
quantity = FixedPoint2.Min(quantity, drainableSolution.CurrentVolume);
|
||||
|
||||
DoMopInteraction(user, used, target, component, drainable.Solution, quantity, 1, "mopping-system-drainable-success", component.TransferSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Empty an absorber into a refillable solution.
|
||||
/// </summary>
|
||||
private bool TryEmptyAbsorber(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, Solution absorberSoln)
|
||||
{
|
||||
if (absorberSoln.CurrentVolume <= 0 || !TryComp(target, out RefillableSolutionComponent? refillable))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var targetSolution))
|
||||
return false;
|
||||
|
||||
var delay = 1.0f; //default do_after delay in seconds.
|
||||
string msg;
|
||||
SoundSpecifier sfx;
|
||||
|
||||
// For our purposes, if our target has a PuddleComponent, treat it as a puddle above all else.
|
||||
if (TryComp<PuddleComponent>(target, out var puddle))
|
||||
if (targetSolution.AvailableVolume <= 0)
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if(!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) // puddle Solution is null
|
||||
|| (puddleSolution.TotalVolume <= 0)) // puddle is completely empty
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (availableVolume < 0) // mop is completely full
|
||||
{
|
||||
msg = "mopping-system-tool-full";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("used", used))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
// adding to puddles
|
||||
else if (puddleSolution.TotalVolume < component.MopLowerLimit // if the puddle is too small for the tool to effectively absorb any more solution from it
|
||||
&& currentVolume > 0) // tool needs a solution to dilute the puddle with.
|
||||
{
|
||||
// Dilutes the puddle with some solution from the tool
|
||||
transferAmount = FixedPoint2.Max(component.ResidueAmount, currentVolume);
|
||||
TryTransfer(used, target, "absorbed", puddle.SolutionName, transferAmount); // Complete the transfer right away, with no doAfter.
|
||||
|
||||
sfx = component.TransferSound;
|
||||
SoundSystem.Play(sfx.GetSound(), Filter.Pvs(user), used); // Give instant feedback for diluting puddle, so that it's clear that the player is adding to the puddle (as opposed to other behaviours, which have a doAfter).
|
||||
|
||||
msg = "mopping-system-puddle-diluted";
|
||||
user.PopupMessage(user, Loc.GetString(msg)); // play message now because we are aborting.
|
||||
|
||||
return; // Do not begin a doAfter.
|
||||
}
|
||||
else
|
||||
{
|
||||
// Taking from puddles:
|
||||
|
||||
// Determine transferAmount:
|
||||
transferAmount = FixedPoint2.Min(component.PickupAmount, puddleSolution.TotalVolume, availableVolume);
|
||||
|
||||
// TODO: consider onelining this with the above, using additional args on Min()?
|
||||
if ((puddleSolution.TotalVolume - transferAmount) < component.MopLowerLimit) // If the transferAmount would bring the puddle below the MopLowerLimit
|
||||
{
|
||||
transferAmount = puddleSolution.TotalVolume - component.MopLowerLimit; // Then the transferAmount should bring the puddle down to the MopLowerLimit exactly
|
||||
}
|
||||
|
||||
donor = target; // the puddle Uid
|
||||
donorSolutionName = puddle.SolutionName;
|
||||
|
||||
acceptor = used; // the mop/tool Uid
|
||||
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
delay = (component.PickupAmount.Float() / 10.0f) * component.MopSpeed; // Delay should scale with PickupAmount, which represents the maximum we can pick up per click.
|
||||
msg = "mopping-system-puddle-success";
|
||||
sfx = component.PickupSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
msg = Loc.GetString("mopping-system-target-container-full", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
else if ((TryComp<RefillableSolutionComponent>(target, out var refillable)) // We can put solution from the tool into the target
|
||||
&& (currentVolume > 0)) // And the tool is wet
|
||||
|
||||
// check if the target container is too small (e.g. syringe)
|
||||
// TODO this should really be a tag or something, not a capacity check.
|
||||
if (targetSolution.MaxVolume <= FixedPoint2.New(20))
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if (!_solutionSystem.TryGetRefillableSolution(target, out var refillableSolution)) // refillable Solution is null
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (refillableSolution.AvailableVolume <= 0) // target container is full (liquid destination)
|
||||
{
|
||||
msg = "mopping-system-target-container-full";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
else if (refillableSolution.MaxVolume <= FixedPoint2.New(20)) // target container is too small (e.g. syringe)
|
||||
{
|
||||
msg = "mopping-system-target-container-too-small";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are aborting.
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Determine transferAmount
|
||||
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
|
||||
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
|
||||
{
|
||||
delay = 5.0f; // Should take much longer if you don't have a wringer
|
||||
|
||||
if ((currentVolume / (currentVolume + availableVolume) ) > 0.25) // mop is more than one-quarter full
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume * 0.6); // squeeze up to 60% of the solution from the mop.
|
||||
msg = "mopping-system-hand-squeeze-little-wet";
|
||||
|
||||
if ((currentVolume / (currentVolume + availableVolume) ) > 0.5) // if the mop is more than half full
|
||||
msg = "mopping-system-hand-squeeze-still-wet"; // overwrites the above
|
||||
|
||||
}
|
||||
else // mop is less than one-quarter full
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); // squeeze remainder of solution from the mop.
|
||||
msg = "mopping-system-hand-squeeze-dry";
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
transferAmount = FixedPoint2.Min(refillableSolution.AvailableVolume, currentVolume); //Transfer all liquid from the tool to the container, but only if it will fit.
|
||||
msg = "mopping-system-refillable-success";
|
||||
delay = 1.0f;
|
||||
}
|
||||
|
||||
donor = used; // the mop/tool Uid
|
||||
donorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
acceptor = target; // the refillable container's Uid
|
||||
acceptorSolutionName = refillable.Solution;
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
|
||||
sfx = component.TransferSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
msg = Loc.GetString("mopping-system-target-container-too-small", ("target", target));
|
||||
_popups.PopupEntity(msg, user, user);
|
||||
return true;
|
||||
}
|
||||
else if (TryComp<DrainableSolutionComponent>(target, out var drainable) // We can take solution from the target
|
||||
&& currentVolume <= 0 ) // tool is dry
|
||||
|
||||
float delay;
|
||||
FixedPoint2 quantity = absorberSoln.CurrentVolume;
|
||||
|
||||
// TODO this really needs cleaning up. Less magic numbers, more data-fields.
|
||||
|
||||
if (_tagSystem.HasTag(used, "Mop") // if the tool used is a literal mop (and not a sponge, rag, etc.)
|
||||
&& !_tagSystem.HasTag(target, "Wringer")) // and if the target does not have a wringer for properly drying the mop
|
||||
{
|
||||
// These return conditions will abort BEFORE the do_after is called:
|
||||
if (!_solutionSystem.TryGetDrainableSolution(target, out var drainableSolution))
|
||||
{
|
||||
return;
|
||||
}
|
||||
else if (drainableSolution.CurrentVolume <= 0) // target container is empty (liquid source)
|
||||
{
|
||||
msg = "mopping-system-target-container-empty";
|
||||
user.PopupMessage(user, Loc.GetString(msg, ("target", target))); // play message now because we are returning.
|
||||
return;
|
||||
}
|
||||
delay = 5.0f; // Should take much longer if you don't have a wringer
|
||||
|
||||
var frac = quantity / absorberSoln.MaxVolume;
|
||||
|
||||
// squeeze up to 60% of the solution from the mop if the mop is more than one-quarter full
|
||||
if (frac > 0.25)
|
||||
quantity *= 0.6;
|
||||
|
||||
if (frac > 0.5)
|
||||
msg = "mopping-system-hand-squeeze-still-wet";
|
||||
else if (frac > 0.5)
|
||||
msg = "mopping-system-hand-squeeze-little-wet";
|
||||
else
|
||||
{
|
||||
// Determine transferAmount
|
||||
transferAmount = FixedPoint2.Min(availableVolume * 0.5, drainableSolution.CurrentVolume); // Let's transfer up to to half the tool's available capacity to the tool.
|
||||
|
||||
donor = target; // the drainable container's Uid
|
||||
donorSolutionName = drainable.Solution;
|
||||
|
||||
acceptor = used; // the mop/tool Uid
|
||||
acceptorSolutionName = "absorbed"; // by definition on AbsorbentComponent
|
||||
|
||||
// Set delay/popup/sound if nondefault. Popup and sound will only play on a successful doAfter.
|
||||
// default delay is fine for this case.
|
||||
msg = "mopping-system-drainable-success";
|
||||
sfx = component.TransferSound;
|
||||
|
||||
DoMopInteraction(user, used, target, donor, acceptor, component, donorSolutionName, acceptorSolutionName, transferAmount, delay, msg, sfx);
|
||||
}
|
||||
msg = "mopping-system-hand-squeeze-dry";
|
||||
}
|
||||
else
|
||||
{
|
||||
msg = "mopping-system-refillable-success";
|
||||
delay = 1.0f;
|
||||
}
|
||||
|
||||
// negative quantity as we are removing solutions from the mop
|
||||
quantity = -FixedPoint2.Min(targetSolution.AvailableVolume, quantity);
|
||||
|
||||
DoMopInteraction(user, used, target, component, refillable.Solution, quantity, delay, msg, component.TransferSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, EntityUid donor, EntityUid acceptor,
|
||||
AbsorbentComponent component, string donorSolutionName, string acceptorSolutionName,
|
||||
/// <summary>
|
||||
/// Logic for an absorbing entity interacting with a puddle.
|
||||
/// </summary>
|
||||
private bool TryPuddleInteract(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent absorber, Solution absorberSoln)
|
||||
{
|
||||
if (!TryComp(target, out PuddleComponent? puddle))
|
||||
return false;
|
||||
|
||||
if (!_solutionSystem.TryGetSolution(target, puddle.SolutionName, out var puddleSolution) || puddleSolution.TotalVolume <= 0)
|
||||
return false;
|
||||
|
||||
FixedPoint2 quantity;
|
||||
|
||||
// Get lower limit for mopping
|
||||
FixedPoint2 lowerLimit = FixedPoint2.Zero;
|
||||
if (TryComp(target, out EvaporationComponent? evaporation)
|
||||
&& evaporation.EvaporationToggle
|
||||
&& evaporation.LowerLimit == 0)
|
||||
{
|
||||
lowerLimit = absorber.LowerLimit;
|
||||
}
|
||||
|
||||
// Can our absorber even absorb any liquid?
|
||||
if (puddleSolution.TotalVolume <= lowerLimit)
|
||||
{
|
||||
// Cannot absorb any more liquid. So clearly the user wants to add liquid to the puddle... right?
|
||||
// This is the old behavior and I CBF fixing this, for the record I don't like this.
|
||||
|
||||
quantity = FixedPoint2.Min(absorber.ResidueAmount, absorberSoln.CurrentVolume);
|
||||
if (quantity <= 0)
|
||||
return false;
|
||||
|
||||
// Dilutes the puddle with some solution from the tool
|
||||
_solutionSystem.TryTransferSolution(used, target, absorberSoln, puddleSolution, quantity);
|
||||
_audio.PlayPvs(absorber.TransferSound, used);
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-puddle-diluted"), user);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (absorberSoln.AvailableVolume < 0)
|
||||
{
|
||||
_popups.PopupEntity(Loc.GetString("mopping-system-tool-full", ("used", used)), user, user);
|
||||
return true;
|
||||
}
|
||||
|
||||
quantity = FixedPoint2.Min(absorber.PickupAmount, puddleSolution.TotalVolume - lowerLimit, absorberSoln.AvailableVolume);
|
||||
if (quantity <= 0)
|
||||
return false;
|
||||
|
||||
var delay = absorber.PickupAmount.Float() / absorber.Speed;
|
||||
DoMopInteraction(user, used, target, absorber, puddle.SolutionName, quantity, delay, "mopping-system-puddle-success", absorber.PickupSound);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void DoMopInteraction(EntityUid user, EntityUid used, EntityUid target, AbsorbentComponent component, string targetSolution,
|
||||
FixedPoint2 transferAmount, float delay, string msg, SoundSpecifier sfx)
|
||||
{
|
||||
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.2f,
|
||||
BroadcastCancelledEvent = new TransferCancelledEvent()
|
||||
{
|
||||
Target = target,
|
||||
Component = component // (the AbsorbentComponent)
|
||||
},
|
||||
BroadcastFinishedEvent = new TransferCompleteEvent()
|
||||
{
|
||||
User = user,
|
||||
Tool = used,
|
||||
Target = target,
|
||||
Donor = donor,
|
||||
Acceptor = acceptor,
|
||||
Component = component,
|
||||
DonorSolutionName = donorSolutionName,
|
||||
AcceptorSolutionName = acceptorSolutionName,
|
||||
Message = msg,
|
||||
Sound = sfx,
|
||||
TransferAmount = transferAmount
|
||||
}
|
||||
};
|
||||
|
||||
// Can't interact with too many entities at once.
|
||||
if (component.MaxInteractingEntities < component.InteractingEntities.Count + 1)
|
||||
return;
|
||||
@@ -301,60 +228,63 @@ public sealed class MoppingSystem : EntitySystem
|
||||
if (!component.InteractingEntities.Add(target))
|
||||
return;
|
||||
|
||||
var result = _doAfterSystem.WaitDoAfter(doAfterArgs);
|
||||
var doAfterArgs = new DoAfterEventArgs(user, delay, target: target)
|
||||
{
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
MovementThreshold = 0.2f,
|
||||
BroadcastCancelledEvent = new TransferCancelledEvent(target, component),
|
||||
BroadcastFinishedEvent = new TransferCompleteEvent(used, target, component, targetSolution, msg, sfx, transferAmount)
|
||||
};
|
||||
|
||||
_doAfterSystem.DoAfter(doAfterArgs);
|
||||
}
|
||||
|
||||
private void OnTransferComplete(TransferCompleteEvent ev)
|
||||
{
|
||||
SoundSystem.Play(ev.Sound.GetSound(), Filter.Pvs(ev.User), ev.Tool); // Play the After SFX
|
||||
|
||||
ev.User.PopupMessage(ev.User, Loc.GetString(ev.Message, ("target", ev.Target), ("used", ev.Tool))); // Play the After popup message
|
||||
|
||||
TryTransfer(ev.Donor, ev.Acceptor, ev.DonorSolutionName, ev.AcceptorSolutionName, ev.TransferAmount);
|
||||
|
||||
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
|
||||
return;
|
||||
_audio.PlayPvs(ev.Sound, ev.Tool);
|
||||
_popups.PopupEntity(ev.Message, ev.Tool);
|
||||
_solutionSystem.TryTransferSolution(ev.Target, ev.Tool, ev.TargetSolution, AbsorbentComponent.SolutionName, ev.TransferAmount);
|
||||
ev.Component.InteractingEntities.Remove(ev.Target);
|
||||
}
|
||||
|
||||
private void OnTransferCancelled(TransferCancelledEvent ev)
|
||||
{
|
||||
if (!ev.Component.Deleted)
|
||||
ev.Component.InteractingEntities.Remove(ev.Target); // Tell the absorbentComponent that we have stopped interacting with the target.
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
private void TryTransfer(EntityUid donor, EntityUid acceptor, string donorSolutionName, string acceptorSolutionName, FixedPoint2 transferAmount)
|
||||
{
|
||||
if (_solutionSystem.TryGetSolution(donor, donorSolutionName, out var donorSolution) // If the donor solution is valid
|
||||
&& _solutionSystem.TryGetSolution(acceptor, acceptorSolutionName, out var acceptorSolution)) // And the acceptor solution is valid
|
||||
{
|
||||
var solutionToTransfer = _solutionSystem.SplitSolution(donor, donorSolution, transferAmount); // Split a portion of the donor solution
|
||||
_solutionSystem.TryAddSolution(acceptor, acceptorSolution, solutionToTransfer); // And add it to the acceptor solution
|
||||
}
|
||||
ev.Component.InteractingEntities.Remove(ev.Target);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public sealed class TransferCompleteEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User;
|
||||
public EntityUid Tool;
|
||||
public EntityUid Target;
|
||||
public EntityUid Donor;
|
||||
public EntityUid Acceptor;
|
||||
public AbsorbentComponent Component { get; init; } = default!;
|
||||
public string DonorSolutionName = "";
|
||||
public string AcceptorSolutionName = "";
|
||||
public string Message = "";
|
||||
public SoundSpecifier Sound { get; init; } = default!;
|
||||
public FixedPoint2 TransferAmount;
|
||||
public readonly EntityUid Tool;
|
||||
public readonly EntityUid Target;
|
||||
public readonly AbsorbentComponent Component;
|
||||
public readonly string TargetSolution;
|
||||
public readonly string Message;
|
||||
public readonly SoundSpecifier Sound;
|
||||
public readonly FixedPoint2 TransferAmount;
|
||||
|
||||
public TransferCompleteEvent(EntityUid tool, EntityUid target, AbsorbentComponent component, string targetSolution, string message, SoundSpecifier sound, FixedPoint2 transferAmount)
|
||||
{
|
||||
Tool = tool;
|
||||
Target = target;
|
||||
Component = component;
|
||||
TargetSolution = targetSolution;
|
||||
Message = Loc.GetString(message, ("target", target), ("used", tool));
|
||||
Sound = sound;
|
||||
TransferAmount = transferAmount;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class TransferCancelledEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid Target;
|
||||
public AbsorbentComponent Component { get; init; } = default!;
|
||||
public readonly EntityUid Target;
|
||||
public readonly AbsorbentComponent Component;
|
||||
|
||||
public TransferCancelledEvent(EntityUid target, AbsorbentComponent component)
|
||||
{
|
||||
Target = target;
|
||||
Component = component;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
using Content.Server.Chemistry.Components.SolutionManager;
|
||||
using Content.Server.Chemistry.EntitySystems;
|
||||
using Content.Server.Fluids.Components;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Fluids;
|
||||
using Content.Shared.Slippery;
|
||||
using Content.Shared.StepTrigger.Components;
|
||||
using Content.Shared.StepTrigger.Systems;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Solution = Content.Shared.Chemistry.Components.Solution;
|
||||
|
||||
namespace Content.Server.Fluids.EntitySystems
|
||||
@@ -24,9 +19,10 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
[Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
|
||||
[Dependency] private readonly FluidSpreaderSystem _fluidSpreaderSystem = default!;
|
||||
[Dependency] private readonly StepTriggerSystem _stepTrigger = default!;
|
||||
[Dependency] private readonly SlipperySystem _slipSystem = default!;
|
||||
[Dependency] private readonly EvaporationSystem _evaporationSystem = default!;
|
||||
|
||||
// Using local deletion queue instead of the standard queue so that we can easily "undelete" if a puddle
|
||||
// loses & then gains reagents in a single tick.
|
||||
private HashSet<EntityUid> _deletionQueue = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -39,6 +35,16 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
SubscribeLocalEvent<PuddleComponent, ComponentInit>(OnPuddleInit);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
foreach (var ent in _deletionQueue)
|
||||
{
|
||||
Del(ent);
|
||||
}
|
||||
_deletionQueue.Clear();
|
||||
}
|
||||
|
||||
private void OnPuddleInit(EntityUid uid, PuddleComponent component, ComponentInit args)
|
||||
{
|
||||
var solution = _solutionContainerSystem.EnsureSolution(uid, component.SolutionName);
|
||||
@@ -47,6 +53,16 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
|
||||
private void OnSolutionUpdate(EntityUid uid, PuddleComponent component, SolutionChangedEvent args)
|
||||
{
|
||||
if (args.Solution.Name != component.SolutionName)
|
||||
return;
|
||||
|
||||
if (args.Solution.CurrentVolume <= 0)
|
||||
{
|
||||
_deletionQueue.Add(uid);
|
||||
return;
|
||||
}
|
||||
|
||||
_deletionQueue.Remove(uid);
|
||||
UpdateSlip(uid, component);
|
||||
UpdateAppearance(uid, component);
|
||||
}
|
||||
@@ -157,13 +173,12 @@ namespace Content.Server.Fluids.EntitySystems
|
||||
}
|
||||
|
||||
solution.AddSolution(addedSolution);
|
||||
_solutionContainerSystem.UpdateChemicals(puddleUid, solution, true);
|
||||
if (checkForOverflow && IsOverflowing(puddleUid, puddleComponent))
|
||||
{
|
||||
_fluidSpreaderSystem.AddOverflowingPuddle(puddleComponent.Owner, puddleComponent);
|
||||
}
|
||||
|
||||
RaiseLocalEvent(puddleComponent.Owner, new SolutionChangedEvent(), true);
|
||||
|
||||
if (!sound)
|
||||
{
|
||||
return true;
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Server.Roles;
|
||||
using Content.Server.Traitor;
|
||||
using Content.Server.Traitor.Uplink;
|
||||
using Content.Server.MobState;
|
||||
using Content.Server.NPC.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Dataset;
|
||||
using Content.Shared.Roles;
|
||||
@@ -26,7 +27,7 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IObjectivesManager _objectivesManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly FactionSystem _faction = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
[Dependency] private readonly UplinkSystem _uplink = default!;
|
||||
|
||||
@@ -168,6 +169,12 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (mind.OwnedEntity is not { } entity)
|
||||
{
|
||||
Logger.ErrorS("preset", "Mind picked for traitor did not have an attached entity.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// creadth: we need to create uplink for the antag.
|
||||
// PDA should be in place already
|
||||
DebugTools.AssertNotNull(mind.OwnedEntity);
|
||||
@@ -186,6 +193,9 @@ public sealed class TraitorRuleSystem : GameRuleSystem
|
||||
Traitors.Add(traitorRole);
|
||||
traitorRole.GreetTraitor(Codewords);
|
||||
|
||||
_faction.RemoveFaction(entity, "NanoTrasen", false);
|
||||
_faction.AddFaction(entity, "Syndicate");
|
||||
|
||||
var maxDifficulty = _cfg.GetCVar(CCVars.TraitorMaxDifficulty);
|
||||
var maxPicks = _cfg.GetCVar(CCVars.TraitorMaxPicks);
|
||||
|
||||
|
||||
21
Content.Server/Magic/Events/ChangeComponentsSpellEvent.cs
Normal file
21
Content.Server/Magic/Events/ChangeComponentsSpellEvent.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Content.Shared.Actions;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Magic.Events;
|
||||
|
||||
/// <summary>
|
||||
/// Spell that uses the magic of ECS to add & remove components. Components are first removed, then added.
|
||||
/// </summary>
|
||||
public sealed class ChangeComponentsSpellEvent : EntityTargetActionEvent
|
||||
{
|
||||
// TODO allow it to set component data-fields?
|
||||
// for now a Hackish way to do that is to remove & add, but that doesn't allow you to selectively set specific data fields.
|
||||
|
||||
[DataField("toAdd")]
|
||||
[AlwaysPushInheritance]
|
||||
public EntityPrototype.ComponentRegistry ToAdd = new();
|
||||
|
||||
[DataField("toRemove")]
|
||||
[AlwaysPushInheritance]
|
||||
public HashSet<string> ToRemove = new();
|
||||
}
|
||||
@@ -18,11 +18,13 @@ using Content.Shared.Spawners.Components;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Serialization.Manager;
|
||||
|
||||
namespace Content.Server.Magic;
|
||||
|
||||
@@ -31,6 +33,8 @@ namespace Content.Server.Magic;
|
||||
/// </summary>
|
||||
public sealed class MagicSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ISerializationManager _seriMan = default!;
|
||||
[Dependency] private readonly IComponentFactory _compFact = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
@@ -58,6 +62,7 @@ public sealed class MagicSystem : EntitySystem
|
||||
SubscribeLocalEvent<SmiteSpellEvent>(OnSmiteSpell);
|
||||
SubscribeLocalEvent<WorldSpawnSpellEvent>(OnWorldSpawn);
|
||||
SubscribeLocalEvent<ProjectileSpellEvent>(OnProjectileSpell);
|
||||
SubscribeLocalEvent<ChangeComponentsSpellEvent>(OnChangeComponentsSpell);
|
||||
}
|
||||
|
||||
private void OnInit(EntityUid uid, SpellbookComponent component, ComponentInit args)
|
||||
@@ -173,6 +178,27 @@ public sealed class MagicSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnChangeComponentsSpell(ChangeComponentsSpellEvent ev)
|
||||
{
|
||||
foreach (var toRemove in ev.ToRemove)
|
||||
{
|
||||
if (_compFact.TryGetRegistration(toRemove, out var registration))
|
||||
RemComp(ev.Target, registration.Type);
|
||||
}
|
||||
|
||||
foreach (var (name, data) in ev.ToAdd)
|
||||
{
|
||||
if (HasComp(ev.Target, data.Component.GetType()))
|
||||
continue;
|
||||
|
||||
var component = (Component) _compFact.GetComponent(name);
|
||||
component.Owner = ev.Target;
|
||||
var temp = (object) component;
|
||||
_seriMan.CopyTo(data.Component, ref temp);
|
||||
EntityManager.AddComponent(ev.Target, (Component) temp!);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EntityCoordinates> GetSpawnPositions(TransformComponent casterXform, MagicSpawnData data)
|
||||
{
|
||||
switch (data)
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Server.Radio.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Radio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Radio.Components;
|
||||
|
||||
@@ -15,6 +16,15 @@ public sealed class HeadsetComponent : Component
|
||||
[DataField("channels", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
|
||||
public readonly HashSet<string> Channels = new() { "Common" };
|
||||
|
||||
// Maybe make the defaultChannel an actual channel type some day, and use that for parsing messages
|
||||
// [DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<RadioChannelPrototype>))]
|
||||
// public readonly HashSet<string> defaultChannel = new();
|
||||
|
||||
|
||||
|
||||
[DataField("defaultChannel", customTypeSerializer: typeof(PrototypeIdSerializer<RadioChannelPrototype>))]
|
||||
public readonly string? defaultChannel;
|
||||
|
||||
[DataField("enabled")]
|
||||
public bool Enabled = true;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Popups;
|
||||
using Content.Shared.Rotatable;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Server.Rotatable
|
||||
{
|
||||
@@ -38,7 +39,7 @@ namespace Content.Server.Rotatable
|
||||
|
||||
// Check if the object is anchored, and whether we are still allowed to rotate it.
|
||||
if (!component.RotateWhileAnchored &&
|
||||
EntityManager.TryGetComponent(component.Owner, out IPhysBody? physics) &&
|
||||
EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? physics) &&
|
||||
physics.BodyType == BodyType.Static)
|
||||
return;
|
||||
|
||||
@@ -82,7 +83,7 @@ namespace Content.Server.Rotatable
|
||||
/// </summary>
|
||||
public void TryFlip(FlippableComponent component, EntityUid user)
|
||||
{
|
||||
if (EntityManager.TryGetComponent(component.Owner, out IPhysBody? physics) &&
|
||||
if (EntityManager.TryGetComponent(component.Owner, out PhysicsComponent? physics) &&
|
||||
physics.BodyType == BodyType.Static)
|
||||
{
|
||||
component.Owner.PopupMessage(user, Loc.GetString("flippable-component-try-flip-is-stuck"));
|
||||
|
||||
@@ -126,7 +126,6 @@ public sealed partial class ShuttleSystem
|
||||
var targetGridGrid = Comp<MapGridComponent>(targetGrid);
|
||||
var targetGridXform = xformQuery.GetComponent(targetGrid);
|
||||
var targetGridAngle = targetGridXform.WorldRotation.Reduced();
|
||||
var targetGridRotation = targetGridAngle.ToVec();
|
||||
|
||||
var shuttleDocks = GetDocks(component.Owner);
|
||||
var shuttleAABB = Comp<MapGridComponent>(component.Owner).LocalAABB;
|
||||
@@ -147,7 +146,7 @@ public sealed partial class ShuttleSystem
|
||||
if (!CanDock(
|
||||
shuttleDock, shuttleDockXform,
|
||||
gridDock, gridXform,
|
||||
targetGridRotation,
|
||||
targetGridAngle,
|
||||
shuttleAABB,
|
||||
targetGridGrid,
|
||||
out var dockedAABB,
|
||||
@@ -191,7 +190,7 @@ public sealed partial class ShuttleSystem
|
||||
xformQuery.GetComponent(other.Owner),
|
||||
otherGrid,
|
||||
xformQuery.GetComponent(otherGrid.Owner),
|
||||
targetGridRotation,
|
||||
targetGridAngle,
|
||||
shuttleAABB, targetGridGrid,
|
||||
out var otherDockedAABB,
|
||||
out _,
|
||||
@@ -203,16 +202,12 @@ public sealed partial class ShuttleSystem
|
||||
}
|
||||
}
|
||||
|
||||
var spawnRotation = shuttleDockXform.LocalRotation +
|
||||
gridXform.LocalRotation +
|
||||
targetGridXform.LocalRotation;
|
||||
|
||||
validDockConfigs.Add(new DockingConfig()
|
||||
{
|
||||
Docks = dockedPorts,
|
||||
Area = dockedAABB.Value,
|
||||
Coordinates = spawnPosition,
|
||||
Angle = spawnRotation,
|
||||
Angle = targetAngle,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -304,40 +299,46 @@ public sealed partial class ShuttleSystem
|
||||
/// </summary>
|
||||
private bool CanDock(
|
||||
DockingComponent shuttleDock,
|
||||
TransformComponent shuttleXform,
|
||||
TransformComponent shuttleDockXform,
|
||||
DockingComponent gridDock,
|
||||
TransformComponent gridXform,
|
||||
Vector2 targetGridRotation,
|
||||
TransformComponent gridDockXform,
|
||||
Angle targetGridRotation,
|
||||
Box2 shuttleAABB,
|
||||
MapGridComponent grid,
|
||||
[NotNullWhen(true)] out Box2? shuttleDockedAABB,
|
||||
out Matrix3 matty,
|
||||
out Vector2 gridRotation)
|
||||
out Angle gridRotation)
|
||||
{
|
||||
gridRotation = Vector2.Zero;
|
||||
gridRotation = Angle.Zero;
|
||||
matty = Matrix3.Identity;
|
||||
shuttleDockedAABB = null;
|
||||
|
||||
if (shuttleDock.Docked ||
|
||||
gridDock.Docked ||
|
||||
!shuttleXform.Anchored ||
|
||||
!gridXform.Anchored)
|
||||
!shuttleDockXform.Anchored ||
|
||||
!gridDockXform.Anchored)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// First, get the station dock's position relative to the shuttle, this is where we rotate it around
|
||||
var stationDockPos = shuttleXform.LocalPosition +
|
||||
shuttleXform.LocalRotation.RotateVec(new Vector2(0f, -1f));
|
||||
var stationDockPos = shuttleDockXform.LocalPosition +
|
||||
shuttleDockXform.LocalRotation.RotateVec(new Vector2(0f, -1f));
|
||||
|
||||
var stationDockMatrix = Matrix3.CreateInverseTransform(stationDockPos, -shuttleXform.LocalRotation);
|
||||
var gridXformMatrix = Matrix3.CreateTransform(gridXform.LocalPosition, gridXform.LocalRotation);
|
||||
// Need to invert the grid's angle.
|
||||
var shuttleDockAngle = shuttleDockXform.LocalRotation;
|
||||
var gridDockAngle = gridDockXform.LocalRotation.Opposite();
|
||||
|
||||
var stationDockMatrix = Matrix3.CreateInverseTransform(stationDockPos, shuttleDockAngle);
|
||||
var gridXformMatrix = Matrix3.CreateTransform(gridDockXform.LocalPosition, gridDockAngle);
|
||||
Matrix3.Multiply(in stationDockMatrix, in gridXformMatrix, out matty);
|
||||
shuttleDockedAABB = matty.TransformBox(shuttleAABB);
|
||||
// Rounding moment
|
||||
shuttleDockedAABB = shuttleDockedAABB.Value.Enlarged(-0.01f);
|
||||
|
||||
if (!ValidSpawn(grid, shuttleDockedAABB.Value)) return false;
|
||||
|
||||
gridRotation = matty.Transform(targetGridRotation);
|
||||
gridRotation = targetGridRotation + gridDockAngle - shuttleDockAngle;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -378,7 +379,7 @@ public sealed partial class ShuttleSystem
|
||||
_commsConsole.UpdateCommsConsoleInterface();
|
||||
}
|
||||
|
||||
private List<DockingComponent> GetDocks(EntityUid uid)
|
||||
public List<DockingComponent> GetDocks(EntityUid uid)
|
||||
{
|
||||
var result = new List<DockingComponent>();
|
||||
|
||||
|
||||
@@ -15,20 +15,12 @@ namespace Content.Server.Singularity.Components
|
||||
// Whether the power switch is on AND the machine has enough power (so is actively firing)
|
||||
[ViewVariables] public bool IsPowered;
|
||||
|
||||
// For the "emitter fired" sound
|
||||
public const float Variation = 0.25f;
|
||||
public const float Volume = 0.5f;
|
||||
public const float Distance = 6f;
|
||||
|
||||
/// <summary>
|
||||
/// counts the number of consecutive shots fired.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public int FireShotCounter;
|
||||
|
||||
[DataField("fireSound")]
|
||||
public SoundSpecifier FireSound = new SoundPathSpecifier("/Audio/Weapons/emitter.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// The entity that is spawned when the emitter fires.
|
||||
/// </summary>
|
||||
|
||||
@@ -6,16 +6,17 @@ using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Projectiles;
|
||||
using Content.Server.Singularity.Components;
|
||||
using Content.Server.Storage.Components;
|
||||
using Content.Server.Weapons.Ranged.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Singularity.Components;
|
||||
using Content.Shared.Weapons.Ranged.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using Timer = Robust.Shared.Timing.Timer;
|
||||
@@ -28,10 +29,9 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly ProjectileSystem _projectile = default!;
|
||||
[Dependency] private readonly SharedPhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly GunSystem _gun = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -207,33 +207,16 @@ namespace Content.Server.Singularity.EntitySystems
|
||||
private void Fire(EmitterComponent component)
|
||||
{
|
||||
var uid = component.Owner;
|
||||
var projectile = EntityManager.SpawnEntity(component.BoltType, EntityManager.GetComponent<TransformComponent>(uid).Coordinates);
|
||||
|
||||
if (!EntityManager.TryGetComponent<PhysicsComponent?>(projectile, out var physicsComponent))
|
||||
{
|
||||
Logger.Error("Emitter tried firing a bolt, but it was spawned without a PhysicsComponent");
|
||||
if (!TryComp<GunComponent>(uid, out var guncomp))
|
||||
return;
|
||||
}
|
||||
|
||||
physicsComponent.BodyStatus = BodyStatus.InAir;
|
||||
var xform = Transform(uid);
|
||||
var ent = Spawn(component.BoltType, xform.Coordinates);
|
||||
var proj = EnsureComp<ProjectileComponent>(ent);
|
||||
_projectile.SetShooter(proj, uid);
|
||||
|
||||
if (!EntityManager.TryGetComponent<ProjectileComponent?>(projectile, out var projectileComponent))
|
||||
{
|
||||
Logger.Error("Emitter tried firing a bolt, but it was spawned without a ProjectileComponent");
|
||||
return;
|
||||
}
|
||||
|
||||
_projectile.SetShooter(projectileComponent, component.Owner);
|
||||
|
||||
var worldRotation = Transform(uid).WorldRotation;
|
||||
_physics.SetLinearVelocity(physicsComponent, worldRotation.ToWorldVec() * 20f);
|
||||
Transform(projectile).WorldRotation = worldRotation;
|
||||
|
||||
// TODO: Move to projectile's code.
|
||||
Timer.Spawn(3000, () => EntityManager.DeleteEntity(projectile));
|
||||
|
||||
_audio.PlayPvs(component.FireSound, component.Owner,
|
||||
AudioParams.Default.WithVariation(EmitterComponent.Variation).WithVolume(EmitterComponent.Volume).WithMaxDistance(EmitterComponent.Distance));
|
||||
var targetPos = new EntityCoordinates(uid, (0, -1));
|
||||
_gun.Shoot(guncomp, ent, xform.Coordinates, targetPos);
|
||||
}
|
||||
|
||||
private void UpdateAppearance(EmitterComponent component)
|
||||
|
||||
@@ -18,6 +18,7 @@ using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Storage.EntitySystems;
|
||||
@@ -320,7 +321,7 @@ public sealed class EntityStorageSystem : EntitySystem
|
||||
if (toAdd == container)
|
||||
return false;
|
||||
|
||||
if (TryComp<IPhysBody>(toAdd, out var phys))
|
||||
if (TryComp<PhysicsComponent>(toAdd, out var phys))
|
||||
{
|
||||
if (component.MaxSize < phys.GetWorldAABB().Size.X || component.MaxSize < phys.GetWorldAABB().Size.Y)
|
||||
return false;
|
||||
|
||||
@@ -5,7 +5,6 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Item;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Storage.EntitySystems
|
||||
{
|
||||
@@ -29,7 +28,7 @@ namespace Content.Server.Storage.EntitySystems
|
||||
|
||||
private void OnDestroyed(EntityUid uid, SecretStashComponent component, DestructionEventArgs args)
|
||||
{
|
||||
component.ItemContainer.EmptyContainer();
|
||||
_containerSystem.EmptyContainer(component.ItemContainer, attachToGridOrMap: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
122
Content.Server/Teleportation/HandTeleporterSystem.cs
Normal file
122
Content.Server/Teleportation/HandTeleporterSystem.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
using System.Threading;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Shared.Interaction.Events;
|
||||
using Content.Shared.Teleportation.Components;
|
||||
using Content.Shared.Teleportation.Systems;
|
||||
using Robust.Server.GameObjects;
|
||||
|
||||
namespace Content.Server.Teleportation;
|
||||
|
||||
/// <summary>
|
||||
/// This handles creating portals from a hand teleporter.
|
||||
/// </summary>
|
||||
public sealed class HandTeleporterSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly LinkedEntitySystem _link = default!;
|
||||
[Dependency] private readonly AudioSystem _audio = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doafter = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<HandTeleporterComponent, UseInHandEvent>(OnUseInHand);
|
||||
|
||||
SubscribeLocalEvent<HandTeleporterComponent, HandTeleporterSuccessEvent>(OnPortalSuccess);
|
||||
SubscribeLocalEvent<HandTeleporterComponent, HandTeleporterCancelledEvent>(OnPortalCancelled);
|
||||
}
|
||||
|
||||
private void OnPortalSuccess(EntityUid uid, HandTeleporterComponent component, HandTeleporterSuccessEvent args)
|
||||
{
|
||||
component.CancelToken = null;
|
||||
HandlePortalUpdating(uid, component, args.User);
|
||||
}
|
||||
|
||||
private void OnPortalCancelled(EntityUid uid, HandTeleporterComponent component, HandTeleporterCancelledEvent args)
|
||||
{
|
||||
component.CancelToken = null;
|
||||
}
|
||||
|
||||
private void OnUseInHand(EntityUid uid, HandTeleporterComponent component, UseInHandEvent args)
|
||||
{
|
||||
if (Deleted(component.FirstPortal))
|
||||
component.FirstPortal = null;
|
||||
|
||||
if (Deleted(component.SecondPortal))
|
||||
component.SecondPortal = null;
|
||||
|
||||
if (component.CancelToken != null)
|
||||
{
|
||||
component.CancelToken.Cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.FirstPortal != null && component.SecondPortal != null)
|
||||
{
|
||||
// handle removing portals immediately as opposed to a doafter
|
||||
HandlePortalUpdating(uid, component, args.User);
|
||||
}
|
||||
else
|
||||
{
|
||||
var xform = Transform(args.User);
|
||||
if (xform.ParentUid != xform.GridUid)
|
||||
return;
|
||||
|
||||
component.CancelToken = new CancellationTokenSource();
|
||||
var doafterArgs = new DoAfterEventArgs(args.User, component.PortalCreationDelay,
|
||||
component.CancelToken.Token, used: uid)
|
||||
{
|
||||
BreakOnDamage = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnUserMove = true,
|
||||
MovementThreshold = 0.5f,
|
||||
UsedCancelledEvent = new HandTeleporterCancelledEvent(),
|
||||
UsedFinishedEvent = new HandTeleporterSuccessEvent(args.User)
|
||||
};
|
||||
|
||||
_doafter.DoAfter(doafterArgs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Creates or removes a portal given the state of the hand teleporter.
|
||||
/// </summary>
|
||||
private void HandlePortalUpdating(EntityUid uid, HandTeleporterComponent component, EntityUid user)
|
||||
{
|
||||
if (Deleted(user))
|
||||
return;
|
||||
|
||||
var xform = Transform(user);
|
||||
|
||||
// Create the first portal.
|
||||
if (component.FirstPortal == null && component.SecondPortal == null)
|
||||
{
|
||||
// don't portal
|
||||
if (xform.ParentUid != xform.GridUid)
|
||||
return;
|
||||
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.FirstPortal = Spawn(component.FirstPortalPrototype, Transform(user).Coordinates);
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
}
|
||||
else if (component.SecondPortal == null)
|
||||
{
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(user);
|
||||
timeout.EnteredPortal = null;
|
||||
component.SecondPortal = Spawn(component.SecondPortalPrototype, Transform(user).Coordinates);
|
||||
_link.TryLink(component.FirstPortal!.Value, component.SecondPortal.Value, true);
|
||||
_audio.PlayPvs(component.NewPortalSound, uid);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clear both portals
|
||||
QueueDel(component.FirstPortal!.Value);
|
||||
QueueDel(component.SecondPortal!.Value);
|
||||
|
||||
component.FirstPortal = null;
|
||||
component.SecondPortal = null;
|
||||
_audio.PlayPvs(component.ClearPortalsSound, uid);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ using Content.Shared.Atmos;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Server.Temperature.Components
|
||||
{
|
||||
@@ -51,7 +52,7 @@ namespace Content.Server.Temperature.Components
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<IPhysBody?>(Owner, out var physics) && physics.FixturesMass != 0)
|
||||
if (IoCManager.Resolve<IEntityManager>().TryGetComponent<PhysicsComponent?>(Owner, out var physics) && physics.FixturesMass != 0)
|
||||
{
|
||||
return SpecificHeat * physics.FixturesMass;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Tools.Components;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Tools.Components;
|
||||
@@ -7,6 +9,7 @@ namespace Content.Server.Tools.Systems;
|
||||
|
||||
public sealed class WeldableSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ToolSystem _toolSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -64,6 +67,9 @@ public sealed class WeldableSystem : EntitySystem
|
||||
component.WeldingTime.Seconds, component.WeldingQuality,
|
||||
new WeldFinishedEvent(user, tool), new WeldCancelledEvent(), uid);
|
||||
|
||||
// Log attempt
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} is {(component.IsWelded ? "un" : "")}welding {ToPrettyString(uid):target} at {Transform(uid).Coordinates:targetlocation}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -79,6 +85,9 @@ public sealed class WeldableSystem : EntitySystem
|
||||
RaiseLocalEvent(uid, new WeldableChangedEvent(component.IsWelded), true);
|
||||
|
||||
UpdateAppearance(uid, component);
|
||||
|
||||
// Log success
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} {(!component.IsWelded ? "un" : "")}welded {ToPrettyString(uid):target}");
|
||||
}
|
||||
|
||||
private void OnWeldCanceled(EntityUid uid, WeldableComponent component, WeldCancelledEvent args)
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Threading;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.Server.VendingMachines.Restock
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed class VendingMachineRestockComponent : Component
|
||||
{
|
||||
public CancellationTokenSource? CancelToken;
|
||||
|
||||
/// <summary>
|
||||
/// The time (in seconds) that it takes to restock a machine.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("restockDelay")]
|
||||
public TimeSpan RestockDelay = TimeSpan.FromSeconds(8.0f);
|
||||
|
||||
/// <summary>
|
||||
/// What sort of machine inventory does this restock?
|
||||
/// This is checked against the VendingMachineComponent's pack value.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("canRestock", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<VendingMachineInventoryPrototype>))]
|
||||
public HashSet<string> CanRestock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sound that plays when starting to restock a machine.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("soundRestockStart")]
|
||||
public SoundSpecifier SoundRestockStart = new SoundPathSpecifier("/Audio/Machines/vending_restock_start.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Sound that plays when finished restocking a machine.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("soundRestockDone")]
|
||||
public SoundSpecifier SoundRestockDone = new SoundPathSpecifier("/Audio/Machines/vending_restock_done.ogg");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.VendingMachines;
|
||||
|
||||
namespace Content.Server.VendingMachines.Restock
|
||||
{
|
||||
public sealed class VendingMachineRestockSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly AudioSystem _audioSystem = default!;
|
||||
[Dependency] private readonly PricingSystem _pricingSystem = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<VendingMachineRestockComponent, AfterInteractEvent>(OnAfterInteract);
|
||||
SubscribeLocalEvent<VendingMachineRestockComponent, PriceCalculationEvent>(OnPriceCalculation);
|
||||
SubscribeLocalEvent<VendingMachineRestockComponent, RestockCancelledEvent>(OnRestockCancelled);
|
||||
}
|
||||
|
||||
public bool TryAccessMachine(EntityUid uid,
|
||||
VendingMachineRestockComponent component,
|
||||
VendingMachineComponent machineComponent,
|
||||
EntityUid user,
|
||||
EntityUid target)
|
||||
{
|
||||
if (!TryComp<WiresComponent>(target, out var wires) || !wires.IsPanelOpen) {
|
||||
_popupSystem.PopupCursor(Loc.GetString("vending-machine-restock-needs-panel-open",
|
||||
("this", uid),
|
||||
("user", user),
|
||||
("target", target)),
|
||||
user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryMatchPackageToMachine(EntityUid uid,
|
||||
VendingMachineRestockComponent component,
|
||||
VendingMachineComponent machineComponent,
|
||||
EntityUid user,
|
||||
EntityUid target)
|
||||
{
|
||||
if (!component.CanRestock.Contains(machineComponent.PackPrototypeId)) {
|
||||
_popupSystem.PopupCursor(Loc.GetString("vending-machine-restock-invalid-inventory",
|
||||
("this", uid),
|
||||
("user", user),
|
||||
("target", target)
|
||||
),
|
||||
user);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void OnAfterInteract(EntityUid uid, VendingMachineRestockComponent component, AfterInteractEvent args)
|
||||
{
|
||||
if (component.CancelToken != null || args.Target == null || !args.CanReach)
|
||||
return;
|
||||
|
||||
if (!TryComp<VendingMachineComponent>(args.Target, out var machineComponent))
|
||||
return;
|
||||
|
||||
if (!TryMatchPackageToMachine(uid, component, machineComponent, args.User, args.Target.Value))
|
||||
return;
|
||||
|
||||
if (!TryAccessMachine(uid, component, machineComponent, args.User, args.Target.Value))
|
||||
return;
|
||||
|
||||
component.CancelToken = new CancellationTokenSource();
|
||||
|
||||
_doAfterSystem.DoAfter(new DoAfterEventArgs(
|
||||
args.User,
|
||||
(float) component.RestockDelay.TotalSeconds,
|
||||
component.CancelToken.Token,
|
||||
args.Target,
|
||||
args.Used)
|
||||
{
|
||||
TargetFinishedEvent = new VendingMachineRestockEvent(args.User, uid),
|
||||
UsedCancelledEvent = new RestockCancelledEvent(),
|
||||
BreakOnTargetMove = true,
|
||||
BreakOnUserMove = true,
|
||||
BreakOnStun = true,
|
||||
BreakOnDamage = true,
|
||||
NeedHand = true
|
||||
});
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("vending-machine-restock-start",
|
||||
("this", uid),
|
||||
("user", args.User),
|
||||
("target", args.Target)
|
||||
),
|
||||
args.User,
|
||||
PopupType.Medium);
|
||||
|
||||
_audioSystem.PlayPvs(component.SoundRestockStart, component.Owner,
|
||||
AudioParams.Default
|
||||
.WithVolume(-2f)
|
||||
.WithVariation(0.2f));
|
||||
}
|
||||
|
||||
private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args)
|
||||
{
|
||||
List<double> priceSets = new();
|
||||
|
||||
// Find the most expensive inventory and use that as the highest price.
|
||||
foreach (var vendingInventory in component.CanRestock)
|
||||
{
|
||||
double total = 0;
|
||||
|
||||
if (_prototypeManager.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype))
|
||||
{
|
||||
foreach (var (item, amount) in inventoryPrototype.StartingInventory)
|
||||
{
|
||||
if (_prototypeManager.TryIndex(item, out EntityPrototype? entity))
|
||||
total += _pricingSystem.GetEstimatedPrice(entity) * amount;
|
||||
}
|
||||
}
|
||||
|
||||
priceSets.Add(total);
|
||||
}
|
||||
|
||||
args.Price += priceSets.Max();
|
||||
}
|
||||
|
||||
private void OnRestockCancelled(EntityUid uid, VendingMachineRestockComponent component, RestockCancelledEvent args)
|
||||
{
|
||||
component.CancelToken?.Cancel();
|
||||
component.CancelToken = null;
|
||||
}
|
||||
|
||||
public readonly struct RestockCancelledEvent { }
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Popups;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.UserInterface;
|
||||
using Content.Server.VendingMachines.Restock;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Actions;
|
||||
@@ -10,6 +11,7 @@ using Content.Shared.Actions.ActionTypes;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Destructible;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Throwing;
|
||||
using Content.Shared.VendingMachines;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -51,6 +53,8 @@ namespace Content.Server.VendingMachines
|
||||
SubscribeLocalEvent<VendingMachineComponent, VendingMachineEjectMessage>(OnInventoryEjectMessage);
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, VendingMachineSelfDispenseEvent>(OnSelfDispense);
|
||||
|
||||
SubscribeLocalEvent<VendingMachineComponent, VendingMachineRestockEvent>(OnRestock);
|
||||
}
|
||||
|
||||
private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args)
|
||||
@@ -160,6 +164,31 @@ namespace Content.Server.VendingMachines
|
||||
EjectRandom(uid, throwItem: true, forceEject: false, component);
|
||||
}
|
||||
|
||||
private void OnRestock(EntityUid uid, VendingMachineComponent component, VendingMachineRestockEvent args)
|
||||
{
|
||||
if (!TryComp<VendingMachineRestockComponent>(args.RestockBox, out var restockComponent))
|
||||
{
|
||||
_sawmill.Error($"{ToPrettyString(args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.RestockBox)} which did not have a VendingMachineRestockComponent.");
|
||||
return;
|
||||
}
|
||||
|
||||
TryRestockInventory(uid, component);
|
||||
|
||||
_popupSystem.PopupEntity(Loc.GetString("vending-machine-restock-done",
|
||||
("this", args.RestockBox),
|
||||
("user", args.User),
|
||||
("target", uid)),
|
||||
args.User,
|
||||
PopupType.Medium);
|
||||
|
||||
_audioSystem.PlayPvs(restockComponent.SoundRestockDone, component.Owner,
|
||||
AudioParams.Default
|
||||
.WithVolume(-2f)
|
||||
.WithVariation(0.2f));
|
||||
|
||||
Del(args.RestockBox);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="VendingMachineComponent.CanShoot"/> property of the vending machine.
|
||||
/// </summary>
|
||||
@@ -412,5 +441,29 @@ namespace Content.Server.VendingMachines
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null)
|
||||
{
|
||||
if (!Resolve(uid, ref vendComponent))
|
||||
return;
|
||||
|
||||
RestockInventoryFromPrototype(uid, vendComponent);
|
||||
|
||||
UpdateVendingMachineInterfaceState(vendComponent);
|
||||
TryUpdateVisualState(uid, vendComponent);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public sealed class VendingMachineRestockEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid RestockBox { get; }
|
||||
|
||||
public VendingMachineRestockEvent(EntityUid user, EntityUid restockBox)
|
||||
{
|
||||
User = user;
|
||||
RestockBox = restockBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.DoAfter;
|
||||
using Content.Server.Hands.Components;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Tools;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.GameTicking;
|
||||
using Content.Shared.Interaction;
|
||||
@@ -23,6 +25,7 @@ namespace Content.Server.Wires;
|
||||
public sealed class WiresSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _protoMan = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly AppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly DoAfterSystem _doAfter = default!;
|
||||
[Dependency] private readonly ToolSystem _toolSystem = default!;
|
||||
@@ -467,10 +470,13 @@ public sealed class WiresSystem : EntitySystem
|
||||
{
|
||||
component.IsScrewing = _toolSystem.UseTool(args.Used, args.User, uid,
|
||||
0f, ScrewTime, new[] { "Screwing" },
|
||||
new WireToolFinishedEvent(uid),
|
||||
new WireToolFinishedEvent(uid, args.User),
|
||||
new WireToolCanceledEvent(uid),
|
||||
toolComponent: tool);
|
||||
args.Handled = component.IsScrewing;
|
||||
|
||||
// Log attempt
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} is screwing {ToPrettyString(uid):target}'s {(component.IsPanelOpen ? "open" : "closed")} maintenance panel at {Transform(uid).Coordinates:targetlocation}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +489,9 @@ public sealed class WiresSystem : EntitySystem
|
||||
component.IsPanelOpen = !component.IsPanelOpen;
|
||||
UpdateAppearance(args.Target);
|
||||
|
||||
// Log success
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(args.User):user} screwed {ToPrettyString(args.Target):target}'s maintenance panel {(component.IsPanelOpen ? "open" : "closed")}");
|
||||
|
||||
if (component.IsPanelOpen)
|
||||
{
|
||||
_audio.PlayPvs(component.ScrewdriverOpenSound, args.Target);
|
||||
@@ -918,11 +927,13 @@ public sealed class WiresSystem : EntitySystem
|
||||
#region Events
|
||||
private sealed class WireToolFinishedEvent : EntityEventArgs
|
||||
{
|
||||
public EntityUid User { get; }
|
||||
public EntityUid Target { get; }
|
||||
|
||||
public WireToolFinishedEvent(EntityUid target)
|
||||
public WireToolFinishedEvent(EntityUid target, EntityUid user)
|
||||
{
|
||||
Target = target;
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Server.Cargo.Systems;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Server.Xenoarchaeology.Equipment.Components;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Triggers.Components;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
@@ -24,6 +26,7 @@ public sealed partial class ArtifactSystem : EntitySystem
|
||||
|
||||
SubscribeLocalEvent<ArtifactComponent, MapInitEvent>(OnInit);
|
||||
SubscribeLocalEvent<ArtifactComponent, PriceCalculationEvent>(GetPrice);
|
||||
SubscribeLocalEvent<RoundEndTextAppendEvent>(OnRoundEnd);
|
||||
|
||||
InitializeCommands();
|
||||
}
|
||||
@@ -251,4 +254,17 @@ public sealed partial class ArtifactSystem : EntitySystem
|
||||
|
||||
component.CurrentNode.NodeData[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make shit go ape on round-end
|
||||
/// </summary>
|
||||
private void OnRoundEnd(RoundEndTextAppendEvent ev)
|
||||
{
|
||||
foreach (var artifactComp in EntityQuery<ArtifactComponent>())
|
||||
{
|
||||
artifactComp.CooldownTime = TimeSpan.Zero;
|
||||
var timerTrigger = EnsureComp<ArtifactTimerTriggerComponent>(artifactComp.Owner);
|
||||
timerTrigger.ActivationRate = TimeSpan.FromSeconds(0.5); //HAHAHAHAHAHAHAHAHAH -emo
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for recharging all nearby batteries when activated
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed class ChargeBatteryArtifactComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The radius of entities that will be affected
|
||||
/// </summary>
|
||||
[DataField("radius")]
|
||||
public float Radius = 15f;
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
|
||||
using Content.Shared.Storage;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
|
||||
@@ -11,18 +9,8 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
[RegisterComponent]
|
||||
public sealed class SpawnArtifactComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of possible prototypes to spawn that it picks from.
|
||||
/// </summary>
|
||||
[DataField("possiblePrototypes", customTypeSerializer:typeof(PrototypeIdListSerializer<EntityPrototype>))]
|
||||
public List<string> PossiblePrototypes = new();
|
||||
|
||||
/// <summary>
|
||||
/// The prototype it selected to spawn.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("prototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string? Prototype;
|
||||
[DataField("spawns")]
|
||||
public List<EntitySpawnEntry>? Spawns;
|
||||
|
||||
/// <summary>
|
||||
/// The range around the artifact that it will spawn the entity
|
||||
@@ -34,12 +22,5 @@ public sealed class SpawnArtifactComponent : Component
|
||||
/// The maximum number of times the spawn will occur
|
||||
/// </summary>
|
||||
[DataField("maxSpawns")]
|
||||
public int MaxSpawns = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not the artifact spawns the same entity every time
|
||||
/// or picks through the list each time.
|
||||
/// </summary>
|
||||
[DataField("consistentSpawn")]
|
||||
public bool ConsistentSpawn = true;
|
||||
public int MaxSpawns = 10;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// This handles <see cref="ChargeBatteryArtifactComponent"/>
|
||||
/// </summary>
|
||||
public sealed class ChargeBatteryArtifactSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly EntityLookupSystem _lookup = default!;
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<ChargeBatteryArtifactComponent, ArtifactActivatedEvent>(OnActivated);
|
||||
}
|
||||
|
||||
private void OnActivated(EntityUid uid, ChargeBatteryArtifactComponent component, ArtifactActivatedEvent args)
|
||||
{
|
||||
foreach (var battery in _lookup.GetComponentsInRange<BatteryComponent>(Transform(uid).MapPosition, component.Radius))
|
||||
{
|
||||
battery.CurrentCharge = battery.MaxCharge;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,7 @@ public sealed class RandomInstrumentArtifactSystem : EntitySystem
|
||||
|
||||
private void OnStartup(EntityUid uid, RandomInstrumentArtifactComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!TryComp<SharedInstrumentComponent>(uid, out var instrument))
|
||||
return;
|
||||
|
||||
var instrument = EnsureComp<InstrumentComponent>(uid);
|
||||
_instrument.SetInstrumentProgram(instrument, (byte) _random.Next(0, 127), 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Components;
|
||||
using Content.Server.Xenoarchaeology.XenoArtifacts.Events;
|
||||
using Content.Shared.Hands.EntitySystems;
|
||||
using Content.Shared.Storage;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
||||
@@ -8,7 +8,6 @@ namespace Content.Server.Xenoarchaeology.XenoArtifacts.Effects.Systems;
|
||||
public sealed class SpawnArtifactSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly SharedHandsSystem _handsSystem = default!;
|
||||
[Dependency] private readonly ArtifactSystem _artifact = default!;
|
||||
|
||||
public const string NodeDataSpawnAmount = "nodeDataSpawnAmount";
|
||||
@@ -16,45 +15,28 @@ public sealed class SpawnArtifactSystem : EntitySystem
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactNodeEnteredEvent>(OnNodeEntered);
|
||||
SubscribeLocalEvent<SpawnArtifactComponent, ArtifactActivatedEvent>(OnActivate);
|
||||
}
|
||||
private void OnNodeEntered(EntityUid uid, SpawnArtifactComponent component, ArtifactNodeEnteredEvent args)
|
||||
{
|
||||
if (component.PossiblePrototypes.Count == 0)
|
||||
return;
|
||||
|
||||
var proto = component.PossiblePrototypes[args.RandomSeed % component.PossiblePrototypes.Count];
|
||||
component.Prototype = proto;
|
||||
}
|
||||
|
||||
private void OnActivate(EntityUid uid, SpawnArtifactComponent component, ArtifactActivatedEvent args)
|
||||
{
|
||||
if (component.Prototype == null)
|
||||
return;
|
||||
|
||||
if (!_artifact.TryGetNodeData(uid, NodeDataSpawnAmount, out int amount))
|
||||
amount = 0;
|
||||
|
||||
if (amount >= component.MaxSpawns)
|
||||
return;
|
||||
|
||||
var toSpawn = component.Prototype;
|
||||
if (!component.ConsistentSpawn)
|
||||
toSpawn = _random.Pick(component.PossiblePrototypes);
|
||||
if (component.Spawns is not {} spawns)
|
||||
return;
|
||||
|
||||
// select spawn position near artifact
|
||||
var artifactCord = Transform(uid).MapPosition;
|
||||
var dx = _random.NextFloat(-component.Range, component.Range);
|
||||
var dy = _random.NextFloat(-component.Range, component.Range);
|
||||
var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
|
||||
|
||||
// spawn entity
|
||||
var spawned = EntityManager.SpawnEntity(toSpawn, spawnCord);
|
||||
_artifact.SetNodeData(uid, NodeDataSpawnAmount, amount+1);
|
||||
|
||||
// if there is an user - try to put spawned item in their hands
|
||||
// doesn't work for spawners
|
||||
_handsSystem.PickupOrDrop(args.Activator, spawned);
|
||||
foreach (var spawn in EntitySpawnCollection.GetSpawns(spawns, _random))
|
||||
{
|
||||
var dx = _random.NextFloat(-component.Range, component.Range);
|
||||
var dy = _random.NextFloat(-component.Range, component.Range);
|
||||
var spawnCord = artifactCord.Offset(new Vector2(dx, dy));
|
||||
EntityManager.SpawnEntity(spawn, spawnCord);
|
||||
}
|
||||
_artifact.SetNodeData(uid, NodeDataSpawnAmount, amount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ public abstract class ActionType : IEquatable<ActionType>, IComparable, ICloneab
|
||||
/// <summary>
|
||||
/// Name to show in UI.
|
||||
/// </summary>
|
||||
[DataField("name", required: true)]
|
||||
[DataField("name")]
|
||||
public string DisplayName = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -202,7 +202,7 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
performEvent.Performer = user;
|
||||
|
||||
// All checks passed. Perform the action!
|
||||
PerformAction(component, act, performEvent, curTime);
|
||||
PerformAction(user, component, act, performEvent, curTime);
|
||||
}
|
||||
|
||||
public bool ValidateEntityTarget(EntityUid user, EntityUid target, EntityTargetAction action)
|
||||
@@ -265,7 +265,7 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
return _interactionSystem.InRangeUnobstructed(user, coords, range: action.Range);
|
||||
}
|
||||
|
||||
public void PerformAction(ActionsComponent component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime)
|
||||
public void PerformAction(EntityUid performer, ActionsComponent? component, ActionType action, BaseActionEvent? actionEvent, TimeSpan curTime, bool predicted = true)
|
||||
{
|
||||
var handled = false;
|
||||
|
||||
@@ -277,7 +277,7 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
actionEvent.Handled = false;
|
||||
|
||||
if (action.Provider == null)
|
||||
RaiseLocalEvent(component.Owner, (object) actionEvent, broadcast: true);
|
||||
RaiseLocalEvent(performer, (object) actionEvent, broadcast: true);
|
||||
else
|
||||
RaiseLocalEvent(action.Provider.Value, (object) actionEvent, broadcast: true);
|
||||
|
||||
@@ -285,7 +285,7 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
}
|
||||
|
||||
// Execute convenience functionality (pop-ups, sound, speech)
|
||||
handled |= PerformBasicActions(component.Owner, action);
|
||||
handled |= PerformBasicActions(performer, action, predicted);
|
||||
|
||||
if (!handled)
|
||||
return; // no interaction occurred.
|
||||
@@ -309,19 +309,19 @@ public abstract class SharedActionsSystem : EntitySystem
|
||||
action.Cooldown = (curTime, curTime + action.UseDelay.Value);
|
||||
}
|
||||
|
||||
if (dirty)
|
||||
if (dirty && component != null)
|
||||
Dirty(component);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute convenience functionality for actions (pop-ups, sound, speech)
|
||||
/// </summary>
|
||||
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action)
|
||||
protected virtual bool PerformBasicActions(EntityUid performer, ActionType action, bool predicted)
|
||||
{
|
||||
if (action.Sound == null && string.IsNullOrWhiteSpace(action.Popup))
|
||||
return false;
|
||||
|
||||
var filter = Filter.PvsExcept(performer);
|
||||
var filter = predicted ? Filter.PvsExcept(performer) : Filter.Pvs(performer);
|
||||
|
||||
_audio.Play(action.Sound, filter, performer, true, action.AudioParams);
|
||||
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Construction
|
||||
{
|
||||
[NetSerializable]
|
||||
public enum MachineFrameVisuals
|
||||
{
|
||||
State,
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using Content.Shared.MobState.Components;
|
||||
using Content.Shared.Radiation.Events;
|
||||
using Content.Shared.Rejuvenate;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -16,6 +17,7 @@ namespace Content.Shared.Damage
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly INetManager _netMan = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -231,7 +233,15 @@ namespace Content.Shared.Damage
|
||||
|
||||
private void DamageableGetState(EntityUid uid, DamageableComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId);
|
||||
if (_netMan.IsServer)
|
||||
{
|
||||
args.State = new DamageableComponentState(component.Damage.DamageDict, component.DamageModifierSetId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// avoid mispredicting damage on newly spawned entities.
|
||||
args.State = new DamageableComponentState(component.Damage.DamageDict.ShallowClone(), component.DamageModifierSetId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIrradiated(EntityUid uid, DamageableComponent component, OnIrradiatedEvent args)
|
||||
|
||||
@@ -68,7 +68,13 @@ namespace Content.Shared.Doors
|
||||
/// </summary>
|
||||
public sealed class DoorGetPryTimeModifierEvent : EntityEventArgs
|
||||
{
|
||||
public readonly EntityUid User;
|
||||
public float PryTimeModifier = 1.0f;
|
||||
|
||||
public DoorGetPryTimeModifierEvent(EntityUid user)
|
||||
{
|
||||
User = user;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -363,7 +363,7 @@ namespace Content.Shared.Movement.Systems
|
||||
/// <summary>
|
||||
/// Used for weightlessness to determine if we are near a wall.
|
||||
/// </summary>
|
||||
private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, IPhysBody collider)
|
||||
private bool IsAroundCollider(SharedPhysicsSystem broadPhaseSystem, TransformComponent transform, MobMoverComponent mover, PhysicsComponent collider)
|
||||
{
|
||||
var enlargedAABB = collider.GetWorldAABB().Enlarged(mover.GrabRangeVV);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.Physics.Pull
|
||||
{
|
||||
public sealed class PullAttemptEvent : PullMessage
|
||||
{
|
||||
public PullAttemptEvent(IPhysBody puller, IPhysBody pulled) : base(puller, pulled) { }
|
||||
public PullAttemptEvent(PhysicsComponent puller, PhysicsComponent pulled) : base(puller, pulled) { }
|
||||
|
||||
public bool Cancelled { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.Physics.Pull
|
||||
{
|
||||
public abstract class PullMessage : EntityEventArgs
|
||||
{
|
||||
public readonly IPhysBody Puller;
|
||||
public readonly IPhysBody Pulled;
|
||||
public readonly PhysicsComponent Puller;
|
||||
public readonly PhysicsComponent Pulled;
|
||||
|
||||
protected PullMessage(IPhysBody puller, IPhysBody pulled)
|
||||
protected PullMessage(PhysicsComponent puller, PhysicsComponent pulled)
|
||||
{
|
||||
Puller = puller;
|
||||
Pulled = pulled;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.Physics.Pull
|
||||
{
|
||||
public sealed class PullStartedMessage : PullMessage
|
||||
{
|
||||
public PullStartedMessage(IPhysBody puller, IPhysBody pulled) :
|
||||
public PullStartedMessage(PhysicsComponent puller, PhysicsComponent pulled) :
|
||||
base(puller, pulled)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
|
||||
namespace Content.Shared.Physics.Pull
|
||||
{
|
||||
public sealed class PullStoppedMessage : PullMessage
|
||||
{
|
||||
public PullStoppedMessage(IPhysBody puller, IPhysBody pulled) : base(puller, pulled)
|
||||
public PullStoppedMessage(PhysicsComponent puller, PhysicsComponent pulled) : base(puller, pulled)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ namespace Content.Shared.Pulling
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!EntityManager.TryGetComponent<IPhysBody>(pulled, out var physics))
|
||||
if (!EntityManager.TryGetComponent<PhysicsComponent>(pulled, out var physics))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -17,14 +17,20 @@ namespace Content.Shared.Research.Prototypes
|
||||
[DataField("name")]
|
||||
private string _name = string.Empty;
|
||||
|
||||
[DataField("icon")]
|
||||
private SpriteSpecifier _icon = SpriteSpecifier.Invalid;
|
||||
|
||||
[DataField("description")]
|
||||
private string _description = string.Empty;
|
||||
|
||||
[DataField("result", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
private string _result = string.Empty;
|
||||
/// <summary>
|
||||
/// The prototype name of the resulting entity when the recipe is printed.
|
||||
/// </summary>
|
||||
[DataField("result", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string Result = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// An entity whose sprite is displayed in the ui in place of the actual recipe result.
|
||||
/// </summary>
|
||||
[DataField("icon")]
|
||||
public SpriteSpecifier? Icon;
|
||||
|
||||
[DataField("completetime")]
|
||||
private TimeSpan _completeTime = TimeSpan.FromSeconds(5);
|
||||
@@ -42,7 +48,7 @@ namespace Content.Shared.Research.Prototypes
|
||||
{
|
||||
if (_name.Trim().Length != 0) return _name;
|
||||
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
protoMan.TryIndex(_result, out EntityPrototype? prototype);
|
||||
protoMan.TryIndex(Result, out EntityPrototype? prototype);
|
||||
if (prototype?.Name != null)
|
||||
_name = prototype.Name;
|
||||
return _name;
|
||||
@@ -59,25 +65,13 @@ namespace Content.Shared.Research.Prototypes
|
||||
{
|
||||
if (_description.Trim().Length != 0) return _description;
|
||||
var protoMan = IoCManager.Resolve<IPrototypeManager>();
|
||||
protoMan.TryIndex(_result, out EntityPrototype? prototype);
|
||||
protoMan.TryIndex(Result, out EntityPrototype? prototype);
|
||||
if (prototype?.Description != null)
|
||||
_description = prototype.Description;
|
||||
return _description;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Texture path used in the lathe GUI.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public SpriteSpecifier Icon => _icon;
|
||||
|
||||
/// <summary>
|
||||
/// The prototype name of the resulting entity when the recipe is printed.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string Result => _result;
|
||||
|
||||
/// <summary>
|
||||
/// The materials required to produce this recipe.
|
||||
/// Takes a material ID as string.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.StepTrigger.Components;
|
||||
using Robust.Shared.Collections;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Physics;
|
||||
using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Events;
|
||||
|
||||
@@ -16,6 +17,18 @@ public sealed class StepTriggerSystem : EntitySystem
|
||||
SubscribeLocalEvent<StepTriggerComponent, ComponentHandleState>(TriggerHandleState);
|
||||
|
||||
SubscribeLocalEvent<StepTriggerComponent, StartCollideEvent>(HandleCollide);
|
||||
#if DEBUG
|
||||
SubscribeLocalEvent<StepTriggerComponent, ComponentStartup>(OnStartup);
|
||||
}
|
||||
|
||||
private void OnStartup(EntityUid uid, StepTriggerComponent component, ComponentStartup args)
|
||||
{
|
||||
if (!component.Active)
|
||||
return;
|
||||
|
||||
if (!TryComp(uid, out FixturesComponent? fixtures) || fixtures.FixtureCount == 0)
|
||||
Logger.Warning($"{ToPrettyString(uid)} has an active step trigger without any fixtures.");
|
||||
#endif
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
using System.Threading;
|
||||
using Content.Shared.Audio;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Shared.Teleportation.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Creates portals. If two are created, both are linked together--otherwise the first teleports randomly.
|
||||
/// Using it with both portals active deactivates both.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class HandTeleporterComponent : Component
|
||||
{
|
||||
[ViewVariables, DataField("firstPortal")]
|
||||
public EntityUid? FirstPortal = null;
|
||||
|
||||
[ViewVariables, DataField("secondPortal")]
|
||||
public EntityUid? SecondPortal = null;
|
||||
|
||||
[DataField("firstPortalPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string FirstPortalPrototype = "PortalRed";
|
||||
|
||||
[DataField("secondPortalPrototype", customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
|
||||
public string SecondPortalPrototype = "PortalBlue";
|
||||
|
||||
[DataField("newPortalSound")]
|
||||
public SoundSpecifier NewPortalSound = new SoundPathSpecifier("/Audio/Machines/high_tech_confirm.ogg")
|
||||
{
|
||||
Params = AudioParams.Default.WithVolume(-2f)
|
||||
};
|
||||
|
||||
[DataField("clearPortalsSound")]
|
||||
public SoundSpecifier ClearPortalsSound = new SoundPathSpecifier("/Audio/Machines/button.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Delay for creating the portals in seconds.
|
||||
/// </summary>
|
||||
[DataField("portalCreationDelay")]
|
||||
public float PortalCreationDelay = 2.5f;
|
||||
|
||||
public CancellationTokenSource? CancelToken = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Raised on doafter success for creating a portal.
|
||||
/// </summary>
|
||||
public record HandTeleporterSuccessEvent(EntityUid User);
|
||||
|
||||
/// <summary>
|
||||
/// Raised on doafter cancel for creating a portal.
|
||||
/// </summary>
|
||||
public record HandTeleporterCancelledEvent;
|
||||
@@ -0,0 +1,42 @@
|
||||
using Content.Shared.Teleportation.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Teleportation.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entity which is linked to other entities (perhaps portals), and which can be walked through/
|
||||
/// thrown into to teleport an entity.
|
||||
/// </summary>
|
||||
[RegisterComponent, Access(typeof(LinkedEntitySystem)), NetworkedComponent]
|
||||
public sealed class LinkedEntityComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The entities that this entity is linked to.
|
||||
/// </summary>
|
||||
[DataField("linkedEntities")]
|
||||
public HashSet<EntityUid> LinkedEntities = new();
|
||||
|
||||
/// <summary>
|
||||
/// Should this entity be deleted if all of its links are removed?
|
||||
/// </summary>
|
||||
[DataField("deleteOnEmptyLinks")]
|
||||
public bool DeleteOnEmptyLinks = false;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class LinkedEntityComponentState : ComponentState
|
||||
{
|
||||
public HashSet<EntityUid> LinkedEntities;
|
||||
|
||||
public LinkedEntityComponentState(HashSet<EntityUid> linkedEntities)
|
||||
{
|
||||
LinkedEntities = linkedEntities;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum LinkedEntityVisuals : byte
|
||||
{
|
||||
HasAnyLinks
|
||||
}
|
||||
31
Content.Shared/Teleportation/Components/PortalComponent.cs
Normal file
31
Content.Shared/Teleportation/Components/PortalComponent.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Teleportation.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Marks an entity as being a 'portal' which teleports entities sent through it to linked entities.
|
||||
/// Relies on <see cref="LinkedEntityComponent"/> being set up.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class PortalComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Sound played on arriving to this portal, centered on the destination.
|
||||
/// The arrival sound of the entered portal will play if the destination is not a portal.
|
||||
/// </summary>
|
||||
[DataField("arrivalSound")]
|
||||
public SoundSpecifier ArrivalSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// Sound played on departing from this portal, centered on the original portal.
|
||||
/// </summary>
|
||||
[DataField("departureSound")]
|
||||
public SoundSpecifier DepartureSound = new SoundPathSpecifier("/Audio/Effects/teleport_departure.ogg");
|
||||
|
||||
/// <summary>
|
||||
/// If no portals are linked, the subject will be teleported a random distance at maximum this far away.
|
||||
/// </summary>
|
||||
[DataField("maxRandomRadius")]
|
||||
public float MaxRandomRadius = 7.0f;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Teleportation.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Attached to an entity after portal transit to mark that they should not immediately be portaled back
|
||||
/// at the end destination.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent]
|
||||
public sealed class PortalTimeoutComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The portal that was entered. Null if coming from a hand teleporter, etc.
|
||||
/// </summary>
|
||||
[ViewVariables, DataField("enteredPortal")]
|
||||
public EntityUid? EnteredPortal = null;
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class PortalTimeoutComponentState : ComponentState
|
||||
{
|
||||
public EntityUid? EnteredPortal;
|
||||
|
||||
public PortalTimeoutComponentState(EntityUid? enteredPortal)
|
||||
{
|
||||
EnteredPortal = enteredPortal;
|
||||
}
|
||||
}
|
||||
115
Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs
Normal file
115
Content.Shared/Teleportation/Systems/LinkedEntitySystem.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Teleportation.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Teleportation.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// Handles symmetrically linking two entities together, and removing links properly.
|
||||
/// This does not do anything on its own (outside of deleting entities that have 0 links, if that option is true)
|
||||
/// Systems can do whatever they please with the linked entities, such as <see cref="PortalSystem"/>.
|
||||
/// </summary>
|
||||
public sealed class LinkedEntitySystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<LinkedEntityComponent, ComponentShutdown>(OnLinkShutdown);
|
||||
|
||||
SubscribeLocalEvent<LinkedEntityComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<LinkedEntityComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, LinkedEntityComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new LinkedEntityComponentState(component.LinkedEntities);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, LinkedEntityComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is LinkedEntityComponentState state)
|
||||
component.LinkedEntities = state.LinkedEntities;
|
||||
}
|
||||
|
||||
private void OnLinkShutdown(EntityUid uid, LinkedEntityComponent component, ComponentShutdown args)
|
||||
{
|
||||
// Remove any links to this entity when deleted.
|
||||
foreach (var ent in component.LinkedEntities.ToArray())
|
||||
{
|
||||
if (LifeStage(ent) < EntityLifeStage.Terminating && TryComp<LinkedEntityComponent>(ent, out var link))
|
||||
{
|
||||
TryUnlink(uid, ent, component, link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <summary>
|
||||
/// Links two entities together. Does not require the existence of <see cref="LinkedEntityComponent"/> on either
|
||||
/// already. Linking is symmetrical, so order doesn't matter.
|
||||
/// </summary>
|
||||
/// <param name="first">The first entity to link</param>
|
||||
/// <param name="second">The second entity to link</param>
|
||||
/// <param name="deleteOnEmptyLinks">Whether both entities should now delete once their links are removed</param>
|
||||
/// <returns>Whether linking was successful (e.g. they weren't already linked)</returns>
|
||||
public bool TryLink(EntityUid first, EntityUid second, bool deleteOnEmptyLinks=false)
|
||||
{
|
||||
var firstLink = EnsureComp<LinkedEntityComponent>(first);
|
||||
var secondLink = EnsureComp<LinkedEntityComponent>(second);
|
||||
|
||||
firstLink.DeleteOnEmptyLinks = deleteOnEmptyLinks;
|
||||
secondLink.DeleteOnEmptyLinks = deleteOnEmptyLinks;
|
||||
|
||||
_appearance.SetData(first, LinkedEntityVisuals.HasAnyLinks, true);
|
||||
_appearance.SetData(second, LinkedEntityVisuals.HasAnyLinks, true);
|
||||
|
||||
Dirty(firstLink);
|
||||
Dirty(secondLink);
|
||||
|
||||
return firstLink.LinkedEntities.Add(second)
|
||||
&& secondLink.LinkedEntities.Add(first);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unlinks two entities. Deletes either entity if <see cref="LinkedEntityComponent.DeleteOnEmptyLinks"/>
|
||||
/// was true and its links are now empty. Symmetrical, so order doesn't matter.
|
||||
/// </summary>
|
||||
/// <param name="first">The first entity to unlink</param>
|
||||
/// <param name="second">The second entity to unlink</param>
|
||||
/// <param name="firstLink">Resolve comp</param>
|
||||
/// <param name="secondLink">Resolve comp</param>
|
||||
/// <returns>Whether unlinking was successful (e.g. they both were actually linked to one another)</returns>
|
||||
public bool TryUnlink(EntityUid first, EntityUid second,
|
||||
LinkedEntityComponent? firstLink=null, LinkedEntityComponent? secondLink=null)
|
||||
{
|
||||
if (!Resolve(first, ref firstLink))
|
||||
return false;
|
||||
|
||||
if (!Resolve(second, ref secondLink))
|
||||
return false;
|
||||
|
||||
var success = firstLink.LinkedEntities.Remove(second)
|
||||
&& secondLink.LinkedEntities.Remove(first);
|
||||
|
||||
_appearance.SetData(first, LinkedEntityVisuals.HasAnyLinks, firstLink.LinkedEntities.Any());
|
||||
_appearance.SetData(second, LinkedEntityVisuals.HasAnyLinks, secondLink.LinkedEntities.Any());
|
||||
|
||||
Dirty(firstLink);
|
||||
Dirty(secondLink);
|
||||
|
||||
if (firstLink.LinkedEntities.Count == 0 && firstLink.DeleteOnEmptyLinks)
|
||||
QueueDel(first);
|
||||
|
||||
if (secondLink.LinkedEntities.Count == 0 && secondLink.DeleteOnEmptyLinks)
|
||||
QueueDel(second);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
146
Content.Shared/Teleportation/Systems/PortalSystem.cs
Normal file
146
Content.Shared/Teleportation/Systems/PortalSystem.cs
Normal file
@@ -0,0 +1,146 @@
|
||||
using System.Linq;
|
||||
using Content.Shared.Projectiles;
|
||||
using Content.Shared.Teleportation.Components;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Physics.Dynamics;
|
||||
using Robust.Shared.Physics.Events;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Shared.Teleportation.Systems;
|
||||
|
||||
/// <summary>
|
||||
/// This handles teleporting entities through portals, and creating new linked portals.
|
||||
/// </summary>
|
||||
public sealed class PortalSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly INetManager _netMan = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
private const string PortalFixture = "portalFixture";
|
||||
private const string ProjectileFixture = "projectile";
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<PortalComponent, StartCollideEvent>(OnCollide);
|
||||
SubscribeLocalEvent<PortalComponent, EndCollideEvent>(OnEndCollide);
|
||||
|
||||
SubscribeLocalEvent<PortalTimeoutComponent, ComponentGetState>(OnGetState);
|
||||
SubscribeLocalEvent<PortalTimeoutComponent, ComponentHandleState>(OnHandleState);
|
||||
}
|
||||
|
||||
private void OnGetState(EntityUid uid, PortalTimeoutComponent component, ref ComponentGetState args)
|
||||
{
|
||||
args.State = new PortalTimeoutComponentState(component.EnteredPortal);
|
||||
}
|
||||
|
||||
private void OnHandleState(EntityUid uid, PortalTimeoutComponent component, ref ComponentHandleState args)
|
||||
{
|
||||
if (args.Current is PortalTimeoutComponentState state)
|
||||
component.EnteredPortal = state.EnteredPortal;
|
||||
}
|
||||
|
||||
private bool ShouldCollide(Fixture our, Fixture other)
|
||||
{
|
||||
// most non-hard fixtures shouldn't pass through portals, but projectiles are non-hard as well
|
||||
// and they should still pass through
|
||||
return our.ID == PortalFixture && (other.Hard || other.ID == ProjectileFixture);
|
||||
}
|
||||
|
||||
private void OnCollide(EntityUid uid, PortalComponent component, ref StartCollideEvent args)
|
||||
{
|
||||
if (!ShouldCollide(args.OurFixture, args.OtherFixture))
|
||||
return;
|
||||
|
||||
var subject = args.OtherFixture.Body.Owner;
|
||||
|
||||
// best not.
|
||||
if (Transform(subject).Anchored)
|
||||
return;
|
||||
|
||||
// if they came from another portal, just return and wait for them to exit the portal
|
||||
if (HasComp<PortalTimeoutComponent>(subject))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (TryComp<LinkedEntityComponent>(uid, out var link))
|
||||
{
|
||||
if (!link.LinkedEntities.Any())
|
||||
return;
|
||||
|
||||
// client can't predict outside of simple portal-to-portal interactions due to randomness involved
|
||||
// --also can't predict if the target doesn't exist on the client / is outside of PVS
|
||||
if (_netMan.IsClient)
|
||||
{
|
||||
var first = link.LinkedEntities.First();
|
||||
var exists = Exists(first);
|
||||
if (link.LinkedEntities.Count != 1 || !exists || (exists && Transform(first).MapID == MapId.Nullspace))
|
||||
return;
|
||||
}
|
||||
|
||||
// pick a target and teleport there
|
||||
var target = _random.Pick(link.LinkedEntities);
|
||||
|
||||
if (HasComp<PortalComponent>(target))
|
||||
{
|
||||
// if target is a portal, signal that they shouldn't be immediately portaled back
|
||||
var timeout = EnsureComp<PortalTimeoutComponent>(subject);
|
||||
timeout.EnteredPortal = uid;
|
||||
Dirty(timeout);
|
||||
}
|
||||
|
||||
TeleportEntity(uid, subject, Transform(target).Coordinates, target);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_netMan.IsClient)
|
||||
return;
|
||||
|
||||
// no linked entity--teleport randomly
|
||||
var randVector = _random.NextVector2(component.MaxRandomRadius);
|
||||
var newCoords = Transform(uid).Coordinates.Offset(randVector);
|
||||
TeleportEntity(uid, subject, newCoords);
|
||||
}
|
||||
|
||||
private void OnEndCollide(EntityUid uid, PortalComponent component, ref EndCollideEvent args)
|
||||
{
|
||||
if (!ShouldCollide(args.OurFixture, args.OtherFixture))
|
||||
return;
|
||||
|
||||
var subject = args.OtherFixture.Body.Owner;
|
||||
|
||||
// if they came from (not us), remove the timeout
|
||||
if (TryComp<PortalTimeoutComponent>(subject, out var timeout) && timeout.EnteredPortal != uid)
|
||||
{
|
||||
RemComp<PortalTimeoutComponent>(subject);
|
||||
}
|
||||
}
|
||||
|
||||
private void TeleportEntity(EntityUid portal, EntityUid subject, EntityCoordinates target, EntityUid? targetEntity=null,
|
||||
PortalComponent? portalComponent = null)
|
||||
{
|
||||
if (!Resolve(portal, ref portalComponent))
|
||||
return;
|
||||
|
||||
var arrivalSound = CompOrNull<PortalComponent>(targetEntity)?.ArrivalSound ?? portalComponent.ArrivalSound;
|
||||
var departureSound = portalComponent.DepartureSound;
|
||||
|
||||
// Some special cased stuff: projectiles should stop ignoring shooter when they enter a portal, to avoid
|
||||
// stacking 500 bullets in between 2 portals and instakilling people--you'll just hit yourself instead
|
||||
// (as expected)
|
||||
if (TryComp<ProjectileComponent>(subject, out var projectile))
|
||||
{
|
||||
projectile.IgnoreShooter = false;
|
||||
}
|
||||
|
||||
Transform(subject).Coordinates = target;
|
||||
|
||||
_audio.PlayPredicted(departureSound, portal, subject);
|
||||
_audio.PlayPredicted(arrivalSound, subject, subject);
|
||||
}
|
||||
}
|
||||
@@ -143,7 +143,7 @@ namespace Content.Shared.Throwing
|
||||
/// <summary>
|
||||
/// Raises collision events on the thrown and target entities.
|
||||
/// </summary>
|
||||
public void ThrowCollideInteraction(EntityUid? user, IPhysBody thrown, IPhysBody target)
|
||||
public void ThrowCollideInteraction(EntityUid? user, PhysicsComponent thrown, PhysicsComponent target)
|
||||
{
|
||||
if (user is not null)
|
||||
_adminLogger.Add(LogType.ThrowHit, LogImpact.Low,
|
||||
|
||||
@@ -15,6 +15,17 @@ public abstract class SharedVendingMachineSystem : EntitySystem
|
||||
|
||||
protected virtual void OnComponentInit(EntityUid uid, VendingMachineComponent component, ComponentInit args)
|
||||
{
|
||||
RestockInventoryFromPrototype(uid, component);
|
||||
}
|
||||
|
||||
public void RestockInventoryFromPrototype(EntityUid uid,
|
||||
VendingMachineComponent? component = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_prototypeManager.TryIndex(component.PackPrototypeId, out VendingMachineInventoryPrototype? packPrototype))
|
||||
return;
|
||||
|
||||
@@ -64,28 +75,38 @@ public abstract class SharedVendingMachineSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
var inventory = new Dictionary<string, VendingMachineInventoryEntry>();
|
||||
Dictionary<string, VendingMachineInventoryEntry> inventory;
|
||||
switch (type)
|
||||
{
|
||||
case InventoryType.Regular:
|
||||
inventory = component.Inventory;
|
||||
break;
|
||||
case InventoryType.Emagged:
|
||||
inventory = component.EmaggedInventory;
|
||||
break;
|
||||
case InventoryType.Contraband:
|
||||
inventory = component.ContrabandInventory;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (id, amount) in entries)
|
||||
{
|
||||
if (_prototypeManager.HasIndex<EntityPrototype>(id))
|
||||
{
|
||||
inventory.Add(id, new VendingMachineInventoryEntry(type, id, amount));
|
||||
if (inventory.TryGetValue(id, out VendingMachineInventoryEntry? entry))
|
||||
// Prevent a machine's stock from going over three times
|
||||
// the prototype's normal amount. This is an arbitrary
|
||||
// number and meant to be a convenience for someone
|
||||
// restocking a machine who doesn't want to force vend out
|
||||
// all the items just to restock one empty slot without
|
||||
// losing the rest of the restock.
|
||||
entry.Amount = Math.Min(entry.Amount + amount, 3 * amount);
|
||||
else
|
||||
inventory.Add(id, new VendingMachineInventoryEntry(type, id, amount));
|
||||
}
|
||||
}
|
||||
|
||||
switch (type)
|
||||
{
|
||||
case InventoryType.Regular:
|
||||
component.Inventory = inventory;
|
||||
break;
|
||||
case InventoryType.Emagged:
|
||||
component.EmaggedInventory = inventory;
|
||||
break;
|
||||
case InventoryType.Contraband:
|
||||
component.ContrabandInventory = inventory;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,3 +2,7 @@
|
||||
license: "CC-BY-NC-SA-3.0"
|
||||
copyright: "Amateur foley and audio editing by Bright0."
|
||||
source: "https://github.com/Bright0"
|
||||
- files: ["teleport_arrival.ogg", "teleport_departure.ogg"]
|
||||
license: "CC-BY-SA-3.0"
|
||||
copyright: "tgstation"
|
||||
source: "https://github.com/tgstation/tgstation/commit/906fb0682bab6a0975b45036001c54f021f58ae7"
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,13 @@
|
||||
- files: ["vending_restock_start.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "https://freesound.org/people/Defaultv/"
|
||||
source: "https://freesound.org/people/Defaultv/sounds/534362/"
|
||||
|
||||
- files: ["vending_restock_done.ogg"]
|
||||
license: "CC-BY-3.0"
|
||||
copyright: "https://freesound.org/people/felipelnv/"
|
||||
source: "https://freesound.org/people/felipelnv/sounds/153298/"
|
||||
|
||||
- files: ["short_print_and_rip.ogg"]
|
||||
license: "CC0-1.0"
|
||||
copyright: "receipt printing.wav by 13F_Panska_Tlolkova_Matilda. This version is cleaned up, shortened, and converted to OGG."
|
||||
|
||||
BIN
Resources/Audio/Machines/vending_restock_done.ogg
Normal file
BIN
Resources/Audio/Machines/vending_restock_done.ogg
Normal file
Binary file not shown.
BIN
Resources/Audio/Machines/vending_restock_start.ogg
Normal file
BIN
Resources/Audio/Machines/vending_restock_start.ogg
Normal file
Binary file not shown.
@@ -1,108 +1,4 @@
|
||||
Entries:
|
||||
- author: themias
|
||||
changes:
|
||||
- {message: Added lathe recipe for glass chemistry bottles, type: Add}
|
||||
id: 2313
|
||||
time: '2022-09-15T17:27:46.0000000+00:00'
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: 'Suppressed artifacts ($4,000) now sell for more than unsuppressed artifacts
|
||||
($2,000).', type: Add}
|
||||
id: 2314
|
||||
time: '2022-09-15T17:28:07.0000000+00:00'
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: Most cargo crate prices have been tweaked down to reflect the value
|
||||
of the crate. Security items in particular are much cheaper., type: Tweak}
|
||||
- {message: Cargo passive income reduced from 2 to 1 per second., type: Tweak}
|
||||
- {message: 'Pipes, racks, and walls should be worth a token amount of money.',
|
||||
type: Tweak}
|
||||
- {message: Dead organics can be sold., type: Tweak}
|
||||
id: 2315
|
||||
time: '2022-09-15T17:29:07.0000000+00:00'
|
||||
- author: lapatison
|
||||
changes:
|
||||
- {message: Soda water can no longer breaks into glass shards, type: Fix}
|
||||
id: 2316
|
||||
time: '2022-09-15T19:07:12.0000000+00:00'
|
||||
- author: lapatison
|
||||
changes:
|
||||
- {message: chance to find salami lid in a pizza crate has been decreased, type: Tweak}
|
||||
id: 2317
|
||||
time: '2022-09-15T22:15:45.0000000+00:00'
|
||||
- author: ashtronaut
|
||||
changes:
|
||||
- {message: Added icons for Lock and Unlock in the right click menu!, type: Add}
|
||||
id: 2318
|
||||
time: '2022-09-15T23:26:49.0000000+00:00'
|
||||
- author: Peptide90
|
||||
changes:
|
||||
- {message: NSS Pillar has been rebuilt from scratch! Expect a lot of changes and
|
||||
follow the directional signs not to get lost., type: Add}
|
||||
id: 2319
|
||||
time: '2022-09-16T02:55:43.0000000+00:00'
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: Gas tanks can now also be sold (no longer just canisters)., type: Add}
|
||||
id: 2320
|
||||
time: '2022-09-16T14:04:25.0000000+00:00'
|
||||
- author: JustinTrotter
|
||||
changes:
|
||||
- {message: Gas canisters now come with access locks., type: Add}
|
||||
id: 2321
|
||||
time: '2022-09-16T14:06:30.0000000+00:00'
|
||||
- author: och
|
||||
changes:
|
||||
- {message: Fix all clones being young neuter humans, type: Fix}
|
||||
id: 2322
|
||||
time: '2022-09-16T14:13:46.0000000+00:00'
|
||||
- author: rolfero
|
||||
changes:
|
||||
- {message: Added strip button to examine tooltip., type: Add}
|
||||
id: 2323
|
||||
time: '2022-09-16T14:16:10.0000000+00:00'
|
||||
- author: metalgearsloth
|
||||
changes:
|
||||
- {message: Bible heal has been reduced from 70 damage per 5 seconds to 30 damage
|
||||
per 10 seconds., type: Tweak}
|
||||
id: 2324
|
||||
time: '2022-09-16T14:27:05.0000000+00:00'
|
||||
- author: lapatison
|
||||
changes:
|
||||
- {message: 'Added 2 bottle boxes to chemistry supplies crate, price slightly increased',
|
||||
type: Add}
|
||||
id: 2325
|
||||
time: '2022-09-16T14:27:28.0000000+00:00'
|
||||
- author: rolfero
|
||||
changes:
|
||||
- {message: Deconstructing certain things dont leave their materials stuck in the
|
||||
wall., type: Fix}
|
||||
id: 2326
|
||||
time: '2022-09-16T14:41:21.0000000+00:00'
|
||||
- author: Morb0
|
||||
changes:
|
||||
- {message: More variants of equipped headsets sprite, type: Add}
|
||||
id: 2327
|
||||
time: '2022-09-16T14:43:18.0000000+00:00'
|
||||
- author: Jukereise
|
||||
changes:
|
||||
- {message: Beginners can now spend up to 2 hours (90 extra minutes after unlocking
|
||||
the non-beginner jobs) as an intern (originally 30 minutes), type: Tweak}
|
||||
id: 2328
|
||||
time: '2022-09-16T14:45:20.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: 'Entity storage (lockers, animal crates, etc) now hold some air inside
|
||||
them when closed. Entities inside will be exposed to that air instead of the
|
||||
environment surrounding the storage. Now you can take animals on spacewalks!',
|
||||
type: Add}
|
||||
id: 2329
|
||||
time: '2022-09-16T18:46:09.0000000+00:00'
|
||||
- author: corentt
|
||||
changes:
|
||||
- {message: You can now buy jetpacks from cargo., type: Add}
|
||||
id: 2330
|
||||
time: '2022-09-16T21:21:46.0000000+00:00'
|
||||
- author: Rolfero
|
||||
changes:
|
||||
- {message: Forensic scanners can be used by a verb in the right-clicking menu.,
|
||||
@@ -2950,3 +2846,117 @@ Entries:
|
||||
type: Fix}
|
||||
id: 2812
|
||||
time: '2022-12-31T21:51:44.0000000+00:00'
|
||||
- author: Theomund
|
||||
changes:
|
||||
- {message: Added missing interaction outline component to cardboard boxes., type: Fix}
|
||||
id: 2813
|
||||
time: '2023-01-01T00:42:36.0000000+00:00'
|
||||
- author: Theomund
|
||||
changes:
|
||||
- {message: 'Flashlights now have an empty variant, which can be manufactured by
|
||||
a protolathe.', type: Add}
|
||||
- {message: Changed the protolathe recipe for flashlights to use the empty variant.,
|
||||
type: Fix}
|
||||
id: 2814
|
||||
time: '2023-01-01T05:48:58.0000000+00:00'
|
||||
- author: Theomund
|
||||
changes:
|
||||
- {message: Added logic for sorting crew manifest entries by job titles and names.,
|
||||
type: Fix}
|
||||
id: 2815
|
||||
time: '2023-01-01T05:49:30.0000000+00:00'
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- {message: Added several new beneficial artifact effects., type: Add}
|
||||
- {message: Fixed some broken artifact effects., type: Fix}
|
||||
id: 2816
|
||||
time: '2023-01-01T05:59:39.0000000+00:00'
|
||||
- author: ElectroJr
|
||||
changes:
|
||||
- {message: Fixed being unable to mop up blood stains., type: Fix}
|
||||
- {message: Various stains no longer evaporate., type: Tweak}
|
||||
id: 2817
|
||||
time: '2023-01-01T06:03:26.0000000+00:00'
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- {message: Fixed an issue where deconstructed machines showed an empty frame.,
|
||||
type: Fix}
|
||||
- {message: Fixed an issue where you'd have to crowbar an empty machine frame in
|
||||
order to deconstruct it., type: Fix}
|
||||
id: 2818
|
||||
time: '2023-01-01T06:06:32.0000000+00:00'
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- {message: 'Something about the mystical energy of the end of the round seems to
|
||||
be causing artifacts to go absolutely crazy. Best keep them far away (or perhaps
|
||||
really close?)', type: Add}
|
||||
id: 2819
|
||||
time: '2023-01-01T06:16:09.0000000+00:00'
|
||||
- author: Kupo
|
||||
changes:
|
||||
- {message: 'Radio channel ":h" will now use the headset''s department radio', type: Add}
|
||||
id: 2820
|
||||
time: '2023-01-01T06:20:04.0000000+00:00'
|
||||
- author: Vordenburg
|
||||
changes:
|
||||
- {message: All gas canisters now dump their contents when destroyed., type: Fix}
|
||||
id: 2821
|
||||
time: '2023-01-01T06:21:26.0000000+00:00'
|
||||
- author: Theomund
|
||||
changes:
|
||||
- {message: The warning popups when opening dangerous firelocks are now specific
|
||||
to the user prying them open and are much more noticeable., type: Tweak}
|
||||
id: 2822
|
||||
time: '2023-01-01T19:42:22.0000000+00:00'
|
||||
- author: Whisper
|
||||
changes:
|
||||
- {message: Station mugs now have uniform capacity with vending machine coffee mugs
|
||||
(10 -> 20), type: Tweak}
|
||||
id: 2823
|
||||
time: '2023-01-01T23:50:36.0000000+00:00'
|
||||
- author: keronshb
|
||||
changes:
|
||||
- {message: Added a delay for internals alerts to match the action button delay.,
|
||||
type: Add}
|
||||
id: 2824
|
||||
time: '2023-01-02T00:03:19.0000000+00:00'
|
||||
- author: EmoGarbage404
|
||||
changes:
|
||||
- {message: Grenade penguins no longer attack syndicate agents., type: Fix}
|
||||
- {message: 'Grenade penguins are now only 5 TC and explode on death. You can still
|
||||
trigger them manually, if you''d like.', type: Tweak}
|
||||
id: 2825
|
||||
time: '2023-01-02T01:18:48.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: Hamster cages no longer clip through doors, type: Fix}
|
||||
id: 2826
|
||||
time: '2023-01-02T03:52:30.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: Air alarms should be less annoying around small quantities of water
|
||||
vapor., type: Tweak}
|
||||
id: 2827
|
||||
time: '2023-01-02T05:05:37.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: 'Portals have been added, which teleport you randomly (if unlinked)
|
||||
or between portals (if linked). Expect to see them used more.', type: Add}
|
||||
- {message: 'The research director has received a new device, the ''hand teleporter''.
|
||||
It can create portals at will, and is a traitor steal objective.', type: Add}
|
||||
id: 2828
|
||||
time: '2023-01-03T01:58:25.0000000+00:00'
|
||||
- author: ElectroJr
|
||||
changes:
|
||||
- {message: Fixed a bug that would sometimes cause players to appear as if they
|
||||
were dead/horizontal., type: Fix}
|
||||
id: 2829
|
||||
time: '2023-01-03T06:43:35.0000000+00:00'
|
||||
- author: mirrorcult
|
||||
changes:
|
||||
- {message: 'Hand teleporter now requires a doafter, and can''t be used when inside
|
||||
anything.', type: Tweak}
|
||||
- {message: Portal random teleport now teleports you less farther away at maximum.,
|
||||
type: Tweak}
|
||||
id: 2830
|
||||
time: '2023-01-03T07:53:16.0000000+00:00'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
construction-condition-machine-container-empty = Remove the parts from the frame using a [color=cyan]Crowbar[/color].
|
||||
|
||||
# MachineFrameComplete
|
||||
construction-condition-machine-frame-requirement-label = Requires:
|
||||
construction-condition-machine-frame-insert-circuit-board-message = Insert [color=cyan]any machine circuit board[/color].
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
ent-CrateVendingMachineRestockBooze = { ent-CrateVendingMachineRestockBoozeFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockBoozeFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockClothes = { ent-CrateVendingMachineRestockClothesFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockClothesFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockDinnerware = { ent-CrateVendingMachineRestockDinnerwareFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockDinnerwareFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockEngineering = { ent-CrateVendingMachineRestockEngineeringFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockEngineeringFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockGames = { ent-CrateVendingMachineRestockGamesFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockGamesFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockHotDrinks = { ent-CrateVendingMachineRestockHotDrinksFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockHotDrinksFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockMedical = { ent-CrateVendingMachineRestockMedicalFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockMedicalFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockNutriMax = { ent-CrateVendingMachineRestockNutriMaxFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockNutriMaxFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockPTech = { ent-CrateVendingMachineRestockPTechFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockPTechFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockRobustSoftdrinks = { ent-CrateVendingMachineRestockRobustSoftdrinksFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockRobustSoftdrinksFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockSalvageEquipment = { ent-CrateVendingMachineRestockSalvageEquipmentFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockSalvageEquipmentFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockSecTech = { ent-CrateVendingMachineRestockSecTechFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockSecTechFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockSeeds = { ent-CrateVendingMachineRestockSeedsFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockSeedsFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockSmokes = { ent-CrateVendingMachineRestockSmokesFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockSmokesFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockSnacks = { ent-CrateVendingMachineRestockSnacksFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockSnacksFilled.desc }
|
||||
|
||||
ent-CrateVendingMachineRestockTankDispenser = { ent-CrateVendingMachineRestockTankDispenserFilled }
|
||||
.desc = { ent-CrateVendingMachineRestockTankDispenserFilled.desc }
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
ent-CrateVendingMachineRestockBoozeFilled = Booze-O-Mat restock crate
|
||||
.desc = Contains a restock box for the Booze-O-Mat.
|
||||
|
||||
ent-CrateVendingMachineRestockClothesFilled = Clothing restock crate
|
||||
.desc = Contains a pair of restock boxes, one for the ClothesMate and one for the AutoDrobe.
|
||||
|
||||
ent-CrateVendingMachineRestockDinnerwareFilled = Plasteel Chef restock crate
|
||||
.desc = Contains a restock box for the Plasteel Chef vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockEngineeringFilled = EngiVend restock crate
|
||||
.desc = Contains a restock box for the EngiVend. Also supports the YouTool.
|
||||
|
||||
ent-CrateVendingMachineRestockGamesFilled = Good Clean Fun restock crate
|
||||
.desc = Contains a restock box for the Good Clean Fun vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockHotDrinksFilled = Solar's Best restock crate
|
||||
.desc = Contains two restock boxes for Solar's Best Hot Drinks vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockMedicalFilled = NanoMed restock crate
|
||||
.desc = Contains a restock box, compatible with the NanoMed and NanoMedPlus.
|
||||
|
||||
ent-CrateVendingMachineRestockNutriMaxFilled = NutriMax restock crate
|
||||
.desc = Contains a restock box for the NutriMax vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockPTechFilled = PTech restock crate
|
||||
.desc = Contains a restock box for the PTech bureaucracy dispenser.
|
||||
|
||||
ent-CrateVendingMachineRestockRobustSoftdrinksFilled = Robust Softdrinks restock crate
|
||||
.desc = Contains two restock boxes for the Robust Softdrinks LLC vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockSalvageEquipmentFilled = Salvage restock crate
|
||||
.desc = Contains a restock box for the salvage vendor.
|
||||
|
||||
ent-CrateVendingMachineRestockSecTechFilled = SecTech restock crate
|
||||
.desc = Contains a restock box for the SecTech vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockSeedsFilled = MegaSeed restock crate
|
||||
.desc = Contains a restock box for the MegaSeed vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockSmokesFilled = ShadyCigs restock crate
|
||||
.desc = Contains two restock boxes for the ShadyCigs vending machine.
|
||||
|
||||
ent-CrateVendingMachineRestockSnacksFilled = Snack restock crate
|
||||
.desc = Contains four restock boxes, each covering a different snack vendor. Mr. Chang's, Discount Dans, Robust Donuts, and Getmore Chocolate are featured on the advertisement.
|
||||
|
||||
ent-CrateVendingMachineRestockTankDispenserFilled = Tank dispenser restock crate
|
||||
.desc = Contains a restock box for an Engineering or Atmospherics tank dispenser.
|
||||
@@ -0,0 +1,4 @@
|
||||
vending-machine-restock-invalid-inventory = { CAPITALIZE(THE($this)) } isn't the right package to restock { THE($target) }.
|
||||
vending-machine-restock-needs-panel-open = { CAPITALIZE($target) } needs { POSS-ADJ($target) } maintenance panel opened first.
|
||||
vending-machine-restock-start = { $user } starts restocking { $target }.
|
||||
vending-machine-restock-done = { $user } finishes restocking { $target }.
|
||||
@@ -15,6 +15,7 @@ artifact-effect-hint-multitool = Utility conglomerate
|
||||
artifact-effect-hint-storage = Internal chamber
|
||||
artifact-effect-hint-drill = Serrated rotator
|
||||
artifact-effect-hint-soap = Lubricated surface
|
||||
artifact-effect-hint-communication = Long-distance communication
|
||||
|
||||
# the triggers should be more obvious than the effects
|
||||
# gives people an idea of what to do: don't be too specific (i.e. no "welders")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
blink-artifact-popup = The artifact disappeared in an instant!
|
||||
foam-artifact-popup = Strange foam pours out of the artifact!
|
||||
|
||||
shuffle-artifact-popup = You feel yourself teleport instantly!
|
||||
shuffle-artifact-popup = You feel yourself teleport instantly!
|
||||
charge-artifact-popup = You feel the air buzz with electricity.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user