using Content.Shared.Store;
using JetBrains.Annotations;
using System.Linq;
+using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Client.Store.Ui;
[ViewVariables]
private StoreMenu? _menu;
- [ViewVariables]
- private string _windowName = Loc.GetString("store-ui-default-title");
-
[ViewVariables]
private string _search = string.Empty;
protected override void Open()
{
- _menu = new StoreMenu(_windowName);
+ _menu = new StoreMenu();
+ if (EntMan.TryGetComponent<StoreComponent>(Owner, out var store))
+ _menu.Title = Loc.GetString(store.Name);
_menu.OpenCentered();
_menu.OnClose += Close;
{
base.UpdateState(state);
- if (_menu == null)
- return;
-
switch (state)
{
case StoreUpdateState msg:
_listings = msg.Listings;
- _menu.UpdateBalance(msg.Balance);
+ _menu?.UpdateBalance(msg.Balance);
UpdateListingsWithSearchFilter();
- _menu.SetFooterVisibility(msg.ShowFooter);
- _menu.UpdateRefund(msg.AllowRefund);
- break;
- case StoreInitializeState msg:
- _windowName = msg.Name;
- if (_menu != null && _menu.Window != null)
- {
- _menu.Window.Title = msg.Name;
- }
+ _menu?.SetFooterVisibility(msg.ShowFooter);
+ _menu?.UpdateRefund(msg.AllowRefund);
break;
}
}
private List<ListingData> _cachedListings = new();
- public StoreMenu(string name)
+ public StoreMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
WithdrawButton.OnButtonDown += OnWithdrawButtonDown;
RefundButton.OnButtonDown += OnRefundButtonDown;
SearchBar.OnTextChanged += _ => SearchTextUpdated?.Invoke(this, SearchBar.Text);
-
- if (Window != null)
- Window.Title = name;
}
public void UpdateBalance(Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> balance)
using Robust.Shared.Utility;
using System.Linq;
using Content.Server.GameTicking.Components;
+using Content.Shared.Store.Components;
namespace Content.Server.GameTicking.Rules;
using System.Numerics;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Movement.Pulling.Systems;
+using Content.Shared.Store.Components;
using Robust.Shared.Collections;
using Robust.Shared.Map.Components;
using Content.Server.Station.Systems;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
+using Content.Server.Traitor.Uplink;
using Content.Shared.Access.Components;
using Content.Shared.CartridgeLoader;
using Content.Shared.Chat;
using Content.Shared.Light.Components;
using Content.Shared.Light.EntitySystems;
using Content.Shared.PDA;
+using Content.Shared.Store.Components;
using Robust.Server.Containers;
using Robust.Server.GameObjects;
using Robust.Shared.Containers;
var address = GetDeviceNetAddress(uid);
var hasInstrument = HasComp<InstrumentComponent>(uid);
- var showUplink = HasComp<StoreComponent>(uid) && IsUnlocked(uid);
+ var showUplink = HasComp<UplinkComponent>(uid) && IsUnlocked(uid);
UpdateStationName(uid, pda);
UpdateAlertLevel(uid, pda);
return;
// check if its locked again to prevent malicious clients opening locked uplinks
- if (TryComp<StoreComponent>(uid, out var store) && IsUnlocked(uid))
- _store.ToggleUi(msg.Actor, uid, store);
+ if (HasComp<UplinkComponent>(uid) && IsUnlocked(uid))
+ _store.ToggleUi(msg.Actor, uid);
}
private void OnUiMessage(EntityUid uid, PdaComponent pda, PdaLockUplinkMessage msg)
using Content.Shared.PDA.Ringer;
using Content.Shared.Popups;
using Content.Shared.Store;
+using Content.Shared.Store.Components;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Network;
using Content.Shared.Revenant;
using Content.Shared.Revenant.Components;
using Content.Shared.StatusEffect;
+using Content.Shared.Store.Components;
using Content.Shared.Stunnable;
using Content.Shared.Tag;
using Robust.Server.GameObjects;
using Content.Server.Store.Components;
using Content.Server.Store.Systems;
using Content.Shared.Store;
+using Content.Shared.Store.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Store.Conditions;
+using System.Linq;
using Content.Server.Store.Components;
using Content.Shared.FixedPoint;
using Content.Server.Administration;
using Content.Shared.Administration;
+using Content.Shared.Store.Components;
using Robust.Shared.Console;
namespace Content.Server.Store.Systems;
if (args.Length == 2 && NetEntity.TryParse(args[0], out var uidNet) && TryGetEntity(uidNet, out var uid))
{
if (TryComp<StoreComponent>(uid, out var store))
- return CompletionResult.FromHintOptions(store.CurrencyWhitelist, "<currency prototype>");
+ return CompletionResult.FromHintOptions(store.CurrencyWhitelist.Select(p => p.ToString()), "<currency prototype>");
}
return CompletionResult.Empty;
-using Content.Server.Store.Components;
using Content.Shared.Store;
+using Content.Shared.Store.Components;
+using Robust.Shared.Prototypes;
namespace Content.Server.Store.Systems;
/// <param name="categories">What categories to filter by.</param>
/// <param name="storeEntity">The physial entity of the store. Can be null.</param>
/// <returns>The available listings.</returns>
- public IEnumerable<ListingData> GetAvailableListings(EntityUid buyer, HashSet<ListingData>? listings, HashSet<string> categories, EntityUid? storeEntity = null)
+ public IEnumerable<ListingData> GetAvailableListings(
+ EntityUid buyer,
+ HashSet<ListingData>? listings,
+ HashSet<ProtoId<StoreCategoryPrototype>> categories,
+ EntityUid? storeEntity = null)
{
listings ??= GetAllListings();
/// <param name="listing">The listing itself.</param>
/// <param name="categories">The categories to check through.</param>
/// <returns>If the listing was present in one of the categories.</returns>
- public bool ListingHasCategory(ListingData listing, HashSet<string> categories)
+ public bool ListingHasCategory(ListingData listing, HashSet<ProtoId<StoreCategoryPrototype>> categories)
{
foreach (var cat in categories)
{
using Content.Server.Store.Components;
+using Content.Shared.Store.Components;
using Robust.Shared.Containers;
namespace Content.Server.Store.Systems;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Mind;
using Content.Shared.Store;
+using Content.Shared.Store.Components;
using Content.Shared.UserInterface;
using Robust.Server.GameObjects;
using Robust.Shared.Audio.Systems;
/// <param name="user">The person who if opening the store ui. Listings are filtered based on this.</param>
/// <param name="store">The store entity itself</param>
/// <param name="component">The store component being refreshed.</param>
- /// <param name="ui"></param>
public void UpdateUserInterface(EntityUid? user, EntityUid store, StoreComponent? component = null)
{
if (!Resolve(store, ref component))
return;
- // TODO: Why is the state not being set unless this?
- if (!_ui.HasUi(store, StoreUiKey.Key))
- return;
-
//this is the person who will be passed into logic for all listing filtering.
if (user != null) //if we have no "buyer" for this update, then don't update the listings
{
}
//log dat shit.
- _admin.Add(LogType.StorePurchase, LogImpact.Low,
+ _admin.Add(LogType.StorePurchase,
+ LogImpact.Low,
$"{ToPrettyString(buyer):player} purchased listing \"{ListingLocalisationHelpers.GetLocalisedNameOrEntityName(listing, _prototypeManager)}\" from {ToPrettyString(uid)}");
listing.PurchaseAmount++; //track how many times something has been purchased
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Content.Shared.Stacks;
-using Content.Shared.Store;
using JetBrains.Annotations;
-using Robust.Server.GameObjects;
using Robust.Shared.Prototypes;
using System.Linq;
+using Content.Shared.Store.Components;
using Robust.Shared.Utility;
namespace Content.Server.Store.Systems;
private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent args)
{
RefreshAllListings(component);
- InitializeFromPreset(component.Preset, uid, component);
component.StartingMap = Transform(uid).MapUid;
}
if (MetaData(uid).EntityLifeStage == EntityLifeStage.MapInitialized)
{
RefreshAllListings(component);
- InitializeFromPreset(component.Preset, uid, component);
}
var ev = new StoreAddedEvent();
UpdateUserInterface(null, uid, store);
return true;
}
-
- /// <summary>
- /// Initializes a store based on a preset ID
- /// </summary>
- /// <param name="preset">The ID of a store preset prototype</param>
- /// <param name="uid"></param>
- /// <param name="component">The store being initialized</param>
- public void InitializeFromPreset(string? preset, EntityUid uid, StoreComponent component)
- {
- if (preset == null)
- return;
-
- if (!_proto.TryIndex<StorePresetPrototype>(preset, out var proto))
- return;
-
- InitializeFromPreset(proto, uid, component);
- }
-
- /// <summary>
- /// Initializes a store based on a given preset
- /// </summary>
- /// <param name="preset">The StorePresetPrototype</param>
- /// <param name="uid"></param>
- /// <param name="component">The store being initialized</param>
- public void InitializeFromPreset(StorePresetPrototype preset, EntityUid uid, StoreComponent component)
- {
- component.Preset = preset.ID;
- component.CurrencyWhitelist.UnionWith(preset.CurrencyWhitelist);
- component.Categories.UnionWith(preset.Categories);
- if (component.Balance == new Dictionary<string, FixedPoint2>() && preset.InitialBalance != null) //if we don't have a value stored, use the preset
- TryAddCurrency(preset.InitialBalance, uid, component);
-
- if (_ui.HasUi(uid, StoreUiKey.Key))
- {
- _ui.SetUiState(uid, StoreUiKey.Key, new StoreInitializeState(preset.StoreName));
- }
- }
}
public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs
-using Content.Shared.Store;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-
namespace Content.Server.Traitor.Uplink.SurplusBundle;
/// <summary>
/// <summary>
/// Total price of all content inside bundle.
/// </summary>
- [ViewVariables(VVAccess.ReadOnly)]
- [DataField("totalPrice")]
+ [DataField]
public int TotalPrice = 20;
-
- /// <summary>
- /// The preset that will be used to get all the listings.
- /// Currently just defaults to the basic uplink.
- /// </summary>
- [DataField("storePreset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))]
- public string StorePreset = "StorePresetUplink";
}
using Content.Server.Store.Systems;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
-using Robust.Shared.Prototypes;
+using Content.Shared.Store.Components;
using Robust.Shared.Random;
namespace Content.Server.Traitor.Uplink.SurplusBundle;
public sealed class SurplusBundleSystem : EntitySystem
{
- [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly EntityStorageSystem _entityStorage = default!;
[Dependency] private readonly StoreSystem _store = default!;
- private ListingData[] _listings = default!;
-
public override void Initialize()
{
base.Initialize();
- SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit);
-
- SubscribeLocalEvent<SurplusBundleComponent, ComponentInit>(OnInit);
- }
- private void OnInit(EntityUid uid, SurplusBundleComponent component, ComponentInit args)
- {
- var storePreset = _prototypeManager.Index<StorePresetPrototype>(component.StorePreset);
-
- _listings = _store.GetAvailableListings(uid, null, storePreset.Categories).ToArray();
-
- Array.Sort(_listings, (a, b) => (int) (b.Cost.Values.Sum() - a.Cost.Values.Sum())); //this might get weird with multicurrency but don't think about it
+ SubscribeLocalEvent<SurplusBundleComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(EntityUid uid, SurplusBundleComponent component, MapInitEvent args)
{
- FillStorage(uid, component);
- }
-
- private void FillStorage(EntityUid uid, SurplusBundleComponent? component = null)
- {
- if (!Resolve(uid, ref component))
+ if (!TryComp<StoreComponent>(uid, out var store))
return;
- var cords = Transform(uid).Coordinates;
+ FillStorage((uid, component, store));
+ }
- var content = GetRandomContent(component.TotalPrice);
+ private void FillStorage(Entity<SurplusBundleComponent, StoreComponent> ent)
+ {
+ var cords = Transform(ent).Coordinates;
+ var content = GetRandomContent(ent);
foreach (var item in content)
{
- var ent = EntityManager.SpawnEntity(item.ProductEntity, cords);
- _entityStorage.Insert(ent, uid);
+ var dode = Spawn(item.ProductEntity, cords);
+ _entityStorage.Insert(dode, ent);
}
}
// wow, is this leetcode reference?
- private List<ListingData> GetRandomContent(FixedPoint2 targetCost)
+ private List<ListingData> GetRandomContent(Entity<SurplusBundleComponent, StoreComponent> ent)
{
var ret = new List<ListingData>();
- if (_listings.Length == 0)
+
+ var listings = _store.GetAvailableListings(ent, null, ent.Comp2.Categories)
+ .OrderBy(p => p.Cost.Values.Sum())
+ .ToList();
+
+ if (listings.Count == 0)
return ret;
var totalCost = FixedPoint2.Zero;
var index = 0;
- while (totalCost < targetCost)
+ while (totalCost < ent.Comp1.TotalPrice)
{
// All data is sorted in price descending order
// Find new item with the lowest acceptable price
// All expansive items will be before index, all acceptable after
- var remainingBudget = targetCost - totalCost;
- while (_listings[index].Cost.Values.Sum() > remainingBudget)
+ var remainingBudget = ent.Comp1.TotalPrice - totalCost;
+ while (listings[index].Cost.Values.Sum() > remainingBudget)
{
index++;
- if (index >= _listings.Length)
+ if (index >= listings.Count)
{
// Looks like no cheap items left
// It shouldn't be case for ss14 content
}
// Select random listing and add into crate
- var randomIndex = _random.Next(index, _listings.Length);
- var randomItem = _listings[randomIndex];
+ var randomIndex = _random.Next(index, listings.Count);
+ var randomItem = listings[randomIndex];
ret.Add(randomItem);
totalCost += randomItem.Cost.Values.Sum();
}
--- /dev/null
+namespace Content.Server.Traitor.Uplink;
+
+/// <summary>
+/// This is used for identifying something as a hidden uplink and showing the UI.
+/// </summary>
+[RegisterComponent]
+public sealed partial class UplinkComponent : Component;
using Content.Server.Store.Components;
using Content.Shared.FixedPoint;
using Content.Shared.Store;
+using Content.Shared.Store.Components;
namespace Content.Server.Traitor.Uplink
{
[ValidatePrototypeId<CurrencyPrototype>]
public const string TelecrystalCurrencyPrototype = "Telecrystal";
- /// <summary>
- /// Gets the amount of TC on an "uplink"
- /// Mostly just here for legacy systems based on uplink.
- /// </summary>
- /// <param name="component"></param>
- /// <returns>the amount of TC</returns>
- public int GetTCBalance(StoreComponent component)
- {
- FixedPoint2? tcBalance = component.Balance.GetValueOrDefault(TelecrystalCurrencyPrototype);
- return tcBalance?.Int() ?? 0;
- }
-
/// <summary>
/// Adds an uplink to the target
/// </summary>
/// <param name="uplinkPresetId">The id of the storepreset</param>
/// <param name="uplinkEntity">The entity that will actually have the uplink functionality. Defaults to the PDA if null.</param>
/// <returns>Whether or not the uplink was added successfully</returns>
- public bool AddUplink(EntityUid user, FixedPoint2? balance, string uplinkPresetId = "StorePresetUplink", EntityUid? uplinkEntity = null)
+ public bool AddUplink(EntityUid user, FixedPoint2? balance, EntityUid? uplinkEntity = null)
{
// Try to find target item
if (uplinkEntity == null)
return false;
}
+ EnsureComp<UplinkComponent>(uplinkEntity.Value);
var store = EnsureComp<StoreComponent>(uplinkEntity.Value);
- _store.InitializeFromPreset(uplinkPresetId, uplinkEntity.Value, store);
store.AccountOwner = user;
store.Balance.Clear();
-
if (balance != null)
{
store.Balance.Clear();
using Content.Shared.FixedPoint;
-using Content.Shared.Store;
using Robust.Shared.Audio;
-using Robust.Shared.Map;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
-using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
-namespace Content.Server.Store.Components;
+namespace Content.Shared.Store.Components;
/// <summary>
/// This component manages a store which players can use to purchase different listings
/// through the ui. The currency, listings, and categories are defined in yaml.
/// </summary>
-[RegisterComponent]
+[RegisterComponent, NetworkedComponent]
public sealed partial class StoreComponent : Component
{
- /// <summary>
- /// The default preset for the store. Is overriden by default values specified on the component.
- /// </summary>
- [DataField("preset", customTypeSerializer: typeof(PrototypeIdSerializer<StorePresetPrototype>))]
- public string? Preset;
+ [DataField]
+ public LocId Name = "store-ui-default-title";
/// <summary>
/// All the listing categories that are available on this store.
/// The available listings are partially based on the categories.
/// </summary>
- [DataField("categories", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<StoreCategoryPrototype>))]
- public HashSet<string> Categories = new();
+ [DataField]
+ public HashSet<ProtoId<StoreCategoryPrototype>> Categories = new();
/// <summary>
/// The total amount of currency that can be used in the store.
/// The string represents the ID of te currency prototype, where the
/// float is that amount.
/// </summary>
- [ViewVariables(VVAccess.ReadWrite), DataField("balance", customTypeSerializer: typeof(PrototypeIdDictionarySerializer<FixedPoint2, CurrencyPrototype>))]
- public Dictionary<string, FixedPoint2> Balance = new();
+ [DataField]
+ public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> Balance = new();
/// <summary>
/// The list of currencies that can be inserted into this store.
/// </summary>
- [ViewVariables(VVAccess.ReadOnly), DataField("currencyWhitelist", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<CurrencyPrototype>))]
- public HashSet<string> CurrencyWhitelist = new();
+ [DataField]
+ public HashSet<ProtoId<CurrencyPrototype>> CurrencyWhitelist = new();
/// <summary>
/// The person who "owns" the store/account. Used if you want the listings to be fixed
/// <summary>
/// All listings, including those that aren't available to the buyer
/// </summary>
+ [DataField]
public HashSet<ListingData> Listings = new();
/// <summary>
/// The total balance spent in this store. Used for refunds.
/// </summary>
[ViewVariables, DataField]
- public Dictionary<string, FixedPoint2> BalanceSpent = new();
+ public Dictionary<ProtoId<CurrencyPrototype>, FixedPoint2> BalanceSpent = new();
/// <summary>
/// Controls if the store allows refunds
/// <summary>
/// The sound played to the buyer when a purchase is succesfully made.
/// </summary>
- [DataField("buySuccessSound")]
+ [DataField]
public SoundSpecifier BuySuccessSound = new SoundPathSpecifier("/Audio/Effects/kaching.ogg");
#endregion
}
}
}
-/// <summary>
-/// initializes miscellaneous data about the store.
-/// </summary>
-[Serializable, NetSerializable]
-public sealed class StoreInitializeState : BoundUserInterfaceState
-{
- public readonly string Name;
-
- public StoreInitializeState(string name)
- {
- Name = name;
- }
-}
-
[Serializable, NetSerializable]
public sealed class StoreRequestUpdateInterfaceMessage : BoundUserInterfaceMessage
{
store-withdraw-button-ui = Withdraw {$currency}
store-ui-button-out-of-stock = {""} (Out of Stock)
store-not-account-owner = This {$store} is not bound to you!
+
+store-preset-name-uplink = Uplink
+store-preset-name-spellbook = Spellbook
- type: entity
id: CrateSyndicateSurplusBundle
- parent: CrateSyndicate
+ parent: [ CrateSyndicate, StorePresetUplink ]
name: Syndicate surplus crate
description: Contains 50 telecrystals worth of completely random Syndicate items. It can be useless junk or really good.
components:
- type: entity
id: CrateSyndicateSuperSurplusBundle
- parent: CrateSyndicate
+ parent: [ CrateSyndicate, StorePresetUplink ]
name: Syndicate super surplus crate
description: Contains 125 telecrystals worth of completely random Syndicate items.
components:
- type: entity
abstract: true
- parent: BaseItem
+ parent: [ BaseItem, StorePresetUplink ] #PDA's have uplinks so they have to inherit the data.
id: BasePDA
name: PDA
description: Personal Data Assistant.
id: WizardsGrimoire
name: wizards grimoire
suffix: Wizard
- parent: BaseItem
+ parent: [ BaseItem, StorePresetSpellbook ]
components:
- type: Sprite
sprite: Objects/Misc/books.rsi
- type: Store
refundAllowed: true
ownerOnly: true # get your own tome!
- preset: StorePresetSpellbook
balance:
WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are
id: WizardsGrimoireNoRefund
name: wizards grimoire
suffix: Wizard, No Refund
- parent: WizardsGrimoire
+ parent: [ WizardsGrimoire, StorePresetSpellbook ]
components:
- type: Store
refundAllowed: false
ownerOnly: true # get your own tome!
- preset: StorePresetSpellbook
balance:
WizCoin: 10 # prices are balanced around this 10 point maximum and how strong the spells are
- Cuffable # useless if you cant be cuffed
- type: entity
- parent: BaseSubdermalImplant
+ parent: [ BaseSubdermalImplant, StorePresetUplink ]
id: UplinkImplant
name: uplink implant
description: This implant lets the user access a hidden Syndicate uplink at will.
components:
- Hands # prevent mouse buying grenade penguin since its not telepathic
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 0
- type: UserInterface
# Uplinks
- type: entity
- parent: BaseItem
+ parent: [ BaseItem, StorePresetUplink ]
id: BaseUplinkRadio
name: syndicate uplink
description: Suspiciously looking old radio...
- type: ActivatableUI
key: enum.StoreUiKey.Key
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 0
suffix: 20 TC
components:
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 20
suffix: 25 TC
components:
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 25
suffix: 40 TC, NukeOps
components:
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 40
- type: Tag
suffix: 60 TC, LoneOps
components:
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 60
- type: Tag
suffix: DEBUG
components:
- type: Store
- preset: StorePresetUplink
balance:
Telecrystal: 99999
-- type: storePreset
+- type: entity
id: StorePresetUplink
- storeName: Uplink
- categories:
- - UplinkWeaponry
- - UplinkAmmo
- - UplinkExplosives
- - UplinkChemicals
- - UplinkDeception
- - UplinkDisruption
- - UplinkImplants
- - UplinkAllies
- - UplinkWearables
- - UplinkJob
- - UplinkPointless
- currencyWhitelist:
- - Telecrystal
+ abstract: true
+ components:
+ - type: Store
+ name: store-preset-name-uplink
+ categories:
+ - UplinkWeaponry
+ - UplinkAmmo
+ - UplinkExplosives
+ - UplinkChemicals
+ - UplinkDeception
+ - UplinkDisruption
+ - UplinkImplants
+ - UplinkAllies
+ - UplinkWearables
+ - UplinkJob
+ - UplinkPointless
+ currencyWhitelist:
+ - Telecrystal
+ balance:
+ Telecrystal: 0
-- type: storePreset
+- type: entity
id: StorePresetSpellbook
- storeName: Spellbook
- categories:
- - SpellbookOffensive #Fireball, Rod Form
- - SpellbookDefensive #Magic Missile, Wall of Force
- - SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph
- - SpellbookEquipment #Battlemage Robes, Staff of Locker
- - SpellbookEvents #Summon Weapons, Summon Ghosts
- currencyWhitelist:
+ abstract: true
+ components:
+ - type: Store
+ name: store-preset-name-spellbook
+ categories:
+ - SpellbookOffensive #Fireball, Rod Form
+ - SpellbookDefensive #Magic Missile, Wall of Force
+ - SpellbookUtility #Body Swap, Lich, Teleport, Knock, Polymorph
+ - SpellbookEquipment #Battlemage Robes, Staff of Locker
+ - SpellbookEvents #Summon Weapons, Summon Ghosts
+ currencyWhitelist:
- WizCoin