commit 6c5c717e320ab8d6434b76b97b5281741ebc9a36 Author: iisaev Date: Thu Sep 10 10:59:31 2026 +0300 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c301ca8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Godot 4.x +.godot/ +.idea/ +sdk/ +bin/ +obj/ + +#ai +.omo/ +.opencode/ + +gen/ +obj/ +bin/ + +*.tres~ +*.tscn~ + +# GDScript LSP +.godot/ + +# Export +*.exe +*.dmg +*.apk +*.aab +*.pck +*.zip +*.log + +# OS +.DS_Store +Thumbs.db + +# Editor +*.import +*.godot.uid diff --git a/backgrounds/cook_table.png b/backgrounds/cook_table.png new file mode 100644 index 0000000..4fb192b Binary files /dev/null and b/backgrounds/cook_table.png differ diff --git a/items/items.json b/items/items.json new file mode 100644 index 0000000..cc8d268 --- /dev/null +++ b/items/items.json @@ -0,0 +1,5 @@ +{ + "Items": [ + ${recipe/wolf_meat_to_ration} + ] +} \ No newline at end of file diff --git a/items/recipe/cooking_recipe.png b/items/recipe/cooking_recipe.png new file mode 100644 index 0000000..c5490e9 Binary files /dev/null and b/items/recipe/cooking_recipe.png differ diff --git a/items/recipe/wolf_meat_to_ration.json b/items/recipe/wolf_meat_to_ration.json new file mode 100644 index 0000000..dc5e8c1 --- /dev/null +++ b/items/recipe/wolf_meat_to_ration.json @@ -0,0 +1,9 @@ +{ + "Id": "recipe/wolf_meat_to_ration", + "Name": "items/recipe/wolf_meat_to_ration/name", + "Description": "items/recipe/wolf_meat_to_ration/desc", + "Cost": 1, + "Sprite": "recipe/cooking_recipe", + "ItemType": "Recipe,Used", + "ItemAction": "action/recipe/wolf_meat_to_ration" +} diff --git a/locale/en.properties b/locale/en.properties new file mode 100644 index 0000000..db76392 --- /dev/null +++ b/locale/en.properties @@ -0,0 +1,20 @@ +mod_cooking_name = Cooking +mod_cooking_desc = Cooking mechanic — full autonomous mod + +ui/cook/cook = Cook +ui/cook/close = Close +ui/cook/title = Food preparation +ui/cook/recipe_time = {0} minutes +ui/cook/recipe_out = Cooking result: {0} +ui/diner/rationcount = Rations: {0} +ui/diner/rationday = Daily consumption: {0} +ui/diner/rationmax = Supplies will last {0} days +items/food/ration/name = Ration +items/food/ration/desc = A daily ration unit for one person +items/food/wolf_meat/name = Wolf cutlet +items/food/wolf_meat/desc = Raw wolf meat +items/resource/branches/name = Branches +items/resource/branches/desc = Fuel for a campfire +recipe/wolf_meat_to_ration/name = Ration from wolf meat +recipe/wolf_meat_to_ration/desc = A recipe describing how to properly cook wolf meat to make it safe to eat +mod_cooking/rations_per_character = Rations per person per day diff --git a/locale/ru.properties b/locale/ru.properties new file mode 100644 index 0000000..ea62b24 --- /dev/null +++ b/locale/ru.properties @@ -0,0 +1,20 @@ +mod_cooking_name = Готовка +mod_cooking_desc = Механика готовки — полностью автономный мод + +ui/cook/cook = Готовить +ui/cook/close = Выйти +ui/cook/title = Приготовление еды +ui/cook/recipe_time = {0} минут +ui/cook/recipe_out = Результат приготовления: {0} +ui/diner/rationcount = Пайков: {0} +ui/diner/rationday = Потребление в сутки: {0} +ui/diner/rationmax = Запасов хватит на {0} дней +items/food/ration/name = Рацион +items/food/ration/desc = Единица суточного рациона для одного человека +items/food/wolf_meat/name = Волчья вырезка +items/food/wolf_meat/desc = Сырое мясо волка +items/resource/branches/name = Ветки +items/resource/branches/desc = Топливо для костра +recipe/wolf_meat_to_ration/name = Рацион из волчьего мяса +recipe/wolf_meat_to_ration/desc = Рецепт описывает как правильно приготовить волчатину, чтобы безопасно употребить её в пищу +mod_cooking/rations_per_character = Рационов на персонажа в сутки diff --git a/locale/scenarios_en.properties b/locale/scenarios_en.properties new file mode 100644 index 0000000..ecf6c7e --- /dev/null +++ b/locale/scenarios_en.properties @@ -0,0 +1,3 @@ +rooms/kitchen/prompt = A room equipped as a kitchen +rooms/kitchen/opt_exit = Leave +rooms/kitchen/opt_cook = Cook diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..c358726 --- /dev/null +++ b/manifest.json @@ -0,0 +1,34 @@ +{ + "id": "cooking", + "name": "Cooking", + "version": "1.0.0", + "author": "ct", + "description": "Cooking mechanic - full autonomous mod", + "sources": [ + "src/*.cs" + ], + "depends": [ "core>=1", "interface>=1" ], + "subscriptions": [ + { + "scene": "home_kitchen", + "template": "default", + "code": "cooking_panel_init", + "trigger": "on_enter", + "priority": 10 + }, + { + "scene": "home_kitchen", + "template": "default", + "code": "dinner_tools_show", + "trigger": "on_enter", + "priority": 20 + }, + { + "scene": "home_kitchen", + "template": "default", + "code": "dinner_tools_hide", + "trigger": "on_exit", + "priority": 10 + } + ] +} diff --git a/mod.png b/mod.png new file mode 100644 index 0000000..2edfb90 Binary files /dev/null and b/mod.png differ diff --git a/recipes/cooking.json b/recipes/cooking.json new file mode 100644 index 0000000..e8b07ab --- /dev/null +++ b/recipes/cooking.json @@ -0,0 +1,27 @@ +{ + "Items": [ + { + "Id": "recipe/wolf_meat_to_ration", + "Name": "recipe/wolf_meat_to_ration/name", + "Description": "recipe/wolf_meat_to_ration/desc", + "Type": "Cooking", + "Time": 15, + "Input": [ + { + "Id": "food/wolf_meat", + "Count": 1 + }, + { + "Id": "resource/branches", + "Count": 2 + } + ], + "Output": [ + { + "Id": "food/ration", + "Count": 1 + } + ] + } + ] +} diff --git a/scenarios/home/home_kitchen_cooking.scenario b/scenarios/home/home_kitchen_cooking.scenario new file mode 100644 index 0000000..1cb23f0 --- /dev/null +++ b/scenarios/home/home_kitchen_cooking.scenario @@ -0,0 +1,15 @@ +scene: home_kitchen +priority: 5 +--- +light_set [{"x":0.43052286,"y":0.5760054,"radius":500,"influence":1,"color":"#ffff00"},{"x":0.103702,"y":0.26946074,"radius":500,"influence":1,"color":"#ffff00"}] +select "Комната оборудованная под кухню" key=rooms/kitchen/prompt +option "Выйти" key=rooms/kitchen/opt_exit -> :exit +option "Готовить" key=rooms/kitchen/opt_cook -> :cook + +:exit +switch_scene home_vestibule +end + +:cook +action toggle_cooking_workbench +end diff --git a/scenes/cooking_panel.tscn b/scenes/cooking_panel.tscn new file mode 100644 index 0000000..457dd28 --- /dev/null +++ b/scenes/cooking_panel.tscn @@ -0,0 +1,69 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/ui/workbench/WorkbenchPanel.cs" id="1_panel"] +[ext_resource type="Texture2D" path="res://backgrounds/cook_table.png" id="2_bg"] + +[node name="CookingWorkbenchPanel" type="Control" script=ExtResource("1_panel")] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +[node name="BgImage" type="TextureRect" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +offset_right = 0.0 +mouse_filter = 2 +expand_mode = 1 +stretch_mode = 4 +texture = ExtResource("2_bg") +[node name="HBox" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 +theme_override_constants/separation = 10 + +[node name="LeftPanel" type="VBoxContainer" parent="HBox"] +layout_mode = 2 +size_flags_horizontal = 3 +custom_minimum_size = Vector2(400, 0) + +[node name="LeftTitle" type="Label" parent="HBox/LeftPanel"] +layout_mode = 2 + +[node name="Scroll" type="ScrollContainer" parent="HBox/LeftPanel"] +layout_mode = 2 +size_flags_vertical = 3 +horizontal_scroll_mode = 3 + +[node name="Content" type="VBoxContainer" parent="HBox/LeftPanel/Scroll"] +layout_mode = 2 + +[node name="RightPanel" type="VBoxContainer" parent="HBox"] +layout_mode = 2 +size_flags_horizontal = 3 +custom_minimum_size = Vector2(300, 0) + +[node name="TxtDescription" type="RichTextLabel" parent="HBox/RightPanel"] +layout_mode = 2 +mouse_filter = 2 +bbcode_enabled = true +scroll_active = true +custom_minimum_size = Vector2(0, 100) +size_flags_vertical = 3 + +[node name="BtnCook" type="Button" parent="HBox/RightPanel"] +layout_mode = 2 + +[node name="BtnClose" type="Button" parent="HBox/RightPanel"] +layout_mode = 2 diff --git a/scenes/ration_statistics_panel.tscn b/scenes/ration_statistics_panel.tscn new file mode 100644 index 0000000..7006274 --- /dev/null +++ b/scenes/ration_statistics_panel.tscn @@ -0,0 +1,18 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/ui/RationStatisticsPanel.cs" id="1_script"] + +[node name="RationStatisticsPanel" type="Control" script=ExtResource("1_script")] + +[node name="RationMargin" type="MarginContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +theme_override_constants/margin_left = 50 +theme_override_constants/margin_top = 100 +theme_override_constants/margin_bottom = 100 + +[node name="RationLabel" type="RichTextLabel" parent="RationMargin"] diff --git a/scenes/recipe_icon_item.tscn b/scenes/recipe_icon_item.tscn new file mode 100644 index 0000000..e400e45 --- /dev/null +++ b/scenes/recipe_icon_item.tscn @@ -0,0 +1,18 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/ui/workbench/RecipeIconItemBehaviour.cs" id="1_script"] + +[node name="RecipeIconItem" type="Control" script=ExtResource("1_script")] + +[node name="Img" type="TextureRect" parent="."] +custom_minimum_size = Vector2(32, 32) +layout_mode = 0 +expand_mode = 1 +stretch_mode = 5 + +[node name="Txt" type="Label" parent="."] +layout_mode = 0 +position = Vector2(24, 20) +horizontal_alignment = 2 +vertical_alignment = 2 +theme_override_font_sizes/font_size = 10 diff --git a/scenes/recipe_item.tscn b/scenes/recipe_item.tscn new file mode 100644 index 0000000..7fdcf20 --- /dev/null +++ b/scenes/recipe_item.tscn @@ -0,0 +1,38 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="src/ui/workbench/RecipeItemBehaviour.cs" id="1_script"] + +[node name="RecipeItem" type="Control" script=ExtResource("1_script")] +custom_minimum_size = Vector2(0, 30) + +[node name="Background" type="Control" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +mouse_filter = 2 + +[node name="HBox" type="HBoxContainer" parent="."] +layout_mode = 1 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="RecipeContent" type="Control" parent="HBox"] +custom_minimum_size = Vector2(100, 30) + +[node name="RecipeContentHbox" type="HBoxContainer" parent="HBox/RecipeContent"] + +[node name="TxtName" type="Label" parent="HBox"] +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(1, 1, 0, 1) +theme_override_font_sizes/font_size = 12 + +[node name="TxtTime" type="Label" parent="HBox"] +custom_minimum_size = Vector2(40, 30) +theme_override_colors/font_color = Color(1, 0, 0, 1) +theme_override_font_sizes/font_size = 12 diff --git a/sounds/ui/cook.ogg b/sounds/ui/cook.ogg new file mode 100644 index 0000000..acf4ac1 Binary files /dev/null and b/sounds/ui/cook.ogg differ diff --git a/src/CookingSettings.cs b/src/CookingSettings.cs new file mode 100644 index 0000000..fa71733 --- /dev/null +++ b/src/CookingSettings.cs @@ -0,0 +1,56 @@ +using System.Collections.Generic; +using Cthangover.Core.Mods; +using Cthangover.Core.Settings; + +namespace Cthangover.Cooking +{ + /// + /// Exposes a user-configurable setting for the number of rations + /// consumed per character per in-game day. Registered automatically + /// via when the cooking mod + /// assembly loads. + /// + /// The static property is the + /// canonical source of truth shared with RationStatisticsPanel + /// and any future consumption logic. It is updated by the core on + /// boot (from persisted JSON) and on every UI change. + /// + public class CookingSettings : IModSettings + { + /// + /// Current value for rations consumed per character per day. + /// Default is 1. Updated by on boot + /// and whenever the player changes the setting in the menu. + /// + public static int RationsPerCharacter { get; private set; } = 1; + + public IReadOnlyList GetDefinitions() + { + return new List + { + 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; + } + } +} diff --git a/src/NutritionBehaviour.cs b/src/NutritionBehaviour.cs new file mode 100644 index 0000000..a93dbce --- /dev/null +++ b/src/NutritionBehaviour.cs @@ -0,0 +1,64 @@ +using Cthangover.Core.Relationship; +using Cthangover.Core.Settings; + +namespace Cthangover.Core.Relationship +{ + /// + /// Tracks nutrition and starvation for party recruits. + /// Works by recording the tick when a recruit last ate (FullnessTime), + /// 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 behaviour pipeline and called + /// by the time system on each global tick. + /// + public class NutritionBehaviour : IRecruitBehaviour + { + /// + /// Unique behaviour identifier used to look up and configure this + /// behaviour from 's behaviour collection. + /// + public string Id => "nutrition"; + + /// + /// Called when the behaviour is first attached to a recruit. + /// Seeds FullnessTime with the current global tick so that + /// starve-damage begins counting from this point. + /// + public void ConfigureRecruit(Recruit recruit, RuntimeData runtime) + { + recruit.Properties.SetLong("FullnessTime", runtime.Time.Tick); + } + + /// + /// Evaluates whether the recruit has gone beyond the fullness window + /// (24 in-game hours, represented as 24 * 4 * 60 ticks) since + /// FullnessTime. If the window is exceeded, reduces the + /// recruit's PROP_HEALTH by 1 per tick until they are fed. + /// Tick-driven starvation allows gradual death when rations run out. + /// + 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); + } + } + + /// + /// Cleanup hook invoked when the behaviour is detached from a recruit. + /// No persistent state requires teardown; exists for interface contract. + /// + public void OnRemove(Recruit recruit, RuntimeData runtime) + { + } + } +} diff --git a/src/items/actions/WolfMeatToRation.cs b/src/items/actions/WolfMeatToRation.cs new file mode 100644 index 0000000..7ceda72 --- /dev/null +++ b/src/items/actions/WolfMeatToRation.cs @@ -0,0 +1,37 @@ +using Cthangover.Core.Settings; + +namespace Cthangover.Core.Items.Actions +{ + /// + /// Performs the "Wolf Meat to Ration" recipe-unlock action. + /// When a player uses raw wolf meat, this action checks whether the + /// recipe/wolf_meat_to_ration flag is already present in + /// ; if not, it adds it, permanently unlocking + /// the recipe in the . + /// Returns false when already unlocked (no-op) and true + /// on first unlock. + /// + public class WolfMeatToRation : IItemAction + { + /// + /// Globally unique action identifier, matched by the item system + /// to trigger this action when an item with this ID is used. + /// + public string ID => "action/recipe/wolf_meat_to_ration"; + + /// + /// Idempotently unlocks the wolf-meat-to-ration recipe. + /// Adds recipe/wolf_meat_to_ration to + /// so that the cooking workbench can list it. + /// + /// true if the recipe was newly unlocked; false if already known. + 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; + } + } +} diff --git a/src/ui/RationStatisticsPanel.cs b/src/ui/RationStatisticsPanel.cs new file mode 100644 index 0000000..7a80dcb --- /dev/null +++ b/src/ui/RationStatisticsPanel.cs @@ -0,0 +1,64 @@ +using Cthangover.Cooking; + +using System; +using Cthangover.Core.Settings; +using Cthangover.Core.UI; +using Godot; + +namespace Mods.Cooking.Rations +{ + /// + /// 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 + /// events to refresh + /// automatically whenever the inventory is modified. + /// + public class RationStatisticsPanel : Widget + { + private RichTextLabel label; + private Action _onInventoryChange; + + protected override void OnceConstruct() + { + label = GetNode("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; + } + + /// + /// Recomputes and refreshes the ration-statistics text. + /// Reads current ration inventory via + /// for "food/ration" and the total character count from + /// . Formats three localized lines: + /// total rations, daily consumption, and remaining days + /// (rations divided by character count, clamped to 1 minimum). + /// + 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)); + } + } +} diff --git a/src/ui/workbench/RecipeIconItemBehaviour.cs b/src/ui/workbench/RecipeIconItemBehaviour.cs new file mode 100644 index 0000000..0ae39a0 --- /dev/null +++ b/src/ui/workbench/RecipeIconItemBehaviour.cs @@ -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 +{ + /// + /// Renders a single ingredient row inside a + /// on the cooking workbench. Displays the ingredient's item icon and the + /// required count. When is called, compares + /// inventory count against the recipe requirement and tints the icon and + /// count text grey if the player lacks enough items. + /// + 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("Img"); + txt = GetNode