Merge pull request 'feature/farming_bugfixes_and_magic_word' (#30) from feature/farming_bugfixes_and_magic_word into develop

Reviewed-on: #30
This commit was merged in pull request #30.
This commit is contained in:
2025-11-18 17:25:59 +01:00
65 changed files with 1286 additions and 352 deletions
@@ -8,7 +8,6 @@ namespace Babushka.scripts.CSharp.Common.Animation;
public partial class VesnaAnimations : Node
{
[Export] private AnimatedSprite2D _sprite;
[Export] private CpuParticles2D _wateringParticles;
private bool anyActionPressed;
private string _toolString;
@@ -144,7 +143,6 @@ public partial class VesnaAnimations : Node
_sprite.Animation = "diagonal wateringcan";
_sprite.Play();
InputService.Instance.InputEnabled = false;
_wateringParticles.Emitting = true;
Task.Run(DelayedInputHandlerReset);
}
}
@@ -152,7 +150,6 @@ public partial class VesnaAnimations : Node
private async Task DelayedInputHandlerReset()
{
await Task.Delay(1000);
_wateringParticles.Emitting = false;
InputService.Instance.InputEnabled = true;
}
@@ -7,12 +7,12 @@ namespace Babushka.scripts.CSharp.Common.CharacterControls;
public partial class InteractionArea2D : Node2D
{
[Export] private Area2D _area;
[Export] private Label _label;
[Export] private bool _active = true;
[Export] private bool _useOutline = true;
[Export] private ShaderMaterial _outlineMaterial;
[Export] private CanvasItem? _spriteToOutline; // keep to not break old usages. TODO: remove later
[Export] private CanvasItem[] _spritesToOutline = [];
[Export] private bool _showLabel = true;
[Export] private int _id = -1; // TODO: remove
@@ -38,21 +38,7 @@ public partial class InteractionArea2D : Node2D
{
if (_useOutline)
{
try
{
// support old implementations of the script. If the sprite to outline is set, add it to the array
if (_spriteToOutline != null)
{
Array.Resize(ref _spritesToOutline, _spritesToOutline.Length + 1);
_spritesToOutline[^1] = _spriteToOutline;
}
_backupMaterials = _spritesToOutline.Select(s => s.Material).ToArray();
}
catch (Exception exception)
{
GD.PrintErr($"No sprite to outline found on: {GetParent().Name}" + exception.Message);
}
_backupMaterials = _spritesToOutline.Select(s => s.Material).ToArray();
}
}
@@ -76,9 +62,6 @@ public partial class InteractionArea2D : Node2D
public void OnPlayerExited(Node2D player)
{
if (!_active)
return;
_label.Hide();
if (!_useOutline)
@@ -129,7 +112,6 @@ public partial class InteractionArea2D : Node2D
public void SetSpriteActiveState(bool success, int id) // TODO: remove
{
GD.PrintErr("SetSpriteActiveState is being called.");
if (!_active)
return;
}
@@ -1,3 +1,5 @@
using System;
using Babushka.scripts.CSharp.Low_Code.Variables;
using Godot;
using Godot.Collections;
@@ -6,19 +8,17 @@ namespace Babushka.scripts.CSharp.Common.Farming;
[GlobalClass]
public partial class FarmingControls2D : Node2D
{
[Export] private PackedScene _fieldPrefab;
[Export] private VariableResource _sceneKeyProvider;
[Export] private Node2D _movingPlayer;
[Export] private Camera2D _camera;
[Export] private CpuParticles2D _wateringParticles;
[Export] private float _wateringCanParticlesVerticalOffset = 50f;
public FieldService2D FieldService;
[Export] private float _wateringCanParticlesVerticalOffset = 50f;
[Export] private Vector2I _fieldOffsetVector = new Vector2I(735, 651);
[Export] private Node2D _fieldParent;
private int _toolId = -1;
private bool _wateringCanFilled = false;
[Signal] public delegate void WateringFieldEventHandler();
#region Tools
@@ -49,59 +49,19 @@ public partial class FarmingControls2D : Node2D
}
#endregion
public override void _Input(InputEvent @event)
{
if (@event.IsActionPressed("click")
&& _toolId == WateringCanState.WATERING_CAN_ID
&& WateringCanState.GetFillState() > 0)
{
Vector2I adjustedPosition = GetAdjustedMousePosition();
WaterTheField(adjustedPosition);
}
}
private Vector2I GetAdjustedMousePosition()
{
Vector2 mousePosition = _camera.GetGlobalMousePosition();
Vector2I mousePositionInteger = (Vector2I) mousePosition;
Vector2I adjustedPosition = AdjustValue(mousePositionInteger, new Vector2I(735, 651));
Vector2I adjustedPosition = AdjustValue(mousePositionInteger, _fieldOffsetVector);
return adjustedPosition;
}
/// <summary>
/// Called by the allowed farming area collision area 2d.
/// </summary>
/// <param name="node"></param>
/// <param name="inputEvent"></param>
/// <param name="shapeIndex"></param>
public void InputEventPressedOn(Node node, InputEvent inputEvent, int shapeIndex)
private Vector2I AdjustValue(Vector2I input, Vector2I step)
{
if (!inputEvent.IsPressed())
{
return;
}
if (!inputEvent.IsActionPressed("click"))
return;
if (inputEvent is InputEventMouseButton inputEventMouseButton)
{
if (!inputEventMouseButton.Pressed)
{
return;
}
}
else
{
return;
}
if (_toolId == 0)
{
Vector2I adjustedPosition = GetAdjustedMousePosition();
MakeField(adjustedPosition);
}
return input.Snapped(step);
}
#region WATERING
@@ -112,57 +72,7 @@ public partial class FarmingControls2D : Node2D
WateringCanState.Fill();
}
}
private void WaterTheField(Vector2I fieldPosition)
{
FieldBehaviour2D field = FieldService.Get(fieldPosition);
if (field == null || field.FieldState == FieldState.Watered)
return;
field.Water();
_wateringParticles.GlobalPosition = new Vector2(field.GlobalPosition.X, field.GlobalPosition.Y + _wateringCanParticlesVerticalOffset);
WateringCanState.Water();
EmitSignal(SignalName.WateringField);
}
#endregion
#region FIELD CREATION
private void MakeField(Vector2I fieldPosition)
{
if(FieldService == null || _fieldPrefab == null)
return;
// only instantiate a field if there isn't one already.
if(FieldService.Get(fieldPosition) == null)
{
Node fieldInstance = _fieldPrefab.Instantiate();
if (fieldInstance is Node2D field2d)
{
// add dictionary entry for the field
Array<Node> fields = field2d.FindChildren("*", nameof(FieldBehaviour2D));
if (fields.Count > 0)
FieldService.TryAddEntry(fieldPosition, fields[0] as FieldBehaviour2D);
// reposition and reparent the instance
field2d.Position = new Vector2(fieldPosition.X, fieldPosition.Y);;
FieldService.AddChild(fieldInstance);
}
}
}
private int AdjustValue(float value)
{
float adjustedValue = value / 500;
adjustedValue = Mathf.RoundToInt(adjustedValue);
adjustedValue *= 500;
return (int)adjustedValue;
}
private Vector2I AdjustValue(Vector2I input, Vector2I step)
{
return input.Snapped(step);
}
#endregion
}
@@ -0,0 +1,54 @@
using Babushka.scripts.CSharp.Common.CharacterControls;
using Godot;
namespace Babushka.scripts.CSharp.Common.Farming;
/// <summary>
/// Enables a preset field in the scene sothat it can be used for farming.
/// </summary>
public partial class FieldActivator : Node
{
[Export] private FieldBehaviour2D _field;
[Export] private InteractionArea2D _activatorArea;
private bool _used = false;
private bool _rakeInHand;
[Signal] public delegate void FieldCreatedEventHandler();
public override void _Ready()
{
ToggleInteractionArea();
}
/// <summary>
/// Activates the fieldbehaviour node and sets it to the tilled state.
/// </summary>
public void ActivateField()
{
if (!_used && _rakeInHand)
{
_field.Visible = true;
_field.UpdateFieldState(FieldState.Tilled);
EmitSignal(SignalName.FieldCreated, _field);
_used = true;
}
}
/// <summary>
/// Reacts to changes in the inventory.
/// If setup correctly, the field activator interactable should only trigger when using the rake.
/// </summary>
/// <param name="activated"></param>
public void RakeActivated(bool activated)
{
_rakeInHand = activated;
ToggleInteractionArea();
}
private void ToggleInteractionArea()
{
_activatorArea.IsActive = !_used && _rakeInHand;
}
}
@@ -0,0 +1 @@
uid://dlbjjgbs0n4b0
+177 -22
View File
@@ -1,31 +1,88 @@
using System;
using Babushka.scripts.CSharp.Common.CharacterControls;
using Babushka.scripts.CSharp.Common.Inventory;
using Babushka.scripts.CSharp.Common.Savegame;
using Babushka.scripts.CSharp.Low_Code.Events;
using Babushka.scripts.CSharp.Low_Code.Variables;
using Godot;
using Godot.Collections;
namespace Babushka.scripts.CSharp.Common.Farming;
/// <summary>
/// Defines the behaviour of the field, i.e. interactions, states and effects.
/// </summary>
[GlobalClass]
public partial class FieldBehaviour2D : Sprite2D
{
[ExportGroup("Persistence")]
[Export] public string SaveId = "";
[Export] private VariableNode _fieldIndex;
[Export] public VariableResource _sceneKeyProvider;
[Export] public FieldState FieldState = FieldState.Tilled;
[ExportGroup("Field Visuals")]
[Export] private Sprite2D _fieldSprite;
[Export] private Sprite2D _maskSprite;
[Export] private Sprite2D _outlineSprite;
[Export] private Texture2D[] _maskOutlineTextures;
[Export] private Texture2D[] _maskTexture;
[Export] private Texture2D Tilled;
[Export] private Texture2D Watered;
[Export] public FieldState FieldState = FieldState.Tilled;
[ExportGroup("Field Interactions")]
[Export] public InteractionArea2D PlantingInteraction;
[Export] public InteractionArea2D FieldInteractionArea;
[ExportGroup("Configuration")]
[Export] public Node2D PlantingPlaceholder;
[Export] public ItemRepository ItemRepository;
[Export] private CpuParticles2D _wateringParticles;
[Export] private EventResource _wateringEvent;
private bool _seedsActive;
private bool _wateringCanActive;
public Vector2 FieldPosition;
private bool _canPlant;
private bool _canWater;
private PlantBehaviour2D? _currentPlant;
[Signal] public delegate void PlantedEventHandler();
private void UpdateInteractionArea()
{
// fieldstate == tilled / watered && samen im Inventar
_canPlant = (FieldState == FieldState.Tilled || FieldState == FieldState.Watered) && _seedsActive;
// fieldstate == tilled && watering can ausgewählt
_canWater = (FieldState == FieldState.Tilled || FieldState == FieldState.Planted) && _wateringCanActive;
FieldInteractionArea.IsActive = _canPlant || _canWater;
}
public void ActivatedSeedInInventory(bool activated)
{
_seedsActive = activated;
UpdateInteractionArea();
}
public void ActivateWateringCanInInventory(bool activated)
{
_wateringCanActive = activated;
UpdateInteractionArea();
}
public override void _Ready()
{
LoadFromSaveData();
if(PlantingPlaceholder.GetChildCount() > 0)
_currentPlant = PlantingPlaceholder.GetChild<PlantBehaviour2D>(0);
UpdateFieldState(FieldState);
FieldService.Instance.TryAddEntry(_sceneKeyProvider.Payload.AsString(),_fieldIndex.Payload.AsInt32(), this);
int randomIndex = new Random().Next(0, _maskTexture.Length);
_maskSprite.Texture = _maskTexture[randomIndex];
_outlineSprite.Texture = _maskOutlineTextures[randomIndex];
base._Ready();
}
@@ -56,22 +113,36 @@ public partial class FieldBehaviour2D : Sprite2D
FieldState = FieldState.NotFound;
break;
}
UpdateInteractionArea();
UpdateSaveData();
}
public void Water()
{
UpdateFieldState(FieldState.Watered);
if (WateringCanState.GetFillState() > 0)
{
UpdateFieldState(FieldState.Watered);
_wateringParticles.Emitting = true;
WateringCanState.Water();
_wateringEvent.Raise();
}
}
/// <summary>
/// Called when the player enters the field's interaction area and presses <E>.
/// Called when the player enters the field's interaction area and presses <E> or clicks.
/// </summary>
public void Farm()
{
if (TryPlant())
if (_canPlant && TryPlant())
{
EmitSignal(SignalName.Planted);
UpdateFieldState(FieldState.Planted);
}
if (_canWater)
{
UpdateFieldState(FieldState.Planted);
Water();
}
}
@@ -80,30 +151,114 @@ public partial class FieldBehaviour2D : Sprite2D
bool success = false;
int currentSlotIndex = InventoryManager.Instance.CurrentSelectedSlotIndex;
ItemInstance? item = InventoryManager.Instance.playerInventory.Slots[currentSlotIndex].itemInstance;
if (item == null || PlantingPlaceholder.GetChildCount() > 0 || item.amount == 0)
return success;
string prefabPath = ItemRepository.TryGetPrefabPath(item.blueprint);
string plantPrefabPath = ItemRepository.TryGetPrefabPath(item.blueprint);
if (prefabPath != null)
if (!string.IsNullOrEmpty(plantPrefabPath))
{
PackedScene prefab = ResourceLoader.Load<PackedScene>(prefabPath, nameof(PackedScene));
Node2D plant2d = prefab.Instantiate<Node2D>();
PlantingPlaceholder.AddChild(plant2d);
plant2d.GlobalPosition = PlantingPlaceholder.GlobalPosition;
PlantBehaviour2D? plantBehaviour = plant2d as PlantBehaviour2D;
if (plantBehaviour != null)
{
plantBehaviour.Field = this;
}
PlantPrefab(plantPrefabPath);
InventoryManager.Instance.playerInventory.RemoveItem(currentSlotIndex);
success = true;
}
return success;
}
}
private void PlantPrefab(string prefabPath)
{
PackedScene prefab = ResourceLoader.Load<PackedScene>(prefabPath, nameof(PackedScene));
Node2D plant2d = prefab.Instantiate<Node2D>();
PlantingPlaceholder.AddChild(plant2d);
plant2d.GlobalPosition = PlantingPlaceholder.GlobalPosition;
_currentPlant = plant2d as PlantBehaviour2D;
if (_currentPlant != null)
{
_currentPlant.Field = this;
}
}
#region SAVE AND LOAD
public void UpdateSaveData()
{
var saveData = new SaveData();
saveData.SceneName = _sceneKeyProvider.Payload.AsString();
saveData.Id = SaveId + _fieldIndex.Payload.AsString();
var payloadData = new Dictionary<string, Variant>
{
{ "field_state", (int)FieldState }
};
if (_currentPlant != null)
{
payloadData.Add(
"plant_data", new Dictionary<string, Variant>()
{
{ "prefab_path", _currentPlant.PrefabPath },
{ "plant_state", (int)_currentPlant.State },
{ "plant_days_growing", _currentPlant.DaysGrowing }
}
);
}
saveData.JsonPayload = Json.Stringify(payloadData, indent: "\t");
SavegameService.AppendSave(saveData);
}
public void LoadFromSaveData()
{
var sceneName = _sceneKeyProvider.Payload.AsString();
var id = SaveId + _fieldIndex.Payload.AsString();
string jsonPayload = SavegameService.GetSaveData(sceneName, id);
Dictionary<string, Variant> save = Json.ParseString(jsonPayload).AsGodotDictionary<string, Variant>();
if (save.Count > 0)
{
if (save.TryGetValue("field_state", out Variant fieldStateVar))
{
int fieldStateInt = fieldStateVar.AsInt32();
FieldState = (FieldState) fieldStateInt;
if (fieldStateInt != 0)
{
Visible = true;
UpdateFieldState(FieldState);
}
}
if (save.TryGetValue("plant_data", out Variant plantDataVar))
{
Dictionary<string, Variant> plantDataDict = plantDataVar.AsGodotDictionary<string, Variant>();
if (plantDataDict.TryGetValue("prefab_path", out Variant prefabPathVar))
{
PlantPrefab(prefabPathVar.AsString());
}
else
{
return;
}
if (plantDataDict.TryGetValue("plant_state", out Variant plantStateVar) && _currentPlant != null)
{
_currentPlant.State = (PlantState) plantStateVar.AsInt32();
_currentPlant.GrowPlant();
}
if (plantDataDict.TryGetValue("plant_days_growing", out Variant plantDaysGrowingVar) && _currentPlant != null)
{
_currentPlant.DaysGrowing = plantDaysGrowingVar.AsInt32();
}
}
}
}
#endregion
}
@@ -0,0 +1,100 @@
using System.Collections.Generic;
using Godot;
namespace Babushka.scripts.CSharp.Common.Farming;
public partial class FieldService : Node
{
private Dictionary<string, FieldsInScene>? _outerDict = null!;
public static FieldService Instance { get; private set; } = null!;
public override void _EnterTree()
{
Instance = this;
_outerDict = new Dictionary<string, FieldsInScene>();
}
public override void _ExitTree()
{
Instance = null;
_outerDict = null;
}
//Create
public bool TryAddEntry(string sceneName, int fieldIndex, FieldBehaviour2D field)
{
if (_outerDict != null )
{
FieldsInScene innerDict;
bool outerDictEntryExists = _outerDict.TryGetValue(sceneName, out innerDict);
if (!outerDictEntryExists)
{
innerDict = new FieldsInScene();
_outerDict.Add(sceneName, innerDict);
}
if (!innerDict.fields.ContainsKey(fieldIndex))
{
innerDict.fields.Add(fieldIndex, field);
return true;
}
}
return false;
}
// Read
public FieldBehaviour2D? TryGet(string key, int fieldIndex)
{
if (_outerDict != null && _outerDict.TryGetValue(key, out FieldsInScene? field))
{
if (field.fields.TryGetValue(fieldIndex, out FieldBehaviour2D? fieldInstance))
{
return fieldInstance;
}
}
return null;
}
//Update
public void UpdateEntry(string key, int fieldIndex, FieldBehaviour2D state)
{
if (_outerDict != null && _outerDict.TryGetValue(key, out FieldsInScene? field))
{
if (field.fields.ContainsKey(fieldIndex))
{
field.fields[fieldIndex] = state;
}
else
{
TryAddEntry(key, fieldIndex, state);
}
}
}
//Delete
public void RemoveEntry(string key, int fieldIndex)
{
if (_outerDict != null && _outerDict.TryGetValue(key, out FieldsInScene? field))
{
if (field.fields.ContainsKey(fieldIndex))
{
field.fields.Remove(fieldIndex);
}
}
}
}
internal class FieldsInScene
{
public Dictionary<int, FieldBehaviour2D?> fields;
public FieldsInScene()
{
fields = new Dictionary<int, FieldBehaviour2D?>();
}
}
@@ -0,0 +1 @@
uid://slo0uydmmvnu
@@ -1,17 +1,15 @@
using System;
using Babushka.scripts.CSharp.Common.Animation;
using Babushka.scripts.CSharp.Common.Inventory;
using Godot;
namespace Babushka.scripts.CSharp.Common.Farming;
/// <summary>
/// Determines the behaviour of a plant in Babushka.
/// </summary>
public partial class PlantBehaviour2D : Node2D
{
[Export] private string _prefabPath;
[Export] private Sprite2D[] _seeds;
[Export] private Sprite2D[] _smallPlants;
[Export] private Sprite2D[] _bigPlants;
@@ -23,8 +21,19 @@ public partial class PlantBehaviour2D : Node2D
[Export] private bool _magicWordNeeded = true;
private string _magicWordDialogicEventName = "MagicWord";
private Sprite2D _currentPlantSprite = null;
private Sprite2D? _currentPlantSprite = null;
private bool _magicWordSaid = false;
private bool _calledOnReady = false;
public PlantState State
{
get => _state;
set => _state = value;
}
public int DaysGrowing { get; set; }
public string PrefabPath => _prefabPath;
/// <summary>
/// public accessor for the field reference
@@ -39,20 +48,19 @@ public partial class PlantBehaviour2D : Node2D
{
if (_state == PlantState.None)
{
GetTree().CallGroup("PlantGrowing", VesnaAnimations.MethodName.PlayFarmingAnimation);
_state = PlantState.Planted;
_currentPlantSprite = GetRandomSprite(_seeds);
_currentPlantSprite.Visible = true;
}
else
{
_calledOnReady = true;
GrowPlant();
}
}
public void Grow()
{
GetTree().CallGroup("PlantGrowing", VesnaAnimations.MethodName.PlayFarmingAnimation);
GrowPlant();
}
@@ -62,8 +70,11 @@ public partial class PlantBehaviour2D : Node2D
/// </summary>
public void GrowPlant()
{
if (_field.FieldState != FieldState.Watered || _magicWordSaid != _magicWordNeeded)
return;
if (!_calledOnReady)
{
if (_field.FieldState != FieldState.Watered || _magicWordSaid != _magicWordNeeded)
return;
}
switch (_state)
{
@@ -107,6 +118,7 @@ public partial class PlantBehaviour2D : Node2D
_field.UpdateFieldState(FieldState.Tilled);
_magicWordSaid = false;
_calledOnReady = false;
}
private Sprite2D GetRandomSprite(Sprite2D[] sprites)
@@ -9,7 +9,6 @@ namespace Babushka.scripts.CSharp.Common.Farming;
public partial class VesnaBehaviour2D : Node
{
[ExportGroup("Farming")]
[Export] private FieldService2D _fieldParent;
[Export] private FarmingControls2D _farmingControls;
[Export] private PlayerMovement _player2d;
[Export] private VesnaAnimations _vesnaAnimations;
@@ -26,7 +25,6 @@ public partial class VesnaBehaviour2D : Node
public override void _Ready()
{
_farmingControls.FieldService = _fieldParent;
_inventoryManager = InventoryManager.Instance;
_inventoryInstance = _inventoryManager.playerInventory;
_inventoryManager.SlotIndexChanged += HandleInventorySelectedSlotIndexChanged;
@@ -0,0 +1,48 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Inventory;
public partial class InventoryListener : Node
{
[Export] private ItemResource[] _itemResourcesToListenFor;
[Signal] public delegate void ItemInstanceActivatedEventHandler(bool activated);
public override void _Ready()
{
InventoryManager.Instance.playerInventory.InventoryContentsChanged += HandleNewItemInInventory;
InventoryManager.Instance.SlotIndexChanged += HandleNewItemInInventory;
}
public override void _ExitTree()
{
InventoryManager.Instance.playerInventory.InventoryContentsChanged -= HandleNewItemInInventory;
InventoryManager.Instance.SlotIndexChanged -= HandleNewItemInInventory;
}
private void HandleNewItemInInventory(int newIndex)
{
HandleNewItemInInventory();
}
private void HandleNewItemInInventory()
{
int currentSlotIndex = InventoryManager.Instance.CurrentSelectedSlotIndex;
ItemInstance? instance = InventoryManager.Instance.playerInventory.Slots[currentSlotIndex].itemInstance;
if (instance != null)
{
ItemResource? item = instance.blueprint;
foreach (var res in _itemResourcesToListenFor)
{
if (item == res)
{
EmitSignal(SignalName.ItemInstanceActivated, true);
return;
}
}
}
EmitSignal(SignalName.ItemInstanceActivated, false);
}
}
@@ -0,0 +1 @@
uid://3t0af586fimq
@@ -1,13 +0,0 @@
using Babushka.scripts.CSharp.Common.Animation;
using Godot;
namespace Babushka.scripts.CSharp.Common.Items;
public partial class NonInventoryPickup : Node2D
{
public void PlayPickupAnimation()
{
// todo: replace with EventBus implementation as soon as this is possible
GetTree().CallGroup("Pickup", VesnaAnimations.MethodName.PlayPickUpAnimation);
}
}
@@ -1 +0,0 @@
uid://dkk1vjijvgrd7
@@ -0,0 +1,20 @@
using System;
namespace Babushka.scripts.CSharp.Common.Savegame;
/// <summary>
/// Central structure for SaveData entries.
/// SceneName and ID are later combined and used as keys for the save system.
/// </summary>
[Serializable]
public class SaveData
{
public string SceneName;
public string Id;
public string JsonPayload;
public string ToString()
{
return SceneName + " " + Id + " " + JsonPayload;
}
}
@@ -0,0 +1 @@
uid://bkftl5mj33eah
@@ -0,0 +1,19 @@
using Godot;
namespace Babushka.scripts.CSharp.Common.Savegame;
/// <summary>
/// This class manages the loading of the savegame throughout the gameplay flow.
/// </summary>
public partial class SaveGameManager : Node
{
public static SaveGameManager? Instance { get; private set; } = null!;
public override void _EnterTree()
{
Instance = this;
SavegameService.Load();
}
}
@@ -0,0 +1 @@
uid://c1srnefvhigef
@@ -0,0 +1,97 @@
using System;
using Godot;
using Godot.Collections;
using FileAccess = Godot.FileAccess;
namespace Babushka.scripts.CSharp.Common.Savegame;
/// <summary>
/// Handles the saving and loading of game states.
/// Holds the central SaveData object that serves as a temporary SaveFile.
/// Automatically writes to disk on every scene change.
/// </summary>
public static class SavegameService
{
public static readonly string SavePath = "user://babushka_savegame.json";
public static Dictionary<string, string> SaveDatas = new ();
public static bool _loaded = false;
/// <summary>
/// Adds or overwrites an entry in the SaveData dictionary.
/// </summary>
/// <param name="saveData"></param>
public static void AppendSave(SaveData saveData)
{
string key = string.Concat(saveData.SceneName, "_", saveData.Id);
if (SaveDatas.TryGetValue(key, out var value))
{
SaveDatas[key] = saveData.JsonPayload;
}
else
{
SaveDatas.Add(key, saveData.JsonPayload);
}
}
/// <summary>
/// Checks the SaveData dictionary for an entry and returns the jsondata as a string for it.
/// Requires the scenename and object ID for lookup.
/// </summary>
/// <param name="sceneName"></param>
/// <param name="id"></param>
/// <returns></returns>
public static string GetSaveData(string sceneName, string id)
{
string saveData = "";
string key = string.Concat(sceneName, "_", id);
if (!_loaded)
{
GD.Print("SavegameService: SaveFile not loaded.");
return saveData;
}
if (SaveDatas.ContainsKey(key))
{
saveData = SaveDatas[key];
}
return saveData;
}
/// <summary>
/// Writes the contents of the current SaveData dictionary to disk as a json file.
/// </summary>
public static void Save()
{
string json = Json.Stringify(SaveDatas, indent: "\t");
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
file.StoreString(json);
}
/// <summary>
/// Loads the current savegame file from disk and parses it into the SaveData dictionary.
/// </summary>
public static void Load()
{
try
{
string saveDataJson = FileAccess.GetFileAsString(SavePath);
SaveDatas = Json.ParseString(saveDataJson).AsGodotDictionary<string, string>();
}
catch(Exception e)
{
GD.PrintRich(e.Message);
return;
}
_loaded = true;
}
}
@@ -0,0 +1 @@
uid://rm23q50boqe5
+2
View File
@@ -1,3 +1,4 @@
using Babushka.scripts.CSharp.Common.Savegame;
using Babushka.scripts.CSharp.Common.SceneManagement;
using Godot;
@@ -16,6 +17,7 @@ public partial class SceneTransition : Node
public void LoadSceneAtIndex(int index)
{
SavegameService.Save();
string sceneName = _sceneNamesToLoad[index];
SceneTransitionThreaded.Instance.ChangeSceneToFileThreaded(sceneName);
UnloadAfterDelay();
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Godot;
namespace Babushka.scripts.CSharp.Common.Util;
@@ -23,4 +24,54 @@ public static class NodeExtension
}
throw new Exception($"Parent of type {typeof(T)} not found for node {self.Name}");
}
//acts like Unity's GetComponent<T> / GetComponentInChildren<T>
// only works with Godot's built-in types.
public static T GetChildByType<T>(this Node node, bool recursive = true)
where T : Node
{
int childCount = node.GetChildCount();
for (int i = 0; i < childCount; i++)
{
Node child = node.GetChild(i);
if (child is T childT)
return childT;
if (recursive && child.GetChildCount() > 0)
{
T recursiveResult = child.GetChildByType<T>(true);
if (recursiveResult != null)
return recursiveResult;
}
}
return null;
}
/// <summary>
/// Another reimplementation of Unity's GetComponent<T>.
/// Verified to work with all types, also derived ones, but only when used from within a scene and at runtime.
/// </summary>
/// <param name="node"></param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T GetComponent<T>(Node node)
{
if (node is T)
{
return (T)(object)node;
}
foreach (Node child in node.GetChildren())
{
if (child is T)
{
return (T)(object)child;
}
}
return (T)(object)null;
}
}
@@ -0,0 +1,11 @@
using Godot;
namespace Babushka.scripts.CSharp.Low_Code.Variables;
/// <summary>
/// A Node type that carries a Variant payload.
/// </summary>
public partial class VariableNode : Node
{
[Export] public Variant Payload { get; set; }
}
@@ -0,0 +1 @@
uid://j2mhvb45egej
@@ -0,0 +1,14 @@
using Godot;
namespace Babushka.scripts.CSharp.Low_Code.Variables;
public partial class VariableSetter : Node
{
[Export] private VariableResource _variableResource;
[Export] private Variant _payloadToSet;
public void Set()
{
_variableResource.Payload = _payloadToSet;
}
}
@@ -0,0 +1 @@
uid://dfpyjxivcuidr
+69
View File
@@ -0,0 +1,69 @@
extends Node
@export_node_path("Node2D")
var target_node_path = NodePath()
@export var flip_bend_direction = false
@export var joint_one_bone_index = -1
@export var joint_two_bone_index = -1
var _angle_a = 0.0
var _angle_b = 0.0
func _process(delta: float) -> void:
_update_two_bone_ik_angles()
func _update_two_bone_ik_angles():
assert(joint_one_bone_index != -1)
assert(joint_two_bone_index != -1)
if target_node_path.is_empty():
return
var target = get_node(target_node_path) as Node2D
var bone_a = get_parent().get_bone(joint_one_bone_index)
var bone_b = get_parent().get_bone(joint_two_bone_index)
var bone_a_len = bone_a.get_length()
var bone_b_len = bone_b.get_length()
var sin_angle2 = 0.0
var cos_angle2 = 1.0
_angle_b = 0.0
var cos_angle2_denom = 2.0 * bone_a_len * bone_b_len
if not is_zero_approx(cos_angle2_denom):
var target_len_sqr = _distance_squared_between(bone_a, target)
var bone_a_len_sqr = bone_a_len * bone_a_len
var bone_b_len_sqr = bone_b_len * bone_b_len
cos_angle2 = (target_len_sqr - bone_a_len_sqr - bone_b_len_sqr) / cos_angle2_denom
cos_angle2 = clamp(cos_angle2, -1.0, 1.0);
_angle_b = acos(cos_angle2)
if flip_bend_direction:
_angle_b = -_angle_b
sin_angle2 = sin(_angle_b)
var tri_adjacent = bone_a_len + bone_b_len * cos_angle2
var tri_opposite = bone_b_len * sin_angle2
var xform_inv = bone_a.get_parent().global_transform.affine_inverse()
var target_pos = xform_inv * target.global_position - bone_a.position
var tan_y = target_pos.y * tri_adjacent - target_pos.x * tri_opposite
var tan_x = target_pos.x * tri_adjacent + target_pos.y * tri_opposite
_angle_a = atan2(tan_y, tan_x)
var bone_a_angle = bone_a.get_bone_angle()
var bone_b_angle = bone_b.get_bone_angle()
bone_a.rotation = _angle_a - bone_a_angle
bone_b.rotation = _angle_b - angle_difference(bone_a_angle, bone_b_angle)
func _distance_squared_between(node_a: Node2D, node_b: Node2D) -> float:
return node_a.global_position.distance_squared_to(node_b.global_position)
+1
View File
@@ -0,0 +1 @@
uid://p5wxvjx24brs