Start restructure of game data to support save/load

This commit is contained in:
2025-03-04 02:39:27 -08:00
parent a9cb43b79e
commit d35e81a9a8
8 changed files with 238 additions and 54 deletions

View File

@@ -13,10 +13,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" /> <PackageReference Include="Chickensoft.AutoInject" Version="2.5.0" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" /> <PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.0" />
<PackageReference Include="Chickensoft.Introspection" Version="2.2.0" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" /> <PackageReference Include="Chickensoft.Introspection.Generator" Version="2.2.0" />
<PackageReference Include="Chickensoft.LogicBlocks" Version="5.15.0" /> <PackageReference Include="Chickensoft.LogicBlocks" Version="5.15.0" />
<PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.15.0" /> <PackageReference Include="Chickensoft.LogicBlocks.DiagramGenerator" Version="5.15.0" />
<PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" /> <PackageReference Include="Chickensoft.SaveFileBuilder" Version="1.1.0" />
<PackageReference Include="Chickensoft.Serialization.Godot" Version="0.7.6" />
<PackageReference Include="GodotSharp.SourceGenerators" Version="2.5.0" /> <PackageReference Include="GodotSharp.SourceGenerators" Version="2.5.0" />
<PackageReference Include="SSH.NET" Version="2024.2.0" /> <PackageReference Include="SSH.NET" Version="2024.2.0" />
<PackageReference Include="System.IO.Abstractions" Version="21.2.1" /> <PackageReference Include="System.IO.Abstractions" Version="21.2.1" />

9
GlobalSuppressions.cs Normal file
View File

@@ -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 = "<Pending>", Scope = "member", Target = "~M:GameJamDungeon.Game.OnResolved")]
[assembly: SuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>", Scope = "member", Target = "~M:GameJamDungeon.Game.OnResolved")]

View File

@@ -2,9 +2,16 @@
namespace GameJamDungeon; namespace GameJamDungeon;
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.Collections;
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Chickensoft.SaveFileBuilder;
using Chickensoft.Serialization;
using Chickensoft.Serialization.Godot;
using Godot; using Godot;
using Org.BouncyCastle.Asn1.Pkcs;
using System; using System;
using System.IO.Abstractions;
using System.Text.Json;
using static GameJamDungeon.GameLogic.State; using static GameJamDungeon.GameLogic.State;
[Meta(typeof(IAutoNode))] [Meta(typeof(IAutoNode))]
@@ -53,10 +60,30 @@ public partial class Game : Node3D, IGame
[Node] private IPlayer Player { get; set; } = default!; [Node] private IPlayer Player { get; set; } = default!;
#endregion #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<GameData> SaveFile { get; set; } = default!;
public ISaveChunk<GameData> GameChunk { get; set; } = default!;
ISaveChunk<GameData> IProvide<ISaveChunk<GameData>>.Value() => GameChunk;
#endregion
public RescuedItemDatabase RescuedItems { get; set; } = default!; public RescuedItemDatabase RescuedItems { get; set; } = default!;
public void Setup() public void Setup()
{ {
FileSystem = new FileSystem();
SaveFilePath = FileSystem.Path.Join(OS.GetUserDataDir(), SAVE_FILE_NAME);
GameRepo = new GameRepo(); GameRepo = new GameRepo();
GameLogic = new GameLogic(); GameLogic = new GameLogic();
GameEventDepot = new GameEventDepot(); GameEventDepot = new GameEventDepot();
@@ -66,10 +93,78 @@ public partial class Game : Node3D, IGame
GameLogic.Set(Player); GameLogic.Set(Player);
Instantiator = new Instantiator(GetTree()); Instantiator = new Instantiator(GetTree());
RescuedItems = new RescuedItemDatabase(); 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<GameData>(
(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() public void OnResolved()
{ {
SaveFile = new SaveFile<GameData>(
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<GameData>(json, JsonOptions);
}
);
GameBinding = GameLogic.Bind(); GameBinding = GameLogic.Bind();
GameBinding GameBinding
.Handle((in GameLogic.Output.StartGame _) => .Handle((in GameLogic.Output.StartGame _) =>
@@ -131,6 +226,8 @@ public partial class Game : Node3D, IGame
public void ToggleInventory() public void ToggleInventory()
{ {
SaveFile.Load();
if (GameLogic.Value is InventoryOpened) if (GameLogic.Value is InventoryOpened)
GameLogic.Input(new GameLogic.Input.CloseInventory()); GameLogic.Input(new GameLogic.Input.CloseInventory());
else else

View File

@@ -3,10 +3,11 @@ namespace GameJamDungeon;
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.GodotNodeInterfaces; using Chickensoft.GodotNodeInterfaces;
using Chickensoft.SaveFileBuilder;
using Godot; using Godot;
using System; using System;
public interface IGame : IProvide<IGameRepo>, IProvide<IGameEventDepot>, IProvide<IGame>, IProvide<IPlayer>, INode3D public interface IGame : IProvide<IGameRepo>, IProvide<IGameEventDepot>, IProvide<IGame>, IProvide<IPlayer>, IProvide<ISaveChunk<GameData>>, INode3D
{ {
event Game.StatRaisedAlertEventHandler StatRaisedAlert; event Game.StatRaisedAlertEventHandler StatRaisedAlert;

View File

@@ -37,14 +37,14 @@ public partial class ConsumableItem : Node3D, IUsableItem
public void Use() 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); 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); 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); 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); Player.HealVT(_consumableItemStats.HealVTAmount);
} }

View File

@@ -1,9 +1,11 @@
using Chickensoft.Collections; using Chickensoft.AutoInject;
using Chickensoft.Collections;
using Chickensoft.SaveFileBuilder;
using Godot; using Godot;
namespace GameJamDungeon; namespace GameJamDungeon;
public interface IPlayer : IKillable public interface IPlayer : IKillable, IProvide<ISaveChunk<PlayerStats>>
{ {
public void Attack(); public void Attack();
@@ -47,7 +49,7 @@ public interface IPlayer : IKillable
public IInventory Inventory { get; } public IInventory Inventory { get; }
public PlayerStats Stats { get; } public PlayerStatController Stats { get; }
public Vector3 CurrentPosition { get; } public Vector3 CurrentPosition { get; }

View File

@@ -19,12 +19,16 @@ public partial class Player : CharacterBody3D, IPlayer
private PlayerLogic.IBinding PlayerBinding { get; set; } = default!; private PlayerLogic.IBinding PlayerBinding { get; set; } = default!;
#endregion #endregion
#region Save
public ISaveChunk<PlayerStats> PlayerChunk { get; set; } = default!;
#endregion
public double CurrentHP => Stats.CurrentHP.Value; public double CurrentHP => Stats.CurrentHP.Value;
public Vector3 CurrentPosition => GlobalPosition; public Vector3 CurrentPosition => GlobalPosition;
public Basis CurrentBasis => Transform.Basis; public Basis CurrentBasis => Transform.Basis;
public PlayerStats Stats { get; set; } = default!; public PlayerStatController Stats { get; set; } = default!;
public IInventory Inventory { get; private set; } = default!; public IInventory Inventory { get; private set; } = default!;
@@ -48,6 +52,8 @@ public partial class Player : CharacterBody3D, IPlayer
[Dependency] [Dependency]
public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>(); public ISaveChunk<GameData> GameChunk => this.DependOn<ISaveChunk<GameData>>();
ISaveChunk<PlayerStats> IProvide<ISaveChunk<PlayerStats>>.Value() => PlayerChunk;
#endregion #endregion
#region Event Signals #region Event Signals
@@ -110,25 +116,28 @@ public partial class Player : CharacterBody3D, IPlayer
public void Setup() public void Setup()
{ {
Settings = new PlayerLogic.Settings() { RotationSpeed = PlayerStatResource.RotationSpeed, MoveSpeed = PlayerStatResource.MoveSpeed, Acceleration = PlayerStatResource.Acceleration }; 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(); 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 = new PlayerLogic();
PlayerLogic.Set(this as IPlayer); PlayerLogic.Set(this as IPlayer);
PlayerLogic.Set(Settings); PlayerLogic.Set(Settings);
@@ -155,6 +164,30 @@ public partial class Player : CharacterBody3D, IPlayer
public void OnResolved() public void OnResolved()
{ {
PlayerChunk = new SaveChunk<PlayerStats>(
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 = PlayerLogic.Bind();
PlayerBinding PlayerBinding
@@ -169,8 +202,10 @@ public partial class Player : CharacterBody3D, IPlayer
{ {
}); });
this.Provide(); GameChunk.AddChunk(PlayerChunk);
PlayerLogic.Start(); PlayerLogic.Start();
this.Provide();
HealthTimer.Timeout += OnHealthTimerTimeout; HealthTimer.Timeout += OnHealthTimerTimeout;
Hitbox.AreaEntered += Hitbox_AreaEntered; Hitbox.AreaEntered += Hitbox_AreaEntered;
} }
@@ -225,7 +260,7 @@ public partial class Player : CharacterBody3D, IPlayer
public void RaiseVT(int amountToRaise) public void RaiseVT(int amountToRaise)
{ {
if (Stats.CurrentVT.Value == Stats.MaximumVT.Value) if (Stats.CurrentVT == Stats.MaximumVT)
{ {
Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise); Stats.SetMaximumVT(Stats.MaximumVT.Value + amountToRaise);
Stats.SetCurrentVT(Stats.MaximumVT.Value); Stats.SetCurrentVT(Stats.MaximumVT.Value);

View File

@@ -1,38 +1,76 @@
using Chickensoft.Collections; using Chickensoft.Collections;
using Chickensoft.Introspection;
using Chickensoft.Serialization; using Chickensoft.Serialization;
using Godot; using Godot;
namespace GameJamDungeon; namespace GameJamDungeon;
[Meta, Id("player_stats")]
public partial record PlayerStats public partial record PlayerStats
{ {
[Save("currentHP")] [Save("currentHP")]
public IAutoProp<int> CurrentHP => _currentHP; public int CurrentHP { get; init; }
[Save("maximumHP")] [Save("maximumHP")]
public IAutoProp<int> MaximumHP => _maximumHP; public int MaximumHP { get; init; }
[Save("currentVT")] [Save("currentVT")]
public IAutoProp<int> CurrentVT => _currentVT; public int CurrentVT { get; init; }
[Save("maximumVT")] [Save("maximumVT")]
public IAutoProp<int> MaximumVT => _maximumVT; public int MaximumVT { get; init; }
[Save("currentExp")] [Save("currentExp")]
public IAutoProp<int> CurrentExp => _currentExp; public int CurrentExp { get; init; }
[Save("currentLevel")] [Save("currentLevel")]
public IAutoProp<int> CurrentLevel => _currentLevel; public int CurrentLevel { get; init; }
[Save("currentAttack")] [Save("currentAttack")]
public IAutoProp<int> CurrentAttack => _currentAttack; public int CurrentAttack { get; init; }
[Save("bonusAttack")] [Save("bonusAttack")]
public IAutoProp<int> BonusAttack => _bonusAttack; public int BonusAttack { get; init; }
[Save("maxAttack")] [Save("maxAttack")]
public IAutoProp<int> MaxAttack => _maxAttack; public int MaxAttack { get; init; }
[Save("currentDefense")] [Save("currentDefense")]
public IAutoProp<int> CurrentDefense => _currentDefense; public int CurrentDefense { get; init; }
[Save("bonusDefense")] [Save("bonusDefense")]
public IAutoProp<int> BonusDefense => _bonusDefense; public int BonusDefense { get; init; }
[Save("maxDefense")] [Save("maxDefense")]
public IAutoProp<int> MaxDefense => _maxDefense; public int MaxDefense { get; init; }
[Save("expToNextLevel")] [Save("expToNextLevel")]
public IAutoProp<int> ExpToNextLevel => _expToNextLevel; public int ExpToNextLevel { get; init; }
[Save("luck")] [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<int> CurrentHP => _currentHP;
public IAutoProp<int> MaximumHP => _maximumHP;
public IAutoProp<int> CurrentVT => _currentVT;
public IAutoProp<int> MaximumVT => _maximumVT;
public IAutoProp<int> CurrentAttack => _currentAttack;
public IAutoProp<int> MaxAttack => _maxAttack;
public IAutoProp<int> BonusAttack => _bonusAttack;
public IAutoProp<int> CurrentDefense => _currentDefense;
public IAutoProp<int> MaxDefense => _maxDefense;
public IAutoProp<int> BonusDefense => _bonusDefense;
public IAutoProp<int> CurrentExp => _currentExp;
public IAutoProp<int> ExpToNextLevel => _expToNextLevel;
public IAutoProp<int> CurrentLevel => _currentLevel;
public IAutoProp<double> Luck => _luck; public IAutoProp<double> Luck => _luck;
public void SetCurrentHP(int newValue) public void SetCurrentHP(int newValue)
@@ -97,18 +135,18 @@ public partial record PlayerStats
_luck.OnNext(clampedValue); _luck.OnNext(clampedValue);
} }
private readonly AutoProp<int> _currentHP = new(int.MaxValue); private readonly AutoProp<int> _currentHP = new(-1);
private readonly AutoProp<int> _maximumHP = new(int.MaxValue); private readonly AutoProp<int> _maximumHP = new(-1);
private readonly AutoProp<int> _currentVT = new(int.MaxValue); private readonly AutoProp<int> _currentVT = new(-1);
private readonly AutoProp<int> _maximumVT = new(int.MaxValue); private readonly AutoProp<int> _maximumVT = new(-1);
private readonly AutoProp<int> _currentExp = new(int.MaxValue); private readonly AutoProp<int> _currentExp = new(-1);
private readonly AutoProp<int> _currentLevel = new(int.MaxValue); private readonly AutoProp<int> _currentLevel = new(-1);
private readonly AutoProp<int> _currentAttack = new(int.MaxValue); private readonly AutoProp<int> _currentAttack = new(-1);
private readonly AutoProp<int> _bonusAttack = new(int.MaxValue); private readonly AutoProp<int> _bonusAttack = new(-1);
private readonly AutoProp<int> _maxAttack = new(int.MaxValue); private readonly AutoProp<int> _maxAttack = new(-1);
private readonly AutoProp<int> _currentDefense = new(int.MaxValue); private readonly AutoProp<int> _currentDefense = new(-1);
private readonly AutoProp<int> _bonusDefense = new(int.MaxValue); private readonly AutoProp<int> _bonusDefense = new(-1);
private readonly AutoProp<int> _maxDefense = new(int.MaxValue); private readonly AutoProp<int> _maxDefense = new(-1);
private readonly AutoProp<int> _expToNextLevel = new(int.MaxValue); private readonly AutoProp<int> _expToNextLevel = new(-1);
private readonly AutoProp<double> _luck = new(double.MaxValue); private readonly AutoProp<double> _luck = new(-1);
} }