Compare commits

...

4 Commits

Author SHA1 Message Date
Ed
066379e8ca fuck this 2025-05-22 21:39:36 +03:00
Ed
9a0905c53e grid recipe view 2025-05-22 16:59:37 +03:00
Ed
b0dad26252 finish setuping UI 2025-05-22 16:39:25 +03:00
Ed
637e38b4b3 workplace init 2025-05-22 15:29:48 +03:00
13 changed files with 742 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
using Content.Shared._CP14.Workplace;
namespace Content.Client._CP14.Workplace;
public sealed partial class CP14ClientWorkplaceSystem : CP14SharedWorkplaceSystem
{
}

View File

@@ -0,0 +1,34 @@
using Content.Shared._CP14.Workplace;
using Robust.Client.UserInterface;
namespace Content.Client._CP14.Workplace;
public sealed class CP14WorkplaceBoundUserInterface : BoundUserInterface
{
private CP14WorkplaceWindow? _window;
public CP14WorkplaceBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<CP14WorkplaceWindow>();
_window.OnCraft += entry => SendMessage(new CP14WorkplaceCraftMessage(entry.Recipe));
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
switch (state)
{
case CP14WorkplaceState recipesState:
_window?.UpdateState(recipesState);
break;
}
}
}

View File

@@ -0,0 +1,10 @@
<Control xmlns="https://spacestation14.io">
<Button Name="Button" SetSize="48 48">
<EntityPrototypeView Name="View"
MinSize="48 48"
MaxSize="48 48"
Scale="2,2"
HorizontalAlignment="Center"
VerticalExpand="True"/>
</Button>
</Control>

View File

@@ -0,0 +1,53 @@
using Content.Shared._CP14.Workplace;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._CP14.Workplace;
[GenerateTypedNameReferences]
public sealed partial class CP14WorkplaceRecipeControl : Control
{
[Dependency] private readonly IPrototypeManager _prototype = default!;
public event Action<CP14WorkplaceRecipeEntry, EntityPrototype>? OnSelect;
private readonly EntityPrototype _recipePrototype;
private readonly bool _craftable;
public CP14WorkplaceRecipeControl(CP14WorkplaceRecipeEntry entry)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_recipePrototype = _prototype.Index(entry.Recipe);
_craftable = entry.Craftable;
Button.OnPressed += _ => OnSelect?.Invoke(entry, _recipePrototype);
UpdateColor();
//UpdateName();
UpdateView();
}
private void UpdateColor()
{
if (_craftable)
return;
Button.ModulateSelfOverride = Color.FromHex("#302622");
}
private void UpdateName()
{
//var result = _recipePrototype;
//var counter = _recipePrototype.ResultCount > 1 ? $" x{_recipePrototype.ResultCount}" : "";
//Name.Text = $"{Loc.GetString(result.Name)} {counter}" ;
}
private void UpdateView()
{
View.SetPrototype(_recipePrototype);
}
}

View File

@@ -0,0 +1,76 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'cp14-workbench-ui-title'}"
SetSize="700 500"
MinSize="700 300">
<BoxContainer Orientation="Horizontal">
<!-- Main -->
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Horizontal">
<!-- Product list (left side UI) -->
<BoxContainer MinWidth="350" HorizontalExpand="True" Orientation="Vertical" Margin="0 0 10 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<!-- Search Bar -->
<LineEdit Name="SearchBar" Margin="4" PlaceHolder="Search" HorizontalExpand="True" />
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<!-- Crafts container -->
<ScrollContainer HorizontalExpand="True" VerticalExpand="True" MinSize="0 200">
<GridContainer Name="CraftsContainer"
HorizontalExpand="True"
VerticalExpand="True"
Access="Public"
Columns="6" />
</ScrollContainer>
</BoxContainer>
<!-- Craft view (right side UI) -->
<BoxContainer MinWidth="350" Orientation="Vertical" HorizontalExpand="True"
VerticalExpand="True">
<PanelContainer HorizontalExpand="True" VerticalExpand="True">
<!-- Background -->
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#41332f" />
</PanelContainer.PanelOverride>
<!-- Content -->
<BoxContainer HorizontalExpand="True" VerticalExpand="True" Orientation="Vertical">
<!-- Item info -->
<!-- icon -->
<EntityPrototypeView Name="ItemView"
Scale="2,2"
Margin="8"
MinSize="96 96"
MaxSize="96 96"
HorizontalAlignment="Left" />
<!-- name & description -->
<BoxContainer Margin="5 0 0 0" HorizontalExpand="True" VerticalExpand="True"
Orientation="Vertical">
<RichTextLabel Name="ItemName" Margin="5" />
<RichTextLabel Name="ItemDescription" Margin="5" />
</BoxContainer>
<controls:HLine Color="#404040" Thickness="2" Margin="0 5" />
<!-- Required title -->
<Label Margin="5 0 0 0" Text="{Loc 'cp14-workbench-recipe-list'}" />
<!-- Craft requirements content -->
<!-- Added by code -->
<BoxContainer
Margin="5 0 0 0"
Name="ItemRequirements"
Orientation="Vertical" VerticalExpand="True"
HorizontalExpand="True" />
<controls:HLine Color="#404040" Thickness="5" Margin="0 5" />
<!-- Craft button -->
<Button Name="CraftButton" Text="{Loc 'cp14-workbench-craft'}" />
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -0,0 +1,201 @@
using Content.Shared._CP14.Workplace;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client._CP14.Workplace;
[GenerateTypedNameReferences]
public sealed partial class CP14WorkplaceWindow : DefaultWindow
{
private const int AllCategoryId = -1;
[Dependency] private readonly IPrototypeManager _prototype = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IEntityManager _e = default!;
private readonly CP14ClientWorkplaceSystem _workplace = default!;
public event Action<CP14WorkplaceRecipeEntry>? OnCraft;
private readonly Dictionary<int, LocId> _categories = new();
private CP14WorkplaceState? _cachedState;
private EntityUid? user;
private EntityUid? workplace;
private CP14WorkplaceRecipeEntry? _selectedEntry;
private string _searchFilter = string.Empty;
private ISawmill Sawmill { get; init; }
public CP14WorkplaceWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_workplace = _e.System<CP14ClientWorkplaceSystem>();
Sawmill = _log.GetSawmill("cp14_workplace_window");
SearchBar.OnTextChanged += OnSearchChanged;
CraftButton.OnPressed += OnCraftPressed;
OptionCategories.OnItemSelected += OnCategoryItemSelected;
}
public void UpdateRecipesVisibility()
{
if (_cachedState is null)
return;
CraftsContainer.RemoveAllChildren();
var recipes = new List<CP14WorkplaceRecipeEntry>();
foreach (var entry in _cachedState.Recipes)
{
if (!_prototype.TryIndex(entry.Recipe, out var indexedEntry))
{
Sawmill.Error($"No recipe prototype {entry.Recipe} retrieved from cache found");
continue;
}
if (!ProcessSearchFilter(indexedEntry))
continue;
if (!ProcessSearchCategoryFilter(indexedEntry))
continue;
recipes.Add(entry);
}
foreach (var recipe in recipes)
{
var control = new CP14WorkplaceRecipeControl(recipe);
control.OnSelect += RecipeSelect;
CraftsContainer.AddChild(control);
}
if (_selectedEntry is not null && !recipes.Contains(_selectedEntry.Value))
RecipeSelectNull();
}
public void UpdateState(CP14WorkplaceState state)
{
_cachedState = state;
user = _e.GetEntity(state.User);
workplace = _e.GetEntity(state.Workplace);
_categories.Clear();
OptionCategories.Clear();
OptionCategories.AddItem(Loc.GetString("cp14-recipe-category-all"), AllCategoryId);
//Categories update
var categories = new List<LocId>();
var count = 0;
foreach (var recipe in state.Recipes)
{
if (!_prototype.TryIndex(recipe.Recipe, out var indexedRecipe))
continue;
if(!indexedRecipe.Components.TryGetComponent(CP14WorkplaceRecipeComponent.CompName, out var compData) || compData is not CP14WorkplaceRecipeComponent recipeComp)
continue;
if (recipeComp.Category is null)
continue;
if (categories.Contains(recipeComp.Category.Value))
continue;
categories.Add(recipeComp.Category.Value);
}
categories.Sort((a, b) => string.Compare(Loc.GetString(a), Loc.GetString(b), StringComparison.Ordinal));
foreach (var category in categories)
{
OptionCategories.AddItem(Loc.GetString(category), count);
_categories.Add(count, category);
count++;
}
UpdateRecipesVisibility();
}
private void OnSearchChanged(LineEdit.LineEditEventArgs _)
{
_searchFilter = SearchBar.Text.Trim().ToLowerInvariant();
UpdateRecipesVisibility();
}
private bool ProcessSearchFilter(EntityPrototype indexedEntry)
{
if (_searchFilter == string.Empty)
return true;
return indexedEntry.Name.Contains(_searchFilter);
}
private bool ProcessSearchCategoryFilter(EntityPrototype indexedEntry)
{
// If we are searching through all categories, we simply skip the current filter
if (OptionCategories.SelectedId == AllCategoryId)
return true;
if (!_categories.TryGetValue(OptionCategories.SelectedId, out var selectedCategory))
{
Sawmill.Error($"Non-existent {OptionCategories.SelectedId} category id selected. Filter skipped");
return true;
}
if (!indexedEntry.Components.TryGetValue(CP14WorkplaceRecipeComponent.CompName, out var compData) || compData.Component is not CP14WorkplaceRecipeComponent recipeComp)
return false;
if (recipeComp.Category is null)
return false;
return recipeComp.Category == selectedCategory;
}
private void OnCraftPressed(BaseButton.ButtonEventArgs _)
{
if (_selectedEntry is null)
return;
OnCraft?.Invoke(_selectedEntry.Value);
}
private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs obj)
{
OptionCategories.SelectId(obj.Id);
UpdateRecipesVisibility();
}
private void RecipeSelect(CP14WorkplaceRecipeEntry entry, EntityPrototype recipe)
{
_selectedEntry = entry;
ItemView.SetPrototype(recipe);
ItemName.Text = recipe.Name;
ItemDescription.Text = recipe.Description;
ItemRequirements.RemoveAllChildren();
//foreach (var requirement in recipe.Requirements)
//{
// ItemRequirements.AddChild(new CP14WorkbenchRequirementControl(requirement));
//}
CraftButton.Disabled = _workplace.CheckCraftable(entry.Recipe, workplace, user);
}
private void RecipeSelectNull()
{
_selectedEntry = null;
ItemView.SetPrototype(null);
ItemName.Text = string.Empty;
ItemDescription.Text = string.Empty;
ItemRequirements.RemoveAllChildren();
CraftButton.Disabled = true;
}
}

View File

@@ -0,0 +1,12 @@
using Content.Shared._CP14.Workplace;
namespace Content.Server._CP14.Workplace;
public sealed partial class CP14WorkplaceSystem : CP14SharedWorkplaceSystem
{
public override void Initialize()
{
base.Initialize();
}
}

View File

@@ -0,0 +1,148 @@
using Content.Shared.UserInterface;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Workplace;
public abstract partial class CP14SharedWorkplaceSystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
[Dependency] private readonly IPrototypeManager _proto = default!;
/// <summary>
/// All recipes are stored here in the dictionary.
/// These are by design readonly entities for which events are called to collect information on them.
/// </summary>
private Dictionary<EntProtoId, Entity<CP14WorkplaceRecipeComponent>> _cachedRecipes = new();
public override void Initialize()
{
base.Initialize();
CacheAllRecipes();
SubscribeLocalEvent<CP14WorkplaceComponent, BeforeActivatableUIOpenEvent>(OnBeforeUIOpen);
SubscribeLocalEvent<CP14WorkplaceComponent, CP14WorkplaceCraftMessage>(OnCraftAttempt);
SubscribeLocalEvent<CP14WorkplaceComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypeReload);
}
private void OnBeforeUIOpen(Entity<CP14WorkplaceComponent> ent, ref BeforeActivatableUIOpenEvent args)
{
UpdateUIState(ent, args.User);
}
private void OnCraftAttempt(Entity<CP14WorkplaceComponent> ent, ref CP14WorkplaceCraftMessage args)
{
if (!_cachedRecipes.TryGetValue(args.Recipe, out var cachedRecipe))
return;
if (!ent.Comp.CachedRecipes.Contains(cachedRecipe))
return;
}
private void OnMapInit(Entity<CP14WorkplaceComponent> ent, ref MapInitEvent args)
{
CacheWorkplaceRecipes(ent);
}
private void OnPrototypeReload(PrototypesReloadedEventArgs ev)
{
if (!ev.WasModified<EntityPrototype>())
return;
CacheAllRecipes();
var query = EntityQueryEnumerator<CP14WorkplaceComponent>();
while (query.MoveNext(out var uid, out var workplace))
{
CacheWorkplaceRecipes((uid, workplace));
}
}
private void UpdateUIState(Entity<CP14WorkplaceComponent> entity, EntityUid user)
{
var recipes = new List<CP14WorkplaceRecipeEntry>();
foreach (var recipe in entity.Comp.CachedRecipes)
{
var proto = MetaData(recipe).EntityPrototype;
if (proto is null)
continue;
var entry = new CP14WorkplaceRecipeEntry(proto);
recipes.Add(entry);
}
_userInterface.SetUiState(entity.Owner, CP14WorkplaceUiKey.Key, new CP14WorkplaceState(GetNetEntity(user), GetNetEntity(entity), recipes));
}
public bool CheckCraftable(EntProtoId recipe, EntityUid? workplace, EntityUid? user)
{
if (!TryComp<CP14WorkplaceComponent>(workplace, out var workplaceComp))
return false;
if (!_cachedRecipes.TryGetValue(recipe, out var cachedRecipe))
return false;
return CheckCraftable(cachedRecipe, workplace, user);
}
public bool CheckCraftable(EntityUid recipe, EntityUid? workplace, EntityUid? user)
{
if (user is null || workplace is null)
return false;
var ev = new CP14WorkplaceRequirementsPass(user.Value, workplace.Value, recipe);
RaiseLocalEvent(recipe, ev);
if (ev.Cancelled)
return false;
return true;
}
private void CacheAllRecipes()
{
//Delete all old cached recipes entity
foreach (var recipe in _cachedRecipes.Values)
{
QueueDel(recipe);
}
var allEnts = _proto.EnumeratePrototypes<EntityPrototype>();
foreach (var recipe in allEnts)
{
if (!recipe.Components.TryGetComponent(CP14WorkplaceRecipeComponent.CompName, out var compData) || compData is not CP14WorkplaceRecipeComponent recipeComp)
continue;
if (_cachedRecipes.ContainsKey(recipe.ID))
continue;
var ent = Spawn(recipe.ID);
var entComp = EnsureComp<CP14WorkplaceRecipeComponent>(ent);
_cachedRecipes.Add(recipe.ID, (ent, entComp));
}
}
private void CacheWorkplaceRecipes(Entity<CP14WorkplaceComponent> entity)
{
entity.Comp.CachedRecipes.Clear();
foreach (var recipe in _cachedRecipes.Values)
{
if (!entity.Comp.Tags.Contains(recipe.Comp.Tag))
continue;
entity.Comp.CachedRecipes.Add(recipe);
}
}
}
public sealed class CP14WorkplaceRequirementsPass(EntityUid user, EntityUid workplace, EntityUid recipe)
: CancellableEntityEventArgs
{
public EntityUid User { get; } = user;
public EntityUid Workplace { get; } = workplace;
public EntityUid Recipe { get; } = recipe;
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Workplace;
/// <summary>
///
/// </summary>
[RegisterComponent]
public sealed partial class CP14WorkplaceComponent : Component
{
/// <summary>
/// Recipes matching these tags will be available for use on this workplace
/// </summary>
[DataField]
public HashSet<ProtoId<TagPrototype>> Tags = new();
/// <summary>
/// The cached list of recipes available for crafting at this workstation.
/// Cached when initializing a workstation or reloading prototypes
/// </summary>
[DataField]
public HashSet<EntityUid> CachedRecipes = new();
}

View File

@@ -0,0 +1,25 @@
using Content.Shared.Tag;
using Robust.Shared.Prototypes;
namespace Content.Shared._CP14.Workplace;
/// <summary>
///
/// </summary>
[RegisterComponent]
public sealed partial class CP14WorkplaceRecipeComponent : Component
{
public const string CompName = "CP14WorkplaceRecipe";
/// <summary>
/// Recipes matching these tags will be available for use on this workplace
/// </summary>
[DataField(required: true)]
public ProtoId<TagPrototype> Tag;
/// <summary>
/// Recipes will be organized by category if LocId matches
/// </summary>
[DataField]
public LocId? Category;
}

View File

@@ -0,0 +1,29 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared._CP14.Workplace;
[Serializable, NetSerializable]
public enum CP14WorkplaceUiKey
{
Key,
}
[Serializable, NetSerializable]
public sealed class CP14WorkplaceCraftMessage(EntProtoId recipe) : BoundUserInterfaceMessage
{
public readonly EntProtoId Recipe = recipe;
}
[Serializable, NetSerializable]
public sealed class CP14WorkplaceState(NetEntity workplace, NetEntity user, List<CP14WorkplaceRecipeEntry> recipes) : BoundUserInterfaceState
{
public readonly NetEntity User = user;
public readonly NetEntity Workplace = workplace;
public readonly List<CP14WorkplaceRecipeEntry> Recipes = recipes;
}
[Serializable, NetSerializable]
public readonly record struct CP14WorkplaceRecipeEntry(
EntProtoId Recipe
);

View File

@@ -0,0 +1,42 @@
- type: entity
parent:
- BaseStructure
id: CP14BaseWorkplace
categories: [ ForkFiltered ]
name: workplace
components:
- type: Sprite
snapCardinals: true
sprite: _CP14/Structures/Furniture/workbench.rsi
state: filler
- type: Icon
sprite: _CP14/Structures/Furniture/workbench.rsi
state: filler
- type: ActivatableUI
key: enum.CP14WorkplaceUiKey.Key
requiresComplex: true
singleUser: true
- type: CP14Workplace
tags:
- CP14RecipeMeltingFurnace
- type: Climbable
- type: Clickable
- type: InteractionOutline
- type: PlaceableSurface
- type: UserInterface
interfaces:
enum.CP14WorkplaceUiKey.Key:
type: CP14WorkplaceBoundUserInterface
- type: entity
parent: CP14BaseWorkplace
id: CP14BaseWorkplace2
name: workplace 2
components:
- type: Sprite
snapCardinals: true
sprite: _CP14/Structures/Furniture/workbench.rsi
state: cooking_table
- type: CP14Workplace
tags:
- CP14RecipeWorkbench

View File

@@ -0,0 +1,81 @@
- type: entity
id: CP14BaseWorkplaceRecipe
abstract: true
components:
- type: CP14WorkplaceRecipe
tag: CP14RecipeMeltingFurnace
category: cp14-recipe-category-tool
- type: entity
id: CP14RecipeBucket
parent: CP14BaseWorkplaceRecipe
name: Крафт верстака
description: Пара досок и вы прекрасны
components:
- type: CP14WorkplaceRecipe
category: cp14-recipe-category-weapon
- type: Sprite
sprite: _CP14/Structures/Furniture/workbench.rsi
state: filler
- type: entity
id: CP14RecipeBucket2
parent: CP14BaseWorkplaceRecipe
name: Крафт верстака 2
description: Пара досок и вы прекрасны
components:
- type: CP14WorkplaceRecipe
category: cp14-recipe-category-weapon
- type: Sprite
sprite: _CP14/Structures/Furniture/workbench.rsi
state: workbench
- type: entity
id: CP14RecipeBucket3
parent: CP14BaseWorkplaceRecipe
name: Крафт верстака 3
description: Пара досок и вы прекрасны
components:
- type: CP14WorkplaceRecipe
category: cp14-recipe-category-armor
- type: Sprite
sprite: _CP14/Structures/Furniture/workbench.rsi
state: cooking_table
- type: entity
id: CP14RecipeBucket4
parent:
- CP14BaseWorkplaceRecipe
- CP14SmokingPipe
components:
- type: CP14WorkplaceRecipe
category: cp14-recipe-category-tile
- type: entity
id: CP14RecipeBucket5
parent:
- CP14BaseWorkplaceRecipe
- CP14BaseMop
components:
- type: CP14WorkplaceRecipe
category: cp14-recipe-category-tile
- type: entity
id: CP14RecipeBucket6
parent:
- CP14BaseWorkplaceRecipe
- CP14ActionSpellBloodlust
components:
- type: CP14WorkplaceRecipe
tag: CP14RecipeWorkbench
category: cp14-recipe-category-tile
- type: entity
id: CP14RecipeBucket7
parent:
- CP14BaseWorkplaceRecipe
- CP14Lighter
components:
- type: CP14WorkplaceRecipe
tag: CP14RecipeWorkbench
category: cp14-recipe-category-tile