init
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Mods;
|
||||
using Cthangover.Core.Settings;
|
||||
|
||||
namespace Cthangover.Cooking
|
||||
{
|
||||
/// <summary>
|
||||
/// Exposes a user-configurable setting for the number of rations
|
||||
/// consumed per character per in-game day. Registered automatically
|
||||
/// via <see cref="ModSettingsRegistry"/> when the cooking mod
|
||||
/// assembly loads.
|
||||
///
|
||||
/// The static <see cref="RationsPerCharacter"/> property is the
|
||||
/// canonical source of truth shared with <c>RationStatisticsPanel</c>
|
||||
/// and any future consumption logic. It is updated by the core on
|
||||
/// boot (from persisted JSON) and on every UI change.
|
||||
/// </summary>
|
||||
public class CookingSettings : IModSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Current value for rations consumed per character per day.
|
||||
/// Default is 1. Updated by <see cref="ReadValues"/> on boot
|
||||
/// and whenever the player changes the setting in the menu.
|
||||
/// </summary>
|
||||
public static int RationsPerCharacter { get; private set; } = 1;
|
||||
|
||||
public IReadOnlyList<ModSettingDefinition> GetDefinitions()
|
||||
{
|
||||
return new List<ModSettingDefinition>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Key = "rations_per_character",
|
||||
Name = "mod_cooking/rations_per_character",
|
||||
Type = SettingType.Slider,
|
||||
DefaultValue = "1",
|
||||
Min = 1f,
|
||||
Max = 10f,
|
||||
Step = 1f,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public void WriteValues(DataBlob blob)
|
||||
{
|
||||
blob.SetInt("rations_per_character", RationsPerCharacter);
|
||||
}
|
||||
|
||||
public void ReadValues(DataBlob blob)
|
||||
{
|
||||
RationsPerCharacter = blob.GetInt("rations_per_character", 1);
|
||||
if (RationsPerCharacter < 1)
|
||||
RationsPerCharacter = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Cthangover.Core.Relationship;
|
||||
using Cthangover.Core.Settings;
|
||||
|
||||
namespace Cthangover.Core.Relationship
|
||||
{
|
||||
/// <summary>
|
||||
/// Tracks nutrition and starvation for party recruits.
|
||||
/// Works by recording the tick when a recruit last ate (<c>FullnessTime</c>),
|
||||
/// then each tick computes the elapsed time as a percentage of a 24-hour window.
|
||||
/// Once the fullness window expires (100%+), the recruit loses 1 HP per tick
|
||||
/// until fed again via a ration or meal action.
|
||||
/// Registered with the <see cref="Recruit"/> behaviour pipeline and called
|
||||
/// by the time system on each global tick.
|
||||
/// </summary>
|
||||
public class NutritionBehaviour : IRecruitBehaviour
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique behaviour identifier used to look up and configure this
|
||||
/// behaviour from <see cref="Recruit"/>'s behaviour collection.
|
||||
/// </summary>
|
||||
public string Id => "nutrition";
|
||||
|
||||
/// <summary>
|
||||
/// Called when the behaviour is first attached to a recruit.
|
||||
/// Seeds <c>FullnessTime</c> with the current global tick so that
|
||||
/// starve-damage begins counting from this point.
|
||||
/// </summary>
|
||||
public void ConfigureRecruit(Recruit recruit, RuntimeData runtime)
|
||||
{
|
||||
recruit.Properties.SetLong("FullnessTime", runtime.Time.Tick);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether the recruit has gone beyond the fullness window
|
||||
/// (24 in-game hours, represented as <c>24 * 4 * 60</c> ticks) since
|
||||
/// <c>FullnessTime</c>. If the window is exceeded, reduces the
|
||||
/// recruit's <c>PROP_HEALTH</c> by 1 per tick until they are fed.
|
||||
/// Tick-driven starvation allows gradual death when rations run out.
|
||||
/// </summary>
|
||||
public void OnTick(Recruit recruit, RuntimeData runtime, long currentTick)
|
||||
{
|
||||
var fullnessTime = recruit.Properties.GetLong("FullnessTime");
|
||||
if (fullnessTime == 0)
|
||||
return;
|
||||
var windowSize = 24 * 4 * 60L;
|
||||
var deltaTime = currentTick - fullnessTime;
|
||||
var fullness = (float)deltaTime / windowSize * 100f;
|
||||
if (fullness > 100)
|
||||
{
|
||||
var hp = recruit.Properties.GetInt(Recruit.PROP_HEALTH);
|
||||
if (hp > 0)
|
||||
recruit.Properties.SetInt(Recruit.PROP_HEALTH, hp - 1);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup hook invoked when the behaviour is detached from a recruit.
|
||||
/// No persistent state requires teardown; exists for interface contract.
|
||||
/// </summary>
|
||||
public void OnRemove(Recruit recruit, RuntimeData runtime)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using Cthangover.Core.Settings;
|
||||
|
||||
namespace Cthangover.Core.Items.Actions
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs the "Wolf Meat to Ration" recipe-unlock action.
|
||||
/// When a player uses raw wolf meat, this action checks whether the
|
||||
/// <c>recipe/wolf_meat_to_ration</c> flag is already present in
|
||||
/// <see cref="RecipeData"/>; if not, it adds it, permanently unlocking
|
||||
/// the recipe in the <see cref="Mods.Cooking.Workbench.WorkbenchPanel"/>.
|
||||
/// Returns <c>false</c> when already unlocked (no-op) and <c>true</c>
|
||||
/// on first unlock.
|
||||
/// </summary>
|
||||
public class WolfMeatToRation : IItemAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Globally unique action identifier, matched by the item system
|
||||
/// to trigger this action when an item with this ID is used.
|
||||
/// </summary>
|
||||
public string ID => "action/recipe/wolf_meat_to_ration";
|
||||
|
||||
/// <summary>
|
||||
/// Idempotently unlocks the wolf-meat-to-ration recipe.
|
||||
/// Adds <c>recipe/wolf_meat_to_ration</c> to <see cref="RecipeData"/>
|
||||
/// so that the cooking workbench can list it.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if the recipe was newly unlocked; <c>false</c> if already known.</returns>
|
||||
public bool UseAction(IItem item)
|
||||
{
|
||||
var data = GameData.Instance.Runtime.RecipeData;
|
||||
if (data.Has("recipe/wolf_meat_to_ration"))
|
||||
return false;
|
||||
data.Add("recipe/wolf_meat_to_ration");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Cthangover.Cooking;
|
||||
|
||||
using System;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.Cooking.Rations
|
||||
{
|
||||
/// <summary>
|
||||
/// Displays live ration statistics on the dinner/ration UI screen.
|
||||
/// Shows the player the current ration count, how many rations are
|
||||
/// consumed daily (one per character), and the maximum number of days
|
||||
/// the party can survive on available rations. Listens to
|
||||
/// <see cref="Core.Items.InventoryBag.Change"/> events to refresh
|
||||
/// automatically whenever the inventory is modified.
|
||||
/// </summary>
|
||||
public class RationStatisticsPanel : Widget
|
||||
{
|
||||
private RichTextLabel label;
|
||||
private Action<string, int> _onInventoryChange;
|
||||
|
||||
protected override void OnceConstruct()
|
||||
{
|
||||
label = GetNode<RichTextLabel>("RationMargin/RationLabel");
|
||||
|
||||
_onInventoryChange = (_, _) => UpdateInfo();
|
||||
GameData.Instance.Runtime.Inventory.Change += _onInventoryChange;
|
||||
}
|
||||
|
||||
protected override void ShowConstruct()
|
||||
{
|
||||
UpdateInfo();
|
||||
}
|
||||
|
||||
protected override void OnceDestruct()
|
||||
{
|
||||
GameData.Instance.Runtime.Inventory.Change -= _onInventoryChange;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Recomputes and refreshes the ration-statistics text.
|
||||
/// Reads current ration inventory via <see cref="Core.Items.InventoryBag.CheckCount"/>
|
||||
/// for <c>"food/ration"</c> and the total character count from
|
||||
/// <see cref="CharacterData"/>. Formats three localized lines:
|
||||
/// total rations, daily consumption, and remaining days
|
||||
/// (rations divided by character count, clamped to 1 minimum).
|
||||
/// </summary>
|
||||
public void UpdateInfo()
|
||||
{
|
||||
if (label == null)
|
||||
return;
|
||||
|
||||
var rationCount = GameData.Instance.Runtime.Inventory.CheckCount("food/ration");
|
||||
var characterCount = GameData.Instance.Runtime.CharacterData.Characters.Count;
|
||||
var perChar = CookingSettings.RationsPerCharacter;
|
||||
var dailyConsumption = characterCount * perChar;
|
||||
|
||||
label.Text = string.Format(TranslationServer.Translate("ui/diner/rationcount"), rationCount) +
|
||||
"\n" + string.Format(TranslationServer.Translate("ui/diner/rationday"), dailyConsumption) +
|
||||
"\n" + string.Format(TranslationServer.Translate("ui/diner/rationmax"), rationCount / Mathf.Max(1, dailyConsumption));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.Cooking.Workbench
|
||||
{
|
||||
/// <summary>
|
||||
/// Renders a single ingredient row inside a <see cref="RecipeItemBehaviour"/>
|
||||
/// on the cooking workbench. Displays the ingredient's item icon and the
|
||||
/// required count. When <see cref="CheckAndUpdate"/> is called, compares
|
||||
/// inventory count against the recipe requirement and tints the icon and
|
||||
/// count text grey if the player lacks enough items.
|
||||
/// </summary>
|
||||
public class RecipeIconItemBehaviour : Widget
|
||||
{
|
||||
private static readonly Color NormalColor = Colors.White;
|
||||
private static readonly Color NotItemsColor = Colors.Gray;
|
||||
|
||||
private TextureRect img;
|
||||
private Label txt;
|
||||
private IIngredient ingredient;
|
||||
|
||||
protected override void OnceConstruct()
|
||||
{
|
||||
img = GetNode<TextureRect>("Img");
|
||||
txt = GetNode<Label>("Txt");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binds this icon widget to a recipe ingredient definition.
|
||||
/// Sets the icon texture from <c>ingredient.Item.Sprite</c> (logs an error
|
||||
/// if the item reference is null) and displays the required count as text.
|
||||
/// The <paramref name="hasItems"/> flag is currently unused but reserved
|
||||
/// for future visual pre-initialization.
|
||||
/// </summary>
|
||||
public void Init(IIngredient ingredient, bool hasItems)
|
||||
{
|
||||
EnsureConstructed();
|
||||
this.ingredient = ingredient;
|
||||
if (ingredient.Item != null)
|
||||
{
|
||||
img.Texture = ingredient.Item.Sprite;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameLogger.Log("MOD_TEST", $"RecipeIconItemBehaviour::Init ingredient.Item is NULL", LogLevel.Error);
|
||||
}
|
||||
txt.Text = ingredient.Count.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates whether the player's inventory contains enough of the
|
||||
/// required ingredient. Tints the icon and count text to
|
||||
/// <c>NormalColor</c> (white) when satisfied or <c>NotItemsColor</c>
|
||||
/// (grey) when insufficient. Called by the parent
|
||||
/// <see cref="RecipeItemBehaviour"/> during state refresh.
|
||||
/// </summary>
|
||||
/// <returns><c>true</c> if inventory meets or exceeds the required count.</returns>
|
||||
public bool CheckAndUpdate()
|
||||
{
|
||||
var result = GameData.Instance.Runtime.Inventory.CheckCount(ingredient.Item) >= ingredient.Count;
|
||||
txt.AddThemeColorOverride("font_color", result ? NormalColor : NotItemsColor);
|
||||
img.Modulate = result ? Colors.White : NotItemsColor;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
using System.Collections.Generic;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.Cooking.Workbench
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single recipe entry in the <see cref="WorkbenchPanel"/> list.
|
||||
/// Displays the recipe name, preparation time, and a horizontal row of
|
||||
/// ingredient icons. Supports click-to-select with visual highlight
|
||||
/// (tracked statically so only one recipe is selected at a time).
|
||||
/// Fires <see cref="RecipeClicked"/> to notify the parent panel of
|
||||
/// selection changes. Ingredient availability is checked via child
|
||||
/// <see cref="RecipeIconItemBehaviour"/> instances, greying out the
|
||||
/// entire row when any ingredient is missing.
|
||||
/// </summary>
|
||||
public class RecipeItemBehaviour : Widget
|
||||
{
|
||||
private Control background;
|
||||
private Control recipeContent;
|
||||
private Label txtName;
|
||||
private Label txtTime;
|
||||
private HBoxContainer recipeContentHbox;
|
||||
|
||||
private static readonly Color normalColor = new(1f, 1f, 1f, 1f);
|
||||
private static readonly Color selectedColor = new(0.8f, 0.8f, 0.8f, 1f);
|
||||
private static readonly Color disableColor = new(0.5f, 0.5f, 0.5f, 1f);
|
||||
|
||||
private IRecipe recipe;
|
||||
private Node panel;
|
||||
|
||||
private static RecipeItemBehaviour selected;
|
||||
|
||||
/// <summary>
|
||||
/// The recipe currently selected across all <see cref="RecipeItemBehaviour"/>
|
||||
/// instances. Only one recipe can be selected at a time because the
|
||||
/// selection is tracked via a static backing field. Returns <c>null</c>
|
||||
/// when no recipe is selected.
|
||||
/// </summary>
|
||||
public static IRecipe SelectedRecipe => selected?.recipe;
|
||||
private readonly List<Node> items = new();
|
||||
private readonly List<RecipeIconItemBehaviour> ingredients = new();
|
||||
|
||||
/// <summary>
|
||||
/// Fired when this recipe entry is clicked, passing the recipe data
|
||||
/// and this widget so that <see cref="WorkbenchPanel"/> can update
|
||||
/// the description and output preview panel.
|
||||
/// </summary>
|
||||
public event System.Action<IRecipe, RecipeItemBehaviour> RecipeClicked;
|
||||
|
||||
protected override void OnceConstruct()
|
||||
{
|
||||
background = GetNode<Control>("Background");
|
||||
recipeContentHbox = GetNode<HBoxContainer>("HBox/RecipeContent/RecipeContentHbox");
|
||||
recipeContent = recipeContentHbox.GetParent() as Control;
|
||||
txtName = GetNode<Label>("HBox/TxtName");
|
||||
txtTime = GetNode<Label>("HBox/TxtTime");
|
||||
|
||||
GuiInput += OnGuiInput;
|
||||
}
|
||||
|
||||
protected override void OnceDestruct()
|
||||
{
|
||||
GuiInput -= OnGuiInput;
|
||||
}
|
||||
|
||||
private void OnGuiInput(InputEvent @event)
|
||||
{
|
||||
if (@event is InputEventMouseButton mouseButton && mouseButton.Pressed && mouseButton.ButtonIndex == MouseButton.Left)
|
||||
{
|
||||
OnClick();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Refreshes the visual availability state of this recipe.
|
||||
/// Iterates all child <see cref="RecipeIconItemBehaviour"/> widgets,
|
||||
/// calling <c>CheckAndUpdate</c> on each. If any ingredient is missing,
|
||||
/// tints the name and time labels with <c>disableColor</c> (greyed out);
|
||||
/// otherwise restores the default yellow/red colour scheme.
|
||||
/// Call after inventory changes or cooking operations.
|
||||
/// </summary>
|
||||
public void UpdateState()
|
||||
{
|
||||
var hasItems = true;
|
||||
|
||||
foreach (var ingredient in ingredients)
|
||||
if (!ingredient.CheckAndUpdate())
|
||||
hasItems = false;
|
||||
|
||||
if (!hasItems)
|
||||
{
|
||||
txtName.AddThemeColorOverride("font_color", disableColor);
|
||||
txtTime.AddThemeColorOverride("font_color", disableColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
txtName.AddThemeColorOverride("font_color", Colors.Yellow);
|
||||
txtTime.AddThemeColorOverride("font_color", Colors.Red);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialises the recipe row with recipe data and the parent panel
|
||||
/// reference. Spawns <see cref="RecipeIconItemBehaviour"/> children
|
||||
/// for each ingredient, with plus-sign separators between them.
|
||||
/// Sets the translated recipe name and time, then calls
|
||||
/// <see cref="UpdateState"/> to set initial colour state.
|
||||
/// </summary>
|
||||
public void Init(IRecipe recipe, Node panel)
|
||||
{
|
||||
EnsureConstructed();
|
||||
this.recipe = recipe;
|
||||
this.panel = panel;
|
||||
|
||||
for (int i = 0; i < recipe.Input.Count; i++)
|
||||
{
|
||||
var ingredient = recipe.Input[i];
|
||||
AddIngredient(ingredient, i != recipe.Input.Count - 1);
|
||||
}
|
||||
|
||||
txtName.Text = TranslationServer.Translate(recipe.Name);
|
||||
txtTime.Text = string.Format(TranslationServer.Translate("ui/cook/recipe_time"), recipe.Time);
|
||||
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
private void AddIngredient(IIngredient ingredient, bool addPlus)
|
||||
{
|
||||
var inventory = GameData.Instance.Runtime.Inventory;
|
||||
|
||||
var item = (RecipeIconItemBehaviour)TscnScenes.LoadAndBuild("scenes/recipe_icon_item.tscn");
|
||||
recipeContentHbox.AddChild(item);
|
||||
var localHasItems = inventory.CheckCount(ingredient.Item) < ingredient.Count;
|
||||
item.Init(ingredient, localHasItems);
|
||||
items.Add(item);
|
||||
ingredients.Add(item);
|
||||
|
||||
if (addPlus)
|
||||
{
|
||||
var plus = new Label();
|
||||
plus.Text = "+";
|
||||
plus.AddThemeFontSizeOverride("font_size", 12);
|
||||
plus.VerticalAlignment = VerticalAlignment.Center;
|
||||
recipeContentHbox.AddChild(plus);
|
||||
items.Add(plus);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tears down this recipe row by queue-freeing all child nodes
|
||||
/// (ingredient icons, plus labels, and this widget itself),
|
||||
/// then clears the internal ingredient/item lists.
|
||||
/// Called by <see cref="WorkbenchPanel"/> when the panel is hidden.
|
||||
/// </summary>
|
||||
public void Destroy()
|
||||
{
|
||||
foreach (var item in items)
|
||||
item.QueueFree();
|
||||
QueueFree();
|
||||
ingredients.Clear();
|
||||
items.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles left-click selection of this recipe entry.
|
||||
/// Deselects the previously-selected entry (if any) by restoring its
|
||||
/// background to normal colour, highlights the background of this
|
||||
/// entry, updates the static <c>selected</c> tracker, and invokes
|
||||
/// <see cref="RecipeClicked"/> so the parent can update the
|
||||
/// description and output panel.
|
||||
/// </summary>
|
||||
public void OnClick()
|
||||
{
|
||||
if (selected != null)
|
||||
selected.background.Modulate = normalColor;
|
||||
background.Modulate = selectedColor;
|
||||
selected = this;
|
||||
RecipeClicked?.Invoke(recipe, this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Actions.Atomic;
|
||||
using Cthangover.Core.Actions;
|
||||
using Cthangover.Core.Utils;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.Cooking.Workbench
|
||||
{
|
||||
/// <summary>
|
||||
/// Scenario action registered under <c>toggle_cooking_workbench</c>
|
||||
/// that toggles the cooking workbench UI panel on the scenario screen.
|
||||
/// Called from scenario script directives via the action system.
|
||||
/// Locates the <c>ModLastPanel</c> root node in the scene tree,
|
||||
/// finds the <c>CookingWorkbenchPanel</c> child (a <see cref="WorkbenchPanel"/>),
|
||||
/// and calls <c>Switch()</c> to show or hide it. No-op if the root
|
||||
/// or panel node is missing.
|
||||
/// </summary>
|
||||
public class ToggleCookingPanelAction : IScenarioAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique action name used in scenario script commands
|
||||
/// (e.g. <c>action toggle_cooking_workbench</c>).
|
||||
/// </summary>
|
||||
public string Name => "toggle_cooking_workbench";
|
||||
|
||||
/// <summary>
|
||||
/// Executes the toggle: finds the <c>CookingWorkbenchPanel</c>
|
||||
/// under <c>ModLastPanel</c> and calls <c>Switch()</c> to
|
||||
/// alternate its visibility state.
|
||||
/// </summary>
|
||||
public void Run(IActionContext ctx)
|
||||
{
|
||||
GameLogger.Log("COOKING", "ToggleCookingPanelAction.Run: looking for CookingWorkbenchPanel...");
|
||||
var root = SceneContextNode.FindNode<Control>("ModLastPanel");
|
||||
if (root == null)
|
||||
{
|
||||
GameLogger.Log("COOKING", "ToggleCookingPanelAction.Run: ModLastPanel NOT FOUND", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
|
||||
var panel = root.GetNodeOrNull<WorkbenchPanel>("CookingWorkbenchPanel");
|
||||
if (panel == null)
|
||||
{
|
||||
GameLogger.Log("COOKING", "ToggleCookingPanelAction.Run: creating panel via TscnScenes.LoadAndBuild...");
|
||||
var loaded = TscnScenes.LoadAndBuild("scenes/cooking_panel.tscn");
|
||||
if (loaded == null)
|
||||
{
|
||||
GameLogger.Log("COOKING", "ToggleCookingPanelAction.Run: LoadAndBuild returned NULL", LogLevel.Error);
|
||||
return;
|
||||
}
|
||||
var control = loaded as Control;
|
||||
if (control == null) return;
|
||||
control.Name = "CookingWorkbenchPanel";
|
||||
control.Visible = false;
|
||||
control.SetAnchorsAndOffsetsPreset(Control.LayoutPreset.FullRect);
|
||||
root.AddChild(control);
|
||||
panel = control as WorkbenchPanel;
|
||||
}
|
||||
|
||||
GameLogger.Log("COOKING", $"ToggleCookingPanelAction.Run: switching panel (currently Visible={panel?.Visible})");
|
||||
panel?.Switch();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Cthangover.Core.Audio;
|
||||
using Cthangover.Core.Factories.Impls;
|
||||
using Cthangover.Core.Items;
|
||||
using Cthangover.Core.Scenes;
|
||||
using Cthangover.Core.Settings;
|
||||
using Cthangover.Core.UI;
|
||||
using Cthangover.Core.UI.Tool;
|
||||
using Cthangover.Core.UI.Inventory;
|
||||
using Godot;
|
||||
|
||||
namespace Mods.Cooking.Workbench
|
||||
{
|
||||
/// <summary>
|
||||
/// Main cooking workbench UI panel that lists available recipes,
|
||||
/// shows descriptions and output previews, and performs the actual
|
||||
/// cooking operation. Recipes are loaded from <see cref="RecipeData"/>
|
||||
/// filtered by <see cref="WorkbenchType.Cooking"/> and sorted so that
|
||||
/// craftable recipes (all ingredients present) appear before
|
||||
/// un-craftable ones. Selecting a recipe displays its translated
|
||||
/// description and output items via an <see cref="InventoryBagBehaviour"/>.
|
||||
/// Cooking consumes ingredients, produces output items, advances
|
||||
/// in-game time, and plays a sound effect.
|
||||
/// </summary>
|
||||
public class WorkbenchPanel : Widget
|
||||
{
|
||||
private Control content;
|
||||
private RichTextLabel txtDescription;
|
||||
private InventoryBagBehaviour outputContainer;
|
||||
|
||||
private List<RecipeItemBehaviour> recipes = new();
|
||||
|
||||
protected override void OnceConstruct()
|
||||
{
|
||||
MouseFilter = MouseFilterEnum.Ignore;
|
||||
|
||||
var leftTitle = GetNode<Label>("HBox/LeftPanel/LeftTitle");
|
||||
leftTitle.Text = TranslationServer.Translate("ui/cook/title");
|
||||
leftTitle.AddThemeFontSizeOverride("font_size", 16);
|
||||
|
||||
content = GetNode<VBoxContainer>("HBox/LeftPanel/Scroll/Content");
|
||||
|
||||
txtDescription = GetNode<RichTextLabel>("HBox/RightPanel/TxtDescription");
|
||||
|
||||
outputContainer = new InventoryBagBehaviour();
|
||||
outputContainer.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||
outputContainer.Visible = false;
|
||||
GetNode("HBox/RightPanel").AddChild(outputContainer);
|
||||
|
||||
var btnCook = GetNode<Button>("HBox/RightPanel/BtnCook");
|
||||
btnCook.Text = TranslationServer.Translate("ui/cook/cook");
|
||||
btnCook.Pressed += OnCookClick;
|
||||
|
||||
var btnClose = GetNode<Button>("HBox/RightPanel/BtnClose");
|
||||
btnClose.Text = TranslationServer.Translate("ui/cook/close");
|
||||
btnClose.Pressed += OnCloseClick;
|
||||
}
|
||||
|
||||
protected override void ShowConstruct()
|
||||
{
|
||||
foreach (var recipe in GetSortedList())
|
||||
AddItem(recipe);
|
||||
}
|
||||
|
||||
protected override void HideDestruct()
|
||||
{
|
||||
foreach (var recipeItem in recipes)
|
||||
if (recipeItem != null)
|
||||
recipeItem.QueueFree();
|
||||
recipes.Clear();
|
||||
}
|
||||
|
||||
private void AddItem(IRecipe recipe)
|
||||
{
|
||||
var item = (RecipeItemBehaviour)TscnScenes.LoadAndBuild("scenes/recipe_item.tscn");
|
||||
if (item != null && content != null)
|
||||
{
|
||||
content.AddChild(item);
|
||||
item.Init(recipe, this);
|
||||
item.RecipeClicked += ClickRecipe;
|
||||
recipes.Add(item);
|
||||
}
|
||||
}
|
||||
|
||||
private List<IRecipe> GetSortedList()
|
||||
{
|
||||
var list = GameData.Instance.Runtime.RecipeData.GetRecipesByType(WorkbenchType.Cooking);
|
||||
var inventory = GameData.Instance.Runtime.Inventory;
|
||||
list.Sort((o1, o2) =>
|
||||
{
|
||||
var hasNotItems1 = o1.Input.Any(o => inventory.CheckCount(o.Item) < o.Count);
|
||||
var hasNotItems2 = o2.Input.Any(o => inventory.CheckCount(o.Item) < o.Count);
|
||||
if (hasNotItems1 == hasNotItems2)
|
||||
return string.Compare(o1.ID, o2.ID, StringComparison.Ordinal);
|
||||
return hasNotItems1 ? 1 : -1;
|
||||
});
|
||||
return list;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Responds to a recipe being selected in the list.
|
||||
/// Updates the description area with the recipe's translated name
|
||||
/// (styled bold/yellow via BBCode) and description, and populates
|
||||
/// the output preview panel with items the recipe produces by
|
||||
/// wrapping each output into an <see cref="ItemContainer"/>.
|
||||
/// </summary>
|
||||
public void ClickRecipe(IRecipe recipe, RecipeItemBehaviour recipeItemBehaviour)
|
||||
{
|
||||
if (txtDescription != null)
|
||||
txtDescription.Text = "[b][color=yellow]" + TranslationServer.Translate(recipe.Name) + "[/color][/b]\n\n" +
|
||||
TranslationServer.Translate(recipe.Description);
|
||||
|
||||
outputContainer.List = recipe.Output.Select(o => (IItemContainer)new ItemContainer
|
||||
{
|
||||
Item = o.Item,
|
||||
Count = o.Count,
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hides the workbench panel and plays a UI close-click sound
|
||||
/// via the scene's <see cref="AudioService"/> node.
|
||||
/// </summary>
|
||||
public void OnCloseClick()
|
||||
{
|
||||
Hide();
|
||||
var audioService = SceneContextNode.FindNode<AudioService>("AudioService");
|
||||
audioService?.PlaySound("ui/close_click", SoundType.UI);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Executes the cooking operation for the currently selected recipe.
|
||||
/// Validates that the player has enough ingredients in inventory
|
||||
/// (short-circuits if not). Deducts input items, adds output items,
|
||||
/// advances in-game time by the recipe's duration, updates the
|
||||
/// on-screen time display, plays the cooking sound effect,
|
||||
/// and refreshes all recipe rows so their availability colours
|
||||
/// reflect the new inventory state.
|
||||
/// </summary>
|
||||
public void OnCookClick()
|
||||
{
|
||||
var inventory = GameData.Instance.Runtime.Inventory;
|
||||
var receipt = RecipeItemBehaviour.SelectedRecipe;
|
||||
if (receipt == null)
|
||||
return;
|
||||
|
||||
var hasItems = !receipt.Input.Any(o => inventory.CheckCount(o.Item) < o.Count);
|
||||
if (!hasItems)
|
||||
return;
|
||||
|
||||
foreach (var item in receipt.Input)
|
||||
inventory.Remove(item.Item, item.Count);
|
||||
foreach (var item in receipt.Output)
|
||||
inventory.Add(item.Item, item.Count);
|
||||
|
||||
GameData.Instance.Runtime.Time.AddTime(0, 0, 0, 0, receipt.Time);
|
||||
var timer = SceneContextNode.FindNode<TimeController>("TimeController");
|
||||
timer?.UpdateRenderedTime();
|
||||
|
||||
var audioService = SceneContextNode.FindNode<AudioService>("AudioService");
|
||||
audioService?.PlaySound("ui/cook", SoundType.UI);
|
||||
|
||||
foreach (var recipe in recipes)
|
||||
recipe.UpdateState();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user