diff --git a/Content.Server/PAI/PAISystem.cs b/Content.Server/PAI/PAISystem.cs
index fd581f40e4..289b74b258 100644
--- a/Content.Server/PAI/PAISystem.cs
+++ b/Content.Server/PAI/PAISystem.cs
@@ -2,14 +2,17 @@ using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components;
using Content.Server.Instruments;
using Content.Server.Kitchen.Components;
+using Content.Server.Store.Systems;
using Content.Shared.Interaction.Events;
using Content.Shared.Mind.Components;
using Content.Shared.PAI;
using Content.Shared.Popups;
-using Robust.Shared.Random;
-using System.Text;
+using Content.Shared.Store;
+using Content.Shared.Store.Components;
using Content.Shared.Instruments;
-using Robust.Shared.Player;
+using Robust.Shared.Random;
+using Robust.Shared.Prototypes;
+using System.Text;
namespace Content.Server.PAI;
@@ -19,12 +22,13 @@ public sealed class PAISystem : SharedPAISystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly MetaDataSystem _metaData = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
+ [Dependency] private readonly StoreSystem _store = default!;
[Dependency] private readonly ToggleableGhostRoleSystem _toggleableGhostRole = default!;
///
/// Possible symbols that can be part of a scrambled pai's name.
///
- private static readonly char[] SYMBOLS = new[] { '#', '~', '-', '@', '&', '^', '%', '$', '*', ' '};
+ private static readonly char[] SYMBOLS = new[] { '#', '~', '-', '@', '&', '^', '%', '$', '*', ' ' };
public override void Initialize()
{
@@ -34,6 +38,8 @@ public sealed class PAISystem : SharedPAISystem
SubscribeLocalEvent(OnMindAdded);
SubscribeLocalEvent(OnMindRemoved);
SubscribeLocalEvent(OnMicrowaved);
+
+ SubscribeLocalEvent(OnShop);
}
private void OnUseInHand(EntityUid uid, PAIComponent component, UseInHandEvent args)
@@ -101,6 +107,14 @@ public sealed class PAISystem : SharedPAISystem
_metaData.SetEntityName(uid, val);
}
+ private void OnShop(Entity ent, ref PAIShopActionEvent args)
+ {
+ if (!TryComp(ent, out var store))
+ return;
+
+ _store.ToggleUi(args.Performer, ent, store);
+ }
+
public void PAITurningOff(EntityUid uid)
{
// Close the instrument interface if it was open
diff --git a/Content.Shared/PAI/PAIComponent.cs b/Content.Shared/PAI/PAIComponent.cs
index 9d5be30275..fb9d7150e3 100644
--- a/Content.Shared/PAI/PAIComponent.cs
+++ b/Content.Shared/PAI/PAIComponent.cs
@@ -1,3 +1,5 @@
+using Content.Shared.FixedPoint;
+using Content.Shared.Store;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -24,17 +26,11 @@ public sealed partial class PAIComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public EntityUid? LastUser;
- [DataField(serverOnly: true)]
- public EntProtoId? MidiActionId = "ActionPAIPlayMidi";
-
- [DataField(serverOnly: true)] // server only, as it uses a server-BUI event !type
- public EntityUid? MidiAction;
-
[DataField]
- public EntProtoId MapActionId = "ActionPAIOpenMap";
+ public EntProtoId ShopActionId = "ActionPAIOpenShop";
[DataField, AutoNetworkedField]
- public EntityUid? MapAction;
+ public EntityUid? ShopAction;
///
/// When microwaved there is this chance to brick the pai, kicking out its player and preventing it from being used again.
diff --git a/Content.Shared/PAI/SharedPAISystem.cs b/Content.Shared/PAI/SharedPAISystem.cs
index d66365eb85..c100e38a76 100644
--- a/Content.Shared/PAI/SharedPAISystem.cs
+++ b/Content.Shared/PAI/SharedPAISystem.cs
@@ -1,39 +1,38 @@
using Content.Shared.Actions;
-namespace Content.Shared.PAI
+namespace Content.Shared.PAI;
+
+///
+/// pAIs, or Personal AIs, are essentially portable ghost role generators.
+/// In their current implementation, they create a ghost role anyone can access,
+/// and that a player can also "wipe" (reset/kick out player).
+/// Theoretically speaking pAIs are supposed to use a dedicated "offer and select" system,
+/// with the player holding the pAI being able to choose one of the ghosts in the round.
+/// This seems too complicated for an initial implementation, though,
+/// and there's not always enough players and ghost roles to justify it.
+///
+public abstract class SharedPAISystem : EntitySystem
{
- ///
- /// pAIs, or Personal AIs, are essentially portable ghost role generators.
- /// In their current implementation, they create a ghost role anyone can access,
- /// and that a player can also "wipe" (reset/kick out player).
- /// Theoretically speaking pAIs are supposed to use a dedicated "offer and select" system,
- /// with the player holding the pAI being able to choose one of the ghosts in the round.
- /// This seems too complicated for an initial implementation, though,
- /// and there's not always enough players and ghost roles to justify it.
- ///
- public abstract class SharedPAISystem : EntitySystem
+ [Dependency] private readonly SharedActionsSystem _actions = default!;
+
+ public override void Initialize()
{
- [Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
+ base.Initialize();
- public override void Initialize()
- {
- base.Initialize();
+ SubscribeLocalEvent(OnMapInit);
+ SubscribeLocalEvent(OnShutdown);
+ }
- SubscribeLocalEvent(OnMapInit);
- SubscribeLocalEvent(OnShutdown);
- }
+ private void OnMapInit(Entity ent, ref MapInitEvent args)
+ {
+ _actions.AddAction(ent, ent.Comp.ShopActionId);
+ }
- private void OnMapInit(EntityUid uid, PAIComponent component, MapInitEvent args)
- {
- _actionsSystem.AddAction(uid, ref component.MidiAction, component.MidiActionId);
- _actionsSystem.AddAction(uid, ref component.MapAction, component.MapActionId);
- }
-
- private void OnShutdown(EntityUid uid, PAIComponent component, ComponentShutdown args)
- {
- _actionsSystem.RemoveAction(uid, component.MidiAction);
- _actionsSystem.RemoveAction(uid, component.MapAction);
- }
+ private void OnShutdown(Entity ent, ref ComponentShutdown args)
+ {
+ _actions.RemoveAction(ent, ent.Comp.ShopAction);
}
}
-
+public sealed partial class PAIShopActionEvent : InstantActionEvent
+{
+}
diff --git a/Resources/Locale/en-US/store/currency.ftl b/Resources/Locale/en-US/store/currency.ftl
index ada70b5597..1ba66e6481 100644
--- a/Resources/Locale/en-US/store/currency.ftl
+++ b/Resources/Locale/en-US/store/currency.ftl
@@ -9,4 +9,5 @@ store-currency-display-debugdollar = {$amount ->
}
store-currency-display-telecrystal = TC
store-currency-display-stolen-essence = Stolen Essence
+store-currency-display-silicon-memory = Memory
store-currency-display-wizcoin = Wiz€oin™
diff --git a/Resources/Locale/en-US/store/pai-catalog.ftl b/Resources/Locale/en-US/store/pai-catalog.ftl
new file mode 100644
index 0000000000..3054935614
--- /dev/null
+++ b/Resources/Locale/en-US/store/pai-catalog.ftl
@@ -0,0 +1,8 @@
+pai-mass-scanner-name = Mass Scanner
+pai-mass-scanner-desc = Enables you to scan nearby masses to assist in navigation.
+
+pai-midi-player-name = MIDI Player
+pai-midi-player-desc = Enables you to play music to entertain your owner.
+
+pai-station-map-name = Station Map
+pai-station-map-desc = Enables you to view the station map to assist in navigation.
diff --git a/Resources/Prototypes/Catalog/pai_catalog.yml b/Resources/Prototypes/Catalog/pai_catalog.yml
new file mode 100644
index 0000000000..913eae5d0b
--- /dev/null
+++ b/Resources/Prototypes/Catalog/pai_catalog.yml
@@ -0,0 +1,43 @@
+# Some things might seem like great ideas on paper for pAI apps, but have deliberately been excluded.
+# e.g. Monitoring (Power/Atmos/Crew), Cameras, etc. are excluded as apps for performance and balance reasons.
+# Generally speaking, if you're adding an application, it needs to *not* be round-impacting.
+# Remember that pAI's are *assistants*, not players.
+
+- type: listing
+ id: PAIMassScanner
+ name: pai-mass-scanner-name
+ description: pai-mass-scanner-desc
+ productAction: ActionPAIMassScanner
+ cost:
+ SiliconMemory: 10
+ categories:
+ - PAIAbilities
+ conditions:
+ - !type:ListingLimitedStockCondition
+ stock: 1
+
+- type: listing
+ id: PAIMIDIPlayer
+ name: pai-midi-player-name
+ description: pai-midi-player-desc
+ productAction: ActionPAIPlayMidi
+ cost:
+ SiliconMemory: 5
+ categories:
+ - PAIAbilities
+ conditions:
+ - !type:ListingLimitedStockCondition
+ stock: 1
+
+- type: listing
+ id: PAIStationMap
+ name: pai-station-map-name
+ description: pai-station-map-desc
+ productAction: ActionPAIOpenMap
+ cost:
+ SiliconMemory: 5
+ categories:
+ - PAIAbilities
+ conditions:
+ - !type:ListingLimitedStockCondition
+ stock: 1
diff --git a/Resources/Prototypes/Entities/Objects/Fun/pai.yml b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
index 19aec6e0e2..2a8e0e6a7f 100644
--- a/Resources/Prototypes/Entities/Objects/Fun/pai.yml
+++ b/Resources/Prototypes/Entities/Objects/Fun/pai.yml
@@ -13,6 +13,13 @@
program: 2
- type: UserInterface
interfaces:
+ enum.StoreUiKey.Key:
+ type: StoreBoundUserInterface
+ requireInputValidation: false
+ # available in pai shop
+ enum.RadarConsoleUiKey.Key:
+ type: RadarConsoleBoundUserInterface
+ requireInputValidation: false
enum.InstrumentUiKey.Key:
type: InstrumentBoundUserInterface
requireInputValidation: false
@@ -51,6 +58,16 @@
- Common
- type: DoAfter
- type: Actions
+ - type: Store
+ categories:
+ - PAIAbilities
+ currencyWhitelist:
+ - SiliconMemory
+ balance:
+ SiliconMemory: 30
+ - type: RadarConsole
+ maxRange: 256
+ followEntity: true
- type: TypingIndicator
proto: robot
- type: Speech
@@ -141,6 +158,30 @@
graph: PotatoAI
node: potatoai
+- type: entity
+ id: ActionPAIOpenShop
+ name: Software Catalog
+ description: Install new software to assist your owner.
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ checkConsciousness: false
+ icon: Interface/Actions/shop.png
+ event: !type:PAIShopActionEvent
+
+- type: entity
+ id: ActionPAIMassScanner
+ name: Mass Scanner
+ description: View a mass scanner interface.
+ components:
+ - type: InstantAction
+ checkCanInteract: false
+ checkConsciousness: false
+ icon: { sprite: Interface/Actions/actions_ai.rsi, state: mass_scanner }
+ itemIconStyle: NoItem
+ event: !type:OpenUiActionEvent
+ key: enum.RadarConsoleUiKey.Key
+
- type: entity
id: ActionPAIPlayMidi
name: Play MIDI
@@ -150,6 +191,7 @@
checkCanInteract: false
checkConsciousness: false
icon: Interface/Actions/pai-midi.png
+ itemIconStyle: NoItem
event: !type:OpenUiActionEvent
key: enum.InstrumentUiKey.Key
@@ -162,5 +204,6 @@
checkCanInteract: false
checkConsciousness: false
icon: { sprite: Interface/Actions/pai-map.rsi, state: icon }
+ itemIconStyle: NoItem
event: !type:OpenUiActionEvent
key: enum.StationMapUiKey.Key
diff --git a/Resources/Prototypes/Store/categories.yml b/Resources/Prototypes/Store/categories.yml
index 69cde10020..b3c358d388 100644
--- a/Resources/Prototypes/Store/categories.yml
+++ b/Resources/Prototypes/Store/categories.yml
@@ -94,6 +94,11 @@
id: RevenantAbilities
name: store-category-abilities
+#pai
+- type: storeCategory
+ id: PAIAbilities
+ name: store-category-abilities
+
- type: storeCategory
id: DiscountedItems
name: store-discounted-items
diff --git a/Resources/Prototypes/Store/currency.yml b/Resources/Prototypes/Store/currency.yml
index b1cff06be2..0173de62f9 100644
--- a/Resources/Prototypes/Store/currency.yml
+++ b/Resources/Prototypes/Store/currency.yml
@@ -10,6 +10,11 @@
displayName: store-currency-display-stolen-essence
canWithdraw: false
+- type: currency
+ id: SiliconMemory
+ displayName: store-currency-display-silicon-memory
+ canWithdraw: false
+
- type: currency
id: WizCoin
displayName: store-currency-display-wizcoin