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
This commit is contained in:
Pieter-Jan Briers
2024-11-14 18:08:35 +01:00
committed by GitHub
parent 669bc148f9
commit 1bebb3390c
49 changed files with 1337 additions and 1534 deletions

View File

@@ -0,0 +1,155 @@
using Content.Shared.Interaction.Components;
using Content.Shared.Inventory;
using Content.Shared.Radio;
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.Borgs;
/// <summary>
/// Information for a borg type that can be selected by <see cref="BorgSwitchableTypeComponent"/>.
/// </summary>
/// <seealso cref="SharedBorgSwitchableTypeSystem"/>
[Prototype]
public sealed partial class BorgTypePrototype : IPrototype
{
[ValidatePrototypeId<SoundCollectionPrototype>]
private static readonly ProtoId<SoundCollectionPrototype> DefaultFootsteps = new("FootstepBorg");
[IdDataField]
public required string ID { get; init; }
//
// Description info (name/desc) is configured via localization strings directly.
//
/// <summary>
/// The prototype displayed in the selection menu for this type.
/// </summary>
[DataField]
public required EntProtoId DummyPrototype { get; init; }
//
// Functional information
//
/// <summary>
/// The amount of free module slots this borg type has.
/// </summary>
/// <remarks>
/// This count is on top of the modules specified in <see cref="DefaultModules"/>.
/// </remarks>
/// <seealso cref="BorgChassisComponent.ModuleCount"/>
[DataField]
public int ExtraModuleCount { get; set; } = 0;
/// <summary>
/// The whitelist for borg modules that can be inserted into this borg type.
/// </summary>
/// <seealso cref="BorgChassisComponent.ModuleWhitelist"/>
[DataField]
public EntityWhitelist? ModuleWhitelist { get; set; }
/// <summary>
/// Inventory template used by this borg.
/// </summary>
/// <remarks>
/// This template must be compatible with the normal borg templates,
/// so in practice it can only be used to differentiate the visual position of the slots on the character sprites.
/// </remarks>
/// <seealso cref="InventorySystem.SetTemplateId"/>
[DataField]
public ProtoId<InventoryTemplatePrototype> InventoryTemplateId { get; set; } = "borgShort";
/// <summary>
/// Radio channels that this borg will gain access to from this module.
/// </summary>
/// <remarks>
/// These channels are provided on top of the ones specified in
/// <see cref="BorgSwitchableTypeComponent.InherentRadioChannels"/>.
/// </remarks>
[DataField]
public ProtoId<RadioChannelPrototype>[] RadioChannels = [];
/// <summary>
/// Borg module types that are always available to borgs of this type.
/// </summary>
/// <remarks>
/// These modules still work like modules, although they cannot be removed from the borg.
/// </remarks>
/// <seealso cref="BorgModuleComponent.DefaultModule"/>
[DataField]
public EntProtoId[] DefaultModules = [];
/// <summary>
/// Additional components to add to the borg entity when this type is selected.
/// </summary>
[DataField]
public ComponentRegistry? AddComponents { get; set; }
//
// Visual information
//
/// <summary>
/// The sprite state for the main borg body.
/// </summary>
[DataField]
public string SpriteBodyState { get; set; } = "robot";
/// <summary>
/// An optional movement sprite state for the main borg body.
/// </summary>
[DataField]
public string? SpriteBodyMovementState { get; set; }
/// <summary>
/// Sprite state used to indicate that the borg has a mind in it.
/// </summary>
/// <seealso cref="BorgChassisComponent.HasMindState"/>
[DataField]
public string SpriteHasMindState { get; set; } = "robot_e";
/// <summary>
/// Sprite state used to indicate that the borg has no mind in it.
/// </summary>
/// <seealso cref="BorgChassisComponent.NoMindState"/>
[DataField]
public string SpriteNoMindState { get; set; } = "robot_e_r";
/// <summary>
/// Sprite state used when the borg's flashlight is on.
/// </summary>
[DataField]
public string SpriteToggleLightState { get; set; } = "robot_l";
//
// Minor information
//
/// <summary>
/// String to use on petting success.
/// </summary>
/// <seealso cref="InteractionPopupComponent"/>
[DataField]
public string PetSuccessString { get; set; } = "petting-success-generic-cyborg";
/// <summary>
/// String to use on petting failure.
/// </summary>
/// <seealso cref="InteractionPopupComponent"/>
[DataField]
public string PetFailureString { get; set; } = "petting-failure-generic-cyborg";
//
// Sounds
//
/// <summary>
/// Sound specifier for footstep sounds created by this borg.
/// </summary>
[DataField]
public SoundSpecifier FootstepCollection { get; set; } = new SoundCollectionSpecifier(DefaultFootsteps);
}

View File

@@ -89,5 +89,18 @@ public enum BorgVisuals : byte
[Serializable, NetSerializable]
public enum BorgVisualLayers : byte
{
Light
/// <summary>
/// Main borg body layer.
/// </summary>
Body,
/// <summary>
/// Layer for the borg's mind state.
/// </summary>
Light,
/// <summary>
/// Layer for the borg flashlight status.
/// </summary>
LightStatus,
}

View File

@@ -7,6 +7,7 @@ namespace Content.Shared.Silicons.Borgs.Components;
/// to give them unique abilities and attributes.
/// </summary>
[RegisterComponent, NetworkedComponent, Access(typeof(SharedBorgSystem))]
[AutoGenerateComponentState]
public sealed partial class BorgModuleComponent : Component
{
/// <summary>
@@ -16,6 +17,13 @@ public sealed partial class BorgModuleComponent : Component
public EntityUid? InstalledEntity;
public bool Installed => InstalledEntity != null;
/// <summary>
/// If true, this is a "default" module that cannot be removed from a borg.
/// </summary>
[DataField]
[AutoNetworkedField]
public bool DefaultModule;
}
/// <summary>

View File

@@ -0,0 +1,72 @@
using Content.Shared.Actions;
using Content.Shared.Radio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Silicons.Borgs.Components;
/// <summary>
/// Component for borgs that can switch their "type" after being created.
/// </summary>
/// <remarks>
/// <para>
/// This is used by all NT borgs, on construction and round-start spawn.
/// Borgs are effectively useless until they have made their choice of type.
/// Borg type selections are currently irreversible.
/// </para>
/// <para>
/// Available types are specified in <see cref="BorgTypePrototype"/>s.
/// </para>
/// </remarks>
/// <seealso cref="SharedBorgSwitchableTypeSystem"/>
[RegisterComponent, NetworkedComponent]
[AutoGenerateComponentState(raiseAfterAutoHandleState: true)]
[Access(typeof(SharedBorgSwitchableTypeSystem))]
public sealed partial class BorgSwitchableTypeComponent : Component
{
/// <summary>
/// Action entity used by players to select their type.
/// </summary>
[DataField, AutoNetworkedField]
public EntityUid? SelectTypeAction;
/// <summary>
/// The currently selected borg type, if any.
/// </summary>
/// <remarks>
/// This can be set in a prototype to immediately apply a borg type, and not have switching support.
/// </remarks>
[DataField, AutoNetworkedField]
public ProtoId<BorgTypePrototype>? SelectedBorgType;
/// <summary>
/// Radio channels that the borg will always have. These are added on top of the selected type's radio channels.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype>[] InherentRadioChannels = [];
}
/// <summary>
/// Action event used to open the selection menu of a <see cref="BorgSwitchableTypeComponent"/>.
/// </summary>
public sealed partial class BorgToggleSelectTypeEvent : InstantActionEvent;
/// <summary>
/// UI message used by a borg to select their type with <see cref="BorgSwitchableTypeComponent"/>.
/// </summary>
/// <param name="prototype">The borg type prototype that the user selected.</param>
[Serializable, NetSerializable]
public sealed class BorgSelectTypeMessage(ProtoId<BorgTypePrototype> prototype) : BoundUserInterfaceMessage
{
public ProtoId<BorgTypePrototype> Prototype = prototype;
}
/// <summary>
/// UI key used by the selection menu for <see cref="BorgSwitchableTypeComponent"/>.
/// </summary>
[NetSerializable, Serializable]
public enum BorgSwitchableTypeUiKey : byte
{
SelectBorgType,
}

View File

@@ -0,0 +1,125 @@
using Content.Shared.Actions;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Silicons.Borgs.Components;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Shared.Silicons.Borgs;
/// <summary>
/// Implements borg type switching.
/// </summary>
/// <seealso cref="BorgSwitchableTypeComponent"/>
public abstract class SharedBorgSwitchableTypeSystem : EntitySystem
{
// TODO: Allow borgs to be reset to default configuration.
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
[Dependency] protected readonly IPrototypeManager Prototypes = default!;
[Dependency] private readonly InteractionPopupSystem _interactionPopup = default!;
[ValidatePrototypeId<EntityPrototype>]
public const string ActionId = "ActionSelectBorgType";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<BorgSwitchableTypeComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<BorgSwitchableTypeComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<BorgSwitchableTypeComponent, BorgToggleSelectTypeEvent>(OnSelectBorgTypeAction);
Subs.BuiEvents<BorgSwitchableTypeComponent>(BorgSwitchableTypeUiKey.SelectBorgType,
sub =>
{
sub.Event<BorgSelectTypeMessage>(SelectTypeMessageHandler);
});
}
//
// UI-adjacent code
//
private void OnMapInit(Entity<BorgSwitchableTypeComponent> ent, ref MapInitEvent args)
{
_actionsSystem.AddAction(ent, ref ent.Comp.SelectTypeAction, ActionId);
Dirty(ent);
if (ent.Comp.SelectedBorgType != null)
{
SelectBorgModule(ent, ent.Comp.SelectedBorgType.Value);
}
}
private void OnShutdown(Entity<BorgSwitchableTypeComponent> ent, ref ComponentShutdown args)
{
_actionsSystem.RemoveAction(ent, ent.Comp.SelectTypeAction);
}
private void OnSelectBorgTypeAction(Entity<BorgSwitchableTypeComponent> ent, ref BorgToggleSelectTypeEvent args)
{
if (args.Handled || !TryComp<ActorComponent>(ent, out var actor))
return;
args.Handled = true;
_userInterface.TryToggleUi((ent.Owner, null), BorgSwitchableTypeUiKey.SelectBorgType, actor.PlayerSession);
}
private void SelectTypeMessageHandler(Entity<BorgSwitchableTypeComponent> ent, ref BorgSelectTypeMessage args)
{
if (ent.Comp.SelectedBorgType != null)
return;
if (!Prototypes.HasIndex(args.Prototype))
return;
SelectBorgModule(ent, args.Prototype);
}
//
// Implementation
//
protected virtual void SelectBorgModule(
Entity<BorgSwitchableTypeComponent> ent,
ProtoId<BorgTypePrototype> borgType)
{
ent.Comp.SelectedBorgType = borgType;
_actionsSystem.RemoveAction(ent, ent.Comp.SelectTypeAction);
ent.Comp.SelectTypeAction = null;
Dirty(ent);
_userInterface.CloseUi((ent.Owner, null), BorgSwitchableTypeUiKey.SelectBorgType);
UpdateEntityAppearance(ent);
}
protected void UpdateEntityAppearance(Entity<BorgSwitchableTypeComponent> entity)
{
if (!Prototypes.TryIndex(entity.Comp.SelectedBorgType, out var proto))
return;
UpdateEntityAppearance(entity, proto);
}
protected virtual void UpdateEntityAppearance(
Entity<BorgSwitchableTypeComponent> entity,
BorgTypePrototype prototype)
{
if (TryComp(entity, out InteractionPopupComponent? popup))
{
_interactionPopup.SetInteractSuccessString((entity.Owner, popup), prototype.PetSuccessString);
_interactionPopup.SetInteractFailureString((entity.Owner, popup), prototype.PetFailureString);
}
if (TryComp(entity, out FootstepModifierComponent? footstepModifier))
{
footstepModifier.FootstepSoundCollection = prototype.FootstepCollection;
}
}
}

View File

@@ -124,4 +124,13 @@ public abstract partial class SharedBorgSystem : EntitySystem
var sprintDif = movement.BaseWalkSpeed / movement.BaseSprintSpeed;
args.ModifySpeed(1f, sprintDif);
}
/// <summary>
/// Sets <see cref="BorgModuleComponent.DefaultModule"/>.
/// </summary>
public void SetBorgModuleDefault(Entity<BorgModuleComponent> ent, bool newDefault)
{
ent.Comp.DefaultModule = newDefault;
Dirty(ent);
}
}