Files
tbd-station-14/Content.Shared/Interaction/InteractionPopupSystem.cs
Pieter-Jan Briers 1bebb3390c Borg type switching. (#32586)
* Borg type switching.

This allows borgs (new spawn or constructed) to select their chassis type on creation, like in SS13. This removes the need for the many different chassis types, and means round-start borgs can actually play the game immediately instead of waiting for science to unlock everything.

New borgs have an additional action that allows them to select their type. This opens a nice window with basic information about the borgs and a select button. Once a type has been selected it is permanent for that borg chassis.

These borg types also immediately start the borg with specific modules, so they do not need to be printed. Additional modules can still be inserted for upgrades, though this is now less critical. The built-in modules cannot be removed, but are shown in the UI.

The modules that each borg type starts with:

* Generic: tools
* Engineering: advanced tools, construction, RCD, cable
* Salvage: Grappling gun, appraisal, mining
* Janitor: cleaning, light replacer
* Medical: treatment
* Service: music, service, clowning

Specialized borgs have 3 additional module slots available on top of the ones listed above, generic borgs have 5.

Borg types are specified in a new BorgTypePrototype. These prototypes specify all information about the borg type. It is assigned to the borg entity through a mix of client side, server, and shared code. Some of the involved components were made networked, others are just ensured they're set on both sides of the wire.

The most gnarly change is the inventory template prototype, which needs to change purely to modify the borg hat offset. I managed to bodge this in with an API that *probably* won't explode for specifically for this use case, but it's still not the most clean of API designs.

Parts for specific borg chassis have been removed (so much deleted YAML) and specialized borg modules that are in the base set of a type have been removed from the exosuit fab as there's no point to printing those.

The ability to "downgrade" a borg so it can select a new chassis, like in SS13, is something that would be nice, but was not high enough priority for me to block the feature on. I did keep it in mind with some of the code, so it may be possible in the future.

There is no fancy animation when selecting borg types like in SS13, because I didn't think it was high priority, and it would add a lot of complex code.

* Fix sandbox failure due to collection expression.

* Module tweak

Fix salvage borg modules still having research/lathe recipes

Engie borg has regular tool module, not advanced.

* Fix inventory system breakage

* Fix migrations

Some things were missing

* Guidebook rewordings & review

* MinWidth on confirm selection button
2024-11-14 11:08:35 -06:00

185 lines
6.3 KiB
C#

using Content.Shared.Bed.Sleep;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Components;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
namespace Content.Shared.Interaction;
public sealed class InteractionPopupSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly INetManager _netMan = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InteractionPopupComponent, InteractHandEvent>(OnInteractHand);
SubscribeLocalEvent<InteractionPopupComponent, ActivateInWorldEvent>(OnActivateInWorld);
}
private void OnActivateInWorld(EntityUid uid, InteractionPopupComponent component, ActivateInWorldEvent args)
{
if (!args.Complex)
return;
if (!component.OnActivate)
return;
SharedInteract(uid, component, args, args.Target, args.User);
}
private void OnInteractHand(EntityUid uid, InteractionPopupComponent component, InteractHandEvent args)
{
SharedInteract(uid, component, args, args.Target, args.User);
}
private void SharedInteract(
EntityUid uid,
InteractionPopupComponent component,
HandledEntityEventArgs args,
EntityUid target,
EntityUid user)
{
if (args.Handled || user == target)
return;
//Handling does nothing and this thing annoyingly plays way too often.
// HUH? What does this comment even mean?
if (HasComp<SleepingComponent>(uid))
return;
if (TryComp<MobStateComponent>(uid, out var state)
&& !_mobStateSystem.IsAlive(uid, state))
{
return;
}
args.Handled = true;
var curTime = _gameTiming.CurTime;
if (curTime < component.LastInteractTime + component.InteractDelay)
return;
component.LastInteractTime = curTime;
// TODO: Should be an attempt event
// TODO: Need to handle pausing with an accumulator.
var msg = ""; // Stores the text to be shown in the popup message
SoundSpecifier? sfx = null; // Stores the filepath of the sound to be played
var predict = component.SuccessChance is 0 or 1
&& component.InteractSuccessSpawn == null
&& component.InteractFailureSpawn == null;
if (_netMan.IsClient && !predict)
return;
if (_random.Prob(component.SuccessChance))
{
if (component.InteractSuccessString != null)
msg = Loc.GetString(component.InteractSuccessString, ("target", Identity.Entity(uid, EntityManager))); // Success message (localized).
if (component.InteractSuccessSound != null)
sfx = component.InteractSuccessSound;
if (component.InteractSuccessSpawn != null)
Spawn(component.InteractSuccessSpawn, _transform.GetMapCoordinates(uid));
var ev = new InteractionSuccessEvent(user);
RaiseLocalEvent(target, ref ev);
}
else
{
if (component.InteractFailureString != null)
msg = Loc.GetString(component.InteractFailureString, ("target", Identity.Entity(uid, EntityManager))); // Failure message (localized).
if (component.InteractFailureSound != null)
sfx = component.InteractFailureSound;
if (component.InteractFailureSpawn != null)
Spawn(component.InteractFailureSpawn, _transform.GetMapCoordinates(uid));
var ev = new InteractionFailureEvent(user);
RaiseLocalEvent(target, ref ev);
}
if (!string.IsNullOrEmpty(component.MessagePerceivedByOthers))
{
var msgOthers = Loc.GetString(component.MessagePerceivedByOthers,
("user", Identity.Entity(user, EntityManager)), ("target", Identity.Entity(uid, EntityManager)));
_popupSystem.PopupEntity(msgOthers, uid, Filter.PvsExcept(user, entityManager: EntityManager), true);
}
if (!predict)
{
_popupSystem.PopupEntity(msg, uid, user);
if (component.SoundPerceivedByOthers)
_audio.PlayPvs(sfx, target);
else
_audio.PlayEntity(sfx, Filter.Entities(user, target), target, false);
return;
}
_popupSystem.PopupClient(msg, uid, user);
if (sfx == null)
return;
if (component.SoundPerceivedByOthers)
{
_audio.PlayPredicted(sfx, target, user);
return;
}
if (_netMan.IsClient)
{
if (_gameTiming.IsFirstTimePredicted)
_audio.PlayEntity(sfx, Filter.Local(), target, true);
}
else
{
_audio.PlayEntity(sfx, Filter.Empty().FromEntities(target), target, false);
}
}
/// <summary>
/// Sets <see cref="InteractionPopupComponent.InteractSuccessString"/>.
/// </summary>
/// <para>
/// This field is not networked automatically, so this method must be called on both sides of the network.
/// </para>
public void SetInteractSuccessString(Entity<InteractionPopupComponent> ent, string str)
{
ent.Comp.InteractSuccessString = str;
}
/// <summary>
/// Sets <see cref="InteractionPopupComponent.InteractFailureString"/>.
/// </summary>
/// <para>
/// This field is not networked automatically, so this method must be called on both sides of the network.
/// </para>
public void SetInteractFailureString(Entity<InteractionPopupComponent> ent, string str)
{
ent.Comp.InteractFailureString = str;
}
}