diff --git a/GameJamDungeon.csproj b/GameJamDungeon.csproj index be74ec3f..8fad3c53 100644 --- a/GameJamDungeon.csproj +++ b/GameJamDungeon.csproj @@ -13,10 +13,12 @@ + + diff --git a/GlobalSuppressions.cs b/GlobalSuppressions.cs new file mode 100644 index 00000000..0edfdb42 --- /dev/null +++ b/GlobalSuppressions.cs @@ -0,0 +1,9 @@ +// This file is used by Code Analysis to maintain SuppressMessage +// attributes that are applied to this project. +// Project-level suppressions either have no target or are given +// a specific target and scoped to a namespace, type, member, etc. + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "", Scope = "member", Target = "~M:GameJamDungeon.Game.OnResolved")] +[assembly: SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "", Scope = "member", Target = "~M:GameJamDungeon.Game.OnResolved")] diff --git a/src/game/Game.cs b/src/game/Game.cs index d9c66ca0..9fcf5437 100644 --- a/src/game/Game.cs +++ b/src/game/Game.cs @@ -2,9 +2,16 @@ namespace GameJamDungeon; using Chickensoft.AutoInject; +using Chickensoft.Collections; using Chickensoft.Introspection; +using Chickensoft.SaveFileBuilder; +using Chickensoft.Serialization; +using Chickensoft.Serialization.Godot; using Godot; +using Org.BouncyCastle.Asn1.Pkcs; using System; +using System.IO.Abstractions; +using System.Text.Json; using static GameJamDungeon.GameLogic.State; [Meta(typeof(IAutoNode))] @@ -53,10 +60,30 @@ public partial class Game : Node3D, IGame [Node] private IPlayer Player { get; set; } = default!; #endregion + #region Save + public JsonSerializerOptions JsonOptions { get; set; } = default!; + + public const string SAVE_FILE_NAME = "game.json"; + + public IFileSystem FileSystem { get; set; } = default!; + + public string SaveFilePath { get; set; } = default!; + + public ISaveFile SaveFile { get; set; } = default!; + + public ISaveChunk GameChunk { get; set; } = default!; + + ISaveChunk IProvide>.Value() => GameChunk; + #endregion + public RescuedItemDatabase RescuedItems { get; set; } = default!; public void Setup() { + FileSystem = new FileSystem(); + + SaveFilePath = FileSystem.Path.Join(OS.GetUserDataDir(), SAVE_FILE_NAME); + GameRepo = new GameRepo(); GameLogic = new GameLogic(); GameEventDepot = new GameEventDepot(); @@ -66,10 +93,78 @@ public partial class Game : Node3D, IGame GameLogic.Set(Player); Instantiator = new Instantiator(GetTree()); RescuedItems = new RescuedItemDatabase(); + + var resolver = new SerializableTypeResolver(); + GodotSerialization.Setup(); + + var upgradeDependencies = new Blackboard(); + + JsonOptions = new JsonSerializerOptions + { + Converters = { + new SerializableTypeConverter(upgradeDependencies) + }, + TypeInfoResolver = resolver, + WriteIndented = true + }; + + GameChunk = new SaveChunk( + (chunk) => + { + var gameData = new GameData() + { + PlayerData = new PlayerStats + { + CurrentHP = Player.Stats.CurrentHP.Value, + MaximumHP = Player.Stats.MaximumHP.Value, + CurrentVT = Player.Stats.CurrentVT.Value, + MaximumVT = Player.Stats.MaximumVT.Value, + CurrentAttack = Player.Stats.CurrentAttack.Value, + BonusAttack = Player.Stats.BonusAttack.Value, + MaxAttack = Player.Stats.MaxAttack.Value, + CurrentDefense = Player.Stats.CurrentDefense.Value, + BonusDefense = Player.Stats.BonusDefense.Value, + MaxDefense = Player.Stats.MaxDefense.Value, + CurrentExp = Player.Stats.CurrentExp.Value, + CurrentLevel = Player.Stats.CurrentLevel.Value, + ExpToNextLevel = Player.Stats.ExpToNextLevel.Value, + Luck = Player.Stats.Luck.Value + }, + }; + + return gameData; + }, + onLoad: (chunk, data) => + { + chunk.LoadChunkSaveData(data.PlayerData); + } + ); } public void OnResolved() { + SaveFile = new SaveFile( + root: GameChunk, + onSave: async (GameData data) => + { + // Save the game data to disk. + var json = JsonSerializer.Serialize(data, JsonOptions); + await FileSystem.File.WriteAllTextAsync(SaveFilePath, json); + }, + onLoad: async () => + { + // Load the game data from disk. + if (!FileSystem.File.Exists(SaveFilePath)) + { + GD.Print("No save file to load"); + return null; + } + + var json = await FileSystem.File.ReadAllTextAsync(SaveFilePath); + return JsonSerializer.Deserialize(json, JsonOptions); + } + ); + GameBinding = GameLogic.Bind(); GameBinding .Handle((in GameLogic.Output.StartGame _) => @@ -131,6 +226,8 @@ public partial class Game : Node3D, IGame public void ToggleInventory() { + SaveFile.Load(); + if (GameLogic.Value is InventoryOpened) GameLogic.Input(new GameLogic.Input.CloseInventory()); else diff --git a/src/game/IGame.cs b/src/game/IGame.cs index c4af7811..9639fed9 100644 --- a/src/game/IGame.cs +++ b/src/game/IGame.cs @@ -3,10 +3,11 @@ namespace GameJamDungeon; using Chickensoft.AutoInject; using Chickensoft.GodotNodeInterfaces; +using Chickensoft.SaveFileBuilder; using Godot; using System; -public interface IGame : IProvide, IProvide, IProvide, IProvide, INode3D +public interface IGame : IProvide, IProvide, IProvide, IProvide, IProvide>, INode3D { event Game.StatRaisedAlertEventHandler StatRaisedAlert; diff --git a/src/items/consumable/ConsumableItem.cs b/src/items/consumable/ConsumableItem.cs index 8290d134..c9b87f14 100644 --- a/src/items/consumable/ConsumableItem.cs +++ b/src/items/consumable/ConsumableItem.cs @@ -37,14 +37,14 @@ public partial class ConsumableItem : Node3D, IUsableItem public void Use() { - if (Player.Stats.CurrentHP.Value == Player.Stats.MaximumHP.Value && _consumableItemStats.RaiseHPAmount > 0) + if (Player.Stats.CurrentHP == Player.Stats.MaximumHP && _consumableItemStats.RaiseHPAmount > 0) Player.RaiseHP(_consumableItemStats.RaiseHPAmount); - if (Player.Stats.CurrentVT.Value == Player.Stats.MaximumVT.Value && _consumableItemStats.RaiseVTAmount > 0) + if (Player.Stats.CurrentVT == Player.Stats.MaximumVT && _consumableItemStats.RaiseVTAmount > 0) Player.RaiseVT(_consumableItemStats.RaiseVTAmount); - if (_consumableItemStats.HealHPAmount > 0 && Player.Stats.CurrentHP.Value != Player.Stats.MaximumHP.Value) + if (_consumableItemStats.HealHPAmount > 0 && Player.Stats.CurrentHP != Player.Stats.MaximumHP) Player.HealHP(_consumableItemStats.HealHPAmount); - if (_consumableItemStats.HealVTAmount > 0 && Player.Stats.CurrentVT.Value != Player.Stats.MaximumVT.Value) + if (_consumableItemStats.HealVTAmount > 0 && Player.Stats.CurrentVT != Player.Stats.MaximumVT) Player.HealVT(_consumableItemStats.HealVTAmount); } diff --git a/src/player/IPlayer.cs b/src/player/IPlayer.cs index 73fb3bab..381a35d1 100644 --- a/src/player/IPlayer.cs +++ b/src/player/IPlayer.cs @@ -1,9 +1,11 @@ -using Chickensoft.Collections; +using Chickensoft.AutoInject; +using Chickensoft.Collections; +using Chickensoft.SaveFileBuilder; using Godot; namespace GameJamDungeon; -public interface IPlayer : IKillable +public interface IPlayer : IKillable, IProvide> { public void Attack(); @@ -47,7 +49,7 @@ public interface IPlayer : IKillable public IInventory Inventory { get; } - public PlayerStats Stats { get; } + public PlayerStatController Stats { get; } public Vector3 CurrentPosition { get; } diff --git a/src/player/Player.cs b/src/player/Player.cs index 90eff007..06d8fb07 100644 --- a/src/player/Player.cs +++ b/src/player/Player.cs @@ -19,12 +19,16 @@ public partial class Player : CharacterBody3D, IPlayer private PlayerLogic.IBinding PlayerBinding { get; set; } = default!; #endregion + #region Save + public ISaveChunk PlayerChunk { get; set; } = default!; + #endregion + public double CurrentHP => Stats.CurrentHP.Value; public Vector3 CurrentPosition => GlobalPosition; public Basis CurrentBasis => Transform.Basis; - public PlayerStats Stats { get; set; } = default!; + public PlayerStatController Stats { get; set; } = default!; public IInventory Inventory { get; private set; } = default!; @@ -48,6 +52,8 @@ public partial class Player : CharacterBody3D, IPlayer [Dependency] public ISaveChunk GameChunk => this.DependOn>(); + + ISaveChunk IProvide>.Value() => PlayerChunk; #endregion #region Event Signals @@ -110,25 +116,28 @@ public partial class Player : CharacterBody3D, IPlayer public void Setup() { Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration }; + Stats = new PlayerStatController(); + Stats.Init( + new PlayerStats + { + CurrentHP = PlayerStatResource.CurrentHP, + MaximumHP = PlayerStatResource.MaximumHP, + CurrentVT = PlayerStatResource.CurrentVT, + MaximumVT = PlayerStatResource.MaximumVT, + CurrentAttack = PlayerStatResource.CurrentAttack, + BonusAttack = PlayerStatResource.BonusAttack, + MaxAttack = PlayerStatResource.MaxAttack, + CurrentDefense = PlayerStatResource.CurrentDefense, + BonusDefense = PlayerStatResource.BonusDefense, + MaxDefense = PlayerStatResource.MaxDefense, + CurrentExp = PlayerStatResource.CurrentExp, + CurrentLevel = PlayerStatResource.CurrentLevel, + ExpToNextLevel = PlayerStatResource.ExpToNextLevel, + Luck = PlayerStatResource.Luck + }); - Stats = new PlayerStats(); Inventory = new Inventory(); - Stats.SetCurrentHP(PlayerStatResource.CurrentHP); - Stats.SetMaximumHP(PlayerStatResource.MaximumHP); - Stats.SetCurrentVT(PlayerStatResource.CurrentVT); - Stats.SetMaximumVT(PlayerStatResource.MaximumVT); - Stats.SetCurrentAttack(PlayerStatResource.CurrentAttack); - Stats.SetBonusAttack(PlayerStatResource.BonusAttack); - Stats.SetMaxAttack(PlayerStatResource.MaxAttack); - Stats.SetCurrentDefense(PlayerStatResource.CurrentDefense); - Stats.SetBonusDefense(PlayerStatResource.BonusDefense); - Stats.SetMaxDefense(PlayerStatResource.MaxDefense); - Stats.SetCurrentExp(PlayerStatResource.CurrentExp); - Stats.SetCurrentLevel(PlayerStatResource.CurrentLevel); - Stats.SetExpToNextLevel(PlayerStatResource.ExpToNextLevel); - Stats.SetLuck(PlayerStatResource.Luck); - PlayerLogic = new PlayerLogic(); PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(Settings); @@ -155,6 +164,30 @@ public partial class Player : CharacterBody3D, IPlayer public void OnResolved() { + PlayerChunk = new SaveChunk( + onSave: (chunk) => new PlayerStats + { + CurrentHP = Stats.CurrentHP.Value, + MaximumHP = Stats.MaximumHP.Value, + CurrentVT = Stats.CurrentVT.Value, + MaximumVT = Stats.MaximumVT.Value, + CurrentAttack = Stats.CurrentAttack.Value, + BonusAttack = Stats.BonusAttack.Value, + MaxAttack = Stats.MaxAttack.Value, + CurrentDefense = Stats.CurrentDefense.Value, + BonusDefense = Stats.BonusDefense.Value, + MaxDefense = Stats.MaxDefense.Value, + CurrentExp = Stats.CurrentExp.Value, + CurrentLevel = Stats.CurrentLevel.Value, + ExpToNextLevel = Stats.ExpToNextLevel.Value, + Luck = Stats.Luck.Value + }, + onLoad: (chunk, data) => + { + Stats.Init(data); + } + ); + PlayerBinding = PlayerLogic.Bind(); PlayerBinding @@ -169,8 +202,10 @@ public partial class Player : CharacterBody3D, IPlayer { }); - this.Provide(); + GameChunk.AddChunk(PlayerChunk); + PlayerLogic.Start(); + this.Provide(); HealthTimer.Timeout += OnHealthTimerTimeout; Hitbox.AreaEntered += Hitbox_AreaEntered; } @@ -225,7 +260,7 @@ public partial class Player : CharacterBody3D, IPlayer public void RaiseVT(int amountToRaise) { - if (Stats.CurrentVT.Value == Stats.MaximumVT.Value) + if (Stats.CurrentVT == Stats.MaximumVT) { Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise); Stats.SetCurrentVT(Stats.MaximumVT.Value); diff --git a/src/player/PlayerStats.cs b/src/player/PlayerStats.cs index b5e2f526..2149a088 100644 --- a/src/player/PlayerStats.cs +++ b/src/player/PlayerStats.cs @@ -1,38 +1,76 @@ using Chickensoft.Collections; +using Chickensoft.Introspection; using Chickensoft.Serialization; using Godot; namespace GameJamDungeon; +[Meta, Id("player_stats")] public partial record PlayerStats { [Save("currentHP")] - public IAutoProp CurrentHP => _currentHP; + public int CurrentHP { get; init; } [Save("maximumHP")] - public IAutoProp MaximumHP => _maximumHP; + public int MaximumHP { get; init; } [Save("currentVT")] - public IAutoProp CurrentVT => _currentVT; + public int CurrentVT { get; init; } [Save("maximumVT")] - public IAutoProp MaximumVT => _maximumVT; + public int MaximumVT { get; init; } [Save("currentExp")] - public IAutoProp CurrentExp => _currentExp; + public int CurrentExp { get; init; } [Save("currentLevel")] - public IAutoProp CurrentLevel => _currentLevel; + public int CurrentLevel { get; init; } [Save("currentAttack")] - public IAutoProp CurrentAttack => _currentAttack; + public int CurrentAttack { get; init; } [Save("bonusAttack")] - public IAutoProp BonusAttack => _bonusAttack; + public int BonusAttack { get; init; } [Save("maxAttack")] - public IAutoProp MaxAttack => _maxAttack; + public int MaxAttack { get; init; } [Save("currentDefense")] - public IAutoProp CurrentDefense => _currentDefense; + public int CurrentDefense { get; init; } [Save("bonusDefense")] - public IAutoProp BonusDefense => _bonusDefense; + public int BonusDefense { get; init; } [Save("maxDefense")] - public IAutoProp MaxDefense => _maxDefense; + public int MaxDefense { get; init; } [Save("expToNextLevel")] - public IAutoProp ExpToNextLevel => _expToNextLevel; + public int ExpToNextLevel { get; init; } [Save("luck")] + public double Luck { get; init; } +} + +public class PlayerStatController +{ + public void Init(PlayerStats playerStats) + { + _currentHP.OnNext(playerStats.CurrentHP); + _maximumHP.OnNext(playerStats.MaximumHP); + _currentVT.OnNext(playerStats.CurrentVT); + _maximumVT.OnNext(playerStats.MaximumVT); + _currentExp.OnNext(playerStats.CurrentExp); + _currentLevel.OnNext(playerStats.CurrentLevel); + _currentAttack.OnNext(playerStats.CurrentAttack); + _bonusAttack.OnNext(playerStats.BonusAttack); + _maxAttack.OnNext(playerStats.MaxAttack); + _currentDefense.OnNext(playerStats.CurrentDefense); + _bonusDefense.OnNext(playerStats.BonusDefense); + _maxDefense.OnNext(playerStats.MaxDefense); + _expToNextLevel.OnNext(playerStats.ExpToNextLevel); + _luck.OnNext(playerStats.Luck); + } + + public IAutoProp CurrentHP => _currentHP; + public IAutoProp MaximumHP => _maximumHP; + public IAutoProp CurrentVT => _currentVT; + public IAutoProp MaximumVT => _maximumVT; + public IAutoProp CurrentAttack => _currentAttack; + public IAutoProp MaxAttack => _maxAttack; + public IAutoProp BonusAttack => _bonusAttack; + public IAutoProp CurrentDefense => _currentDefense; + public IAutoProp MaxDefense => _maxDefense; + public IAutoProp BonusDefense => _bonusDefense; + public IAutoProp CurrentExp => _currentExp; + public IAutoProp ExpToNextLevel => _expToNextLevel; + public IAutoProp CurrentLevel => _currentLevel; public IAutoProp Luck => _luck; public void SetCurrentHP(int newValue) @@ -97,18 +135,18 @@ public partial record PlayerStats _luck.OnNext(clampedValue); } - private readonly AutoProp _currentHP = new(int.MaxValue); - private readonly AutoProp _maximumHP = new(int.MaxValue); - private readonly AutoProp _currentVT = new(int.MaxValue); - private readonly AutoProp _maximumVT = new(int.MaxValue); - private readonly AutoProp _currentExp = new(int.MaxValue); - private readonly AutoProp _currentLevel = new(int.MaxValue); - private readonly AutoProp _currentAttack = new(int.MaxValue); - private readonly AutoProp _bonusAttack = new(int.MaxValue); - private readonly AutoProp _maxAttack = new(int.MaxValue); - private readonly AutoProp _currentDefense = new(int.MaxValue); - private readonly AutoProp _bonusDefense = new(int.MaxValue); - private readonly AutoProp _maxDefense = new(int.MaxValue); - private readonly AutoProp _expToNextLevel = new(int.MaxValue); - private readonly AutoProp _luck = new(double.MaxValue); + private readonly AutoProp _currentHP = new(-1); + private readonly AutoProp _maximumHP = new(-1); + private readonly AutoProp _currentVT = new(-1); + private readonly AutoProp _maximumVT = new(-1); + private readonly AutoProp _currentExp = new(-1); + private readonly AutoProp _currentLevel = new(-1); + private readonly AutoProp _currentAttack = new(-1); + private readonly AutoProp _bonusAttack = new(-1); + private readonly AutoProp _maxAttack = new(-1); + private readonly AutoProp _currentDefense = new(-1); + private readonly AutoProp _bonusDefense = new(-1); + private readonly AutoProp _maxDefense = new(-1); + private readonly AutoProp _expToNextLevel = new(-1); + private readonly AutoProp _luck = new(-1); }