Compare commits
61 Commits
00772b6330
...
phase-a-pr
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e6d5cf6f9 | |||
| e8d1da8f89 | |||
| 360f73ba24 | |||
| b6a8448743 | |||
| f4411e05a3 | |||
| a1c9042652 | |||
| ca54bf0f82 | |||
| c1c3e7b020 | |||
| 627900b2e8 | |||
| f85c16c641 | |||
| 7a69292066 | |||
| f991bb99fc | |||
| 330e10880c | |||
| 003ec4b010 | |||
| 05149bb4a9 | |||
| ffa2b986ae | |||
| 40bd5c1d86 | |||
| 9767941c65 | |||
| 313a5315b1 | |||
| c3bbf3282a | |||
| dd113af786 | |||
| 6b784303b4 | |||
| 562be1049d | |||
| b2a406ca2b | |||
| 7f0869ffa4 | |||
| 7f1b4b5e0e | |||
| 8769308c85 | |||
| 4b63368286 | |||
| cbf625d2ff | |||
| 057cf7b368 | |||
| 829d081eab | |||
| 38c86f1a67 | |||
| e647cd7b29 | |||
| 9043cfa63c | |||
| dc46769c11 | |||
| 47600be867 | |||
| 3e74959bb9 | |||
| 8cce1866b1 | |||
| 0105698c33 | |||
| 3b18e328b2 | |||
| 2bc0b76050 | |||
| d83e3b4d82 | |||
| 42d7df27ae | |||
| fd54e16bf4 | |||
| 92a3c4f79a | |||
| 654a8b7ea7 | |||
| 53ebbbbac3 | |||
| 551dc09485 | |||
| 814d9736d0 | |||
| adaeb35fdd | |||
| eeefca4d4e | |||
| 3e36e48e97 | |||
| 1e9814a9d8 | |||
| 470b0c3a8c | |||
| cfef49fbc7 | |||
| 321905e68e | |||
| 427bef6509 | |||
| 3616f4ba79 | |||
| 2bf903728b | |||
| bfe951939d | |||
| dde3eaa52e |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,4 +1,13 @@
|
|||||||
# Godot 4+ specific ignores
|
# Godot 4+ specific ignores
|
||||||
.godot/
|
.godot/
|
||||||
/android/
|
/android/
|
||||||
builds/
|
builds/
|
||||||
|
.worktrees/
|
||||||
|
|
||||||
|
# IDE / local tooling
|
||||||
|
.idea/
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
|
# .NET build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
|||||||
@@ -1,23 +1,30 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
using Mr.BrickAdventures.scripts.State;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Manages achievements using GameStateStore.
|
||||||
|
/// </summary>
|
||||||
public partial class AchievementManager : Node
|
public partial class AchievementManager : Node
|
||||||
{
|
{
|
||||||
|
public static AchievementManager Instance { get; private set; }
|
||||||
|
|
||||||
[Export] private string AchievementsFolderPath = "res://achievements/";
|
[Export] private string AchievementsFolderPath = "res://achievements/";
|
||||||
[Export] private PackedScene AchievementPopupScene { get; set; }
|
[Export] private PackedScene AchievementPopupScene { get; set; }
|
||||||
|
|
||||||
private System.Collections.Generic.Dictionary<string, AchievementResource> _achievements = new();
|
private System.Collections.Generic.Dictionary<string, AchievementResource> _achievements = new();
|
||||||
private Array<string> _unlockedAchievementIds = [];
|
|
||||||
private GameManager _gameManager;
|
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
Instance = this;
|
||||||
LoadAchievementsFromFolder();
|
LoadAchievementsFromFolder();
|
||||||
LoadUnlockedAchievements();
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadAchievementsFromFolder()
|
private void LoadAchievementsFromFolder()
|
||||||
@@ -44,7 +51,15 @@ public partial class AchievementManager : Node
|
|||||||
fileName = dir.GetNext();
|
fileName = dir.GetNext();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of unlocked achievement IDs from the store.
|
||||||
|
/// </summary>
|
||||||
|
private System.Collections.Generic.List<string> GetUnlockedIds()
|
||||||
|
{
|
||||||
|
return GameStateStore.Instance?.Player.UnlockedAchievements ?? new System.Collections.Generic.List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
public void UnlockAchievement(string achievementId)
|
public void UnlockAchievement(string achievementId)
|
||||||
{
|
{
|
||||||
if (!_achievements.TryGetValue(achievementId, out var achievement))
|
if (!_achievements.TryGetValue(achievementId, out var achievement))
|
||||||
@@ -53,14 +68,16 @@ public partial class AchievementManager : Node
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_unlockedAchievementIds.Contains(achievementId))
|
var unlockedIds = GetUnlockedIds();
|
||||||
|
if (unlockedIds.Contains(achievementId))
|
||||||
{
|
{
|
||||||
return; // Already unlocked
|
return; // Already unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1. Mark as unlocked internally
|
// 1. Mark as unlocked
|
||||||
_unlockedAchievementIds.Add(achievementId);
|
unlockedIds.Add(achievementId);
|
||||||
GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
|
GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
|
||||||
|
EventBus.EmitAchievementUnlocked(achievementId);
|
||||||
|
|
||||||
// 2. Show the UI popup
|
// 2. Show the UI popup
|
||||||
if (AchievementPopupScene != null)
|
if (AchievementPopupScene != null)
|
||||||
@@ -75,31 +92,19 @@ public partial class AchievementManager : Node
|
|||||||
{
|
{
|
||||||
SteamManager.UnlockAchievement(achievement.Id);
|
SteamManager.UnlockAchievement(achievement.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Save progress
|
|
||||||
SaveUnlockedAchievements();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LockAchievement(string achievementId)
|
|
||||||
{
|
|
||||||
if (_unlockedAchievementIds.Contains(achievementId))
|
|
||||||
{
|
|
||||||
_unlockedAchievementIds.Remove(achievementId);
|
|
||||||
SaveUnlockedAchievements();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveUnlockedAchievements()
|
|
||||||
{
|
|
||||||
_gameManager.PlayerState["unlocked_achievements"] = _unlockedAchievementIds;
|
|
||||||
// You might want to trigger a save game here, depending on your SaveSystem
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LoadUnlockedAchievements()
|
public void LockAchievement(string achievementId)
|
||||||
{
|
{
|
||||||
if (_gameManager.PlayerState.TryGetValue("unlocked_achievements", out var unlocked))
|
var unlockedIds = GetUnlockedIds();
|
||||||
|
if (unlockedIds.Contains(achievementId))
|
||||||
{
|
{
|
||||||
_unlockedAchievementIds = (Array<string>)unlocked;
|
unlockedIds.Remove(achievementId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsAchievementUnlocked(string achievementId)
|
||||||
|
{
|
||||||
|
return GetUnlockedIds().Contains(achievementId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,13 +4,16 @@ namespace Mr.BrickAdventures.Autoloads;
|
|||||||
|
|
||||||
public partial class ConfigFileHandler : Node
|
public partial class ConfigFileHandler : Node
|
||||||
{
|
{
|
||||||
|
public static ConfigFileHandler Instance { get; private set; }
|
||||||
|
|
||||||
private ConfigFile _settingsConfig = new();
|
private ConfigFile _settingsConfig = new();
|
||||||
public const string SettingsPath = "user://settings.ini";
|
public const string SettingsPath = "user://settings.ini";
|
||||||
|
|
||||||
public ConfigFile SettingsConfig => _settingsConfig;
|
public ConfigFile SettingsConfig => _settingsConfig;
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
|
Instance = this;
|
||||||
if (!FileAccess.FileExists(SettingsPath))
|
if (!FileAccess.FileExists(SettingsPath))
|
||||||
{
|
{
|
||||||
var err = _settingsConfig.Save(SettingsPath);
|
var err = _settingsConfig.Save(SettingsPath);
|
||||||
@@ -24,4 +27,9 @@ public partial class ConfigFileHandler : Node
|
|||||||
GD.PushError($"Failed to load settings file at {SettingsPath}: {err}");
|
GD.PushError($"Failed to load settings file at {SettingsPath}: {err}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,173 +1,120 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Limbo.Console.Sharp;
|
|
||||||
using Mr.BrickAdventures.scripts;
|
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
public partial class ConsoleManager : Node
|
public partial class ConsoleManager : Node
|
||||||
{
|
{
|
||||||
private GameManager _gameManager;
|
private GameStateStore Store => GameStateStore.Instance;
|
||||||
|
private GameManager GameManager => GameManager.Instance;
|
||||||
|
private AchievementManager AchievementManager => AchievementManager.Instance;
|
||||||
private SkillManager _skillManager;
|
private SkillManager _skillManager;
|
||||||
private SkillUnlockerComponent _skillUnlockerComponent;
|
private SkillUnlockerComponent _skillUnlockerComponent;
|
||||||
private AchievementManager _achievementManager;
|
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
_skillManager = SkillManager.Instance;
|
||||||
_achievementManager = GetNode<AchievementManager>("/root/AchievementManager");
|
|
||||||
_skillManager = GetNode<SkillManager>("/root/SkillManager");
|
|
||||||
|
|
||||||
RegisterConsoleCommands();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
|
||||||
{
|
|
||||||
UnregisterConsoleCommands();
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConsoleCommand("add_coins", "Adds a specified amount of coins to the player's total.")]
|
|
||||||
private void AddCoinsCommand(int amount)
|
private void AddCoinsCommand(int amount)
|
||||||
{
|
{
|
||||||
_gameManager.AddCoins(amount);
|
if (Store == null) return;
|
||||||
LimboConsole.Info($"Increased coins by {amount}. Total coins: {_gameManager.GetCoins()}");
|
Store.Player.Coins += amount;
|
||||||
|
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("set_coins", "Sets the player's total coins to a specified amount.")]
|
|
||||||
private void SetCoinsCommand(int amount)
|
private void SetCoinsCommand(int amount)
|
||||||
{
|
{
|
||||||
_gameManager.SetCoins(amount);
|
if (Store == null) return;
|
||||||
LimboConsole.Info($"Set coins to {amount}. Total coins: {_gameManager.GetCoins()}");
|
Store.Player.Coins = Mathf.Max(0, amount);
|
||||||
|
EventBus.EmitCoinsChanged(Store.GetTotalCoins());
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("set_lives", "Sets the player's total lives to a specified amount.")]
|
|
||||||
private void SetLivesCommand(int amount)
|
private void SetLivesCommand(int amount)
|
||||||
{
|
{
|
||||||
_gameManager.SetLives(amount);
|
if (Store == null) return;
|
||||||
LimboConsole.Info($"Set lives to {amount}.");
|
Store.Player.Lives = amount;
|
||||||
|
EventBus.EmitLivesChanged(amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("add_lives", "Adds a specified amount of lives to the player's total.")]
|
|
||||||
private void AddLivesCommand(int amount)
|
private void AddLivesCommand(int amount)
|
||||||
{
|
{
|
||||||
_gameManager.AddLives(amount);
|
Store?.AddLives(amount);
|
||||||
LimboConsole.Info($"Increased lives by {amount}. Total lives: {_gameManager.GetLives()}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("set_health", "Sets the player's health to a specified amount.")]
|
|
||||||
private void SetHealthCommand(float amount)
|
private void SetHealthCommand(float amount)
|
||||||
{
|
{
|
||||||
var playerHealthComponent = _gameManager.Player.GetNode<HealthComponent>("HealthComponent");
|
var playerHealthComponent = GameManager?.Player?.GetNode<HealthComponent>("HealthComponent");
|
||||||
if (playerHealthComponent != null)
|
if (playerHealthComponent != null)
|
||||||
{
|
|
||||||
playerHealthComponent.Health = amount;
|
playerHealthComponent.Health = amount;
|
||||||
LimboConsole.Info($"Set player health to {amount}.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
LimboConsole.Warn("Player HealthComponent not found.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("reset_session", "Resets the current session state.")]
|
|
||||||
private void ResetSessionCommand()
|
private void ResetSessionCommand()
|
||||||
{
|
{
|
||||||
_gameManager.ResetCurrentSessionState();
|
Store?.ResetSession();
|
||||||
LimboConsole.Info("Current session state has been reset.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("unlock_skill", "Unlocks and activates a skill by its name.")]
|
|
||||||
private void UnlockSkillCommand(string skillName)
|
private void UnlockSkillCommand(string skillName)
|
||||||
{
|
{
|
||||||
if (!GetSkillManagement()) return;
|
if (!EnsureSkillManagement()) return;
|
||||||
|
|
||||||
var skill = _skillManager.GetSkillByName(skillName);
|
var skill = _skillManager.GetSkillByName(skillName);
|
||||||
if (skill == null)
|
if (skill == null) return;
|
||||||
{
|
|
||||||
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameManager.UnlockSkill(skill);
|
Store?.UnlockSkillPermanently(skill);
|
||||||
_skillManager.ActivateSkill(skill);
|
_skillManager.ActivateSkill(skill);
|
||||||
_skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill);
|
_skillUnlockerComponent.EmitSignal(SkillUnlockerComponent.SignalName.SkillUnlocked, skill);
|
||||||
LimboConsole.Info($"Skill '{skillName}' has been unlocked and activated.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool GetSkillManagement()
|
|
||||||
{
|
|
||||||
var player = _gameManager.Player;
|
|
||||||
if (player == null || !IsInstanceValid(player))
|
|
||||||
{
|
|
||||||
LimboConsole.Warn("Player node not found or is invalid.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_skillUnlockerComponent ??= player.GetNode<SkillUnlockerComponent>("SkillUnlockerComponent");
|
|
||||||
|
|
||||||
if (_skillManager != null && _skillUnlockerComponent != null) return true;
|
|
||||||
|
|
||||||
LimboConsole.Warn("SkillManager or SkillUnlockerComponent not found on the player.");
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConsoleCommand("unlock_all_skills", "Unlocks and activates all available skills.")]
|
|
||||||
private void UnlockAllSkillsCommand()
|
private void UnlockAllSkillsCommand()
|
||||||
{
|
{
|
||||||
if (!GetSkillManagement()) return;
|
if (!EnsureSkillManagement()) return;
|
||||||
|
|
||||||
_skillUnlockerComponent.UnlockAllSkills();
|
_skillUnlockerComponent.UnlockAllSkills();
|
||||||
LimboConsole.Info("All skills have been unlocked and activated.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("remove_skill", "Deactivates and removes a skill by its name.")]
|
|
||||||
private void RemoveSkillCommand(string skillName)
|
private void RemoveSkillCommand(string skillName)
|
||||||
{
|
{
|
||||||
if (!GetSkillManagement()) return;
|
if (!EnsureSkillManagement()) return;
|
||||||
|
|
||||||
var skill = _skillManager.GetSkillByName(skillName);
|
var skill = _skillManager.GetSkillByName(skillName);
|
||||||
if (skill == null)
|
if (skill == null) return;
|
||||||
{
|
|
||||||
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_gameManager.RemoveSkill(skill.Name);
|
Store?.RemoveUnlockedSkill(skill.Name);
|
||||||
_skillManager.DeactivateSkill(skill);
|
_skillManager.DeactivateSkill(skill);
|
||||||
LimboConsole.Info($"Skill '{skillName}' has been deactivated.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ConsoleCommand("remove_all_skills", "Deactivates and removes all skills.")]
|
|
||||||
private void RemoveAllSkillsCommand()
|
private void RemoveAllSkillsCommand()
|
||||||
{
|
{
|
||||||
if (!GetSkillManagement()) return;
|
if (!EnsureSkillManagement()) return;
|
||||||
|
|
||||||
foreach (var skill in _skillManager.AvailableSkills)
|
foreach (var skill in _skillManager.AvailableSkills)
|
||||||
{
|
{
|
||||||
_gameManager.RemoveSkill(skill.Name);
|
Store?.RemoveUnlockedSkill(skill.Name);
|
||||||
_skillManager.DeactivateSkill(skill);
|
_skillManager.DeactivateSkill(skill);
|
||||||
}
|
}
|
||||||
LimboConsole.Info("All skills have been deactivated.");
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConsoleCommand("next_level", "Advances the game to the next level.")]
|
|
||||||
private void GoToNextLevelCommand()
|
|
||||||
{
|
|
||||||
_gameManager.OnLevelComplete();
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConsoleCommand("unlock_achievement", "Unlocks an achievement by its ID.")]
|
|
||||||
private void UnlockAchievementCommand(string achievementId)
|
|
||||||
{
|
|
||||||
_achievementManager.UnlockAchievement(achievementId);
|
|
||||||
LimboConsole.Info($"Attempted to unlock achievement '{achievementId}'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
[ConsoleCommand("reset_achievement", "Resets (locks) an achievement by its ID.")]
|
|
||||||
private void ResetAchievementCommand(string achievementId)
|
|
||||||
{
|
|
||||||
_achievementManager.LockAchievement(achievementId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private void GoToNextLevelCommand()
|
||||||
|
{
|
||||||
|
GameManager?.OnLevelComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnlockAchievementCommand(string achievementId)
|
||||||
|
{
|
||||||
|
AchievementManager?.UnlockAchievement(achievementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetAchievementCommand(string achievementId)
|
||||||
|
{
|
||||||
|
AchievementManager?.LockAchievement(achievementId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureSkillManagement()
|
||||||
|
{
|
||||||
|
var player = GameManager?.Player;
|
||||||
|
if (player == null || !IsInstanceValid(player)) return false;
|
||||||
|
|
||||||
|
_skillUnlockerComponent ??= player.GetNode<SkillUnlockerComponent>("SkillUnlockerComponent");
|
||||||
|
return _skillManager != null && _skillUnlockerComponent != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,194 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.scripts.components;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Global event bus for decoupled communication between game systems.
|
||||||
|
/// Use the static Instance property for easy access from anywhere.
|
||||||
|
/// </summary>
|
||||||
public partial class EventBus : Node
|
public partial class EventBus : Node
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton instance. Available after the autoload is initialized.
|
||||||
|
/// </summary>
|
||||||
|
public static EventBus Instance { get; private set; }
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Level Events
|
||||||
|
|
||||||
[Signal] public delegate void LevelStartedEventHandler(int levelIndex, Node currentScene);
|
[Signal] public delegate void LevelStartedEventHandler(int levelIndex, Node currentScene);
|
||||||
[Signal] public delegate void LevelCompletedEventHandler(int levelIndex, Node currentScene, double completionTime);
|
[Signal] public delegate void LevelCompletedEventHandler(int levelIndex, Node currentScene, double completionTime);
|
||||||
|
[Signal] public delegate void LevelRestartedEventHandler(int levelIndex);
|
||||||
|
|
||||||
|
public static void EmitLevelStarted(int levelIndex, Node currentScene)
|
||||||
|
=> Instance?.EmitSignal(SignalName.LevelStarted, levelIndex, currentScene);
|
||||||
|
|
||||||
|
public static void EmitLevelCompleted(int levelIndex, Node currentScene, double completionTime)
|
||||||
|
=> Instance?.EmitSignal(SignalName.LevelCompleted, levelIndex, currentScene, completionTime);
|
||||||
|
|
||||||
|
public static void EmitLevelRestarted(int levelIndex)
|
||||||
|
=> Instance?.EmitSignal(SignalName.LevelRestarted, levelIndex);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Player Events
|
||||||
|
|
||||||
|
[Signal] public delegate void PlayerSpawnedEventHandler(PlayerController player);
|
||||||
|
[Signal] public delegate void PlayerDiedEventHandler(Vector2 position);
|
||||||
|
[Signal] public delegate void PlayerDamagedEventHandler(float damage, float remainingHealth, Vector2 position);
|
||||||
|
[Signal] public delegate void PlayerHealedEventHandler(float amount, float newHealth, Vector2 position);
|
||||||
|
|
||||||
|
public static void EmitPlayerSpawned(PlayerController player)
|
||||||
|
=> Instance?.EmitSignal(SignalName.PlayerSpawned, player);
|
||||||
|
|
||||||
|
public static void EmitPlayerDied(Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.PlayerDied, position);
|
||||||
|
|
||||||
|
public static void EmitPlayerDamaged(float damage, float remainingHealth, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.PlayerDamaged, damage, remainingHealth, position);
|
||||||
|
|
||||||
|
public static void EmitPlayerHealed(float amount, float newHealth, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.PlayerHealed, amount, newHealth, position);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Combat Events
|
||||||
|
|
||||||
|
[Signal] public delegate void EnemyDefeatedEventHandler(Node enemy, Vector2 position);
|
||||||
|
[Signal] public delegate void EnemyDamagedEventHandler(Node enemy, float damage, Vector2 position);
|
||||||
|
|
||||||
|
public static void EmitEnemyDefeated(Node enemy, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.EnemyDefeated, enemy, position);
|
||||||
|
|
||||||
|
public static void EmitEnemyDamaged(Node enemy, float damage, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.EnemyDamaged, enemy, damage, position);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Collection Events
|
||||||
|
|
||||||
|
[Signal] public delegate void CoinCollectedEventHandler(int amount, Vector2 position);
|
||||||
|
[Signal] public delegate void ItemCollectedEventHandler(CollectableType itemType, float amount, Vector2 position);
|
||||||
|
[Signal] public delegate void ChildRescuedEventHandler(Vector2 position);
|
||||||
|
[Signal] public delegate void SkillCollectedEventHandler(SkillData skill, Vector2 position);
|
||||||
|
|
||||||
|
public static void EmitCoinCollected(int amount, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.CoinCollected, amount, position);
|
||||||
|
|
||||||
|
public static void EmitItemCollected(CollectableType itemType, float amount, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.ItemCollected, (int)itemType, amount, position);
|
||||||
|
|
||||||
|
public static void EmitChildRescued(Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.ChildRescued, position);
|
||||||
|
|
||||||
|
public static void EmitSkillCollected(SkillData skill, Vector2 position)
|
||||||
|
=> Instance?.EmitSignal(SignalName.SkillCollected, skill, position);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Skill Events
|
||||||
|
|
||||||
|
[Signal] public delegate void SkillUnlockedEventHandler(string skillName, int level);
|
||||||
|
[Signal] public delegate void SkillActivatedEventHandler(string skillName);
|
||||||
|
[Signal] public delegate void SkillDeactivatedEventHandler(string skillName);
|
||||||
|
|
||||||
|
public static void EmitSkillUnlocked(string skillName, int level = 1)
|
||||||
|
=> Instance?.EmitSignal(SignalName.SkillUnlocked, skillName, level);
|
||||||
|
|
||||||
|
public static void EmitSkillActivated(string skillName)
|
||||||
|
=> Instance?.EmitSignal(SignalName.SkillActivated, skillName);
|
||||||
|
|
||||||
|
public static void EmitSkillDeactivated(string skillName)
|
||||||
|
=> Instance?.EmitSignal(SignalName.SkillDeactivated, skillName);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Game State Events
|
||||||
|
|
||||||
|
[Signal] public delegate void GamePausedEventHandler();
|
||||||
|
[Signal] public delegate void GameResumedEventHandler();
|
||||||
|
[Signal] public delegate void GameSavedEventHandler();
|
||||||
|
[Signal] public delegate void GameStartedEventHandler();
|
||||||
|
[Signal] public delegate void GameContinuedEventHandler();
|
||||||
|
|
||||||
|
public static void EmitGamePaused()
|
||||||
|
=> Instance?.EmitSignal(SignalName.GamePaused);
|
||||||
|
|
||||||
|
public static void EmitGameResumed()
|
||||||
|
=> Instance?.EmitSignal(SignalName.GameResumed);
|
||||||
|
|
||||||
|
public static void EmitGameSaved()
|
||||||
|
=> Instance?.EmitSignal(SignalName.GameSaved);
|
||||||
|
|
||||||
|
public static void EmitGameStarted()
|
||||||
|
=> Instance?.EmitSignal(SignalName.GameStarted);
|
||||||
|
|
||||||
|
public static void EmitGameContinued()
|
||||||
|
=> Instance?.EmitSignal(SignalName.GameContinued);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Achievement Events
|
||||||
|
|
||||||
|
[Signal] public delegate void AchievementUnlockedEventHandler(string achievementId);
|
||||||
|
|
||||||
|
public static void EmitAchievementUnlocked(string achievementId)
|
||||||
|
=> Instance?.EmitSignal(SignalName.AchievementUnlocked, achievementId);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region State Change Events
|
||||||
|
|
||||||
|
[Signal] public delegate void CoinsChangedEventHandler(int totalCoins);
|
||||||
|
[Signal] public delegate void LivesChangedEventHandler(int lives);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Playthrough has no lives left. Raised as part of the same operation that spends the last
|
||||||
|
/// life, so a subscriber can never observe lives that have not been decremented yet — which is
|
||||||
|
/// the race that lets the death screen sit there forever on the last life.
|
||||||
|
/// </summary>
|
||||||
|
[Signal] public delegate void RunEndedEventHandler();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A death has been settled. The life is already spent and the attempt already discarded, so
|
||||||
|
/// <paramref name="remainingLives"/> is safe to display directly — unlike reading Lives from a
|
||||||
|
/// handler racing the one that writes it.
|
||||||
|
/// </summary>
|
||||||
|
[Signal] public delegate void AttemptFailedEventHandler(int remainingLives, bool runOver);
|
||||||
|
|
||||||
|
public static void EmitAttemptFailed(int remainingLives, bool runOver)
|
||||||
|
=> Instance?.EmitSignal(SignalName.AttemptFailed, remainingLives, runOver);
|
||||||
|
|
||||||
|
public static void EmitRunEnded()
|
||||||
|
=> Instance?.EmitSignal(SignalName.RunEnded);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The set of owned skills, or the level of one of them, changed. SkillManager reconciles its live
|
||||||
|
/// components against this instead of every caller remembering to update it separately — the
|
||||||
|
/// non-atomic pair of calls that could leave a skill owned but inert.
|
||||||
|
/// </summary>
|
||||||
|
[Signal] public delegate void SkillSetChangedEventHandler();
|
||||||
|
|
||||||
|
public static void EmitSkillSetChanged()
|
||||||
|
=> Instance?.EmitSignal(SignalName.SkillSetChanged);
|
||||||
|
|
||||||
|
public static void EmitCoinsChanged(int totalCoins)
|
||||||
|
=> Instance?.EmitSignal(SignalName.CoinsChanged, totalCoins);
|
||||||
|
|
||||||
|
public static void EmitLivesChanged(int lives)
|
||||||
|
=> Instance?.EmitSignal(SignalName.LivesChanged, lives);
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using Godot;
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
using Mr.BrickAdventures.scripts.UI;
|
using Mr.BrickAdventures.scripts.UI;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
@@ -7,22 +9,34 @@ namespace Mr.BrickAdventures.Autoloads;
|
|||||||
public partial class FloatingTextManager : Node
|
public partial class FloatingTextManager : Node
|
||||||
{
|
{
|
||||||
[Export] public PackedScene FloatingTextScene { get; set; }
|
[Export] public PackedScene FloatingTextScene { get; set; }
|
||||||
|
|
||||||
[ExportGroup("Colors")]
|
[ExportGroup("Colors")]
|
||||||
[Export] public Color DamageColor { get; set; } = new Color("#b21030"); // Red
|
[Export] public Color DamageColor { get; set; } = new Color("#b21030"); // Red
|
||||||
[Export] public Color HealColor { get; set; } = new Color("#71f341"); // Green
|
[Export] public Color HealColor { get; set; } = new Color("#71f341"); // Green
|
||||||
[Export] public Color CoinColor { get; set; } = new Color("#ebd320"); // Gold
|
[Export] public Color CoinColor { get; set; } = new Color("#ebd320"); // Gold
|
||||||
[Export] public Color MessageColor { get; set; } = new Color("#ffffff"); // White
|
[Export] public Color MessageColor { get; set; } = new Color("#ffffff"); // White
|
||||||
|
|
||||||
|
public static FloatingTextManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void ShowDamage(float amount, Vector2 position)
|
public void ShowDamage(float amount, Vector2 position)
|
||||||
{
|
{
|
||||||
var text = Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture);
|
var text = Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture);
|
||||||
CreateFloatingText(text, position, DamageColor);
|
CreateFloatingText(text, position, DamageColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowHeal(float amount, Vector2 position)
|
public void ShowHeal(float amount, Vector2 position)
|
||||||
{
|
{
|
||||||
var text = $"+{Mathf.Round(amount)}";
|
var text = $"+{Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture)}";
|
||||||
CreateFloatingText(text, position, HealColor);
|
CreateFloatingText(text, position, HealColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,12 +45,12 @@ public partial class FloatingTextManager : Node
|
|||||||
var text = $"+{amount}";
|
var text = $"+{amount}";
|
||||||
CreateFloatingText(text, position, CoinColor);
|
CreateFloatingText(text, position, CoinColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ShowMessage(string message, Vector2 position)
|
public void ShowMessage(string message, Vector2 position)
|
||||||
{
|
{
|
||||||
CreateFloatingText(message, position, MessageColor);
|
CreateFloatingText(message, position, MessageColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateFloatingText(string text, Vector2 position, Color color)
|
private void CreateFloatingText(string text, Vector2 position, Color color)
|
||||||
{
|
{
|
||||||
if (FloatingTextScene == null)
|
if (FloatingTextScene == null)
|
||||||
|
|||||||
@@ -1,270 +1,191 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.scripts.components;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
|
||||||
using Double = System.Double;
|
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Game orchestrator: scene transitions and game flow.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Which Level the player is on now belongs to <see cref="ProgressionRoot"/>, addressed by a stable
|
||||||
|
/// <see cref="LevelId"/> and resolved to a scene by the level catalogue. The old
|
||||||
|
/// <c>Array<PackedScene> LevelScenes</c> is gone: it made a save's meaning depend on the order
|
||||||
|
/// of an inspector array, so reordering levels silently invalidated every save.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Progression is persisted by <see cref="ProgressionRoot"/>'s stores, which save themselves at the
|
||||||
|
/// points that matter — a First Clear and a death — so there is no separate "save the game" step to
|
||||||
|
/// forget.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public partial class GameManager : Node
|
public partial class GameManager : Node
|
||||||
{
|
{
|
||||||
[Export] public Array<PackedScene> LevelScenes { get; set; } = [];
|
public PlayerController Player
|
||||||
|
{
|
||||||
public PlayerController Player {
|
|
||||||
get => GetPlayer();
|
get => GetPlayer();
|
||||||
private set => _player = value;
|
private set => _player = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Node> _sceneNodes = [];
|
|
||||||
private PlayerController _player;
|
|
||||||
private SpeedRunManager _speedRunManager;
|
|
||||||
private EventBus _eventBus;
|
|
||||||
|
|
||||||
[Export]
|
|
||||||
public Dictionary PlayerState { get; set; } = new()
|
|
||||||
{
|
|
||||||
{ "coins", 0 },
|
|
||||||
{ "lives", 3 },
|
|
||||||
{ "current_level", 0 },
|
|
||||||
{ "completed_levels", new Array<int>() },
|
|
||||||
{ "unlocked_levels", new Array<int>() {0}},
|
|
||||||
{ "unlocked_skills", new Array<SkillData>() }
|
|
||||||
};
|
|
||||||
|
|
||||||
[Export]
|
|
||||||
public Dictionary CurrentSessionState { get; private set; } = new()
|
|
||||||
{
|
|
||||||
{ "coins_collected", 0 },
|
|
||||||
{ "skills_unlocked", new Array<SkillData>() }
|
|
||||||
};
|
|
||||||
|
|
||||||
public override void _EnterTree()
|
private PlayerController _player;
|
||||||
|
private SpeedRunManager SpeedRunManager => SpeedRunManager.Instance;
|
||||||
|
private ProgressionRoot Progression => ProgressionRoot.Instance;
|
||||||
|
|
||||||
|
public static GameManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
GetTree().NodeAdded += OnNodeAdded;
|
Instance = this;
|
||||||
GetTree().NodeRemoved += OnNodeRemoved;
|
EventBus.Instance.PlayerSpawned += OnPlayerSpawned;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _ExitTree()
|
public override void _ExitTree()
|
||||||
{
|
{
|
||||||
GetTree().NodeAdded -= OnNodeAdded;
|
if (Instance == this) Instance = null;
|
||||||
GetTree().NodeRemoved -= OnNodeRemoved;
|
if (EventBus.Instance != null)
|
||||||
_sceneNodes.Clear();
|
EventBus.Instance.PlayerSpawned -= OnPlayerSpawned;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void _Ready()
|
private void OnPlayerSpawned(PlayerController player)
|
||||||
{
|
{
|
||||||
_speedRunManager = GetNode<SpeedRunManager>("/root/SpeedRunManager");
|
_player = player;
|
||||||
_eventBus = GetNode<EventBus>("/root/EventBus");
|
player.TreeExiting += () => { if (_player == player) _player = null; };
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNodeAdded(Node node)
|
#region Game Flow
|
||||||
{
|
|
||||||
_sceneNodes.Add(node);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnNodeRemoved(Node node)
|
|
||||||
{
|
|
||||||
_sceneNodes.Remove(node);
|
|
||||||
if (node == _player)
|
|
||||||
{
|
|
||||||
_player = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddCoins(int amount)
|
|
||||||
{
|
|
||||||
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] + amount);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount);
|
|
||||||
|
|
||||||
public int GetCoins() => (int)PlayerState["coins"] + (int)CurrentSessionState["coins_collected"];
|
|
||||||
|
|
||||||
public void RemoveCoins(int amount)
|
|
||||||
{
|
|
||||||
var sessionCoins = (int)CurrentSessionState["coins_collected"];
|
|
||||||
if (amount <= sessionCoins)
|
|
||||||
{
|
|
||||||
CurrentSessionState["coins_collected"] = sessionCoins - amount;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var remaining = amount - sessionCoins;
|
|
||||||
CurrentSessionState["coins_collected"] = 0;
|
|
||||||
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] - remaining);
|
|
||||||
}
|
|
||||||
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] + amount;
|
|
||||||
public void RemoveLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] - amount;
|
|
||||||
public void SetLives(int amount) => PlayerState["lives"] = amount;
|
|
||||||
public int GetLives() => (int)PlayerState["lives"];
|
|
||||||
|
|
||||||
public bool IsSkillUnlocked(SkillData skill)
|
|
||||||
{
|
|
||||||
return ((Array)PlayerState["unlocked_skills"]).Contains(skill)
|
|
||||||
|| ((Array)CurrentSessionState["skills_unlocked"]).Contains(skill);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnlockSkill(SkillData skill)
|
|
||||||
{
|
|
||||||
if (!IsSkillUnlocked(skill))
|
|
||||||
((Array)PlayerState["unlocked_skills"]).Add(skill);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RemoveSkill(string skillName)
|
|
||||||
{
|
|
||||||
var arr = (Array)PlayerState["unlocked_skills"];
|
|
||||||
foreach (SkillData s in arr)
|
|
||||||
{
|
|
||||||
if (s.Name != skillName) continue;
|
|
||||||
|
|
||||||
arr.Remove(s);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnlockSkills(Array<SkillData> skills)
|
|
||||||
{
|
|
||||||
foreach (var s in skills)
|
|
||||||
UnlockSkill(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetPlayerState()
|
|
||||||
{
|
|
||||||
PlayerState = new Dictionary
|
|
||||||
{
|
|
||||||
{ "coins", 0 },
|
|
||||||
{ "lives", 3 },
|
|
||||||
{ "current_level", 0 },
|
|
||||||
{ "completed_levels", new Array<int>() },
|
|
||||||
{ "unlocked_levels", new Array<int>() {0}},
|
|
||||||
{ "unlocked_skills", new Array<SkillData>() },
|
|
||||||
{ "statistics", new Godot.Collections.Dictionary<string, Variant>()}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void UnlockLevel(int levelIndex)
|
|
||||||
{
|
|
||||||
var unlocked = (Array)PlayerState["unlocked_levels"];
|
|
||||||
if (!unlocked.Contains(levelIndex)) unlocked.Add(levelIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void TryToGoToNextLevel()
|
|
||||||
{
|
|
||||||
var next = (int)PlayerState["current_level"] + 1;
|
|
||||||
var unlocked = (Array)PlayerState["unlocked_levels"];
|
|
||||||
if (next < LevelScenes.Count && unlocked.Contains(next))
|
|
||||||
{
|
|
||||||
PlayerState["current_level"] = next;
|
|
||||||
GetTree().ChangeSceneToPacked(LevelScenes[next]);
|
|
||||||
_eventBus.EmitSignal(EventBus.SignalName.LevelStarted, next, GetTree().CurrentScene);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void MarkLevelComplete(int levelIndex)
|
|
||||||
{
|
|
||||||
UnlockLevel(levelIndex + 1);
|
|
||||||
var completed = (Array)PlayerState["completed_levels"];
|
|
||||||
if (!completed.Contains(levelIndex)) completed.Add(levelIndex);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ResetCurrentSessionState()
|
|
||||||
{
|
|
||||||
CurrentSessionState = new Dictionary
|
|
||||||
{
|
|
||||||
{ "coins_collected", 0 },
|
|
||||||
{ "skills_unlocked", new Array<SkillData>() }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RestartGame()
|
|
||||||
{
|
|
||||||
ResetPlayerState();
|
|
||||||
ResetCurrentSessionState();
|
|
||||||
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
|
||||||
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void QuitGame() => GetTree().Quit();
|
|
||||||
|
|
||||||
public void PauseGame() => Engine.TimeScale = 0;
|
|
||||||
public void ResumeGame() => Engine.TimeScale = 1;
|
|
||||||
|
|
||||||
public void StartNewGame()
|
public void StartNewGame()
|
||||||
{
|
{
|
||||||
ResetPlayerState();
|
if (Progression == null) return;
|
||||||
ResetCurrentSessionState();
|
|
||||||
|
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||||
_speedRunManager?.StartTimer();
|
SpeedRunManager?.StartTimer();
|
||||||
|
EventBus.EmitGameStarted();
|
||||||
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
|
||||||
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
GoTo(Progression.Campaign.First);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ContinueGame()
|
public void ContinueGame()
|
||||||
{
|
{
|
||||||
var save = GetNode<SaveSystem>("/root/SaveSystem");
|
if (Progression == null) return;
|
||||||
if (!save.LoadGame())
|
|
||||||
|
if (!Progression.LoadPlaythrough(ProgressionRoot.SoleSlot))
|
||||||
{
|
{
|
||||||
GD.PrintErr("Failed to load game. Starting a new game instead.");
|
var why = Progression.PeekPlaythrough(ProgressionRoot.SoleSlot);
|
||||||
|
if (why == LoadOutcome.Corrupt)
|
||||||
|
{
|
||||||
|
// Deliberately not overwritten: the file is left where it is so it can be recovered.
|
||||||
|
GD.PushError("Saved progress could not be read. It has been left untouched.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
StartNewGame();
|
StartNewGame();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var idx = (int)PlayerState["current_level"];
|
SpeedRunManager?.StartTimer();
|
||||||
if (idx < LevelScenes.Count)
|
EventBus.EmitGameContinued();
|
||||||
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
|
|
||||||
else
|
GoTo(Progression.CurrentLevel ?? Progression.Campaign.First);
|
||||||
GD.PrintErr("No levels unlocked to continue.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void RestartGame()
|
||||||
|
{
|
||||||
|
if (Progression == null) return;
|
||||||
|
|
||||||
|
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||||
|
SpeedRunManager?.StartTimer();
|
||||||
|
|
||||||
|
GoTo(Progression.Campaign.First);
|
||||||
|
}
|
||||||
|
|
||||||
public void OnLevelComplete()
|
public void OnLevelComplete()
|
||||||
{
|
{
|
||||||
var levelIndex = (int)PlayerState["current_level"];
|
if (Progression?.CurrentLevel is not { } finished) return;
|
||||||
MarkLevelComplete(levelIndex);
|
|
||||||
|
|
||||||
AddCoins((int)CurrentSessionState["coins_collected"]);
|
|
||||||
foreach (var s in (Array)CurrentSessionState["skills_unlocked"])
|
|
||||||
UnlockSkill((SkillData)s);
|
|
||||||
|
|
||||||
var completionTime = _speedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||||
_eventBus.EmitSignal(EventBus.SignalName.LevelCompleted, levelIndex, GetTree().CurrentScene, completionTime);
|
LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null;
|
||||||
|
|
||||||
ResetCurrentSessionState();
|
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
|
||||||
TryToGoToNextLevel();
|
|
||||||
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
// Banks the attempt's coins and skills, marks the level cleared, and advances campaign
|
||||||
|
// position if this was a First Clear.
|
||||||
|
Progression.FinishLevel(time);
|
||||||
|
|
||||||
|
if (Progression.CurrentLevel is { } next && !next.Equals(finished))
|
||||||
|
GoTo(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Array<SkillData> GetUnlockedSkills()
|
/// <summary>
|
||||||
|
/// Loads a Level's scene, then announces the entry once the new scene is actually current.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The announcement is deliberately after a frame. <c>ChangeSceneToPacked</c> is deferred, so at
|
||||||
|
/// the moment it returns <c>CurrentScene</c> is still the outgoing scene — which is why the old
|
||||||
|
/// code handed <c>GhostEventHandler</c> the scene being torn down and ghosts were spawned into it.
|
||||||
|
/// </remarks>
|
||||||
|
private async void GoTo(LevelId level)
|
||||||
{
|
{
|
||||||
var unlocked = (Array<SkillData>)PlayerState["unlocked_skills"];
|
if (!Progression.CanEnter(level))
|
||||||
var session = (Array<SkillData>)CurrentSessionState["skills_unlocked"];
|
{
|
||||||
if (session!.Count == 0) return unlocked;
|
GD.PushError($"Level '{level}' is not reachable in this playthrough.");
|
||||||
if (unlocked!.Count == 0) return session;
|
return;
|
||||||
var joined = new Array<SkillData>();
|
}
|
||||||
joined.AddRange(unlocked);
|
|
||||||
joined.AddRange(session);
|
var scene = Progression.Catalogue.SceneFor(level);
|
||||||
return joined;
|
if (scene == null)
|
||||||
|
{
|
||||||
|
GD.PushError($"Level '{level}' has no loadable scene.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
GetTree().ChangeSceneToPacked(scene);
|
||||||
|
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
|
||||||
|
|
||||||
|
Progression.EnterLevel(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position in the catalogue, for the signals that are still index-based because GhostManager
|
||||||
|
/// names its files <c>ghost_level_{index}.json</c>.
|
||||||
|
/// </summary>
|
||||||
|
private int IndexOf(LevelId level)
|
||||||
|
{
|
||||||
|
var levels = Progression?.Catalogue?.Levels;
|
||||||
|
if (levels == null) return -1;
|
||||||
|
|
||||||
|
for (var i = 0; i < levels.Count; i++)
|
||||||
|
if (levels[i]?.Id == level.Value)
|
||||||
|
return i;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void PauseGame()
|
||||||
|
{
|
||||||
|
Engine.TimeScale = 0;
|
||||||
|
EventBus.EmitGamePaused();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResumeGame()
|
||||||
|
{
|
||||||
|
Engine.TimeScale = 1;
|
||||||
|
EventBus.EmitGameResumed();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void QuitGame() => GetTree().Quit();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Player Lookup
|
||||||
|
|
||||||
public PlayerController GetPlayer()
|
public PlayerController GetPlayer()
|
||||||
{
|
{
|
||||||
if (_player != null && IsInstanceValid(_player)) return _player;
|
if (_player != null && IsInstanceValid(_player)) return _player;
|
||||||
|
|
||||||
_player = null;
|
_player = null;
|
||||||
|
|
||||||
foreach (var node in _sceneNodes)
|
|
||||||
{
|
|
||||||
if (node is not PlayerController player) continue;
|
|
||||||
|
|
||||||
_player = player;
|
|
||||||
return _player;
|
|
||||||
}
|
|
||||||
|
|
||||||
GD.PrintErr("PlayerController not found in the scene tree.");
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
|||||||
243
Autoloads/GameStateStore.cs
Normal file
243
Autoloads/GameStateStore.cs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
using Godot;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
using Mr.BrickAdventures.scripts.State;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Central store for game state - single source of truth.
|
||||||
|
/// Use the static Instance property for easy access.
|
||||||
|
/// </summary>
|
||||||
|
public partial class GameStateStore : Node
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Singleton instance.
|
||||||
|
/// </summary>
|
||||||
|
public static GameStateStore Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Persistent player state (saved to disk).
|
||||||
|
/// </summary>
|
||||||
|
public PlayerState Player { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current session state (transient, reset on death/level complete).
|
||||||
|
/// </summary>
|
||||||
|
public SessionState Session { get; set; } = new();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this)
|
||||||
|
Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Coin Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets total coins (saved + session).
|
||||||
|
/// </summary>
|
||||||
|
public int GetTotalCoins() => Player.Coins + Session.CoinsCollected;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds coins to the session (not saved until level complete).
|
||||||
|
/// </summary>
|
||||||
|
public void AddSessionCoins(int amount)
|
||||||
|
{
|
||||||
|
Session.CoinsCollected += amount;
|
||||||
|
EventBus.EmitCoinsChanged(GetTotalCoins());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commits session coins to player state.
|
||||||
|
/// </summary>
|
||||||
|
public void CommitSessionCoins()
|
||||||
|
{
|
||||||
|
Player.Coins += Session.CoinsCollected;
|
||||||
|
Session.CoinsCollected = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes coins, first from session then from saved.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveCoins(int amount)
|
||||||
|
{
|
||||||
|
if (amount <= Session.CoinsCollected)
|
||||||
|
{
|
||||||
|
Session.CoinsCollected -= amount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var remaining = amount - Session.CoinsCollected;
|
||||||
|
Session.CoinsCollected = 0;
|
||||||
|
Player.Coins = Mathf.Max(0, Player.Coins - remaining);
|
||||||
|
}
|
||||||
|
EventBus.EmitCoinsChanged(GetTotalCoins());
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Lives Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Decrements lives by 1.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveLife()
|
||||||
|
{
|
||||||
|
Player.Lives = Mathf.Max(0, Player.Lives - 1);
|
||||||
|
EventBus.EmitLivesChanged(Player.Lives);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds lives.
|
||||||
|
/// </summary>
|
||||||
|
public void AddLives(int amount)
|
||||||
|
{
|
||||||
|
Player.Lives += amount;
|
||||||
|
EventBus.EmitLivesChanged(Player.Lives);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Level Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlocks a level for access.
|
||||||
|
/// </summary>
|
||||||
|
public void UnlockLevel(int levelIndex)
|
||||||
|
{
|
||||||
|
if (!Player.UnlockedLevels.Contains(levelIndex))
|
||||||
|
Player.UnlockedLevels.Add(levelIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks a level as completed and unlocks the next.
|
||||||
|
/// </summary>
|
||||||
|
public void MarkLevelComplete(int levelIndex)
|
||||||
|
{
|
||||||
|
if (!Player.CompletedLevels.Contains(levelIndex))
|
||||||
|
Player.CompletedLevels.Add(levelIndex);
|
||||||
|
UnlockLevel(levelIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a level is unlocked.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLevelUnlocked(int levelIndex) => Player.UnlockedLevels.Contains(levelIndex);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Skill Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks if a skill is unlocked (saved or session).
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSkillUnlocked(SkillData skill)
|
||||||
|
{
|
||||||
|
return Player.UnlockedSkills.Contains(skill) || Session.SkillsUnlocked.Contains(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlocks a skill in the session (lost on death, committed on level complete).
|
||||||
|
/// </summary>
|
||||||
|
public void UnlockSkillInSession(SkillData skill)
|
||||||
|
{
|
||||||
|
if (!IsSkillUnlocked(skill))
|
||||||
|
Session.SkillsUnlocked.Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permanently unlocks a skill directly in player state (bypasses session, e.g. console commands).
|
||||||
|
/// </summary>
|
||||||
|
public void UnlockSkillPermanently(SkillData skill)
|
||||||
|
{
|
||||||
|
if (!Player.UnlockedSkills.Contains(skill))
|
||||||
|
Player.UnlockedSkills.Add(skill);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a permanently unlocked skill from player state by name.
|
||||||
|
/// </summary>
|
||||||
|
public void RemoveUnlockedSkill(string skillName)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Player.UnlockedSkills.Count; i++)
|
||||||
|
{
|
||||||
|
if (Player.UnlockedSkills[i].Name == skillName)
|
||||||
|
{
|
||||||
|
Player.UnlockedSkills.RemoveAt(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Commits session skills to player state.
|
||||||
|
/// </summary>
|
||||||
|
public void CommitSessionSkills()
|
||||||
|
{
|
||||||
|
foreach (var skill in Session.SkillsUnlocked)
|
||||||
|
{
|
||||||
|
if (!Player.UnlockedSkills.Contains(skill))
|
||||||
|
Player.UnlockedSkills.Add(skill);
|
||||||
|
}
|
||||||
|
Session.SkillsUnlocked.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all unlocked skills from player persistence and current session.
|
||||||
|
/// </summary>
|
||||||
|
public List<SkillData> GetAllUnlockedSkills()
|
||||||
|
{
|
||||||
|
var result = new List<SkillData>(Player.UnlockedSkills);
|
||||||
|
foreach (var skill in Session.SkillsUnlocked)
|
||||||
|
{
|
||||||
|
if (!result.Contains(skill)) result.Add(skill);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Statistics Operations
|
||||||
|
|
||||||
|
public void IncrementStat(string name, int amount = 1)
|
||||||
|
{
|
||||||
|
if (Player.Statistics.TryGetValue(name, out var current))
|
||||||
|
Player.Statistics[name] = current + amount;
|
||||||
|
else
|
||||||
|
Player.Statistics[name] = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStat(string name, int value) => Player.Statistics[name] = value;
|
||||||
|
|
||||||
|
public int GetStat(string name) =>
|
||||||
|
Player.Statistics.TryGetValue(name, out var value) ? value : 0;
|
||||||
|
|
||||||
|
public System.Collections.Generic.Dictionary<string, int> GetAllStats() =>
|
||||||
|
new(Player.Statistics);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Reset Operations
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets only the session state.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetSession() => Session.Reset();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets everything to defaults.
|
||||||
|
/// </summary>
|
||||||
|
public void ResetAll()
|
||||||
|
{
|
||||||
|
Player.Reset();
|
||||||
|
Session.ResetAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
1
Autoloads/GameStateStore.cs.uid
Normal file
1
Autoloads/GameStateStore.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://bwrhkipwecytk
|
||||||
@@ -1,14 +1,20 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
|
||||||
using Mr.BrickAdventures.scripts;
|
using Mr.BrickAdventures.scripts;
|
||||||
|
using Mr.BrickAdventures.scripts.State;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
public partial class GhostManager : Node
|
public partial class GhostManager : Node
|
||||||
{
|
{
|
||||||
|
public static GhostManager Instance { get; private set; }
|
||||||
|
|
||||||
[Export] private PackedScene GhostPlayerScene { get; set; }
|
[Export] private PackedScene GhostPlayerScene { get; set; }
|
||||||
|
|
||||||
|
public override void _Ready() => Instance = this;
|
||||||
|
public override void _ExitTree() { if (Instance == this) Instance = null; }
|
||||||
|
|
||||||
public bool IsRecording { get; private set; } = false;
|
public bool IsRecording { get; private set; } = false;
|
||||||
public bool IsPlaybackEnabled { get; private set; } = true;
|
public bool IsPlaybackEnabled { get; private set; } = true;
|
||||||
|
|
||||||
@@ -19,14 +25,14 @@ public partial class GhostManager : Node
|
|||||||
public void StartRecording(int levelIndex)
|
public void StartRecording(int levelIndex)
|
||||||
{
|
{
|
||||||
if (!IsPlaybackEnabled) return;
|
if (!IsPlaybackEnabled) return;
|
||||||
|
|
||||||
_currentLevelIndex = levelIndex;
|
_currentLevelIndex = levelIndex;
|
||||||
_currentRecording.Clear();
|
_currentRecording.Clear();
|
||||||
_startTime = Time.GetTicksMsec() / 1000.0;
|
_startTime = Time.GetTicksMsec() / 1000.0;
|
||||||
IsRecording = true;
|
IsRecording = true;
|
||||||
GD.Print("Ghost recording started.");
|
GD.Print("Ghost recording started.");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StopRecording(bool levelCompleted, double finalTime)
|
public void StopRecording(bool levelCompleted, double finalTime)
|
||||||
{
|
{
|
||||||
if (!IsRecording) return;
|
if (!IsRecording) return;
|
||||||
@@ -43,23 +49,22 @@ public partial class GhostManager : Node
|
|||||||
}
|
}
|
||||||
_currentRecording.Clear();
|
_currentRecording.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecordFrame(Vector2 position)
|
public void RecordFrame(Vector2 position)
|
||||||
{
|
{
|
||||||
if (!IsRecording) return;
|
if (!IsRecording) return;
|
||||||
|
|
||||||
var frame = new GhostFrame
|
_currentRecording.Add(new GhostFrame
|
||||||
{
|
{
|
||||||
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
|
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
|
||||||
Position = position
|
Position = position
|
||||||
};
|
});
|
||||||
_currentRecording.Add(frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SpawnGhostPlayer(int levelIndex, Node parent)
|
public void SpawnGhostPlayer(int levelIndex, Node parent)
|
||||||
{
|
{
|
||||||
if (!IsPlaybackEnabled || GhostPlayerScene == null) return;
|
if (!IsPlaybackEnabled || GhostPlayerScene == null) return;
|
||||||
|
|
||||||
var ghostData = LoadGhostData(levelIndex);
|
var ghostData = LoadGhostData(levelIndex);
|
||||||
if (ghostData.Count > 0)
|
if (ghostData.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -69,44 +74,63 @@ public partial class GhostManager : Node
|
|||||||
GD.Print($"Ghost player spawned for level {levelIndex}.");
|
GD.Print($"Ghost player spawned for level {levelIndex}.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SaveGhostData(int levelIndex, double time)
|
private void SaveGhostData(int levelIndex, double time)
|
||||||
{
|
{
|
||||||
var path = $"user://ghost_level_{levelIndex}.dat";
|
var path = $"user://ghost_level_{levelIndex}.json";
|
||||||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
|
var saveData = new GhostSaveData { BestTime = time };
|
||||||
|
foreach (var frame in _currentRecording)
|
||||||
var dataToSave = new Godot.Collections.Dictionary
|
saveData.Frames.Add(new GhostFrameDto { Timestamp = frame.Timestamp, X = frame.Position.X, Y = frame.Position.Y });
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
{ "time", time },
|
var json = JsonSerializer.Serialize(saveData, SaveSystem.JsonOptions);
|
||||||
{ "frames", _currentRecording.ToArray() }
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
|
||||||
};
|
file.StoreString(json);
|
||||||
file.StoreVar(dataToSave);
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"GhostManager: Failed to save ghost data: {e.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<GhostFrame> LoadGhostData(int levelIndex)
|
private List<GhostFrame> LoadGhostData(int levelIndex)
|
||||||
{
|
{
|
||||||
var path = $"user://ghost_level_{levelIndex}.dat";
|
var path = $"user://ghost_level_{levelIndex}.json";
|
||||||
if (!FileAccess.FileExists(path)) return [];
|
if (!FileAccess.FileExists(path)) return [];
|
||||||
|
|
||||||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
try
|
||||||
var savedData = (Dictionary)file.GetVar();
|
|
||||||
var framesArray = (Array)savedData["frames"];
|
|
||||||
|
|
||||||
var frames = new List<GhostFrame>();
|
|
||||||
foreach (var obj in framesArray)
|
|
||||||
{
|
{
|
||||||
frames.Add((GhostFrame)obj);
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||||||
|
var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
|
||||||
|
if (saveData == null) return [];
|
||||||
|
|
||||||
|
var frames = new List<GhostFrame>();
|
||||||
|
foreach (var dto in saveData.Frames)
|
||||||
|
frames.Add(new GhostFrame { Timestamp = dto.Timestamp, Position = new Vector2(dto.X, dto.Y) });
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"GhostManager: Failed to load ghost data: {e.Message}");
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
return frames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private double LoadBestTime(int levelIndex)
|
private double LoadBestTime(int levelIndex)
|
||||||
{
|
{
|
||||||
var path = $"user://ghost_level_{levelIndex}.dat";
|
var path = $"user://ghost_level_{levelIndex}.json";
|
||||||
if (!FileAccess.FileExists(path)) return double.MaxValue;
|
if (!FileAccess.FileExists(path)) return double.MaxValue;
|
||||||
|
|
||||||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
try
|
||||||
var data = (Dictionary)file.GetVar();
|
{
|
||||||
return (double)data["time"];
|
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Read);
|
||||||
|
var saveData = JsonSerializer.Deserialize<GhostSaveData>(file.GetAsText(), SaveSystem.JsonOptions);
|
||||||
|
return saveData?.BestTime ?? double.MaxValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return double.MaxValue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
54
Autoloads/InputDeviceManager.cs
Normal file
54
Autoloads/InputDeviceManager.cs
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class InputDeviceManager : Node
|
||||||
|
{
|
||||||
|
public enum InputDevice { Mouse, Keyboard, Gamepad }
|
||||||
|
public static InputDeviceManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public InputDevice CurrentDevice { get; private set; } = InputDevice.Mouse;
|
||||||
|
|
||||||
|
public bool IsPointerless => CurrentDevice is InputDevice.Keyboard or InputDevice.Gamepad;
|
||||||
|
|
||||||
|
[Signal] public delegate void DeviceChangedEventHandler(int device);
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
Input.MouseMode = Input.MouseModeEnum.Visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _Input(InputEvent @event)
|
||||||
|
{
|
||||||
|
InputDevice detected;
|
||||||
|
|
||||||
|
if (@event is InputEventMouseMotion or InputEventMouseButton)
|
||||||
|
detected = InputDevice.Mouse;
|
||||||
|
else if (@event is InputEventKey)
|
||||||
|
detected = InputDevice.Keyboard;
|
||||||
|
else if (@event is InputEventJoypadButton)
|
||||||
|
detected = InputDevice.Gamepad;
|
||||||
|
else if (@event is InputEventJoypadMotion joyEvent)
|
||||||
|
{
|
||||||
|
if (Mathf.Abs(joyEvent.AxisValue) <= 0.15f) return;
|
||||||
|
detected = InputDevice.Gamepad;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (detected == CurrentDevice) return;
|
||||||
|
|
||||||
|
CurrentDevice = detected;
|
||||||
|
Input.MouseMode = CurrentDevice == InputDevice.Mouse
|
||||||
|
? Input.MouseModeEnum.Visible
|
||||||
|
: Input.MouseModeEnum.Hidden;
|
||||||
|
|
||||||
|
EmitSignal(SignalName.DeviceChanged, (int)CurrentDevice);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Autoloads/InputDeviceManager.cs.uid
Normal file
1
Autoloads/InputDeviceManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b81610g8fd6p1
|
||||||
88
Autoloads/ProgressionEventRepublisher.cs
Normal file
88
Autoloads/ProgressionEventRepublisher.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Translates domain events into Godot signals on the <see cref="EventBus"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The adapter half of ADR-0004. Aggregates raise plain records with no engine dependency; this is the
|
||||||
|
/// single place that turns them into signals, which is what makes "state changed ⇒ signal fired"
|
||||||
|
/// un-bypassable. Existing subscribers and scene wiring keep working unchanged.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Not a Node — it has no lifecycle of its own. The composition root owns one and hands it every
|
||||||
|
/// batch of drained events.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public sealed class ProgressionEventRepublisher
|
||||||
|
{
|
||||||
|
private readonly LevelCatalogue _catalogue;
|
||||||
|
private readonly SceneTree _tree;
|
||||||
|
|
||||||
|
public ProgressionEventRepublisher(LevelCatalogue catalogue, SceneTree tree)
|
||||||
|
{
|
||||||
|
_catalogue = catalogue;
|
||||||
|
_tree = tree;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Publish(IReadOnlyList<ProgressionEvent> events)
|
||||||
|
{
|
||||||
|
foreach (var @event in events)
|
||||||
|
Publish(@event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Publish(ProgressionEvent @event)
|
||||||
|
{
|
||||||
|
switch (@event)
|
||||||
|
{
|
||||||
|
case LevelStarted started:
|
||||||
|
// The existing signal is keyed by array index because GhostManager names its replay
|
||||||
|
// files ghost_level_{index}.json. Transitional: once ghosts key on LevelId this
|
||||||
|
// becomes a straight pass-through and the index lookup goes away.
|
||||||
|
EventBus.EmitLevelStarted(IndexOf(started.Level), _tree.CurrentScene);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RunEnded:
|
||||||
|
EventBus.EmitRunEnded();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AttemptFailed failed:
|
||||||
|
EventBus.EmitAttemptFailed(failed.Remaining.Value, failed.RunOver);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AchievementUnlocked unlocked:
|
||||||
|
EventBus.EmitAchievementUnlocked(unlocked.Achievement.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LivesChanged lives:
|
||||||
|
EventBus.EmitLivesChanged(lives.Remaining.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TotalCoinsChanged coins:
|
||||||
|
EventBus.EmitCoinsChanged(coins.Total.Value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case SkillSetChanged:
|
||||||
|
EventBus.EmitSkillSetChanged();
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
GD.PushWarning($"No signal is published for domain event {@event.GetType().Name}.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int IndexOf(LevelId level)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < _catalogue.Levels.Count; i++)
|
||||||
|
if (_catalogue.Levels[i]?.Id == level.Value)
|
||||||
|
return i;
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Autoloads/ProgressionEventRepublisher.cs.uid
Normal file
1
Autoloads/ProgressionEventRepublisher.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cjm0g50la7uie
|
||||||
219
Autoloads/ProgressionRoot.cs
Normal file
219
Autoloads/ProgressionRoot.cs
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
using System;
|
||||||
|
using Godot;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
|
using Mr.BrickAdventures.Progression.Adapters;
|
||||||
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Composition root for player progression: builds the stores, loads the Profile, and owns the
|
||||||
|
/// <see cref="ProgressionService"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The only Node in this area. The aggregates are plain objects, so their lifetime is no longer tied
|
||||||
|
/// to the scene tree — which is why the operations below need no null-guarding of the store itself.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Every mutating operation is wrapped so that draining and republishing cannot be forgotten. That is
|
||||||
|
/// the one risk of the DrainEvents design, and wrapping collapses it to this file.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
public partial class ProgressionRoot : Node
|
||||||
|
{
|
||||||
|
public static ProgressionRoot Instance { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The one slot the game currently uses. Phase B replaces this with a picker over several
|
||||||
|
/// Playthroughs; until then a fixed id keeps the file name stable across runs.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly PlaythroughId SoleSlot =
|
||||||
|
PlaythroughId.Of(new Guid("a17e0000-0000-4000-8000-000000000001"));
|
||||||
|
|
||||||
|
[Export] public LevelCatalogue Catalogue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Lives a new Playthrough starts with. A tuning value, so it is authored here rather than baked into the domain.</summary>
|
||||||
|
[Export] public int StartingLives { get; set; } = 3;
|
||||||
|
|
||||||
|
private ProgressionService _service;
|
||||||
|
private ProgressionEventRepublisher _republisher;
|
||||||
|
|
||||||
|
/// <summary>Levels in campaign order, as the domain sees them.</summary>
|
||||||
|
public Campaign Campaign { get; private set; }
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
if (Catalogue == null)
|
||||||
|
{
|
||||||
|
GD.PushError("ProgressionRoot has no LevelCatalogue assigned; progression is unavailable.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var problem in Catalogue.Validate())
|
||||||
|
GD.PushError($"LevelCatalogue: {problem}");
|
||||||
|
|
||||||
|
Campaign = Catalogue.ToCampaign();
|
||||||
|
|
||||||
|
var directory = ProjectSettings.GlobalizePath("user://");
|
||||||
|
var playthroughs = new JsonPlaythroughStore(directory);
|
||||||
|
var profiles = new JsonProfileStore(directory);
|
||||||
|
|
||||||
|
_service = new ProgressionService(
|
||||||
|
LoadProfile(profiles), playthroughs, profiles,
|
||||||
|
Campaign, BuildSkillCatalogue(), Lives.Of(StartingLives));
|
||||||
|
|
||||||
|
_republisher = new ProgressionEventRepublisher(Catalogue, GetTree());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A missing Profile is a first run. A corrupt one is reported loudly and replaced with an empty
|
||||||
|
/// Profile so the game still starts — but the file is left alone, so it can still be recovered.
|
||||||
|
/// </summary>
|
||||||
|
private static Profile LoadProfile(IProfileStore profiles)
|
||||||
|
{
|
||||||
|
var result = profiles.Load();
|
||||||
|
|
||||||
|
if (result.Outcome == LoadOutcome.Corrupt)
|
||||||
|
GD.PushError($"Profile could not be read and has been left untouched: {result.Problem}");
|
||||||
|
|
||||||
|
return result.Outcome == LoadOutcome.Loaded ? result.Value : Profile.Empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads the authored upgrade tables into the domain's view of them. A Skill with no upgrade
|
||||||
|
/// entries yields an empty cost list, which the domain reports as not purchasable rather than
|
||||||
|
/// throwing on <c>Upgrades[0]</c>.
|
||||||
|
/// </summary>
|
||||||
|
private static SkillCatalogue BuildSkillCatalogue()
|
||||||
|
{
|
||||||
|
var available = SkillManager.Instance?.AvailableSkills;
|
||||||
|
if (available == null) return SkillCatalogue.Empty();
|
||||||
|
|
||||||
|
var costs = new System.Collections.Generic.Dictionary<SkillId, System.Collections.Generic.IReadOnlyList<Coins>>();
|
||||||
|
|
||||||
|
foreach (var skill in available)
|
||||||
|
{
|
||||||
|
if (skill == null || string.IsNullOrWhiteSpace(skill.Name)) continue;
|
||||||
|
|
||||||
|
var levels = new System.Collections.Generic.List<Coins>();
|
||||||
|
foreach (var upgrade in skill.Upgrades)
|
||||||
|
levels.Add(Coins.Of(Mathf.Max(0, upgrade.Cost)));
|
||||||
|
|
||||||
|
costs[SkillId.Of(skill.Name)] = levels;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SkillCatalogue.Of(costs);
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Operations — each drains and republishes, so callers cannot forget to
|
||||||
|
|
||||||
|
public void StartPlaythrough(PlaythroughId id) => Run(() => _service.StartPlaythrough(id));
|
||||||
|
|
||||||
|
public bool LoadPlaythrough(PlaythroughId id)
|
||||||
|
{
|
||||||
|
if (_service == null) return false;
|
||||||
|
|
||||||
|
var loaded = _service.LoadPlaythrough(id);
|
||||||
|
Publish();
|
||||||
|
return loaded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoadOutcome PeekPlaythrough(PlaythroughId id) =>
|
||||||
|
_service?.PeekPlaythrough(id) ?? LoadOutcome.Absent;
|
||||||
|
|
||||||
|
public void RecordStat(StatName stat, int amount = 1) => _service?.RecordStat(stat, amount);
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Reads
|
||||||
|
|
||||||
|
public bool IsPlaying => _service?.IsPlaying ?? false;
|
||||||
|
public bool CanEnter(LevelId level) => _service?.CanEnter(level) ?? false;
|
||||||
|
public LevelId? CurrentLevel => _service?.CurrentLevel;
|
||||||
|
public Coins TotalCoins => _service?.TotalCoins ?? Coins.Zero;
|
||||||
|
public Lives Lives => _service?.Lives ?? Lives.Of(0);
|
||||||
|
public bool IsRunOver => _service?.IsRunOver ?? false;
|
||||||
|
public SkillLevel? LevelOf(SkillId skill) => _service?.LevelOf(skill);
|
||||||
|
public bool IsOwned(SkillId skill) => _service?.IsOwned(skill) ?? false;
|
||||||
|
public bool CanAfford(SkillId skill) => _service?.CanAfford(skill) ?? false;
|
||||||
|
public bool HasCleared(LevelId level) => _service?.HasCleared(level) ?? false;
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Operations, continued
|
||||||
|
|
||||||
|
public bool EnterLevel(LevelId level)
|
||||||
|
{
|
||||||
|
if (_service == null) return false;
|
||||||
|
|
||||||
|
var entered = _service.EnterLevel(level);
|
||||||
|
Publish();
|
||||||
|
return entered;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordCoinsFound(Coins amount) => Run(() => _service.RecordCoinsFound(amount));
|
||||||
|
|
||||||
|
public void HandlePlayerDeath() => Run(() => _service.HandlePlayerDeath());
|
||||||
|
|
||||||
|
public void FinishLevel(LevelTime? time) => Run(() => _service.FinishLevel(time));
|
||||||
|
|
||||||
|
public PurchaseOutcome PurchaseSkill(SkillId skill)
|
||||||
|
{
|
||||||
|
if (_service == null) return PurchaseOutcome.NotPlaying;
|
||||||
|
|
||||||
|
var outcome = _service.PurchaseSkill(skill);
|
||||||
|
Publish();
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Free unlock, as a pickup gives. Returns false if it was already owned.</summary>
|
||||||
|
public bool GrantSkill(SkillId skill)
|
||||||
|
{
|
||||||
|
if (_service == null) return false;
|
||||||
|
|
||||||
|
var granted = _service.GrantSkill(skill);
|
||||||
|
Publish();
|
||||||
|
return granted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Takes a Skill away — for Skills that consume themselves.</summary>
|
||||||
|
public bool RevokeSkill(SkillId skill)
|
||||||
|
{
|
||||||
|
if (_service == null) return false;
|
||||||
|
|
||||||
|
var revoked = _service.RevokeSkill(skill);
|
||||||
|
Publish();
|
||||||
|
return revoked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Discards the current Attempt without charging a life — leaving a Level, not dying.</summary>
|
||||||
|
public void AbandonAttempt() => Run(() => _service.AbandonAttempt());
|
||||||
|
|
||||||
|
/// <summary>Console only — see <see cref="ProgressionService.ApplyCheat"/>.</summary>
|
||||||
|
public void ApplyCheat(Func<PlaythroughSnapshot, PlaythroughSnapshot> change) =>
|
||||||
|
Run(() => _service.ApplyCheat(change));
|
||||||
|
|
||||||
|
/// <summary>Whether there is a resumable Playthrough, for enabling the Continue button.</summary>
|
||||||
|
public bool HasSavedPlaythrough =>
|
||||||
|
PeekPlaythrough(SoleSlot) == LoadOutcome.Loaded;
|
||||||
|
|
||||||
|
private void Run(System.Action operation)
|
||||||
|
{
|
||||||
|
if (_service == null) return;
|
||||||
|
|
||||||
|
operation();
|
||||||
|
Publish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Publish() => _republisher?.Publish(_service.DrainEvents());
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
1
Autoloads/ProgressionRoot.cs.uid
Normal file
1
Autoloads/ProgressionRoot.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://b6amqp58yjfc4
|
||||||
@@ -1,61 +1,176 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
using Mr.BrickAdventures.scripts.State;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Save system that serializes state to JSON using DTOs.
|
||||||
|
/// </summary>
|
||||||
public partial class SaveSystem : Node
|
public partial class SaveSystem : Node
|
||||||
{
|
{
|
||||||
[Export] public string SavePath { get; set; } = "user://savegame.save";
|
[Export] public string SavePath { get; set; } = "user://savegame.json";
|
||||||
[Export] public int Version { get; set; } = 1;
|
[Export] public int Version { get; set; } = 2;
|
||||||
|
|
||||||
private GameManager _gameManager;
|
public static SaveSystem Instance { get; private set; }
|
||||||
|
|
||||||
|
internal static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SaveGame()
|
public void SaveGame()
|
||||||
{
|
{
|
||||||
var saveData = new Dictionary
|
var store = GameStateStore.Instance;
|
||||||
|
if (store == null)
|
||||||
{
|
{
|
||||||
{ "player_state", _gameManager.PlayerState},
|
GD.PrintErr("SaveSystem: GameStateStore not available.");
|
||||||
{ "version", Version}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to DTO (only serializable data)
|
||||||
|
var saveData = new SaveDataDto
|
||||||
|
{
|
||||||
|
Version = Version,
|
||||||
|
Coins = store.Player.Coins,
|
||||||
|
Lives = store.Player.Lives,
|
||||||
|
CurrentLevel = store.Player.CurrentLevel,
|
||||||
|
CompletedLevels = [.. store.Player.CompletedLevels],
|
||||||
|
UnlockedLevels = new List<int>(store.Player.UnlockedLevels),
|
||||||
|
UnlockedSkillNames = GetSkillNames(store.Player.UnlockedSkills),
|
||||||
|
UnlockedAchievements = new List<string>(store.Player.UnlockedAchievements),
|
||||||
|
Statistics = new Dictionary<string, int>(store.Player.Statistics)
|
||||||
};
|
};
|
||||||
|
|
||||||
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
|
try
|
||||||
file.StoreVar(saveData);
|
{
|
||||||
GD.Print("Game state saved to: ", SavePath);
|
var json = JsonSerializer.Serialize(saveData, JsonOptions);
|
||||||
|
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
|
||||||
|
file.StoreString(json);
|
||||||
|
GD.Print("Game saved to: ", SavePath);
|
||||||
|
EventBus.EmitGameSaved();
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"SaveSystem: Failed to save game: {e.Message}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool LoadGame()
|
public bool LoadGame()
|
||||||
{
|
{
|
||||||
if (!FileAccess.FileExists(SavePath))
|
if (!FileAccess.FileExists(SavePath))
|
||||||
return false;
|
|
||||||
|
|
||||||
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
|
|
||||||
var saveDataObj = (Dictionary)file.GetVar();
|
|
||||||
|
|
||||||
if (saveDataObj.ContainsKey("version") && (int)saveDataObj["version"] != Version)
|
|
||||||
{
|
{
|
||||||
GD.Print($"Save file version mismatch. Expected: {Version}, Found: {saveDataObj["version"]}");
|
GD.Print("SaveSystem: No save file found.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
GD.Print("Game state loaded from: ", SavePath);
|
try
|
||||||
GD.Print("Player state: ", saveDataObj["player_state"]);
|
|
||||||
_gameManager.PlayerState = (Dictionary)saveDataObj["player_state"];
|
|
||||||
|
|
||||||
var skills = new Array<SkillData>();
|
|
||||||
foreach (var skill in (Array<SkillData>)_gameManager.PlayerState["unlocked_skills"])
|
|
||||||
{
|
{
|
||||||
skills.Add(skill);
|
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Read);
|
||||||
|
var json = file.GetAsText();
|
||||||
|
var saveData = JsonSerializer.Deserialize<SaveDataDto>(json, JsonOptions);
|
||||||
|
|
||||||
|
if (saveData == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr("SaveSystem: Failed to deserialize save data.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveData.Version != Version)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"SaveSystem: Version mismatch. Expected {Version}, found {saveData.Version}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var store = GameStateStore.Instance;
|
||||||
|
if (store == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr("SaveSystem: GameStateStore not available.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply loaded state
|
||||||
|
store.Player.Coins = saveData.Coins;
|
||||||
|
store.Player.Lives = saveData.Lives;
|
||||||
|
store.Player.CurrentLevel = saveData.CurrentLevel;
|
||||||
|
|
||||||
|
store.Player.CompletedLevels = saveData.CompletedLevels ?? new List<int>();
|
||||||
|
store.Player.UnlockedLevels = saveData.UnlockedLevels ?? new List<int> { 0 };
|
||||||
|
store.Player.UnlockedAchievements = saveData.UnlockedAchievements ?? new List<string>();
|
||||||
|
store.Player.Statistics = saveData.Statistics ?? new Dictionary<string, int>();
|
||||||
|
|
||||||
|
// Reload skills by name from SkillManager
|
||||||
|
store.Player.UnlockedSkills = LoadSkillsByName(saveData.UnlockedSkillNames);
|
||||||
|
|
||||||
|
GD.Print("Game loaded from: ", SavePath);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (System.Exception e)
|
||||||
|
{
|
||||||
|
GD.PrintErr($"SaveSystem: Failed to load game: {e.Message}");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
_gameManager.UnlockSkills(skills);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static List<string> GetSkillNames(List<SkillData> skills)
|
||||||
|
{
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var skill in skills)
|
||||||
|
{
|
||||||
|
if (skill != null)
|
||||||
|
names.Add(skill.Name);
|
||||||
|
}
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SkillData> LoadSkillsByName(List<string> skillNames)
|
||||||
|
{
|
||||||
|
var skills = new List<SkillData>();
|
||||||
|
if (skillNames == null) return skills;
|
||||||
|
|
||||||
|
var skillManager = SkillManager.Instance;
|
||||||
|
if (skillManager == null)
|
||||||
|
{
|
||||||
|
GD.PrintErr("SaveSystem: SkillManager not available to resolve skill names.");
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var name in skillNames)
|
||||||
|
{
|
||||||
|
var skill = skillManager.GetSkillByName(name);
|
||||||
|
if (skill != null)
|
||||||
|
{
|
||||||
|
skills.Add(skill);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
GD.PrintErr($"SaveSystem: Skill '{name}' not found in SkillManager.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
|
||||||
public bool CheckSaveExists() => FileAccess.FileExists(SavePath);
|
public bool CheckSaveExists() => FileAccess.FileExists(SavePath);
|
||||||
}
|
|
||||||
|
public void DeleteSave()
|
||||||
|
{
|
||||||
|
if (FileAccess.FileExists(SavePath))
|
||||||
|
{
|
||||||
|
DirAccess.RemoveAbsolute(ProjectSettings.GlobalizePath(SavePath));
|
||||||
|
GD.Print("Save file deleted.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
203
Autoloads/SettingsManager.cs
Normal file
203
Autoloads/SettingsManager.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using Godot;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
public partial class SettingsManager : Node
|
||||||
|
{
|
||||||
|
public static SettingsManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public float GamepadDeadzone { get; set; } = 0.2f;
|
||||||
|
public float GamepadSensitivity { get; set; } = 1.0f;
|
||||||
|
public string WindowMode { get; set; } = "fullscreen";
|
||||||
|
public Vector2I Resolution { get; set; } = new Vector2I(1920, 1080);
|
||||||
|
|
||||||
|
private static readonly List<Vector2I> CuratedResolutions = new()
|
||||||
|
{
|
||||||
|
// 4:3
|
||||||
|
new Vector2I(640, 480),
|
||||||
|
new Vector2I(800, 600),
|
||||||
|
new Vector2I(1024, 768),
|
||||||
|
new Vector2I(1280, 960),
|
||||||
|
new Vector2I(1600, 1200),
|
||||||
|
// 16:9
|
||||||
|
new Vector2I(1280, 720),
|
||||||
|
new Vector2I(1366, 768),
|
||||||
|
new Vector2I(1600, 900),
|
||||||
|
new Vector2I(1920, 1080),
|
||||||
|
new Vector2I(2560, 1440),
|
||||||
|
new Vector2I(3840, 2160),
|
||||||
|
// 16:10
|
||||||
|
new Vector2I(1280, 800),
|
||||||
|
new Vector2I(1440, 900),
|
||||||
|
new Vector2I(1920, 1200),
|
||||||
|
new Vector2I(2560, 1600),
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly List<Vector2I> _customResolutions = new();
|
||||||
|
|
||||||
|
private static readonly string[] DeadzoneActions = { "left", "right", "up", "down" };
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
|
||||||
|
if (ConfigFileHandler.Instance == null)
|
||||||
|
{
|
||||||
|
GD.PushError("SettingsManager: ConfigFileHandler.Instance is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg = ConfigFileHandler.Instance.SettingsConfig;
|
||||||
|
|
||||||
|
// --- display_settings ---
|
||||||
|
WindowMode = cfg.GetValue("display_settings", "window_mode", Variant.From("fullscreen")).AsString();
|
||||||
|
var resStr = cfg.GetValue("display_settings", "resolution", Variant.From("1920x1080")).AsString();
|
||||||
|
Resolution = ParseResolution(resStr, new Vector2I(1920, 1080));
|
||||||
|
|
||||||
|
var customResStr = cfg.GetValue("display_settings", "custom_resolutions", Variant.From("")).AsString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(customResStr))
|
||||||
|
{
|
||||||
|
foreach (var part in customResStr.Split(','))
|
||||||
|
{
|
||||||
|
var r = ParseResolution(part.Trim(), Vector2I.Zero);
|
||||||
|
if (r != Vector2I.Zero)
|
||||||
|
_customResolutions.Add(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyDisplaySettings();
|
||||||
|
|
||||||
|
// --- gameplay_settings ---
|
||||||
|
GamepadDeadzone = (float)cfg.GetValue("gameplay_settings", "gamepad_deadzone", Variant.From(0.2)).AsDouble();
|
||||||
|
GamepadSensitivity = (float)cfg.GetValue("gameplay_settings", "gamepad_sensitivity", Variant.From(1.0)).AsDouble();
|
||||||
|
|
||||||
|
ApplyGamepadDeadzone();
|
||||||
|
|
||||||
|
// --- input_settings ---
|
||||||
|
ApplyInputBindings(cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── public API ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public List<Vector2I> GetAllResolutions()
|
||||||
|
{
|
||||||
|
var all = new List<Vector2I>(CuratedResolutions);
|
||||||
|
all.AddRange(_customResolutions);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveDisplaySettings()
|
||||||
|
{
|
||||||
|
var cfg = ConfigFileHandler.Instance.SettingsConfig;
|
||||||
|
cfg.SetValue("display_settings", "window_mode", WindowMode);
|
||||||
|
cfg.SetValue("display_settings", "resolution", $"{Resolution.X}x{Resolution.Y}");
|
||||||
|
cfg.SetValue("display_settings", "custom_resolutions", BuildCustomResolutionsString());
|
||||||
|
cfg.Save(ConfigFileHandler.SettingsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveGameplaySettings()
|
||||||
|
{
|
||||||
|
var cfg = ConfigFileHandler.Instance.SettingsConfig;
|
||||||
|
cfg.SetValue("gameplay_settings", "gamepad_deadzone", (double)GamepadDeadzone);
|
||||||
|
cfg.SetValue("gameplay_settings", "gamepad_sensitivity", (double)GamepadSensitivity);
|
||||||
|
cfg.Save(ConfigFileHandler.SettingsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SaveInputSettings()
|
||||||
|
{
|
||||||
|
var cfg = ConfigFileHandler.Instance.SettingsConfig;
|
||||||
|
foreach (var action in InputMap.GetActions())
|
||||||
|
{
|
||||||
|
if (action.ToString().StartsWith("ui_")) continue;
|
||||||
|
foreach (var ev in InputMap.ActionGetEvents(action))
|
||||||
|
{
|
||||||
|
if (ev is InputEventKey key)
|
||||||
|
{
|
||||||
|
cfg.SetValue("input_settings", action, (long)key.PhysicalKeycode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cfg.Save(ConfigFileHandler.SettingsPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── apply helpers ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public void ApplyDisplaySettings()
|
||||||
|
{
|
||||||
|
switch (WindowMode)
|
||||||
|
{
|
||||||
|
case "fullscreen":
|
||||||
|
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Fullscreen);
|
||||||
|
break;
|
||||||
|
case "borderless":
|
||||||
|
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
|
||||||
|
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, true);
|
||||||
|
DisplayServer.WindowSetSize(Resolution);
|
||||||
|
break;
|
||||||
|
default: // "windowed"
|
||||||
|
DisplayServer.WindowSetMode(DisplayServer.WindowMode.Windowed);
|
||||||
|
DisplayServer.WindowSetFlag(DisplayServer.WindowFlags.Borderless, false);
|
||||||
|
DisplayServer.WindowSetSize(Resolution);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyGamepadDeadzone()
|
||||||
|
{
|
||||||
|
foreach (var action in DeadzoneActions)
|
||||||
|
{
|
||||||
|
if (InputMap.HasAction(action))
|
||||||
|
InputMap.ActionSetDeadzone(action, GamepadDeadzone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyInputBindings(ConfigFile cfg)
|
||||||
|
{
|
||||||
|
if (!cfg.HasSection("input_settings")) return;
|
||||||
|
|
||||||
|
foreach (var actionName in cfg.GetSectionKeys("input_settings"))
|
||||||
|
{
|
||||||
|
if (actionName.StartsWith("ui_")) continue;
|
||||||
|
if (!InputMap.HasAction(actionName)) continue;
|
||||||
|
|
||||||
|
var scancode = (Key)(long)cfg.GetValue("input_settings", actionName);
|
||||||
|
|
||||||
|
// Remove existing keyboard events for this action
|
||||||
|
var events = InputMap.ActionGetEvents(actionName);
|
||||||
|
foreach (var ev in events)
|
||||||
|
{
|
||||||
|
if (ev is InputEventKey)
|
||||||
|
InputMap.ActionEraseEvent(actionName, ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the saved key
|
||||||
|
var newKey = new InputEventKey { PhysicalKeycode = scancode };
|
||||||
|
InputMap.ActionAddEvent(actionName, newKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── util ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
private static Vector2I ParseResolution(string s, Vector2I fallback)
|
||||||
|
{
|
||||||
|
var parts = s.Split('x');
|
||||||
|
if (parts.Length == 2 && int.TryParse(parts[0], out var w) && int.TryParse(parts[1], out var h))
|
||||||
|
return new Vector2I(w, h);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildCustomResolutionsString()
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
foreach (var r in _customResolutions)
|
||||||
|
parts.Add($"{r.X}x{r.Y}");
|
||||||
|
return string.Join(",", parts);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
Autoloads/SettingsManager.cs.uid
Normal file
1
Autoloads/SettingsManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://d4jyxtm4jgtka
|
||||||
@@ -1,32 +1,61 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
using Godot.Collections;
|
||||||
|
using Mr.BrickAdventures.Progression;
|
||||||
using Mr.BrickAdventures.scripts.components;
|
using Mr.BrickAdventures.scripts.components;
|
||||||
using Mr.BrickAdventures.scripts.interfaces;
|
using Mr.BrickAdventures.scripts.interfaces;
|
||||||
using Mr.BrickAdventures.scripts.Resources;
|
using Mr.BrickAdventures.scripts.Resources;
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Owns the live skill components attached to the player, and keeps them matching what the Playthrough
|
||||||
|
/// says is owned.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// Reconciles from <see cref="EventBus.SkillSetChanged"/> rather than being told by each caller. The old
|
||||||
|
/// arrangement had four call sites doing "record it in the store, then tell SkillManager" as two
|
||||||
|
/// separate calls — anything between them, including <c>AddSkill</c>'s own early return when no player
|
||||||
|
/// is registered, left a skill owned but inert.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Tracks the level each component was built at, so an upgrade rebuilds it. Previously
|
||||||
|
/// <c>AddSkill</c> returned early for an already-active skill, and the upgrade was applied by the
|
||||||
|
/// purchase code reaching into the component instead.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
public partial class SkillManager : Node
|
public partial class SkillManager : Node
|
||||||
{
|
{
|
||||||
private GameManager _gameManager;
|
|
||||||
private PlayerController _player;
|
private PlayerController _player;
|
||||||
|
|
||||||
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
||||||
|
|
||||||
public Dictionary ActiveComponents { get; private set; } = new();
|
public Dictionary ActiveComponents { get; private set; } = new();
|
||||||
|
|
||||||
|
private readonly System.Collections.Generic.Dictionary<string, int> _activeLevels = new();
|
||||||
|
|
||||||
|
public static SkillManager Instance { get; private set; }
|
||||||
|
|
||||||
[Signal]
|
[Signal]
|
||||||
public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent);
|
public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent);
|
||||||
[Signal]
|
[Signal]
|
||||||
public delegate void SkillRemovedEventHandler(SkillData skillData);
|
public delegate void SkillRemovedEventHandler(SkillData skillData);
|
||||||
|
|
||||||
public override void _Ready()
|
public override void _Ready()
|
||||||
{
|
{
|
||||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
Instance = this;
|
||||||
|
EventBus.Instance.SkillSetChanged += Reconcile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (EventBus.Instance != null)
|
||||||
|
EventBus.Instance.SkillSetChanged -= Reconcile;
|
||||||
|
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called by the PlayerController from its _Ready method to register itself with the manager.
|
/// Called by the PlayerController from its _Ready method to register itself with the manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -34,24 +63,17 @@ public partial class SkillManager : Node
|
|||||||
{
|
{
|
||||||
if (_player == player) return;
|
if (_player == player) return;
|
||||||
|
|
||||||
// If a player is already registered (e.g., from a previous scene), unregister it first.
|
|
||||||
if (_player != null && IsInstanceValid(_player))
|
if (_player != null && IsInstanceValid(_player))
|
||||||
{
|
|
||||||
UnregisterPlayer();
|
UnregisterPlayer();
|
||||||
}
|
|
||||||
|
|
||||||
_player = player;
|
_player = player;
|
||||||
if (_player != null)
|
if (_player != null)
|
||||||
{
|
{
|
||||||
// Automatically unregister when the player node is removed from the scene.
|
|
||||||
_player.TreeExiting += UnregisterPlayer;
|
_player.TreeExiting += UnregisterPlayer;
|
||||||
ApplyUnlockedSkills();
|
Reconcile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Cleans up skills and references related to the current player.
|
|
||||||
/// </summary>
|
|
||||||
private void UnregisterPlayer()
|
private void UnregisterPlayer()
|
||||||
{
|
{
|
||||||
if (_player != null && IsInstanceValid(_player))
|
if (_player != null && IsInstanceValid(_player))
|
||||||
@@ -61,46 +83,62 @@ public partial class SkillManager : Node
|
|||||||
}
|
}
|
||||||
_player = null;
|
_player = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddSkill(SkillData skillData)
|
/// <summary>
|
||||||
|
/// Brings the live components in line with what is owned: adds what is missing, rebuilds anything
|
||||||
|
/// whose level changed, removes what is no longer owned.
|
||||||
|
/// </summary>
|
||||||
|
public void Reconcile()
|
||||||
|
{
|
||||||
|
if (_player == null || !IsInstanceValid(_player)) return;
|
||||||
|
|
||||||
|
var progression = ProgressionRoot.Instance;
|
||||||
|
if (progression == null) return;
|
||||||
|
|
||||||
|
foreach (var skill in AvailableSkills)
|
||||||
|
{
|
||||||
|
if (skill == null || string.IsNullOrWhiteSpace(skill.Name)) continue;
|
||||||
|
|
||||||
|
var owned = progression.LevelOf(SkillId.Of(skill.Name))?.Value;
|
||||||
|
var active = _activeLevels.TryGetValue(skill.Name, out var at) ? at : (int?)null;
|
||||||
|
|
||||||
|
if (owned == active) continue;
|
||||||
|
|
||||||
|
if (active is not null) RemoveSkill(skill.Name);
|
||||||
|
if (owned is not null) AddSkill(skill, owned.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Kept for the marketplace's activate/deactivate toggle, which is a display concern.</summary>
|
||||||
|
public void AddSkill(SkillData skillData) =>
|
||||||
|
AddSkill(skillData, ProgressionRoot.Instance?.LevelOf(SkillId.Of(skillData.Name))?.Value ?? 1);
|
||||||
|
|
||||||
|
private void AddSkill(SkillData skillData, int level)
|
||||||
{
|
{
|
||||||
// Ensure a valid player is registered before adding a skill.
|
|
||||||
if (_player == null || !IsInstanceValid(_player))
|
if (_player == null || !IsInstanceValid(_player))
|
||||||
{
|
{
|
||||||
GD.Print("SkillManager: Player not available to add skill.");
|
GD.Print("SkillManager: Player not available to add skill.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ActiveComponents.ContainsKey(skillData.Name))
|
if (ActiveComponents.ContainsKey(skillData.Name))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (skillData.Type == SkillType.Throw)
|
if (skillData.Type == SkillType.Throw)
|
||||||
{
|
{
|
||||||
var unlocked = _gameManager.GetUnlockedSkills();
|
foreach (var sd in AvailableSkills)
|
||||||
foreach (var sd in unlocked)
|
|
||||||
{
|
{
|
||||||
SkillData data = null;
|
if (sd != skillData && sd?.Type == SkillType.Throw)
|
||||||
foreach (var s in AvailableSkills)
|
RemoveSkill(sd.Name);
|
||||||
{
|
|
||||||
if (s == sd)
|
|
||||||
{
|
|
||||||
data = s;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove other throw skills if a new one is added
|
|
||||||
if (data is { Type: SkillType.Throw })
|
|
||||||
RemoveSkill(data.Name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance = skillData.Node.Instantiate();
|
var instance = skillData.Node.Instantiate();
|
||||||
if (instance is ISkill skill)
|
if (instance is ISkill skill)
|
||||||
{
|
{
|
||||||
// Initialize the skill with the registered player instance.
|
skill.Initialize(_player, skillData, level);
|
||||||
skill.Initialize(_player, skillData);
|
|
||||||
skill.Activate();
|
skill.Activate();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!");
|
GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!");
|
||||||
@@ -108,101 +146,62 @@ public partial class SkillManager : Node
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the skill node as a child of the player.
|
|
||||||
_player.AddChild(instance);
|
_player.AddChild(instance);
|
||||||
ActiveComponents[skillData.Name] = instance;
|
ActiveComponents[skillData.Name] = instance;
|
||||||
|
_activeLevels[skillData.Name] = level;
|
||||||
|
|
||||||
if (instance is BrickThrowComponent btc)
|
if (instance is BrickThrowComponent btc)
|
||||||
{
|
EmitSignalActiveThrowSkillChanged(btc);
|
||||||
EmitSignalActiveThrowSkillChanged(btc);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveSkill(string skillName)
|
public void RemoveSkill(string skillName)
|
||||||
{
|
{
|
||||||
|
_activeLevels.Remove(skillName);
|
||||||
|
|
||||||
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (component.AsGodotObject() is BrickThrowComponent)
|
if (component.AsGodotObject() is BrickThrowComponent)
|
||||||
{
|
|
||||||
EmitSignalActiveThrowSkillChanged(null);
|
EmitSignalActiveThrowSkillChanged(null);
|
||||||
}
|
|
||||||
|
|
||||||
var inst = (Node)component;
|
var inst = (Node)component;
|
||||||
if (inst is ISkill skill)
|
if (inst is ISkill skill)
|
||||||
{
|
|
||||||
skill.Deactivate();
|
skill.Deactivate();
|
||||||
}
|
|
||||||
|
|
||||||
if (IsInstanceValid(inst))
|
if (IsInstanceValid(inst))
|
||||||
inst.QueueFree();
|
inst.QueueFree();
|
||||||
|
|
||||||
var skills = _gameManager.GetUnlockedSkills();
|
|
||||||
foreach (var s in skills)
|
|
||||||
{
|
|
||||||
if (s.Name == skillName)
|
|
||||||
{
|
|
||||||
s.IsActive = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ActiveComponents.Remove(skillName);
|
ActiveComponents.Remove(skillName);
|
||||||
var sd = GetSkillByName(skillName);
|
var sd = GetSkillByName(skillName);
|
||||||
if (sd != null) EmitSignalSkillRemoved(sd);
|
if (sd != null) EmitSignalSkillRemoved(sd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RemoveAllActiveSkills()
|
private void RemoveAllActiveSkills()
|
||||||
{
|
{
|
||||||
// Create a copy of keys to avoid modification during iteration
|
foreach (var skillName in ActiveComponents.Keys.Select(k => k.ToString()).ToList())
|
||||||
var keys = ActiveComponents.Keys.ToArray();
|
|
||||||
var skillNames = keys.Select(key => key.ToString()).ToList();
|
|
||||||
|
|
||||||
foreach (var skillName in skillNames)
|
|
||||||
{
|
|
||||||
RemoveSkill(skillName);
|
RemoveSkill(skillName);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyUnlockedSkills()
|
|
||||||
{
|
|
||||||
if (_player == null || !IsInstanceValid(_player)) return;
|
|
||||||
|
|
||||||
foreach (var sd in AvailableSkills)
|
|
||||||
{
|
|
||||||
if (_gameManager.IsSkillUnlocked(sd))
|
|
||||||
{
|
|
||||||
CallDeferred(MethodName.AddSkill, sd);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
RemoveSkill(sd.Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SkillData GetSkillByName(string skillName)
|
public SkillData GetSkillByName(string skillName)
|
||||||
{
|
{
|
||||||
foreach (var sd in AvailableSkills)
|
foreach (var sd in AvailableSkills)
|
||||||
if (sd.Name == skillName) return sd;
|
if (sd?.Name == skillName) return sd;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsSkillActive(SkillData skill) =>
|
||||||
|
skill != null && ActiveComponents.ContainsKey(skill.Name);
|
||||||
|
|
||||||
public void ActivateSkill(SkillData skill)
|
public void ActivateSkill(SkillData skill)
|
||||||
{
|
{
|
||||||
if (!ActiveComponents.ContainsKey(skill.Name))
|
if (!ActiveComponents.ContainsKey(skill.Name))
|
||||||
{
|
|
||||||
AddSkill(skill);
|
AddSkill(skill);
|
||||||
skill.IsActive = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DeactivateSkill(SkillData skill)
|
public void DeactivateSkill(SkillData skill)
|
||||||
{
|
{
|
||||||
if (ActiveComponents.ContainsKey(skill.Name))
|
if (ActiveComponents.ContainsKey(skill.Name))
|
||||||
{
|
|
||||||
RemoveSkill(skill.Name);
|
RemoveSkill(skill.Name);
|
||||||
skill.IsActive = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleSkillActivation(SkillData skill)
|
public void ToggleSkillActivation(SkillData skill)
|
||||||
@@ -214,4 +213,4 @@ public partial class SkillManager : Node
|
|||||||
else
|
else
|
||||||
ActivateSkill(skill);
|
ActivateSkill(skill);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ namespace Mr.BrickAdventures.Autoloads;
|
|||||||
|
|
||||||
public partial class SpeedRunManager : Node
|
public partial class SpeedRunManager : Node
|
||||||
{
|
{
|
||||||
|
public static SpeedRunManager Instance { get; private set; }
|
||||||
|
|
||||||
|
public override void _EnterTree() => Instance = this;
|
||||||
|
public override void _ExitTree() { if (Instance == this) Instance = null; }
|
||||||
|
|
||||||
public bool IsRunning { get; private set; } = false;
|
public bool IsRunning { get; private set; } = false;
|
||||||
public bool IsVisible { get; private set; } = false;
|
|
||||||
|
|
||||||
private double _startTime;
|
private double _startTime;
|
||||||
private double _levelStartTime;
|
private double _levelStartTime;
|
||||||
private List<double> _splits = [];
|
private List<double> _splits = [];
|
||||||
@@ -16,7 +20,7 @@ public partial class SpeedRunManager : Node
|
|||||||
|
|
||||||
public override void _Process(double delta)
|
public override void _Process(double delta)
|
||||||
{
|
{
|
||||||
if (!IsRunning || !IsVisible) return;
|
if (!IsRunning) return;
|
||||||
|
|
||||||
EmitSignalTimeUpdated(GetCurrentTotalTime(), GetCurrentLevelTime());
|
EmitSignalTimeUpdated(GetCurrentTotalTime(), GetCurrentLevelTime());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,78 +0,0 @@
|
|||||||
using Godot;
|
|
||||||
using Godot.Collections;
|
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
|
||||||
|
|
||||||
public partial class StatisticsManager : Node
|
|
||||||
{
|
|
||||||
private GameManager _gameManager;
|
|
||||||
private AchievementManager _achievementManager;
|
|
||||||
private Dictionary<string, Variant> _stats = new();
|
|
||||||
|
|
||||||
public override void _Ready()
|
|
||||||
{
|
|
||||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
|
||||||
_achievementManager = GetNode<AchievementManager>("/root/AchievementManager");
|
|
||||||
LoadStatistics();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LoadStatistics()
|
|
||||||
{
|
|
||||||
if (_gameManager.PlayerState.TryGetValue("statistics", out var statsObj))
|
|
||||||
{
|
|
||||||
_stats = (Dictionary<string, Variant>)statsObj;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_stats = new Dictionary<string, Variant>();
|
|
||||||
_gameManager.PlayerState["statistics"] = _stats;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Increases a numerical statistic by a given amount.
|
|
||||||
/// </summary>
|
|
||||||
public void IncrementStat(string statName, int amount = 1)
|
|
||||||
{
|
|
||||||
if (_stats.TryGetValue(statName, out var currentValue))
|
|
||||||
{
|
|
||||||
_stats[statName] = (int)currentValue + amount;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_stats[statName] = amount;
|
|
||||||
}
|
|
||||||
GD.Print($"Stat '{statName}' updated to: {_stats[statName]}");
|
|
||||||
CheckAchievementsForStat(statName);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the value of a statistic.
|
|
||||||
/// </summary>
|
|
||||||
public Variant GetStat(string statName, Variant defaultValue = default)
|
|
||||||
{
|
|
||||||
return _stats.TryGetValue(statName, out var value) ? value : defaultValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Checks if the updated stat meets the criteria for any achievements.
|
|
||||||
/// </summary>
|
|
||||||
private void CheckAchievementsForStat(string statName)
|
|
||||||
{
|
|
||||||
switch (statName)
|
|
||||||
{
|
|
||||||
case "enemies_defeated":
|
|
||||||
if ((int)GetStat(statName, 0) >= 100)
|
|
||||||
{
|
|
||||||
_achievementManager.UnlockAchievement("slayer_100_enemies");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "jumps_made":
|
|
||||||
if ((int)GetStat(statName, 0) >= 1000)
|
|
||||||
{
|
|
||||||
_achievementManager.UnlockAchievement("super_jumper");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://c5p3l2mhkw0p4
|
|
||||||
@@ -1,11 +1,22 @@
|
|||||||
using Godot;
|
using Godot;
|
||||||
using Godot.Collections;
|
|
||||||
|
|
||||||
namespace Mr.BrickAdventures.Autoloads;
|
namespace Mr.BrickAdventures.Autoloads;
|
||||||
|
|
||||||
public partial class UIManager : Node
|
public partial class UIManager : Node
|
||||||
{
|
{
|
||||||
[Export] public Array<Control> UiStack { get; set; } = new();
|
public static UIManager Instance { get; private set; }
|
||||||
|
|
||||||
|
private readonly System.Collections.Generic.List<Control> UiStack = new();
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
Instance = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
if (Instance == this) Instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
[Signal] public delegate void ScreenPushedEventHandler(Control screen);
|
[Signal] public delegate void ScreenPushedEventHandler(Control screen);
|
||||||
[Signal] public delegate void ScreenPoppedEventHandler(Control screen);
|
[Signal] public delegate void ScreenPoppedEventHandler(Control screen);
|
||||||
@@ -34,19 +45,19 @@ public partial class UIManager : Node
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var top = (Control)UiStack[^1];
|
var top = UiStack[^1];
|
||||||
UiStack.RemoveAt(UiStack.Count - 1);
|
UiStack.RemoveAt(UiStack.Count - 1);
|
||||||
top.Hide();
|
top.Hide();
|
||||||
top.SetProcessInput(false);
|
top.SetProcessInput(false);
|
||||||
EmitSignalScreenPopped(top);
|
EmitSignalScreenPopped(top);
|
||||||
top.AcceptEvent();
|
top.AcceptEvent();
|
||||||
|
|
||||||
if (UiStack.Count > 0) ((Control)UiStack[^1]).GrabFocus();
|
if (UiStack.Count > 0) UiStack[^1].GrabFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Control TopScreen() => UiStack.Count > 0 ? (Control)UiStack[^1] : null;
|
public Control TopScreen() => UiStack.Count > 0 ? UiStack[^1] : null;
|
||||||
|
|
||||||
public bool IsScreenOnTop(Control screen) => UiStack.Count > 0 && (Control)UiStack[^1] == screen;
|
public bool IsScreenOnTop(Control screen) => UiStack.Count > 0 && UiStack[^1] == screen;
|
||||||
|
|
||||||
public bool IsVisibleOnStack(Control screen) => UiStack.Contains(screen) && screen.Visible;
|
public bool IsVisibleOnStack(Control screen) => UiStack.Contains(screen) && screen.Visible;
|
||||||
|
|
||||||
|
|||||||
103
CONTEXT.md
Normal file
103
CONTEXT.md
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# Mr. Brick Adventures
|
||||||
|
|
||||||
|
A 2D platformer. This context covers **player state and progression** — what the player has, what they have done,
|
||||||
|
and at what point each of those becomes permanent. Gameplay behaviour (movement, combat, hazards) lives in the
|
||||||
|
composable components and is deliberately outside this context.
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
### Scopes
|
||||||
|
|
||||||
|
Three lifetimes. Every fact in this context belongs to exactly one.
|
||||||
|
|
||||||
|
**Attempt**:
|
||||||
|
One try at a level, from entering it to either completing it or losing a life. Holds a ledger of everything that
|
||||||
|
happened during the try — coins found, coins spent, skills bought. Discarded whole if the player dies.
|
||||||
|
_Avoid_: Session, run, life
|
||||||
|
|
||||||
|
**Playthrough**:
|
||||||
|
One game from New Game until game over, identified by a `PlaythroughId`. Holds lives, banked coins, owned skills
|
||||||
|
and their levels, campaign position, and which levels it has cleared. Several exist at once and the player picks
|
||||||
|
between them.
|
||||||
|
_Avoid_: Save, save slot, progress, game, run
|
||||||
|
|
||||||
|
**Profile**:
|
||||||
|
Everything shared across all Playthroughs, one per installation. Achievements, best time per Level, and lifetime
|
||||||
|
statistics. Never reset by starting a new Playthrough.
|
||||||
|
_Avoid_: Global state, meta, persistent data, account
|
||||||
|
|
||||||
|
### Operations
|
||||||
|
|
||||||
|
**Settle**:
|
||||||
|
Resolving an ended Attempt against the Playthrough — Commit it on a First Clear, Discard it otherwise. Settling is
|
||||||
|
the only way an Attempt affects a Playthrough, and it decides First Clear versus Replay itself rather than being
|
||||||
|
told which one happened.
|
||||||
|
_Avoid_: Apply, resolve, finalise
|
||||||
|
|
||||||
|
**Commit**:
|
||||||
|
Applying an Attempt's ledger to the Playthrough. Only a First Clear commits — nothing else, ever.
|
||||||
|
_Avoid_: Save, persist, flush, sync
|
||||||
|
|
||||||
|
**First Clear**:
|
||||||
|
Completing a Level this Playthrough has not cleared before. The only event that commits an Attempt and advances
|
||||||
|
campaign position.
|
||||||
|
_Avoid_: Level complete, finish (both ambiguous — they also describe a Replay)
|
||||||
|
|
||||||
|
**Replay**:
|
||||||
|
Completing a Level this Playthrough has already cleared. Records Best Time, achievements and statistics to the
|
||||||
|
Profile; commits nothing and never advances campaign position.
|
||||||
|
_Avoid_: Practice, retry, second run
|
||||||
|
|
||||||
|
**Bank**:
|
||||||
|
The coins a Playthrough holds, as opposed to coins found in the current Attempt. Coins are banked by committing,
|
||||||
|
never on pickup.
|
||||||
|
_Avoid_: Total coins, saved coins
|
||||||
|
|
||||||
|
**Discard**:
|
||||||
|
Throwing away an Attempt's ledger — on death, or at the end of a Replay. Purchases made during the Attempt are
|
||||||
|
undone by discarding, not refunded; under the ledger they never happened.
|
||||||
|
_Avoid_: Reset, rollback, revert
|
||||||
|
|
||||||
|
### Things
|
||||||
|
|
||||||
|
**Level**:
|
||||||
|
One playable stage, identified by a stable `LevelId` — never by position in a list. Knows its scene and display
|
||||||
|
name. Ids must survive inserting and reordering levels, because Playthroughs and Best Times both reference them.
|
||||||
|
_Avoid_: Stage, scene, level index
|
||||||
|
|
||||||
|
**Campaign**:
|
||||||
|
The authored ordering of Levels that defines progression — which Level is first, and what follows a given one. One
|
||||||
|
Campaign is shared by every Playthrough: it is authored data, not per-Playthrough state, and it carries no scene
|
||||||
|
or presentation detail.
|
||||||
|
_Avoid_: Level list, level order, world, chapter
|
||||||
|
|
||||||
|
**Cleared Levels**:
|
||||||
|
The Levels a Playthrough has completed at least once. Gates both campaign progression and level select; whether a
|
||||||
|
Level is reachable is computed from this, never stored alongside it.
|
||||||
|
_Avoid_: Unlocked levels, completed levels, available levels
|
||||||
|
|
||||||
|
**Skill**:
|
||||||
|
An ability the player can own and upgrade. `SkillData` is the authored asset describing one (name, icon, upgrade
|
||||||
|
table) and is immutable at runtime; the level the player has reached in it belongs to the Playthrough.
|
||||||
|
_Avoid_: Ability, power-up, upgrade (for the skill itself)
|
||||||
|
|
||||||
|
**Movement Ability**:
|
||||||
|
A composable behaviour attached to the player controller (jump, wall-jump, gravity). Unrelated to Skill —
|
||||||
|
authored per movement preset, not owned, not upgraded, not persisted.
|
||||||
|
_Avoid_: Skill, movement skill
|
||||||
|
|
||||||
|
**Best Time**:
|
||||||
|
The fastest recorded completion of a Level, from a First Clear or a Replay alike. Belongs to the Profile, so it
|
||||||
|
survives New Game and is shared by every Playthrough. Stored alongside, but distinct from, the ghost replay
|
||||||
|
frames that produced it.
|
||||||
|
_Avoid_: Personal best, high score, record
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Reads may cross scopes freely — affordability spans Attempt and Playthrough. Writes must not.
|
||||||
|
- Derived facts are computed, never stored. Level reachability is derived from Cleared Levels; total coins from
|
||||||
|
Attempt plus Bank. Storing either alongside its source creates the second source of truth this context exists
|
||||||
|
to prevent.
|
||||||
|
- Achievements are mirrored to Steam, which is per-account and cannot be un-earned. That is why they are Profile
|
||||||
|
and not Playthrough: a per-Playthrough achievement list would contradict Steam permanently.
|
||||||
|
- Nothing in this context may reference Godot. Its correctness must be checkable without a scene tree.
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
<Project Sdk="Godot.NET.Sdk/4.7.1">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||||
@@ -8,6 +8,18 @@
|
|||||||
<PackageReference Include="Facepunch.Steamworks" Version="2.3.3" />
|
<PackageReference Include="Facepunch.Steamworks" Version="2.3.3" />
|
||||||
<PackageReference Include="Facepunch.Steamworks.Dlls" Version="2.3.2" />
|
<PackageReference Include="Facepunch.Steamworks.Dlls" Version="2.3.2" />
|
||||||
<PackageReference Include="Facepunch.Steamworks.Library" Version="2.3.3" />
|
<PackageReference Include="Facepunch.Steamworks.Library" Version="2.3.3" />
|
||||||
<PackageReference Include="LimboConsole.Sharp" Version="0.0.1-beta-008" />
|
</ItemGroup>
|
||||||
|
|
||||||
|
<!-- Godot.NET.Sdk globs **/*.cs, which would compile the progression projects into this
|
||||||
|
assembly as well as their own. They are separate projects; exclude them here. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="progression\**" />
|
||||||
|
<Compile Remove="progression.tests\**" />
|
||||||
|
<Compile Remove="progression.adapters\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="progression\Mr.BrickAdventures.Progression.csproj" />
|
||||||
|
<ProjectReference Include="progression.adapters\Mr.BrickAdventures.Progression.Adapters.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,19 +1,139 @@
|
|||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio 2012
|
# Visual Studio 2012
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr. Brick Adventures", "Mr. Brick Adventures.csproj", "{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr. Brick Adventures", "Mr. Brick Adventures.csproj", "{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "progression", "progression", "{73CE8E92-AE0D-34EB-F3CD-A6392CC7A1AA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr.BrickAdventures.Progression", "progression\Mr.BrickAdventures.Progression.csproj", "{4F115C37-24F6-4933-9086-ED5925EE5106}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "progression.tests", "progression.tests", "{B756B2BA-9E41-468E-DBBE-5BA8CC0A1E41}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr.BrickAdventures.Progression.Tests", "progression.tests\Mr.BrickAdventures.Progression.Tests.csproj", "{A4660883-E42A-4D04-99A3-770F36E9D861}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "progression.adapters", "progression.adapters", "{9D5C8041-6C54-0A01-82AE-FC7E0DE079DA}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr.BrickAdventures.Progression.Adapters", "progression.adapters\Mr.BrickAdventures.Progression.Adapters.csproj", "{045D9BF4-8E63-4F14-84F4-F8C55073244C}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
Debug|x64 = Debug|x64
|
||||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
Debug|x86 = Debug|x86
|
||||||
|
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||||
|
ExportDebug|x64 = ExportDebug|x64
|
||||||
|
ExportDebug|x86 = ExportDebug|x86
|
||||||
|
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||||
|
ExportRelease|x64 = ExportRelease|x64
|
||||||
|
ExportRelease|x86 = ExportRelease|x86
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|x64.ActiveCfg = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|x64.Build.0 = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|x86.ActiveCfg = ExportDebug|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportDebug|x86.Build.0 = ExportDebug|Any CPU
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
|
||||||
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|x64.ActiveCfg = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|x64.Build.0 = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|x86.ActiveCfg = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|x86.Build.0 = ExportRelease|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportDebug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|x64.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.ExportRelease|x86.Build.0 = Debug|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{4F115C37-24F6-4933-9086-ED5925EE5106} = {73CE8E92-AE0D-34EB-F3CD-A6392CC7A1AA}
|
||||||
|
{A4660883-E42A-4D04-99A3-770F36E9D861} = {B756B2BA-9E41-468E-DBBE-5BA8CC0A1E41}
|
||||||
|
{045D9BF4-8E63-4F14-84F4-F8C55073244C} = {9D5C8041-6C54-0A01-82AE-FC7E0DE079DA}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArea2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F116d8c5f8dae51522ba398e1d89e3d4722f4af7b6e7f071196b928be44af7_003FArea2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACamera2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fa2e12a1a67ad701a97608de6be85250e3e353951ecf8058a02c703490c753_003FCamera2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACamera2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fa2e12a1a67ad701a97608de6be85250e3e353951ecf8058a02c703490c753_003FCamera2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACanvasItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fef7b819b226fab796d1dfe66d415dd7510bcac87675020ddb8f03a828e763_003FCanvasItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACanvasItem_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fef7b819b226fab796d1dfe66d415dd7510bcac87675020ddb8f03a828e763_003FCanvasItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACecovym_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Ftmp_003FJetBrainsPerUserTemp_002D1000_002D1_003FSandboxFiles_003FSadijuw_003FCecovym_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACecovym_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002E_002E_003F_002E_002E_003Ftmp_003FJetBrainsPerUserTemp_002D1000_002D1_003FSandboxFiles_003FSadijuw_003FCecovym_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
@@ -10,5 +11,7 @@
|
|||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe747192abb38e2df82cbdb37e721567726f559914a7b81f8b26ba537de632f4_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fe747192abb38e2df82cbdb37e721567726f559914a7b81f8b26ba537de632f4_003FList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F80d9408eb7280c15eb4a12b61cdf8f7f1b0c5a2_003FMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F80d9408eb7280c15eb4a12b61cdf8f7f1b0c5a2_003FMr_002EBrickAdventures_002Escripts_002Ecomponents_002ECollectableComponent_005FScriptSignals_002Egenerated_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F86db9cd834346aad02d74c1b66dd9c64d6ef3147435dd9c9c9477b48f7_003FNode2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ANode2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F86db9cd834346aad02d74c1b66dd9c64d6ef3147435dd9c9c9477b48f7_003FNode2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APhysicsBody2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F4fcd28ddaffa68e6fe53288161b788dea7d402b4a41b9d9f0f0f2c52f9af075_003FPhysicsBody2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARectangleShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fa1cc98873548652da0c14ecefa4737431426fcbb24a7f0641e3d9c266c3_003FRectangleShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARectangleShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003Fa1cc98873548652da0c14ecefa4737431426fcbb24a7f0641e3d9c266c3_003FRectangleShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ARigidBody2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003F7bc3ccc1ac5bbc68933d64c7b7eb5ab4aecde2b73c686dd6495b68bdf08ba5b2_003FRigidBody2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3671dbbd9b17cdf2bf9075b468b6bd7e3ab13fc3be7a116484085d3b6cc9fe_003FShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F3671dbbd9b17cdf2bf9075b468b6bd7e3ab13fc3be7a116484085d3b6cc9fe_003FShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
|
||||||
131
addons/ca_level_generator/CaGenerator.cs
Normal file
131
addons/ca_level_generator/CaGenerator.cs
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
public static class CaGenerator
|
||||||
|
{
|
||||||
|
public static bool[,] Generate(CaGeneratorSettings s)
|
||||||
|
{
|
||||||
|
var rng = new Random(s.Seed);
|
||||||
|
return s.Mode switch
|
||||||
|
{
|
||||||
|
CaMode.Cave => GenerateCave(s, rng),
|
||||||
|
CaMode.Platform => GeneratePlatform(s, rng),
|
||||||
|
CaMode.Terrain => GenerateTerrain(s, rng),
|
||||||
|
_ => GenerateCave(s, rng),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool[,] Smooth(bool[,] grid) => SmoothPass(grid, 5);
|
||||||
|
|
||||||
|
// ── Cave ──────────────────────────────────────────────────────────────
|
||||||
|
private static bool[,] GenerateCave(CaGeneratorSettings s, Random rng)
|
||||||
|
{
|
||||||
|
var grid = RandomFill(s.Width, s.Height, s.FillDensity, rng);
|
||||||
|
if (s.BorderWalls) EnforceBorder(grid);
|
||||||
|
for (int i = 0; i < s.SmoothingPasses; i++)
|
||||||
|
{
|
||||||
|
grid = SmoothPass(grid, 5);
|
||||||
|
if (s.BorderWalls) EnforceBorder(grid);
|
||||||
|
}
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Platform ──────────────────────────────────────────────────────────
|
||||||
|
private static bool[,] GeneratePlatform(CaGeneratorSettings s, Random rng)
|
||||||
|
{
|
||||||
|
int w = s.Width, h = s.Height;
|
||||||
|
var grid = new bool[w, h];
|
||||||
|
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
grid[x, h - 1] = true;
|
||||||
|
grid[x, h - 2] = true;
|
||||||
|
|
||||||
|
for (int y = 0; y < h - 2; y++)
|
||||||
|
{
|
||||||
|
// Density linearly increases from 10% at top to 60% at bottom,
|
||||||
|
// scaled by user's FillDensity relative to default 0.48.
|
||||||
|
float t = (float)y / (h - 3);
|
||||||
|
float rowDensity = (0.1f + t * 0.5f) * (s.FillDensity / 0.48f);
|
||||||
|
grid[x, y] = rng.NextDouble() < rowDensity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < s.SmoothingPasses; i++)
|
||||||
|
{
|
||||||
|
// Threshold 4 (not 5): horizontal platform groups survive, isolated
|
||||||
|
// vertical pixels collapse into flat platforms.
|
||||||
|
grid = SmoothPass(grid, 4);
|
||||||
|
for (int x = 0; x < w; x++) { grid[x, h - 1] = true; grid[x, h - 2] = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Terrain ───────────────────────────────────────────────────────────
|
||||||
|
private static bool[,] GenerateTerrain(CaGeneratorSettings s, Random rng)
|
||||||
|
{
|
||||||
|
int w = s.Width, h = s.Height;
|
||||||
|
var grid = new bool[w, h];
|
||||||
|
|
||||||
|
// Start terrain surface at ~60% from bottom (40% from top)
|
||||||
|
int surfaceRow = (int)(h * 0.4f);
|
||||||
|
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
{
|
||||||
|
int delta = rng.Next(-2, 3);
|
||||||
|
surfaceRow = Math.Clamp(surfaceRow + delta, h / 6, h - 2);
|
||||||
|
|
||||||
|
for (int y = surfaceRow; y < h; y++)
|
||||||
|
grid[x, y] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < s.SmoothingPasses; i++)
|
||||||
|
grid = SmoothPass(grid, 5);
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Shared helpers ────────────────────────────────────────────────
|
||||||
|
private static bool[,] RandomFill(int w, int h, float density, Random rng)
|
||||||
|
{
|
||||||
|
var grid = new bool[w, h];
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
grid[x, y] = rng.NextDouble() < density;
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool[,] SmoothPass(bool[,] grid, int threshold)
|
||||||
|
{
|
||||||
|
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||||||
|
var next = new bool[w, h];
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
next[x, y] = CountSolidNeighbors(grid, x, y) >= threshold;
|
||||||
|
return next;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CountSolidNeighbors(bool[,] grid, int cx, int cy)
|
||||||
|
{
|
||||||
|
int w = grid.GetLength(0), h = grid.GetLength(1), count = 0;
|
||||||
|
for (int dx = -1; dx <= 1; dx++)
|
||||||
|
for (int dy = -1; dy <= 1; dy++)
|
||||||
|
{
|
||||||
|
if (dx == 0 && dy == 0) continue;
|
||||||
|
int nx = cx + dx, ny = cy + dy;
|
||||||
|
// Out-of-bounds counts as solid (closes cave edges naturally)
|
||||||
|
if (nx < 0 || nx >= w || ny < 0 || ny >= h) count++;
|
||||||
|
else if (grid[nx, ny]) count++;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnforceBorder(bool[,] grid)
|
||||||
|
{
|
||||||
|
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||||||
|
for (int x = 0; x < w; x++) { grid[x, 0] = true; grid[x, h - 1] = true; }
|
||||||
|
for (int y = 0; y < h; y++) { grid[0, y] = true; grid[w - 1, y] = true; }
|
||||||
|
}
|
||||||
|
}
|
||||||
1
addons/ca_level_generator/CaGenerator.cs.uid
Normal file
1
addons/ca_level_generator/CaGenerator.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://exr10b7nmkgn
|
||||||
23
addons/ca_level_generator/CaGeneratorSettings.cs
Normal file
23
addons/ca_level_generator/CaGeneratorSettings.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
public enum CaMode { Cave, Platform, Terrain }
|
||||||
|
|
||||||
|
public class CaGeneratorSettings
|
||||||
|
{
|
||||||
|
public CaMode Mode { get; set; } = CaMode.Cave;
|
||||||
|
public int Width { get; set; } = 40;
|
||||||
|
public int Height { get; set; } = 22;
|
||||||
|
public Vector2I Offset { get; set; } = Vector2I.Zero;
|
||||||
|
public float FillDensity { get; set; } = 0.48f;
|
||||||
|
public int SmoothingPasses { get; set; } = 4;
|
||||||
|
public bool BorderWalls { get; set; } = true;
|
||||||
|
public int Seed { get; set; } = 12345;
|
||||||
|
// TerrainSet == -1 → raw tile mode
|
||||||
|
public int TerrainSet { get; set; } = 0;
|
||||||
|
public int Terrain { get; set; } = 0;
|
||||||
|
// Used only when TerrainSet == -1
|
||||||
|
public int SourceId { get; set; } = 0;
|
||||||
|
public Vector2I AtlasCoords { get; set; } = Vector2I.Zero;
|
||||||
|
}
|
||||||
1
addons/ca_level_generator/CaGeneratorSettings.cs.uid
Normal file
1
addons/ca_level_generator/CaGeneratorSettings.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://isnscgr1thvc
|
||||||
79
addons/ca_level_generator/CaGeneratorTestRunner.cs
Normal file
79
addons/ca_level_generator/CaGeneratorTestRunner.cs
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#if TOOLS
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
[Tool]
|
||||||
|
public partial class CaGeneratorTestRunner : Node
|
||||||
|
{
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
int pass = 0, fail = 0;
|
||||||
|
|
||||||
|
void Assert(bool cond, string msg)
|
||||||
|
{
|
||||||
|
if (cond) { GD.Print($" PASS: {msg}"); pass++; }
|
||||||
|
else { GD.PrintErr($" FAIL: {msg}"); fail++; }
|
||||||
|
}
|
||||||
|
|
||||||
|
GD.Print("=== CaGenerator Tests ===");
|
||||||
|
|
||||||
|
// Grid dimensions
|
||||||
|
var s = new CaGeneratorSettings { Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
|
||||||
|
var g = CaGenerator.Generate(s);
|
||||||
|
Assert(g.GetLength(0) == 20, "Width == 20");
|
||||||
|
Assert(g.GetLength(1) == 15, "Height == 15");
|
||||||
|
|
||||||
|
// Determinism
|
||||||
|
var g2 = CaGenerator.Generate(s);
|
||||||
|
bool det = true;
|
||||||
|
for (int x = 0; x < 20 && det; x++)
|
||||||
|
for (int y = 0; y < 15 && det; y++)
|
||||||
|
if (g[x, y] != g2[x, y]) det = false;
|
||||||
|
Assert(det, "Same seed → same output");
|
||||||
|
|
||||||
|
// Different seeds → different output
|
||||||
|
var s2 = new CaGeneratorSettings { Width = 20, Height = 15, SmoothingPasses = 0, Seed = 99 };
|
||||||
|
var g3 = CaGenerator.Generate(s2);
|
||||||
|
bool diff = false;
|
||||||
|
for (int x = 0; x < 20 && !diff; x++)
|
||||||
|
for (int y = 0; y < 15 && !diff; y++)
|
||||||
|
if (g[x, y] != g3[x, y]) diff = true;
|
||||||
|
Assert(diff, "Different seeds → different output");
|
||||||
|
|
||||||
|
// Cave border walls
|
||||||
|
var sb = new CaGeneratorSettings { Width = 20, Height = 15, BorderWalls = true, SmoothingPasses = 0, Seed = 42 };
|
||||||
|
var gb = CaGenerator.Generate(sb);
|
||||||
|
bool border = true;
|
||||||
|
for (int x = 0; x < 20; x++) if (!gb[x, 0] || !gb[x, 14]) { border = false; break; }
|
||||||
|
for (int y = 0; y < 15; y++) if (!gb[0, y] || !gb[19, y]) { border = false; break; }
|
||||||
|
Assert(border, "Cave: border cells all solid");
|
||||||
|
|
||||||
|
// Platform floor
|
||||||
|
var sp = new CaGeneratorSettings { Mode = CaMode.Platform, Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
|
||||||
|
var gp = CaGenerator.Generate(sp);
|
||||||
|
bool floor = true;
|
||||||
|
for (int x = 0; x < 20; x++) if (!gp[x, 14] || !gp[x, 13]) { floor = false; break; }
|
||||||
|
Assert(floor, "Platform: bottom 2 rows solid");
|
||||||
|
|
||||||
|
// Platform: top row not all solid (mostly empty at top)
|
||||||
|
int topSolid = 0;
|
||||||
|
for (int x = 0; x < 20; x++) if (gp[x, 0]) topSolid++;
|
||||||
|
Assert(topSolid < 15, "Platform: top row mostly empty");
|
||||||
|
|
||||||
|
// Terrain: bottom always solid
|
||||||
|
var st = new CaGeneratorSettings { Mode = CaMode.Terrain, Width = 20, Height = 15, SmoothingPasses = 0, Seed = 42 };
|
||||||
|
var gt = CaGenerator.Generate(st);
|
||||||
|
bool terrBottom = true;
|
||||||
|
for (int x = 0; x < 20; x++) if (!gt[x, 14]) { terrBottom = false; break; }
|
||||||
|
Assert(terrBottom, "Terrain: bottom row always solid");
|
||||||
|
|
||||||
|
// Smooth: output same dimensions
|
||||||
|
var smoothed = CaGenerator.Smooth(g);
|
||||||
|
Assert(smoothed.GetLength(0) == 20, "Smooth: width preserved");
|
||||||
|
Assert(smoothed.GetLength(1) == 15, "Smooth: height preserved");
|
||||||
|
|
||||||
|
GD.Print($"=== Results: {pass} passed, {fail} failed ===");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1
addons/ca_level_generator/CaGeneratorTestRunner.cs.uid
Normal file
1
addons/ca_level_generator/CaGeneratorTestRunner.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://6y4snwf31boh
|
||||||
360
addons/ca_level_generator/CaLevelGeneratorDock.cs
Normal file
360
addons/ca_level_generator/CaLevelGeneratorDock.cs
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
#if TOOLS
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
[Tool]
|
||||||
|
public partial class CaLevelGeneratorDock : Control
|
||||||
|
{
|
||||||
|
public CaLevelGeneratorPlugin Plugin { get; set; }
|
||||||
|
|
||||||
|
// Controls referenced in Task 8 wiring
|
||||||
|
private OptionButton _layerDropdown;
|
||||||
|
private OptionButton _terrainDropdown;
|
||||||
|
private HBoxContainer _rawTileRow;
|
||||||
|
private SpinBox _sourceIdSpin;
|
||||||
|
private SpinBox _atlasXSpin;
|
||||||
|
private SpinBox _atlasYSpin;
|
||||||
|
private Button _caveBtn;
|
||||||
|
private Button _platformBtn;
|
||||||
|
private Button _terrainBtn;
|
||||||
|
private SpinBox _widthSpin;
|
||||||
|
private SpinBox _heightSpin;
|
||||||
|
private SpinBox _offsetXSpin;
|
||||||
|
private SpinBox _offsetYSpin;
|
||||||
|
private HSlider _densitySlider;
|
||||||
|
private Label _densityLabel;
|
||||||
|
private HSlider _smoothPassSlider;
|
||||||
|
private Label _smoothPassLabel;
|
||||||
|
private CheckBox _borderCheck;
|
||||||
|
private SpinBox _seedSpin;
|
||||||
|
private Button _genBtn;
|
||||||
|
private Button _smoothBtn;
|
||||||
|
private Button _clearBtn;
|
||||||
|
|
||||||
|
private CaMode _mode = CaMode.Cave;
|
||||||
|
|
||||||
|
private System.Collections.Generic.List<TileMapLayer> _layers = new();
|
||||||
|
private TileMapLayer _selectedLayer;
|
||||||
|
|
||||||
|
public override void _Ready()
|
||||||
|
{
|
||||||
|
CustomMinimumSize = new Vector2(200, 0);
|
||||||
|
var scroll = new ScrollContainer();
|
||||||
|
scroll.SetAnchorsAndOffsetsPreset(LayoutPreset.FullRect);
|
||||||
|
scroll.SizeFlagsVertical = SizeFlags.ExpandFill;
|
||||||
|
AddChild(scroll);
|
||||||
|
|
||||||
|
var vbox = new VBoxContainer();
|
||||||
|
vbox.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||||
|
scroll.AddChild(vbox);
|
||||||
|
|
||||||
|
// Target Layer
|
||||||
|
vbox.AddChild(new Label { Text = "Target Layer" });
|
||||||
|
_layerDropdown = new OptionButton();
|
||||||
|
_layerDropdown.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||||
|
vbox.AddChild(_layerDropdown);
|
||||||
|
|
||||||
|
// Terrain
|
||||||
|
vbox.AddChild(new Label { Text = "Terrain" });
|
||||||
|
_terrainDropdown = new OptionButton();
|
||||||
|
_terrainDropdown.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||||
|
vbox.AddChild(_terrainDropdown);
|
||||||
|
|
||||||
|
// Raw tile row (hidden when terrain is available)
|
||||||
|
_rawTileRow = new HBoxContainer();
|
||||||
|
_rawTileRow.Visible = false;
|
||||||
|
_rawTileRow.AddChild(new Label { Text = "Src" });
|
||||||
|
_sourceIdSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
|
||||||
|
_rawTileRow.AddChild(_sourceIdSpin);
|
||||||
|
_rawTileRow.AddChild(new Label { Text = "X" });
|
||||||
|
_atlasXSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
|
||||||
|
_rawTileRow.AddChild(_atlasXSpin);
|
||||||
|
_rawTileRow.AddChild(new Label { Text = "Y" });
|
||||||
|
_atlasYSpin = new SpinBox { MinValue = 0, MaxValue = 99, Value = 0 };
|
||||||
|
_rawTileRow.AddChild(_atlasYSpin);
|
||||||
|
vbox.AddChild(_rawTileRow);
|
||||||
|
|
||||||
|
// Mode toggle
|
||||||
|
vbox.AddChild(new Label { Text = "Mode" });
|
||||||
|
var modeRow = new HBoxContainer();
|
||||||
|
modeRow.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||||
|
_caveBtn = MakeModeButton("Cave", () => SetMode(CaMode.Cave));
|
||||||
|
_platformBtn = MakeModeButton("Platform", () => SetMode(CaMode.Platform));
|
||||||
|
_terrainBtn = MakeModeButton("Terrain", () => SetMode(CaMode.Terrain));
|
||||||
|
_caveBtn.ButtonPressed = true;
|
||||||
|
modeRow.AddChild(_caveBtn);
|
||||||
|
modeRow.AddChild(_platformBtn);
|
||||||
|
modeRow.AddChild(_terrainBtn);
|
||||||
|
vbox.AddChild(modeRow);
|
||||||
|
|
||||||
|
vbox.AddChild(new HSeparator());
|
||||||
|
|
||||||
|
// Width / Height
|
||||||
|
var dimRow = new HBoxContainer();
|
||||||
|
dimRow.AddChild(MakeSpinColumn("Width", out _widthSpin, 5, 500, 40));
|
||||||
|
dimRow.AddChild(MakeSpinColumn("Height", out _heightSpin, 5, 500, 22));
|
||||||
|
vbox.AddChild(dimRow);
|
||||||
|
|
||||||
|
// Offset X / Y
|
||||||
|
var offRow = new HBoxContainer();
|
||||||
|
offRow.AddChild(MakeSpinColumn("Offset X", out _offsetXSpin, -9999, 9999, 0));
|
||||||
|
offRow.AddChild(MakeSpinColumn("Offset Y", out _offsetYSpin, -9999, 9999, 0));
|
||||||
|
vbox.AddChild(offRow);
|
||||||
|
|
||||||
|
// Fill Density
|
||||||
|
var densHeader = new HBoxContainer();
|
||||||
|
densHeader.AddChild(new Label { Text = "Fill Density", SizeFlagsHorizontal = SizeFlags.ExpandFill });
|
||||||
|
_densityLabel = new Label { Text = "48%" };
|
||||||
|
densHeader.AddChild(_densityLabel);
|
||||||
|
vbox.AddChild(densHeader);
|
||||||
|
_densitySlider = new HSlider { MinValue = 0, MaxValue = 100, Value = 48, Step = 1 };
|
||||||
|
_densitySlider.ValueChanged += v => _densityLabel.Text = $"{(int)v}%";
|
||||||
|
vbox.AddChild(_densitySlider);
|
||||||
|
|
||||||
|
// Smoothing Passes
|
||||||
|
var smoothHeader = new HBoxContainer();
|
||||||
|
smoothHeader.AddChild(new Label { Text = "Smoothing Passes", SizeFlagsHorizontal = SizeFlags.ExpandFill });
|
||||||
|
_smoothPassLabel = new Label { Text = "4" };
|
||||||
|
smoothHeader.AddChild(_smoothPassLabel);
|
||||||
|
vbox.AddChild(smoothHeader);
|
||||||
|
_smoothPassSlider = new HSlider { MinValue = 0, MaxValue = 10, Value = 4, Step = 1 };
|
||||||
|
_smoothPassSlider.ValueChanged += v => _smoothPassLabel.Text = $"{(int)v}";
|
||||||
|
vbox.AddChild(_smoothPassSlider);
|
||||||
|
|
||||||
|
// Border walls
|
||||||
|
_borderCheck = new CheckBox { Text = "Solid border walls", ButtonPressed = true };
|
||||||
|
vbox.AddChild(_borderCheck);
|
||||||
|
|
||||||
|
// Seed
|
||||||
|
vbox.AddChild(new Label { Text = "Seed" });
|
||||||
|
var seedRow = new HBoxContainer();
|
||||||
|
_seedSpin = new SpinBox { MinValue = 0, MaxValue = int.MaxValue, Value = 12345,
|
||||||
|
AllowGreater = true, SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
var randBtn = new Button { Text = "🎲" };
|
||||||
|
randBtn.Pressed += () => _seedSpin.Value = GD.Randi();
|
||||||
|
seedRow.AddChild(_seedSpin);
|
||||||
|
seedRow.AddChild(randBtn);
|
||||||
|
vbox.AddChild(seedRow);
|
||||||
|
|
||||||
|
vbox.AddChild(new HSeparator());
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
_genBtn = new Button { Text = "▶ Generate", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
vbox.AddChild(_genBtn);
|
||||||
|
|
||||||
|
var actionRow = new HBoxContainer();
|
||||||
|
actionRow.SizeFlagsHorizontal = SizeFlags.ExpandFill;
|
||||||
|
_smoothBtn = new Button { Text = "✦ Smooth", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
_clearBtn = new Button { Text = "✕ Clear", SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
actionRow.AddChild(_smoothBtn);
|
||||||
|
actionRow.AddChild(_clearBtn);
|
||||||
|
vbox.AddChild(actionRow);
|
||||||
|
|
||||||
|
_layerDropdown.ItemSelected += OnLayerSelected;
|
||||||
|
_terrainDropdown.ItemSelected += _ => SaveSettings();
|
||||||
|
_genBtn.Pressed += OnGenerate;
|
||||||
|
_smoothBtn.Pressed += OnSmooth;
|
||||||
|
_clearBtn.Pressed += OnClear;
|
||||||
|
|
||||||
|
LoadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnSceneChanged(Node sceneRoot)
|
||||||
|
{
|
||||||
|
ScanLayers(sceneRoot);
|
||||||
|
RestoreLastLayer(sceneRoot?.SceneFilePath ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMode(CaMode mode)
|
||||||
|
{
|
||||||
|
_mode = mode;
|
||||||
|
_caveBtn.ButtonPressed = mode == CaMode.Cave;
|
||||||
|
_platformBtn.ButtonPressed = mode == CaMode.Platform;
|
||||||
|
_terrainBtn.ButtonPressed = mode == CaMode.Terrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Button MakeModeButton(string text, System.Action onPressed)
|
||||||
|
{
|
||||||
|
var btn = new Button { Text = text, ToggleMode = true,
|
||||||
|
SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
btn.Pressed += onPressed;
|
||||||
|
return btn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Control MakeSpinColumn(string label, out SpinBox spin,
|
||||||
|
double min, double max, double value)
|
||||||
|
{
|
||||||
|
var col = new VBoxContainer { SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
col.AddChild(new Label { Text = label });
|
||||||
|
spin = new SpinBox { MinValue = min, MaxValue = max, Value = value,
|
||||||
|
SizeFlagsHorizontal = SizeFlags.ExpandFill };
|
||||||
|
col.AddChild(spin);
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScanLayers(Node root)
|
||||||
|
{
|
||||||
|
_layers.Clear();
|
||||||
|
_layerDropdown.Clear();
|
||||||
|
if (root == null) return;
|
||||||
|
CollectLayers(root);
|
||||||
|
foreach (var layer in _layers)
|
||||||
|
_layerDropdown.AddItem(layer.Name);
|
||||||
|
if (_layers.Count > 0) OnLayerSelected(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CollectLayers(Node node)
|
||||||
|
{
|
||||||
|
if (node is TileMapLayer layer) _layers.Add(layer);
|
||||||
|
foreach (Node child in node.GetChildren()) CollectLayers(child);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLayerSelected(long index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _layers.Count) return;
|
||||||
|
_selectedLayer = _layers[(int)index];
|
||||||
|
PopulateTerrainDropdown(_selectedLayer);
|
||||||
|
|
||||||
|
var sceneRoot = EditorInterface.Singleton.GetEditedSceneRoot();
|
||||||
|
if (sceneRoot != null)
|
||||||
|
{
|
||||||
|
var key = $"ca_generator/last_layer_{sceneRoot.SceneFilePath}";
|
||||||
|
EditorInterface.Singleton.GetEditorSettings()
|
||||||
|
.SetSetting(key, _selectedLayer.GetPath().ToString());
|
||||||
|
}
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreLastLayer(string sceneFilePath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sceneFilePath) || _layers.Count == 0) return;
|
||||||
|
var key = $"ca_generator/last_layer_{sceneFilePath}";
|
||||||
|
var es = EditorInterface.Singleton.GetEditorSettings();
|
||||||
|
if (!es.HasSetting(key)) return;
|
||||||
|
var path = es.GetSetting(key).AsString();
|
||||||
|
for (int i = 0; i < _layers.Count; i++)
|
||||||
|
{
|
||||||
|
if (_layers[i].GetPath().ToString() == path)
|
||||||
|
{
|
||||||
|
_layerDropdown.Select(i);
|
||||||
|
OnLayerSelected(i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateTerrainDropdown(TileMapLayer layer)
|
||||||
|
{
|
||||||
|
_terrainDropdown.Clear();
|
||||||
|
var tileSet = layer.TileSet;
|
||||||
|
bool hasTerrains = false;
|
||||||
|
|
||||||
|
if (tileSet != null)
|
||||||
|
{
|
||||||
|
for (int ts = 0; ts < tileSet.GetTerrainSetsCount(); ts++)
|
||||||
|
{
|
||||||
|
for (int t = 0; t < tileSet.GetTerrainsCount(ts); t++)
|
||||||
|
{
|
||||||
|
_terrainDropdown.AddItem(tileSet.GetTerrainName(ts, t));
|
||||||
|
_terrainDropdown.SetItemMetadata(
|
||||||
|
_terrainDropdown.ItemCount - 1,
|
||||||
|
Variant.From((ts << 16) | t));
|
||||||
|
hasTerrains = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_terrainDropdown.Visible = hasTerrains;
|
||||||
|
_rawTileRow.Visible = !hasTerrains;
|
||||||
|
}
|
||||||
|
|
||||||
|
private CaGeneratorSettings BuildSettings()
|
||||||
|
{
|
||||||
|
int terrainSet = -1, terrain = 0;
|
||||||
|
if (_terrainDropdown.Visible && _terrainDropdown.ItemCount > 0)
|
||||||
|
{
|
||||||
|
int packed = _terrainDropdown.GetSelectedMetadata().AsInt32();
|
||||||
|
terrainSet = packed >> 16;
|
||||||
|
terrain = packed & 0xFFFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new CaGeneratorSettings
|
||||||
|
{
|
||||||
|
Mode = _mode,
|
||||||
|
Width = (int)_widthSpin.Value,
|
||||||
|
Height = (int)_heightSpin.Value,
|
||||||
|
Offset = new Vector2I((int)_offsetXSpin.Value, (int)_offsetYSpin.Value),
|
||||||
|
FillDensity = (float)_densitySlider.Value / 100f,
|
||||||
|
SmoothingPasses = (int)_smoothPassSlider.Value,
|
||||||
|
BorderWalls = _borderCheck.ButtonPressed,
|
||||||
|
Seed = (int)_seedSpin.Value,
|
||||||
|
TerrainSet = terrainSet,
|
||||||
|
Terrain = terrain,
|
||||||
|
SourceId = (int)_sourceIdSpin.Value,
|
||||||
|
AtlasCoords = new Vector2I((int)_atlasXSpin.Value, (int)_atlasYSpin.Value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnGenerate()
|
||||||
|
{
|
||||||
|
if (_selectedLayer == null) return;
|
||||||
|
var settings = BuildSettings();
|
||||||
|
var grid = CaGenerator.Generate(settings);
|
||||||
|
TilemapPainter.Paint(_selectedLayer, grid, settings, Plugin.UndoRedo);
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSmooth()
|
||||||
|
{
|
||||||
|
if (_selectedLayer == null) return;
|
||||||
|
TilemapPainter.Smooth(_selectedLayer, BuildSettings(), Plugin.UndoRedo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClear()
|
||||||
|
{
|
||||||
|
if (_selectedLayer == null) return;
|
||||||
|
TilemapPainter.Clear(_selectedLayer, BuildSettings(), Plugin.UndoRedo);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveSettings()
|
||||||
|
{
|
||||||
|
var es = EditorInterface.Singleton.GetEditorSettings();
|
||||||
|
es.SetSetting("ca_generator/mode", (int)_mode);
|
||||||
|
es.SetSetting("ca_generator/width", (int)_widthSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/height", (int)_heightSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/offset_x", (int)_offsetXSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/offset_y", (int)_offsetYSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/fill_density", _densitySlider.Value);
|
||||||
|
es.SetSetting("ca_generator/smoothing_passes", (int)_smoothPassSlider.Value);
|
||||||
|
es.SetSetting("ca_generator/border_walls", _borderCheck.ButtonPressed);
|
||||||
|
es.SetSetting("ca_generator/seed", (int)_seedSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/source_id", (int)_sourceIdSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/atlas_x", (int)_atlasXSpin.Value);
|
||||||
|
es.SetSetting("ca_generator/atlas_y", (int)_atlasYSpin.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadSettings()
|
||||||
|
{
|
||||||
|
var es = EditorInterface.Singleton.GetEditorSettings();
|
||||||
|
if (es == null) return;
|
||||||
|
|
||||||
|
SetMode((CaMode)(es.HasSetting("ca_generator/mode") ? es.GetSetting("ca_generator/mode").AsInt32() : 0));
|
||||||
|
_widthSpin.Value = es.HasSetting("ca_generator/width") ? es.GetSetting("ca_generator/width").AsDouble() : 40.0;
|
||||||
|
_heightSpin.Value = es.HasSetting("ca_generator/height") ? es.GetSetting("ca_generator/height").AsDouble() : 22.0;
|
||||||
|
_offsetXSpin.Value = es.HasSetting("ca_generator/offset_x") ? es.GetSetting("ca_generator/offset_x").AsDouble() : 0.0;
|
||||||
|
_offsetYSpin.Value = es.HasSetting("ca_generator/offset_y") ? es.GetSetting("ca_generator/offset_y").AsDouble() : 0.0;
|
||||||
|
_densitySlider.Value = es.HasSetting("ca_generator/fill_density") ? es.GetSetting("ca_generator/fill_density").AsDouble() : 48.0;
|
||||||
|
_smoothPassSlider.Value = es.HasSetting("ca_generator/smoothing_passes") ? es.GetSetting("ca_generator/smoothing_passes").AsDouble() : 4.0;
|
||||||
|
_borderCheck.ButtonPressed = es.HasSetting("ca_generator/border_walls") ? es.GetSetting("ca_generator/border_walls").AsBool() : true;
|
||||||
|
_seedSpin.Value = es.HasSetting("ca_generator/seed") ? es.GetSetting("ca_generator/seed").AsDouble() : 12345.0;
|
||||||
|
|
||||||
|
_sourceIdSpin.Value = es.HasSetting("ca_generator/source_id") ? es.GetSetting("ca_generator/source_id").AsDouble() : 0.0;
|
||||||
|
_atlasXSpin.Value = es.HasSetting("ca_generator/atlas_x") ? es.GetSetting("ca_generator/atlas_x").AsDouble() : 0.0;
|
||||||
|
_atlasYSpin.Value = es.HasSetting("ca_generator/atlas_y") ? es.GetSetting("ca_generator/atlas_y").AsDouble() : 0.0;
|
||||||
|
|
||||||
|
_densityLabel.Text = $"{(int)_densitySlider.Value}%";
|
||||||
|
_smoothPassLabel.Text = $"{(int)_smoothPassSlider.Value}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1
addons/ca_level_generator/CaLevelGeneratorDock.cs.uid
Normal file
1
addons/ca_level_generator/CaLevelGeneratorDock.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://dnuvfv3rgyywd
|
||||||
37
addons/ca_level_generator/CaLevelGeneratorPlugin.cs
Normal file
37
addons/ca_level_generator/CaLevelGeneratorPlugin.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
#if TOOLS
|
||||||
|
using Godot;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
[Tool]
|
||||||
|
public partial class CaLevelGeneratorPlugin : EditorPlugin
|
||||||
|
{
|
||||||
|
private EditorDock _editorDock;
|
||||||
|
private CaLevelGeneratorDock _dock;
|
||||||
|
|
||||||
|
public override void _EnterTree()
|
||||||
|
{
|
||||||
|
_dock = new CaLevelGeneratorDock { Plugin = this };
|
||||||
|
_editorDock = new EditorDock { Title = "CA Level Generator" };
|
||||||
|
_editorDock.AddChild(_dock);
|
||||||
|
AddDock(_editorDock);
|
||||||
|
SceneChanged += OnSceneChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void _ExitTree()
|
||||||
|
{
|
||||||
|
SceneChanged -= OnSceneChanged;
|
||||||
|
RemoveDock(_editorDock);
|
||||||
|
_editorDock.QueueFree();
|
||||||
|
_editorDock = null;
|
||||||
|
_dock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSceneChanged(Node sceneRoot)
|
||||||
|
{
|
||||||
|
_dock?.OnSceneChanged(sceneRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EditorUndoRedoManager UndoRedo => GetUndoRedo();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1
addons/ca_level_generator/CaLevelGeneratorPlugin.cs.uid
Normal file
1
addons/ca_level_generator/CaLevelGeneratorPlugin.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://cojnhxpcgepi2
|
||||||
100
addons/ca_level_generator/TilemapPainter.cs
Normal file
100
addons/ca_level_generator/TilemapPainter.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
#if TOOLS
|
||||||
|
using Godot;
|
||||||
|
using Godot.Collections;
|
||||||
|
|
||||||
|
namespace Mr.BrickAdventures.Tools.CaLevelGenerator;
|
||||||
|
|
||||||
|
public static class TilemapPainter
|
||||||
|
{
|
||||||
|
public static void Paint(
|
||||||
|
TileMapLayer layer,
|
||||||
|
bool[,] grid,
|
||||||
|
CaGeneratorSettings settings,
|
||||||
|
EditorUndoRedoManager undoRedo)
|
||||||
|
{
|
||||||
|
var before = layer.TileMapData;
|
||||||
|
ApplyGrid(layer, grid, settings);
|
||||||
|
var after = layer.TileMapData;
|
||||||
|
RegisterUndo(layer, before, after, undoRedo, "CA Generate");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Smooth(
|
||||||
|
TileMapLayer layer,
|
||||||
|
CaGeneratorSettings settings,
|
||||||
|
EditorUndoRedoManager undoRedo)
|
||||||
|
{
|
||||||
|
var grid = ReadGrid(layer, settings.Offset, new Vector2I(settings.Width, settings.Height));
|
||||||
|
grid = CaGenerator.Smooth(grid);
|
||||||
|
var before = layer.TileMapData;
|
||||||
|
ApplyGrid(layer, grid, settings);
|
||||||
|
var after = layer.TileMapData;
|
||||||
|
RegisterUndo(layer, before, after, undoRedo, "CA Smooth");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Clear(
|
||||||
|
TileMapLayer layer,
|
||||||
|
CaGeneratorSettings settings,
|
||||||
|
EditorUndoRedoManager undoRedo)
|
||||||
|
{
|
||||||
|
var before = layer.TileMapData;
|
||||||
|
for (int x = 0; x < settings.Width; x++)
|
||||||
|
for (int y = 0; y < settings.Height; y++)
|
||||||
|
layer.EraseCell(settings.Offset + new Vector2I(x, y));
|
||||||
|
var after = layer.TileMapData;
|
||||||
|
RegisterUndo(layer, before, after, undoRedo, "CA Clear");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool[,] ReadGrid(TileMapLayer layer, Vector2I offset, Vector2I size)
|
||||||
|
{
|
||||||
|
var grid = new bool[size.X, size.Y];
|
||||||
|
for (int x = 0; x < size.X; x++)
|
||||||
|
for (int y = 0; y < size.Y; y++)
|
||||||
|
grid[x, y] = layer.GetCellSourceId(offset + new Vector2I(x, y)) != -1;
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplyGrid(TileMapLayer layer, bool[,] grid, CaGeneratorSettings settings)
|
||||||
|
{
|
||||||
|
int w = grid.GetLength(0), h = grid.GetLength(1);
|
||||||
|
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
layer.EraseCell(settings.Offset + new Vector2I(x, y));
|
||||||
|
|
||||||
|
if (settings.TerrainSet >= 0)
|
||||||
|
{
|
||||||
|
var solidCells = new Array<Vector2I>();
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
if (grid[x, y])
|
||||||
|
solidCells.Add(settings.Offset + new Vector2I(x, y));
|
||||||
|
|
||||||
|
if (solidCells.Count > 0)
|
||||||
|
layer.SetCellsTerrainConnect(solidCells, settings.TerrainSet, settings.Terrain);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int x = 0; x < w; x++)
|
||||||
|
for (int y = 0; y < h; y++)
|
||||||
|
if (grid[x, y])
|
||||||
|
layer.SetCell(
|
||||||
|
settings.Offset + new Vector2I(x, y),
|
||||||
|
settings.SourceId,
|
||||||
|
settings.AtlasCoords);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RegisterUndo(
|
||||||
|
TileMapLayer layer,
|
||||||
|
byte[] before,
|
||||||
|
byte[] after,
|
||||||
|
EditorUndoRedoManager undoRedo,
|
||||||
|
string actionName)
|
||||||
|
{
|
||||||
|
undoRedo.CreateAction(actionName);
|
||||||
|
undoRedo.AddUndoProperty(layer, "tile_map_data", before);
|
||||||
|
undoRedo.AddDoProperty(layer, "tile_map_data", after);
|
||||||
|
undoRedo.CommitAction(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
1
addons/ca_level_generator/TilemapPainter.cs.uid
Normal file
1
addons/ca_level_generator/TilemapPainter.cs.uid
Normal file
@@ -0,0 +1 @@
|
|||||||
|
uid://y55l4iw35tih
|
||||||
6
addons/ca_level_generator/ca_generator_tests.tscn
Normal file
6
addons/ca_level_generator/ca_generator_tests.tscn
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://test_ca_generator"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://addons/ca_level_generator/CaGeneratorTestRunner.cs" id="1_catest"]
|
||||||
|
|
||||||
|
[node name="CaGeneratorTestRunner" type="Node"]
|
||||||
|
script = ExtResource("1_catest")
|
||||||
6
addons/ca_level_generator/plugin.cfg
Normal file
6
addons/ca_level_generator/plugin.cfg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
[plugin]
|
||||||
|
name="CA Level Generator"
|
||||||
|
description="Cellular automata level blockout generator"
|
||||||
|
author="Gabriel Kaszewski"
|
||||||
|
version="1.0"
|
||||||
|
script="CaLevelGeneratorPlugin.cs"
|
||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/banner.png-7e9e6a304eef850602c8d5afb80df9c3.c
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.cte
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.c
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryDefault.png-fcf3696b58
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryHover.png-3d2e4d225f6a
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/inventory_container.png-12241277f279bfc4bf7d5
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/level_spritesheet.png-26a44dd21a040a5480d5ccb
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_2d_sprite.png-deab230b83ae03ae
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/player_sprite.svg-8862ecb19e12152eb892607676f
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/sign_prompt.png-18d451127e1cd1a16367acd23cec4
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/checker_pattern_dark.png-70cedad2d3abf4ad6166
|
|||||||
compress/mode=2
|
compress/mode=2
|
||||||
compress/high_quality=true
|
compress/high_quality=true
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/target.png-878c5e8d057c8a9a4c2322d4ab88e9ef.s
|
|||||||
compress/mode=2
|
compress/mode=2
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PriorityOverride.svg-e76e07f4bbd98169f119e17f
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_2d.svg-e5483cbc858fc5f95f7210b
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_3d.svg-41ed612e834470377fb56ee
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_camera_3d_resource.svg-f8bf8d1
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/phantom_camera_gizmo.svg-ba1aacb9b1c5f4ef401d
|
|||||||
compress/mode=2
|
compress/mode=2
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_glow_logo.png-078f944973b55b32
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/phantom_camera_host.svg-3150f8f2d82ca9ecab9a3
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_logo.png-8b8d347b5e4800c86cd80
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_noise_emitter_2d.svg-1b3d37fe3
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/phantom_camera_noise_emitter_3d.svg-9b90fe54a
|
|||||||
compress/mode=2
|
compress/mode=2
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/phantom_camera_noise_emitter_gizmo.svg-9a5938
|
|||||||
compress/mode=2
|
compress/mode=2
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_noise_resource.svg-a81ed223714
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/phantom_camera_tween.svg-16faced08ef4a5f34582
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/phantom_camera_updater_panel_icon.svg-19823e6
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/Camera2DIcon.svg-300e6f57281180711c5ecf391104
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/Camera3DIcon.svg-4805c46004db1c89cc9443dd7406
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/SceneTypesIcon.svg-66e2255bd3398007bec03a5cbf
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/Select.svg-cdf90b8b400d3b91a023b70c6a823894.c
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/warning.svg-c1b21c265e0842bbdc9ed10735995366.
|
|||||||
compress/mode=0
|
compress/mode=0
|
||||||
compress/high_quality=false
|
compress/high_quality=false
|
||||||
compress/lossy_quality=0.7
|
compress/lossy_quality=0.7
|
||||||
|
compress/uastc_level=0
|
||||||
|
compress/rdo_quality_loss=0.0
|
||||||
compress/hdr_compression=1
|
compress/hdr_compression=1
|
||||||
compress/normal_map=0
|
compress/normal_map=0
|
||||||
compress/channel_pack=0
|
compress/channel_pack=0
|
||||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
|||||||
mipmaps/limit=-1
|
mipmaps/limit=-1
|
||||||
roughness/mode=0
|
roughness/mode=0
|
||||||
roughness/src_normal=""
|
roughness/src_normal=""
|
||||||
|
process/channel_remap/red=0
|
||||||
|
process/channel_remap/green=1
|
||||||
|
process/channel_remap/blue=2
|
||||||
|
process/channel_remap/alpha=3
|
||||||
process/fix_alpha_border=true
|
process/fix_alpha_border=true
|
||||||
process/premult_alpha=false
|
process/premult_alpha=false
|
||||||
process/normal_map_invert_y=false
|
process/normal_map_invert_y=false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_resource type="Theme" load_steps=12 format=3 uid="uid://bhppejri5dbsf"]
|
[gd_resource type="Theme" format=3 uid="uid://bhppejri5dbsf"]
|
||||||
|
|
||||||
[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_5rtjh"]
|
[ext_resource type="FontFile" uid="uid://dve7mgsjik4dg" path="res://addons/phantom_camera/fonts/Nunito-Regular.ttf" id="1_5rtjh"]
|
||||||
[ext_resource type="StyleBox" uid="uid://5weqvkjsfso3" path="res://addons/phantom_camera/themes/button_hover.tres" id="2_du6h5"]
|
[ext_resource type="StyleBox" uid="uid://5weqvkjsfso3" path="res://addons/phantom_camera/themes/button_hover.tres" id="2_du6h5"]
|
||||||
|
|||||||
15
docs/adr/0001-three-progression-scopes.md
Normal file
15
docs/adr/0001-three-progression-scopes.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Player progression has three scopes, not two
|
||||||
|
|
||||||
|
Progression state was two scopes — `PlayerState` (persisted) and `SessionState` (transient) — with achievements
|
||||||
|
and best times leaking out as special cases: `PlayerState.Reset()` deliberately skipped `UnlockedAchievements`,
|
||||||
|
and best times lived in the ghost replay files rather than the save. We split progression into **Attempt**,
|
||||||
|
**Playthrough** and **Profile** (defined in `CONTEXT.md`), because introducing save slots makes the third scope
|
||||||
|
mandatory rather than a preference: Steam achievements are per-account and cannot be un-earned, so a
|
||||||
|
per-Playthrough achievement list would permanently contradict Steam the moment a second slot exists. Best times
|
||||||
|
follow the same argument — a completionist's record is not something starting a new game should reset.
|
||||||
|
|
||||||
|
## Considered options
|
||||||
|
|
||||||
|
Keeping one persisted root with a hand-maintained list of fields that survive New Game. Rejected: that is exactly
|
||||||
|
what the old `Reset()` did, and it had already drifted — achievements survived by omission rather than by design,
|
||||||
|
and nothing would have caught the next missed field.
|
||||||
16
docs/adr/0002-attempt-is-a-ledger.md
Normal file
16
docs/adr/0002-attempt-is-a-ledger.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Attempt is a separate aggregate holding a ledger
|
||||||
|
|
||||||
|
**Playthrough** and **Attempt** are separate aggregate roots rather than one root containing the other, and an
|
||||||
|
Attempt records a *ledger* — coins found, coins spent, skills bought — rather than just a purse of coins found.
|
||||||
|
Separate roots keeps each aggregate small and gives the Attempt its own lifecycle, since it is never persisted.
|
||||||
|
The ledger exists so that the rule "nothing survives an Attempt unless you complete the level" applies to
|
||||||
|
purchases too: under the previous model `RemoveCoins` drained session coins and then debited banked coins
|
||||||
|
immediately, so buying a skill mid-level and then dying lost both the coins and the skill.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Affordability is a cross-aggregate **read** (`attempt.CoinsFound + playthrough.Bank >= cost`). Reads may cross
|
||||||
|
scopes; writes may not.
|
||||||
|
- Purchasing writes only the Attempt. The only operation touching both roots is Commit, which has exactly one
|
||||||
|
call site.
|
||||||
|
- Discarding an Attempt undoes purchases rather than refunding them — under the ledger they never happened.
|
||||||
15
docs/adr/0003-only-first-clear-commits.md
Normal file
15
docs/adr/0003-only-first-clear-commits.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# Only a First Clear commits an Attempt
|
||||||
|
|
||||||
|
The game is moving from a purely sequential campaign to one where completionists can select and replay levels
|
||||||
|
they have already beaten. Coins and skills bank only on a **First Clear** — the first completion of a Level
|
||||||
|
within a Playthrough — while a **Replay** records Best Time, achievements and statistics to the Profile and
|
||||||
|
discards the Attempt. We chose this over committing on every completion because grinding the easiest level would
|
||||||
|
otherwise be the optimal way to buy every skill.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- A Replay must never advance campaign position. Sharing one code path between First Clear and Replay without an
|
||||||
|
explicit branch is the obvious way to reintroduce this bug.
|
||||||
|
- `ClearedLevels` answers "is this a First Clear?" for free, since it already deduplicates.
|
||||||
|
- Coins found during a Replay are lost on completion, not just on death. This is intentional and worth surfacing
|
||||||
|
in the UI.
|
||||||
21
docs/adr/0004-domain-does-not-reference-godot.md
Normal file
21
docs/adr/0004-domain-does-not-reference-godot.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# The progression domain does not reference Godot
|
||||||
|
|
||||||
|
The progression domain lives in its own assembly with no Godot reference. Aggregates raise plain C# event records
|
||||||
|
as part of their operations, and a single adapter republishes those as `EventBus` signals. This is the only
|
||||||
|
enforceable version of the goal — correctness checkable without a scene tree, with the compiler enforcing the rule
|
||||||
|
rather than discipline. The leak it removes was already visible: `PlayerState` documented itself as a POCO "for
|
||||||
|
predictable state management" while holding `List<SkillData>`, and `SkillData : Resource`, which is why saves had
|
||||||
|
to store skill *names* and re-resolve them through `SkillManager` on load.
|
||||||
|
|
||||||
|
Republishing rather than replacing `EventBus` keeps existing scene wiring and subscribers intact, and makes
|
||||||
|
"state changed ⇒ signal fired" un-bypassable — the original defect, where `ConsoleManager` mutated state directly
|
||||||
|
and re-emitted signals by hand, sometimes without the invariants the store's own operations applied.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `GameStateStore` stops being a `Node`. The autoload becomes a composition root that owns the aggregates, so the
|
||||||
|
aggregates' lifetime is no longer tied to the scene tree and most `Store?.` null-guards disappear.
|
||||||
|
- Skill progression cannot live on `SkillData`. A `Resource` is one cached instance per process, so with save
|
||||||
|
slots it would leak levels between Playthroughs.
|
||||||
|
- Godot types (`Vector2`, `Mathf`, `GD.Print`) are not available in the domain; where they appear today they are
|
||||||
|
replaced by domain values or moved to adapters.
|
||||||
28
docs/adr/0005-playthrough-settles-attempts.md
Normal file
28
docs/adr/0005-playthrough-settles-attempts.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# Playthrough has no LoseLife; it settles Attempts
|
||||||
|
|
||||||
|
Playthrough and Attempt are separate aggregate roots (ADR-0002), but "one death costs exactly one life" requires
|
||||||
|
ending the Attempt and decrementing lives *together*. Rather than expose bare mutators and rely on the caller to
|
||||||
|
sequence them, Playthrough exposes only what happened — `Commit(attempt)`, `SettleDeath(attempt)`,
|
||||||
|
`SettleReplay(attempt)` — each of which calls the Attempt's own `TryEnd…` internally and does nothing when it
|
||||||
|
returns false. Every operation is therefore atomic and idempotent, and there is no `LoseLife` to call from
|
||||||
|
somewhere else.
|
||||||
|
|
||||||
|
The alternative shape, `if (attempt.TryEndInDeath()) playthrough.LoseLife();`, puts the idempotency in the domain
|
||||||
|
but leaves the sequencing to the caller. A public `LoseLife` is an invitation: the code being replaced has two
|
||||||
|
paths to exactly that operation — `LivesStateHandler.RemoveLife` and `ConsoleManager` writing `Player.Lives`
|
||||||
|
directly, unclamped — because the operation existed to be called.
|
||||||
|
|
||||||
|
## Considered options
|
||||||
|
|
||||||
|
Collapsing the two into one aggregate with Attempt as an inner entity, making the atomicity structural. Rejected:
|
||||||
|
`Commit(Attempt)` already gave the shape we needed, and passing one root into another's method is a transient
|
||||||
|
reference rather than a stored association, so ADR-0002 stands unchanged.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Overlapping hazards in a kill zone, or a hazard re-firing during the two seconds before `DeathScreen` reloads,
|
||||||
|
cost one life rather than two.
|
||||||
|
- `ExitDoorComponent` and `ConsoleManager.GoToNextLevelCommand` can both request completion; only the first banks.
|
||||||
|
- The application layer holds the active Attempt and passes it in. The Attempt carries its own `LevelId`, so
|
||||||
|
`Commit` decides First Clear versus Replay from the Attempt itself rather than trusting a level supplied
|
||||||
|
alongside it — finishing a Replay stops being a way to corrupt campaign position.
|
||||||
322
docs/plans/phase-a-progression-domain.md
Normal file
322
docs/plans/phase-a-progression-domain.md
Normal file
@@ -0,0 +1,322 @@
|
|||||||
|
# Phase A — Progression domain
|
||||||
|
|
||||||
|
Replace the current state layer with the model in [`CONTEXT.md`](../../CONTEXT.md), governed by
|
||||||
|
[ADR-0001..0004](../adr/). **No new player-facing features.** Save files are wiped once; the game is unreleased so
|
||||||
|
this costs nothing (ADR context).
|
||||||
|
|
||||||
|
Phase B — slot picker UI, level select, Replay surfacing, best times out of the ghost files — is out of scope
|
||||||
|
here. Phase A does put the *persistence format* in place for slots, so Phase B is UI work rather than another
|
||||||
|
model change.
|
||||||
|
|
||||||
|
## Two kinds of slice
|
||||||
|
|
||||||
|
| | verified by | `/tdd` |
|
||||||
|
|---|---|---|
|
||||||
|
| **Slices 1–6** — pure domain, no Godot | `dotnet test` | yes — red/green/refactor throughout |
|
||||||
|
| **Slices 7–13** — adapters and wiring | compiler + playing the game | no — write the test where one is possible, otherwise verify by hand |
|
||||||
|
|
||||||
|
Don't try to TDD slices 7–13. You can't red-green an autoload swap or a `.tscn` connection deletion, and forcing
|
||||||
|
it produces tests that assert the mock rather than the behaviour. The value of slices 1–6 is that they absorb
|
||||||
|
*all* the logic worth testing, so the untestable slices become mechanical.
|
||||||
|
|
||||||
|
Each slice ends with the build green and the game runnable. Don't start the next one until it is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 0 — scaffolding
|
||||||
|
|
||||||
|
No tests. Pure setup.
|
||||||
|
|
||||||
|
- `progression/Mr.BrickAdventures.Progression.csproj` — plain `net8.0` library, **no Godot reference**, root
|
||||||
|
namespace `Mr.BrickAdventures.Progression`.
|
||||||
|
- `progression.tests/Mr.BrickAdventures.Progression.Tests.csproj` — xunit (NUnit is equally fine), references the
|
||||||
|
domain library **only**.
|
||||||
|
- Add both to `Mr. Brick Adventures.sln`. Add a `ProjectReference` from the game project to the domain library.
|
||||||
|
- Set `<EnableDynamicLoading>true</EnableDynamicLoading>` in the domain library too, not just the game project —
|
||||||
|
Godot needs it on referenced assemblies for hot reload, and omitting it produces confusing load failures.
|
||||||
|
|
||||||
|
**Done when:** `dotnet test` runs and reports zero tests; Godot still builds; the game still launches and plays.
|
||||||
|
|
||||||
|
**Guard rail for the whole phase:**
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# must print nothing — anchored to real using-directives
|
||||||
|
grep -rnE "^\s*(global\s+)?using\s+Godot" progression progression.adapters progression.tests --include=*.cs
|
||||||
|
# stronger, and cannot be fooled by wording: no project may resolve the engine assembly at all
|
||||||
|
grep -c GodotSharp progression/obj/project.assets.json progression.adapters/obj/project.assets.json
|
||||||
|
```
|
||||||
|
|
||||||
|
That check is what ADR-0004 buys you. Match using-directives **anchored to the start of a line**: a bare
|
||||||
|
`grep -r "using Godot"` also hits `.csproj` comments and doc comments that legitimately mention the engine in prose,
|
||||||
|
and reports a false violation. The `project.assets.json` check is the one that cannot be fooled by wording.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 1 — value objects · TDD
|
||||||
|
|
||||||
|
`Coins`, `Lives`, `LevelId`, `SkillId`, `SkillLevel`, `StatName`, `LevelTime`, `PlaythroughId`.
|
||||||
|
|
||||||
|
Invariants to drive out with tests:
|
||||||
|
|
||||||
|
- `Coins` cannot be negative. Subtraction that would go negative is a failure the caller must handle, not a
|
||||||
|
silent clamp — the current `Mathf.Max(0, ...)` clamp in one path and absence of it in another is exactly the
|
||||||
|
bug this removes.
|
||||||
|
- `Lives` floors at zero, and reaching zero is observable.
|
||||||
|
- `LevelId` is a non-empty stable string. Not an int, not an index.
|
||||||
|
- `SkillLevel` starts at 1 and has no zero value — "unowned" is represented by *absence*, never by level 0. See
|
||||||
|
the `SkillData.Level = 1` ambiguity this replaces.
|
||||||
|
- `LevelTime` is positive; comparison is what "best" means.
|
||||||
|
- `StatName` is a closed set (the five in `StatNames` today), so a typo cannot invent a statistic.
|
||||||
|
|
||||||
|
**Done when:** no value object can hold invalid state, and every clamp/guard currently scattered across
|
||||||
|
`GameStateStore` and `ConsoleManager` exists in exactly one place.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 2 — Attempt · TDD
|
||||||
|
|
||||||
|
The ledger from ADR-0002: `CoinsFound`, `CoinsSpent`, `SkillsBought`, plus an outcome.
|
||||||
|
|
||||||
|
- `Attempt(LevelId level)` and `LevelId Level` — the Attempt knows which Level it is an attempt at, so
|
||||||
|
`Commit` decides First Clear versus Replay from the Attempt rather than from a level the caller passes alongside
|
||||||
|
- `Outcome` — `InProgress | Died | Cleared`
|
||||||
|
- `void RecordCoinsFound(Coins)` — silently ignores late coins once ended
|
||||||
|
- `bool TryRecordPurchase(SkillId, SkillLevel resulting, Coins cost)` — records where the skill ended up, since
|
||||||
|
one Attempt can both unlock and upgrade the same skill
|
||||||
|
- `bool TryEndInDeath()` / `bool TryEndInCompletion()` — return false if the Attempt already ended
|
||||||
|
- No `Discard` method: discarding is throwing the object away.
|
||||||
|
|
||||||
|
The recording methods are deliberately asymmetric. A coin pickup firing after death is a real event — the player
|
||||||
|
node survives for two seconds before `DeathScreen` reloads — and its caller has nothing useful to do with a
|
||||||
|
`false`, so it returns nothing. A purchase's caller does care.
|
||||||
|
|
||||||
|
`CoinsFound` and `CoinsSpent` are both non-negative. There is no signed net value — settlement is
|
||||||
|
`bank.Add(found).TrySubtract(spent)` in `Playthrough.Commit`, which in that order cannot go negative given
|
||||||
|
affordability was checked at purchase time.
|
||||||
|
|
||||||
|
> Corrects an earlier draft of this plan, which had `NetCoins` "may be negative". That would have forced `Coins`
|
||||||
|
> to permit negatives and defeated the point of the type.
|
||||||
|
|
||||||
|
**The outcome is the idempotency guard, and it is the reason this slice matters.** An Attempt is the thing that
|
||||||
|
dies or is cleared, and it can only do either once. `Lives` flooring at zero does not solve this: losing two lives
|
||||||
|
from a single death takes you from 3 to 1, which no clamp catches.
|
||||||
|
|
||||||
|
Tests worth writing:
|
||||||
|
|
||||||
|
- **An Attempt cannot end twice.** Second `TryEndInDeath` returns false and changes nothing.
|
||||||
|
- **An Attempt that died cannot then be cleared**, and vice versa.
|
||||||
|
- Spending more than found is representable — the shortfall is settled at Commit, not rejected here.
|
||||||
|
- Recording anything after the Attempt ended is refused.
|
||||||
|
|
||||||
|
Two real call sites motivate this. Death: overlapping hazards in a kill zone, or a hazard re-triggering during the
|
||||||
|
two seconds before `DeathScreen` reloads the scene — the player node still exists with zero health. Completion:
|
||||||
|
`ExitDoorComponent:46` and `ConsoleManager.GoToNextLevelCommand:99` both call `OnLevelComplete()`.
|
||||||
|
|
||||||
|
`HealthComponent.ApplyHealthChange` currently absorbs repeat deaths via its `delta == 0` early return, but that is
|
||||||
|
incidental and `Health` is a public `[Export]` setter that bypasses it. Do not rely on the engine side.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 3 — Playthrough · TDD
|
||||||
|
|
||||||
|
Aggregate root. `PlaythroughId`, `Lives`, `Bank`, `OwnedSkills : SkillId → SkillLevel`, `CurrentLevel : LevelId`,
|
||||||
|
`ClearedLevels`.
|
||||||
|
|
||||||
|
Operations: `SettleDeath(Attempt)`, `SettleCompletion(Attempt, Campaign)`, `CanAfford(Attempt, Coins)`,
|
||||||
|
`HasCleared(LevelId)`, `ReachableLevels(Campaign)`, `IsRunOver`.
|
||||||
|
|
||||||
|
**There is no `LoseLife`** — see ADR-0005. Each settle operation calls the Attempt's `TryEnd…` internally and does
|
||||||
|
nothing when it returns false, so it is atomic and idempotent, and no bare mutator exists to be called from a
|
||||||
|
second place.
|
||||||
|
|
||||||
|
**Two settle operations, not three.** An earlier draft had `Commit` and `SettleReplay` as separate calls, which
|
||||||
|
made the *caller* decide whether a completion was a First Clear or a Replay — precisely the ADR-0003 regression.
|
||||||
|
`SettleCompletion` branches on `HasCleared(attempt.Level)` itself, so it cannot be selected wrongly.
|
||||||
|
|
||||||
|
**`Campaign`** is a domain value object holding the authored ordering of `LevelId`s (`First`, `Next`, `Contains`).
|
||||||
|
`ILevelCatalogue` stays on the adapter side resolving a `LevelId` to a scene handle and display name; the ordering
|
||||||
|
is pure data handed in as a `Campaign`, so the aggregate never touches a port.
|
||||||
|
|
||||||
|
**`Commit` does not validate skill levels against the upgrade cap.** Max level comes from
|
||||||
|
`SkillData.Upgrades.Count`, which is authored data the domain cannot see. The cap is a purchase-time rule enforced
|
||||||
|
in slice 6's `UpgradeSkill`, which is also where `TryUpgradeSkill`'s current `Upgrades[skill.Level]` overrun gets a
|
||||||
|
test. `Commit` applies what the ledger recorded.
|
||||||
|
|
||||||
|
Events leave via `IReadOnlyList<ProgressionEvent> DrainEvents()`. The composition root drains once after every use
|
||||||
|
case and republishes (ADR-0004), so operations stay `void` and the "forgot to drain" risk collapses to one line in
|
||||||
|
one place.
|
||||||
|
|
||||||
|
Raises: `LifeLost(remaining)`, `RunEnded`, `BankChanged(total)`, `SkillSetChanged`.
|
||||||
|
|
||||||
|
This is where the report's bugs become tests. Write these first, watch them fail:
|
||||||
|
|
||||||
|
- **Losing the last life sets lives to zero *and* raises `RunEnded` in one operation.** This is the last-life
|
||||||
|
soft-lock — today two screens read `Lives` before the handler writes it. Make it one atomic operation and the
|
||||||
|
race cannot be expressed.
|
||||||
|
- **Two deaths in one Attempt cost one life.** Pairs with slice 2's outcome guard; assert it here at the
|
||||||
|
Playthrough level too, because this is the invariant a player would actually notice.
|
||||||
|
- **`Commit` on an already-cleared level does not advance and does not bank** (ADR-0003).
|
||||||
|
- **`Commit` settles as `bank.Add(found).TrySubtract(spent)`**, in that order, so an Attempt that overspent
|
||||||
|
reduces the Bank correctly and can never drive it negative. A discarded Attempt leaves the Bank untouched —
|
||||||
|
the "buy mid-level, die, lose coins *and* skill" bug.
|
||||||
|
- **`ReachableLevels` is computed** from `ClearedLevels` plus level order, never stored. `UnlockedLevels` and
|
||||||
|
`IsLevelUnlocked` are deleted, not ported.
|
||||||
|
- **`Bank` cannot go negative** even through Commit.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 4 — Profile · TDD
|
||||||
|
|
||||||
|
Aggregate root, one per installation. `Achievements`, `BestTimes : LevelId → LevelTime`, `Statistics`.
|
||||||
|
|
||||||
|
- `RecordTime(LevelId, LevelTime)` — only replaces a slower time. Recording a worse time is a no-op, not an error.
|
||||||
|
- `UnlockAchievement(id)` — idempotent, raises `AchievementUnlocked` only on the transition.
|
||||||
|
- `Record(StatName, int)`.
|
||||||
|
- `RelockAchievement(id)` exists for the console only, and is documented as **not** reaching Steam — that
|
||||||
|
asymmetry is unavoidable (Steam achievements cannot be un-earned) so make it explicit rather than surprising.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 5 — ports and in-memory adapters · TDD
|
||||||
|
|
||||||
|
Interfaces in the domain: `IPlaythroughStore`, `IProfileStore`, `ILevelCatalogue`, `IGhostStore`.
|
||||||
|
|
||||||
|
In-memory implementations live in the **test project**. That's what makes the seam real rather than hypothetical —
|
||||||
|
two adapters, one of which ships and one of which doesn't.
|
||||||
|
|
||||||
|
- Round-trip: save a Playthrough, load it, assert equality. Same for Profile.
|
||||||
|
- Loading an absent Playthrough and loading a *corrupt* one are different outcomes. Today both surface as
|
||||||
|
`false` from `LoadGame`, which is why a version bump silently overwrites saves.
|
||||||
|
- `ILevelCatalogue` returns ordered `LevelId`s and resolves one to a scene reference — as an opaque handle, since
|
||||||
|
the domain can't name `PackedScene`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 6 — use cases · TDD
|
||||||
|
|
||||||
|
`StartPlaythrough`, `LoadPlaythrough`, `EnterLevel`, `FinishLevel`, `HandlePlayerDeath`, `PurchaseSkill`,
|
||||||
|
`UpgradeSkill`.
|
||||||
|
|
||||||
|
Four of these carry the orchestration that is currently spread across `GameManager`, the `Events/` handlers and
|
||||||
|
the UI. Each of the remaining report bugs gets a test here:
|
||||||
|
|
||||||
|
- **`EnterLevel` always announces `LevelStarted`.** Today only `TryToGoToNextLevel` emits it, so ghost recording
|
||||||
|
never starts on the first level or after Continue, and a continued run records completion times of zero.
|
||||||
|
- **`FinishLevel` branches First Clear vs Replay** and a Replay never advances `CurrentLevel` (ADR-0003 flags this
|
||||||
|
as the obvious regression).
|
||||||
|
- **`HandlePlayerDeath` loses a life and discards the Attempt as one operation.**
|
||||||
|
- **`PurchaseSkill` writes only the Attempt**, checks affordability across Attempt + Bank, and rejects rather than
|
||||||
|
throwing when short.
|
||||||
|
- **`UpgradeSkill` reads the level from the Playthrough**, and cannot index past the upgrade table.
|
||||||
|
|
||||||
|
**Done when:** every bug listed in the architecture review has a test that failed before the fix and passes after.
|
||||||
|
That is the deliverable of Phase A — the point where "hidden bugs" stops being a category.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> Everything above is pure C# with no Godot. Everything below touches the engine. Switch verification mode here.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Slice 7 — level catalogue adapter
|
||||||
|
|
||||||
|
`resources/levels/` already holds six `LevelResource` files (`village_1..5`, `forest_1`) but there are eight level
|
||||||
|
scenes and only two scenes reference a resource at all. Complete and adopt them.
|
||||||
|
|
||||||
|
- Add `LevelId` and explicit order to `LevelResource`; create the two missing entries (`tutorial_1`,
|
||||||
|
`village_6`).
|
||||||
|
- A catalogue resource lists them in order. Adapter implements `ILevelCatalogue`.
|
||||||
|
- Delete `GameManager.LevelScenes` and all array-index addressing.
|
||||||
|
- `LevelResource.ScenePath` is currently never read — either it becomes how the catalogue resolves scenes, or it
|
||||||
|
goes. Don't leave it.
|
||||||
|
|
||||||
|
**Verify:** game boots into the first level; order matches the catalogue; `DeathScreen`'s level-name label still
|
||||||
|
populates.
|
||||||
|
|
||||||
|
## Slice 8 — JSON store adapters
|
||||||
|
|
||||||
|
- `user://profile.json` and `user://playthrough_{id}.json`. The slot format lands now even though the UI only
|
||||||
|
ever creates one; Phase B then adds a picker and nothing about the format changes.
|
||||||
|
- Write to a temp file and rename, so a crash mid-save can't corrupt a slot. With N slots this stops being
|
||||||
|
theoretical.
|
||||||
|
- Keep a version field, but replace the equality check with a migration hook that can return
|
||||||
|
"unmigratable" — distinct from "absent".
|
||||||
|
|
||||||
|
**Verify:** play, quit, continue. Then hand-corrupt a file and confirm you get an error rather than a silent new
|
||||||
|
game.
|
||||||
|
|
||||||
|
## Slice 9 — composition root and republisher
|
||||||
|
|
||||||
|
- New autoload owns the `Profile` and the active `Playthrough`. It is the only `Node` in this area.
|
||||||
|
- Republisher subscribes to domain events and emits the existing `EventBus` signals (ADR-0004), so `Hud`,
|
||||||
|
`DeathScreen` and the rest keep working unchanged.
|
||||||
|
- Delete `GameStateStore`. Register the new autoload in `project.godot` in its place.
|
||||||
|
|
||||||
|
**Verify:** HUD shows coins and lives; a coin pickup updates them.
|
||||||
|
|
||||||
|
## Slice 10 — swap the call sites
|
||||||
|
|
||||||
|
Mechanical and compiler-driven — sealing the aggregates turns every old write into a compile error, which is the
|
||||||
|
cheapest way to find them all.
|
||||||
|
|
||||||
|
- `Events/` handlers become thin adapters that call use cases. `CoinStateHandler` already has this shape.
|
||||||
|
- `ConsoleManager` calls use cases instead of writing fields and hand-re-emitting signals.
|
||||||
|
- `AchievementManager` reads and writes the `Profile` instead of reaching into `PlayerState`.
|
||||||
|
- `SkillManager` reconciles `ActiveComponents` from `SkillSetChanged` — `ApplyUnlockedSkills` becomes the only
|
||||||
|
path, so the four two-call pairs collapse to one call each and a skill can no longer end up owned but inert.
|
||||||
|
- Delete `SaveSystem`, `PlayerState`, `SessionState`, `SaveDataDto`.
|
||||||
|
|
||||||
|
**Verify:** compiler first, then play one level start to finish including a purchase.
|
||||||
|
|
||||||
|
## Slice 11 — skill level off the asset
|
||||||
|
|
||||||
|
- `ISkill.Initialize(Node owner, SkillData data, int level)`. Update `BrickThrowComponent:90`,
|
||||||
|
`MagneticSkillComponent:97`, `BrickArmorSkillComponent:25` to use the passed level rather than `Data.Level`.
|
||||||
|
`BrickArmorSkillComponent` indexes `Upgrades[Level - 1]` unguarded — fix while you're there.
|
||||||
|
- Delete `SkillData.Level`. `SkillData` becomes immutable authored data (CONTEXT.md).
|
||||||
|
- `MarketplaceButton` and `Marketplace` read the level from the Playthrough.
|
||||||
|
|
||||||
|
**Verify:** buy a skill, upgrade it, quit, continue — level persists. Game over, New Game — level is gone. Both
|
||||||
|
are currently wrong.
|
||||||
|
|
||||||
|
**Landmine to note:** five skills (`brick_armor`, `brick_shield`, `double_jump`, `ground_pound_skill`,
|
||||||
|
`wall_jump`) have no `upgrades/` entries, so `Upgrades.Count == 0`. `TryUnlockSkill` would throw on
|
||||||
|
`Upgrades[0].Cost`; it's currently unreachable only because `MarketplaceButton` hides zero-upgrade skills. Decide
|
||||||
|
whether those skills are purchasable at all and make the domain say so.
|
||||||
|
|
||||||
|
## Slice 12 — one death path
|
||||||
|
|
||||||
|
- Delete the 18 `Death` → `OnPlayerDeath` connections across `objects/level/base_level.tscn` and the eight level
|
||||||
|
scenes.
|
||||||
|
- `DeathScreen` and `GameOverScreen` subscribe to republished `LivesChanged` / `RunEnded`.
|
||||||
|
- `DeathScreen` currently reads `Player.Lives` directly *and* subscribes to `LivesChanged` — it keeps only the
|
||||||
|
subscription.
|
||||||
|
|
||||||
|
**Verify:** die with lives remaining → scene reloads. Die on the last life → game over appears. The second case
|
||||||
|
does not work today.
|
||||||
|
|
||||||
|
## Slice 13 — delete the dead surface
|
||||||
|
|
||||||
|
- The four never-emitted signals (`LevelRestarted`, `EnemyDamaged`, `SkillActivated`, `SkillDeactivated`).
|
||||||
|
- Of the eleven emitted-but-unheard: keep the ones the republisher now feeds, delete the speculative ones.
|
||||||
|
- Rename `EventBus.SkillUnlocked` — it collides with `SkillUnlockerComponent.SkillUnlocked`, and every apparent
|
||||||
|
subscriber is bound to the component's.
|
||||||
|
- `SaveSystem.DeleteSave`, `SpeedRunManager.StopTimer`, `AchievementManager.IsAchievementUnlocked`,
|
||||||
|
`Constants.CoinsGroup` — zero callers each.
|
||||||
|
- `SpeedRunManager`: `StopTimer` carries a "save personal best" comment for logic that never existed. Best times
|
||||||
|
are the Profile's job now; delete the comment and the dead method.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Order dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
0 ──> 1 ──> 2 ──┐
|
||||||
|
3 ──┼──> 5 ──> 6 ──> 9 ──> 10 ──> 11 ──> 12 ──> 13
|
||||||
|
4 ──┘ ↑
|
||||||
|
7 ───┘
|
||||||
|
8 ───┘
|
||||||
|
```
|
||||||
|
|
||||||
|
2, 3 and 4 are independent of each other after 1. 7 and 8 can be done any time after 5, and both must precede 9.
|
||||||
|
11, 12 and 13 each require 10.
|
||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
|||||||
msdf_size=48
|
msdf_size=48
|
||||||
allow_system_fallback=true
|
allow_system_fallback=true
|
||||||
force_autohinter=false
|
force_autohinter=false
|
||||||
|
modulate_color_glyphs=false
|
||||||
hinting=1
|
hinting=1
|
||||||
subpixel_positioning=1
|
subpixel_positioning=1
|
||||||
keep_rounding_remainders=true
|
keep_rounding_remainders=true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=7 format=3 uid="uid://bargnp4twtmxg"]
|
[gd_scene format=3 uid="uid://bargnp4twtmxg"]
|
||||||
|
|
||||||
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_aya2w"]
|
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_aya2w"]
|
||||||
[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_htmrw"]
|
[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_htmrw"]
|
||||||
@@ -9,33 +9,33 @@
|
|||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
|
||||||
radius = 9.0
|
radius = 9.0
|
||||||
|
|
||||||
[node name="Big Coin" type="Area2D" groups=["coins"]]
|
[node name="Big Coin" type="Area2D" unique_id=354254828 groups=["coins"]]
|
||||||
scale = Vector2(2, 2)
|
scale = Vector2(2, 2)
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 4
|
collision_mask = 4
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=2066526597]
|
||||||
shape = SubResource("CircleShape2D_3ask2")
|
shape = SubResource("CircleShape2D_3ask2")
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=1626463721]
|
||||||
texture = ExtResource("1_aya2w")
|
texture = ExtResource("1_aya2w")
|
||||||
hframes = 12
|
hframes = 12
|
||||||
vframes = 12
|
vframes = 12
|
||||||
frame = 51
|
frame = 51
|
||||||
|
|
||||||
[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")]
|
[node name="CollectableComponent" type="Node" parent="." unique_id=1124310030 node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")]
|
||||||
script = ExtResource("2_htmrw")
|
script = ExtResource("2_htmrw")
|
||||||
Area2D = NodePath("..")
|
Area2D = NodePath("..")
|
||||||
CollisionShape = NodePath("../CollisionShape2D")
|
CollisionShape = NodePath("../CollisionShape2D")
|
||||||
Data = ExtResource("3_lk3av")
|
Data = ExtResource("3_lk3av")
|
||||||
Sfx = NodePath("../sfx")
|
Sfx = NodePath("../sfx")
|
||||||
|
|
||||||
[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")]
|
[node name="FadeAwayComponent" type="Node" parent="." unique_id=222617739 node_paths=PackedStringArray("Sprite", "Area")]
|
||||||
script = ExtResource("4_62p7g")
|
script = ExtResource("4_62p7g")
|
||||||
Sprite = NodePath("../Sprite2D")
|
Sprite = NodePath("../Sprite2D")
|
||||||
Area = NodePath("..")
|
Area = NodePath("..")
|
||||||
|
|
||||||
[node name="sfx" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx" type="AudioStreamPlayer2D" parent="." unique_id=1978747299]
|
||||||
stream = ExtResource("5_dbffd")
|
stream = ExtResource("5_dbffd")
|
||||||
volume_db = -5.0
|
volume_db = -5.0
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=7 format=3 uid="uid://d08dfqmirnd66"]
|
[gd_scene format=3 uid="uid://d08dfqmirnd66"]
|
||||||
|
|
||||||
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_1co1x"]
|
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_1co1x"]
|
||||||
[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_lthbn"]
|
[ext_resource type="Script" uid="uid://r4jybneigfcn" path="res://scripts/components/CollectableComponent.cs" id="2_lthbn"]
|
||||||
@@ -9,32 +9,32 @@
|
|||||||
[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
|
[sub_resource type="CircleShape2D" id="CircleShape2D_3ask2"]
|
||||||
radius = 9.0
|
radius = 9.0
|
||||||
|
|
||||||
[node name="Big Treasure" type="Area2D" groups=["coins"]]
|
[node name="Big Treasure" type="Area2D" unique_id=1021217632 groups=["coins"]]
|
||||||
collision_layer = 2
|
collision_layer = 2
|
||||||
collision_mask = 4
|
collision_mask = 4
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=458290011]
|
||||||
shape = SubResource("CircleShape2D_3ask2")
|
shape = SubResource("CircleShape2D_3ask2")
|
||||||
|
|
||||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
[node name="Sprite2D" type="Sprite2D" parent="." unique_id=564060301]
|
||||||
texture = ExtResource("1_1co1x")
|
texture = ExtResource("1_1co1x")
|
||||||
hframes = 12
|
hframes = 12
|
||||||
vframes = 12
|
vframes = 12
|
||||||
frame = 64
|
frame = 64
|
||||||
|
|
||||||
[node name="CollectableComponent" type="Node" parent="." node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")]
|
[node name="CollectableComponent" type="Node" parent="." unique_id=2009195182 node_paths=PackedStringArray("Area2D", "CollisionShape", "Sfx")]
|
||||||
script = ExtResource("2_lthbn")
|
script = ExtResource("2_lthbn")
|
||||||
Area2D = NodePath("..")
|
Area2D = NodePath("..")
|
||||||
CollisionShape = NodePath("../CollisionShape2D")
|
CollisionShape = NodePath("../CollisionShape2D")
|
||||||
Data = ExtResource("3_k64cr")
|
Data = ExtResource("3_k64cr")
|
||||||
Sfx = NodePath("../sfx")
|
Sfx = NodePath("../sfx")
|
||||||
|
|
||||||
[node name="FadeAwayComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Area")]
|
[node name="FadeAwayComponent" type="Node" parent="." unique_id=1809580000 node_paths=PackedStringArray("Sprite", "Area")]
|
||||||
script = ExtResource("4_qwwsj")
|
script = ExtResource("4_qwwsj")
|
||||||
Sprite = NodePath("../Sprite2D")
|
Sprite = NodePath("../Sprite2D")
|
||||||
FadeDuration = 0.4
|
FadeDuration = 0.4
|
||||||
Area = NodePath("..")
|
Area = NodePath("..")
|
||||||
|
|
||||||
[node name="sfx" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx" type="AudioStreamPlayer2D" parent="." unique_id=466669009]
|
||||||
stream = ExtResource("5_fxf8v")
|
stream = ExtResource("5_fxf8v")
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
[gd_scene load_steps=58 format=3 uid="uid://bqi5s710xb1ju"]
|
[gd_scene format=3 uid="uid://bqi5s710xb1ju"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://csel4s0e4g5uf" path="res://scripts/components/PlayerController.cs" id="1_yysbb"]
|
[ext_resource type="Script" uid="uid://csel4s0e4g5uf" path="res://scripts/components/PlayerController.cs" id="1_yysbb"]
|
||||||
|
[ext_resource type="Resource" uid="uid://vgutbpovj8hc" path="res://resources/movement_presets/platform_movement.tres" id="2_7til7"]
|
||||||
[ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="2_lgb3u"]
|
[ext_resource type="Shader" uid="uid://bs4xvm4qkurpr" path="res://shaders/hit_flash.tres" id="2_lgb3u"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bcv8kx6bc7u5e" path="res://objects/movement_abilities/ground_ability.tscn" id="2_oefns"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="3_2srrh"]
|
[ext_resource type="Texture2D" uid="uid://jl1gwqchhpdc" path="res://sprites/left_eye.png" id="3_2srrh"]
|
||||||
[ext_resource type="PackedScene" uid="uid://d0r5edxnx5jqx" path="res://objects/movement_abilities/variable_jump_ability.tscn" id="3_bnap0"]
|
|
||||||
[ext_resource type="Script" uid="uid://bf4yclropol43" path="res://scripts/components/Movement/GroundMovementAbility.cs" id="4_7til7"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="4_ccn81"]
|
[ext_resource type="Texture2D" uid="uid://iiawtnwmeny3" path="res://sprites/right_eye.png" id="4_ccn81"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cala7bpo1v4no" path="res://objects/movement_abilities/gravity_ability.tscn" id="4_qec3q"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://bty3jq8u0pxkf" path="res://objects/movement_abilities/one_way_platform_ability.tscn" id="5_dhjci"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://0l454rfplmqg" path="res://sprites/MrBrick_base-sheet.png" id="5_yysbb"]
|
[ext_resource type="Texture2D" uid="uid://0l454rfplmqg" path="res://sprites/MrBrick_base-sheet.png" id="5_yysbb"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bu3vuxlrvoo1t" path="res://objects/movement_abilities/spaceship_ability.tscn" id="6_721q0"]
|
|
||||||
[ext_resource type="Script" uid="uid://chgw53qwt7rt8" path="res://scripts/components/Movement/GravityAbility.cs" id="6_xuhvf"]
|
|
||||||
[ext_resource type="Script" uid="uid://ccksp2e76s7sr" path="res://scripts/components/Movement/VariableJumpAbility.cs" id="7_bl1gx"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://chjbi5mgtwhsh" path="res://objects/movement_abilities/wall_jump_ability.tscn" id="7_bnap0"]
|
|
||||||
[ext_resource type="Script" uid="uid://ck6kmnbwhsttt" path="res://scripts/components/Movement/OneWayPlatformAbility.cs" id="7_uno3u"]
|
|
||||||
[ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="8_6lsog"]
|
[ext_resource type="Texture2D" uid="uid://dhkwyv6ayb5qb" path="res://sprites/flying_ship.png" id="8_6lsog"]
|
||||||
[ext_resource type="PackedScene" uid="uid://dre1vit1m4d2n" path="res://objects/movement_abilities/grid_movement_ability.tscn" id="8_xuhvf"]
|
|
||||||
[ext_resource type="Script" uid="uid://dy78ak8eykw6e" path="res://scripts/components/FlipComponent.cs" id="9_yysbb"]
|
[ext_resource type="Script" uid="uid://dy78ak8eykw6e" path="res://scripts/components/FlipComponent.cs" id="9_yysbb"]
|
||||||
[ext_resource type="Script" uid="uid://mnjg3p0aw1ow" path="res://scripts/components/CanPickUpComponent.cs" id="10_yysbb"]
|
[ext_resource type="Script" uid="uid://mnjg3p0aw1ow" path="res://scripts/components/CanPickUpComponent.cs" id="10_yysbb"]
|
||||||
[ext_resource type="Script" uid="uid://ccqb8kd5m0eh7" path="res://scripts/components/ScoreComponent.cs" id="11_o1ihh"]
|
|
||||||
[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="12_ur2y5"]
|
[ext_resource type="Script" uid="uid://dgb8bqcri7nsj" path="res://scripts/components/HealthComponent.cs" id="12_ur2y5"]
|
||||||
[ext_resource type="Script" uid="uid://byw1legrv1ep2" path="res://scripts/components/PlayerDeathComponent.cs" id="13_7til7"]
|
[ext_resource type="Script" uid="uid://byw1legrv1ep2" path="res://scripts/components/PlayerDeathComponent.cs" id="13_7til7"]
|
||||||
[ext_resource type="Script" uid="uid://cecelixl41t3j" path="res://scripts/components/InvulnerabilityComponent.cs" id="15_xuhvf"]
|
[ext_resource type="Script" uid="uid://cecelixl41t3j" path="res://scripts/components/InvulnerabilityComponent.cs" id="15_xuhvf"]
|
||||||
@@ -76,7 +65,7 @@ point_count = 2
|
|||||||
[sub_resource type="CurveTexture" id="CurveTexture_xoue7"]
|
[sub_resource type="CurveTexture" id="CurveTexture_xoue7"]
|
||||||
curve = SubResource("Curve_82d6e")
|
curve = SubResource("Curve_82d6e")
|
||||||
|
|
||||||
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_lgb3u"]
|
[sub_resource type="ParticleProcessMaterial" id="ParticleProcessMaterial_oxudy"]
|
||||||
resource_local_to_scene = true
|
resource_local_to_scene = true
|
||||||
lifetime_randomness = 1.0
|
lifetime_randomness = 1.0
|
||||||
particle_flag_disable_z = true
|
particle_flag_disable_z = true
|
||||||
@@ -93,230 +82,202 @@ scale_curve = SubResource("CurveTexture_xoue7")
|
|||||||
color = Color(0.764706, 0.443137, 0, 1)
|
color = Color(0.764706, 0.443137, 0, 1)
|
||||||
color_ramp = SubResource("GradientTexture1D_lgb3u")
|
color_ramp = SubResource("GradientTexture1D_lgb3u")
|
||||||
|
|
||||||
[node name="Brick Player" type="CharacterBody2D" node_paths=PackedStringArray("MovementAbilitiesContainer") groups=["player"]]
|
[node name="Brick Player" type="CharacterBody2D" unique_id=634211282 node_paths=PackedStringArray("MovementAbilitiesContainer") groups=["player"]]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 107
|
collision_mask = 107
|
||||||
script = ExtResource("1_yysbb")
|
script = ExtResource("1_yysbb")
|
||||||
MovementAbilitiesContainer = NodePath("Movements")
|
MovementAbilitiesContainer = NodePath("Movements")
|
||||||
GroundMovementScene = ExtResource("2_oefns")
|
DefaultPreset = ExtResource("2_7til7")
|
||||||
JumpMovementScene = ExtResource("3_bnap0")
|
|
||||||
GravityScene = ExtResource("4_qec3q")
|
|
||||||
OneWayPlatformScene = ExtResource("5_dhjci")
|
|
||||||
SpaceshipMovementScene = ExtResource("6_721q0")
|
|
||||||
WallJumpScene = ExtResource("7_bnap0")
|
|
||||||
GridMovementScene = ExtResource("8_xuhvf")
|
|
||||||
metadata/_custom_type_script = "uid://csel4s0e4g5uf"
|
metadata/_custom_type_script = "uid://csel4s0e4g5uf"
|
||||||
|
|
||||||
[node name="Movements" type="Node" parent="."]
|
[node name="Movements" type="Node" parent="." unique_id=1545001]
|
||||||
|
|
||||||
[node name="GroundMovementAbility" type="Node" parent="Movements"]
|
[node name="Graphics" type="Node2D" parent="." unique_id=271317654]
|
||||||
script = ExtResource("4_7til7")
|
|
||||||
MaxSpeed = 376.0
|
|
||||||
Friction = 2500.0
|
|
||||||
metadata/_custom_type_script = "uid://bf4yclropol43"
|
|
||||||
|
|
||||||
[node name="GravityAbility" type="Node" parent="Movements"]
|
[node name="Root" type="Node2D" parent="Graphics" unique_id=2012260442]
|
||||||
script = ExtResource("6_xuhvf")
|
|
||||||
metadata/_custom_type_script = "uid://chgw53qwt7rt8"
|
|
||||||
|
|
||||||
[node name="VariableJumpAbility" type="Node" parent="Movements"]
|
[node name="Base" type="Sprite2D" parent="Graphics/Root" unique_id=1178176210]
|
||||||
script = ExtResource("7_bl1gx")
|
|
||||||
JumpCutMultiplier = 0.507
|
|
||||||
metadata/_custom_type_script = "uid://ccksp2e76s7sr"
|
|
||||||
|
|
||||||
[node name="OneWayPlatformAbility" type="Node" parent="Movements"]
|
|
||||||
script = ExtResource("7_uno3u")
|
|
||||||
metadata/_custom_type_script = "uid://ck6kmnbwhsttt"
|
|
||||||
|
|
||||||
[node name="Graphics" type="Node2D" parent="."]
|
|
||||||
|
|
||||||
[node name="Root" type="Node2D" parent="Graphics"]
|
|
||||||
|
|
||||||
[node name="Base" type="Sprite2D" parent="Graphics/Root"]
|
|
||||||
material = SubResource("ShaderMaterial_xoue7")
|
material = SubResource("ShaderMaterial_xoue7")
|
||||||
texture = ExtResource("5_yysbb")
|
texture = ExtResource("5_yysbb")
|
||||||
hframes = 5
|
hframes = 5
|
||||||
|
|
||||||
[node name="Left Eye" type="Sprite2D" parent="Graphics/Root"]
|
[node name="Left Eye" type="Sprite2D" parent="Graphics/Root" unique_id=653633051]
|
||||||
position = Vector2(-7, -6)
|
position = Vector2(-7, -6)
|
||||||
texture = ExtResource("3_2srrh")
|
texture = ExtResource("3_2srrh")
|
||||||
hframes = 2
|
hframes = 2
|
||||||
|
|
||||||
[node name="Right Eye" type="Sprite2D" parent="Graphics/Root"]
|
[node name="Right Eye" type="Sprite2D" parent="Graphics/Root" unique_id=721230787]
|
||||||
position = Vector2(6, -5)
|
position = Vector2(6, -5)
|
||||||
texture = ExtResource("4_ccn81")
|
texture = ExtResource("4_ccn81")
|
||||||
hframes = 2
|
hframes = 2
|
||||||
|
|
||||||
[node name="Ship" type="Sprite2D" parent="Graphics"]
|
[node name="Ship" type="Sprite2D" parent="Graphics" unique_id=1074443059]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector2(1, 7)
|
position = Vector2(1, 7)
|
||||||
texture = ExtResource("8_6lsog")
|
texture = ExtResource("8_6lsog")
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="." unique_id=22428429]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector2(0, 0.5)
|
position = Vector2(0, 0.5)
|
||||||
shape = SubResource("RectangleShape2D_hdsg1")
|
shape = SubResource("RectangleShape2D_hdsg1")
|
||||||
|
|
||||||
[node name="FlipPlayerComponent" type="Node2D" parent="." node_paths=PackedStringArray("LeftEye", "RightEye", "PlayerController")]
|
[node name="FlipPlayerComponent" type="Node2D" parent="." unique_id=290535881 node_paths=PackedStringArray("LeftEye", "RightEye", "PlayerController")]
|
||||||
script = ExtResource("9_yysbb")
|
script = ExtResource("9_yysbb")
|
||||||
LeftEye = NodePath("../Graphics/Root/Left Eye")
|
LeftEye = NodePath("../Graphics/Root/Left Eye")
|
||||||
RightEye = NodePath("../Graphics/Root/Right Eye")
|
RightEye = NodePath("../Graphics/Root/Right Eye")
|
||||||
PlayerController = NodePath("..")
|
PlayerController = NodePath("..")
|
||||||
|
|
||||||
[node name="StompDamageArea" type="Area2D" parent="."]
|
[node name="StompDamageArea" type="Area2D" parent="." unique_id=2071262827]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 8
|
collision_mask = 8
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="StompDamageArea"]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="StompDamageArea" unique_id=1230518642]
|
||||||
visible = false
|
visible = false
|
||||||
position = Vector2(0, 1)
|
position = Vector2(0, 1)
|
||||||
shape = SubResource("RectangleShape2D_vad0t")
|
shape = SubResource("RectangleShape2D_vad0t")
|
||||||
|
|
||||||
[node name="CanPickUpComponent" type="Node" parent="."]
|
[node name="CanPickUpComponent" type="Node" parent="." unique_id=1912343790]
|
||||||
script = ExtResource("10_yysbb")
|
script = ExtResource("10_yysbb")
|
||||||
|
|
||||||
[node name="ScoreComponent" type="Node" parent="."]
|
[node name="HealthComponent" type="Node2D" parent="." unique_id=278902432 node_paths=PackedStringArray("HurtSfx", "HealSfx")]
|
||||||
script = ExtResource("11_o1ihh")
|
|
||||||
|
|
||||||
[node name="HealthComponent" type="Node2D" parent="." node_paths=PackedStringArray("HurtSfx", "HealSfx")]
|
|
||||||
script = ExtResource("12_ur2y5")
|
script = ExtResource("12_ur2y5")
|
||||||
HurtSfx = NodePath("../sfx_hurt")
|
HurtSfx = NodePath("../sfx_hurt")
|
||||||
HealSfx = NodePath("../sfx_heal")
|
HealSfx = NodePath("../sfx_heal")
|
||||||
|
|
||||||
[node name="PlayerDeathComponent" type="Node2D" parent="." node_paths=PackedStringArray("DeathSfx", "HealthComponent")]
|
[node name="PlayerDeathComponent" type="Node2D" parent="." unique_id=1989490803 node_paths=PackedStringArray("DeathSfx", "HealthComponent")]
|
||||||
process_mode = 3
|
process_mode = 3
|
||||||
script = ExtResource("13_7til7")
|
script = ExtResource("13_7til7")
|
||||||
DeathSfx = NodePath("../sfx_hurt")
|
DeathSfx = NodePath("../sfx_hurt")
|
||||||
HealthComponent = NodePath("../HealthComponent")
|
HealthComponent = NodePath("../HealthComponent")
|
||||||
|
|
||||||
[node name="InvulnerabilityComponent" type="Node" parent="." node_paths=PackedStringArray("FlashingComponent")]
|
[node name="InvulnerabilityComponent" type="Node" parent="." unique_id=963173254 node_paths=PackedStringArray("FlashingComponent")]
|
||||||
script = ExtResource("15_xuhvf")
|
script = ExtResource("15_xuhvf")
|
||||||
FlashingComponent = NodePath("../FlashingComponent Base")
|
FlashingComponent = NodePath("../FlashingComponent Base")
|
||||||
|
|
||||||
[node name="FlashingComponent Base" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
[node name="FlashingComponent Base" type="Node" parent="." unique_id=533766453 node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
||||||
script = ExtResource("16_uno3u")
|
script = ExtResource("16_uno3u")
|
||||||
Sprite = NodePath("../Graphics/Root/Base")
|
Sprite = NodePath("../Graphics/Root/Base")
|
||||||
FlashDuration = 1.0
|
FlashDuration = 1.0
|
||||||
HealthComponent = NodePath("../HealthComponent")
|
HealthComponent = NodePath("../HealthComponent")
|
||||||
|
|
||||||
[node name="FlashingComponent LEye" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
[node name="FlashingComponent LEye" type="Node" parent="." unique_id=835038717 node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
||||||
script = ExtResource("16_uno3u")
|
script = ExtResource("16_uno3u")
|
||||||
Sprite = NodePath("../Graphics/Root/Left Eye")
|
Sprite = NodePath("../Graphics/Root/Left Eye")
|
||||||
FlashDuration = 1.0
|
FlashDuration = 1.0
|
||||||
HealthComponent = NodePath("../HealthComponent")
|
HealthComponent = NodePath("../HealthComponent")
|
||||||
|
|
||||||
[node name="FlashingComponent REye" type="Node" parent="." node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
[node name="FlashingComponent REye" type="Node" parent="." unique_id=601265429 node_paths=PackedStringArray("Sprite", "HealthComponent")]
|
||||||
script = ExtResource("16_uno3u")
|
script = ExtResource("16_uno3u")
|
||||||
Sprite = NodePath("../Graphics/Root/Right Eye")
|
Sprite = NodePath("../Graphics/Root/Right Eye")
|
||||||
FlashDuration = 1.0
|
FlashDuration = 1.0
|
||||||
HealthComponent = NodePath("../HealthComponent")
|
HealthComponent = NodePath("../HealthComponent")
|
||||||
|
|
||||||
[node name="StompDamageComponent" type="Node" parent="." node_paths=PackedStringArray("Area", "Root")]
|
[node name="StompDamageComponent" type="Node" parent="." unique_id=1027107459 node_paths=PackedStringArray("Area", "Root")]
|
||||||
script = ExtResource("17_bl1gx")
|
script = ExtResource("17_bl1gx")
|
||||||
Damage = 4.0
|
Damage = 4.0
|
||||||
Area = NodePath("../StompDamageArea")
|
Area = NodePath("../StompDamageArea")
|
||||||
Root = NodePath("..")
|
Root = NodePath("..")
|
||||||
|
|
||||||
[node name="SkillUnlockerComponent" type="Node" parent="."]
|
[node name="SkillUnlockerComponent" type="Node" parent="." unique_id=496647555]
|
||||||
script = ExtResource("25_yysbb")
|
script = ExtResource("25_yysbb")
|
||||||
|
|
||||||
[node name="HitComponent" type="Node" parent="." node_paths=PackedStringArray("Sprite", "Health", "HitFx")]
|
[node name="HitComponent" type="Node" parent="." unique_id=131916578 node_paths=PackedStringArray("Sprite", "Health", "HitFx")]
|
||||||
script = ExtResource("26_6n1ss")
|
script = ExtResource("26_6n1ss")
|
||||||
Sprite = NodePath("../Graphics/Root/Base")
|
Sprite = NodePath("../Graphics/Root/Base")
|
||||||
Health = NodePath("../HealthComponent")
|
Health = NodePath("../HealthComponent")
|
||||||
HitFx = NodePath("../HitParticles")
|
HitFx = NodePath("../HitParticles")
|
||||||
|
|
||||||
[node name="MagneticArea" type="Area2D" parent="."]
|
[node name="MagneticArea" type="Area2D" parent="." unique_id=1477305842]
|
||||||
collision_layer = 0
|
collision_layer = 0
|
||||||
collision_mask = 2
|
collision_mask = 2
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="MagneticArea"]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="MagneticArea" unique_id=1552683633]
|
||||||
visible = false
|
visible = false
|
||||||
shape = SubResource("CircleShape2D_ps31c")
|
shape = SubResource("CircleShape2D_ps31c")
|
||||||
|
|
||||||
[node name="CanBeLaunchedComponent" type="Node" parent="."]
|
[node name="CanBeLaunchedComponent" type="Node" parent="." unique_id=1998429478]
|
||||||
script = ExtResource("27_oefns")
|
script = ExtResource("27_oefns")
|
||||||
|
|
||||||
[node name="TriggerLeverComponent" type="Node" parent="."]
|
[node name="TriggerLeverComponent" type="Node" parent="." unique_id=366225548]
|
||||||
script = ExtResource("28_bnap0")
|
script = ExtResource("28_bnap0")
|
||||||
|
|
||||||
[node name="sfx_jump" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx_jump" type="AudioStreamPlayer2D" parent="." unique_id=218072388]
|
||||||
stream = ExtResource("18_pysae")
|
stream = ExtResource("18_pysae")
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|
||||||
[node name="sfx_hurt" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx_hurt" type="AudioStreamPlayer2D" parent="." unique_id=1815263121]
|
||||||
stream = ExtResource("19_7anly")
|
stream = ExtResource("19_7anly")
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|
||||||
[node name="sfx_heal" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx_heal" type="AudioStreamPlayer2D" parent="." unique_id=1792916109]
|
||||||
stream = ExtResource("20_bptj5")
|
stream = ExtResource("20_bptj5")
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|
||||||
[node name="sfx_shoot" type="AudioStreamPlayer2D" parent="."]
|
[node name="sfx_shoot" type="AudioStreamPlayer2D" parent="." unique_id=1414786458]
|
||||||
stream = ExtResource("32_x2b7c")
|
stream = ExtResource("32_x2b7c")
|
||||||
bus = &"sfx"
|
bus = &"sfx"
|
||||||
|
|
||||||
[node name="ChargingBarLayer" parent="." instance=ExtResource("28_3f5nm")]
|
[node name="ChargingBarLayer" parent="." unique_id=664675361 instance=ExtResource("28_3f5nm")]
|
||||||
offset_left = -17.0
|
offset_left = -17.0
|
||||||
offset_top = -30.0
|
offset_top = -30.0
|
||||||
offset_right = 23.0
|
offset_right = 23.0
|
||||||
offset_bottom = -20.0
|
offset_bottom = -20.0
|
||||||
|
|
||||||
[node name="HitParticles" parent="." instance=ExtResource("28_jh5m0")]
|
[node name="HitParticles" parent="." unique_id=1322585720 instance=ExtResource("28_jh5m0")]
|
||||||
process_material = SubResource("ParticleProcessMaterial_lgb3u")
|
process_material = SubResource("ParticleProcessMaterial_oxudy")
|
||||||
|
|
||||||
[node name="ShipShooter" type="Node" parent="." node_paths=PackedStringArray("BulletSpawn", "ShootSfx")]
|
[node name="ShipShooter" type="Node" parent="." unique_id=1147013800 node_paths=PackedStringArray("BulletSpawn", "ShootSfx")]
|
||||||
script = ExtResource("30_usc1p")
|
script = ExtResource("30_usc1p")
|
||||||
BulletScene = ExtResource("36_oxudy")
|
BulletScene = ExtResource("36_oxudy")
|
||||||
BulletSpawn = NodePath("../Ship shoot spawn")
|
BulletSpawn = NodePath("../Ship shoot spawn")
|
||||||
ShootSfx = NodePath("../sfx_shoot")
|
ShootSfx = NodePath("../sfx_shoot")
|
||||||
|
|
||||||
[node name="Ship shoot spawn" type="Marker2D" parent="."]
|
[node name="Ship shoot spawn" type="Marker2D" parent="." unique_id=1653596970]
|
||||||
position = Vector2(17, 5)
|
position = Vector2(17, 5)
|
||||||
gizmo_extents = 1.0
|
gizmo_extents = 1.0
|
||||||
|
|
||||||
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="."]
|
[node name="VisibleOnScreenNotifier2D" type="VisibleOnScreenNotifier2D" parent="." unique_id=65174812]
|
||||||
position = Vector2(0, 3)
|
position = Vector2(0, 3)
|
||||||
scale = Vector2(0.8, 1.9)
|
scale = Vector2(0.8, 1.9)
|
||||||
|
|
||||||
[node name="PlayerInputHandler" type="Node" parent="."]
|
[node name="PlayerInputHandler" type="Node" parent="." unique_id=1581385683]
|
||||||
script = ExtResource("42_e5pae")
|
script = ExtResource("42_e5pae")
|
||||||
metadata/_custom_type_script = "uid://dssa2taiwktis"
|
metadata/_custom_type_script = "uid://dssa2taiwktis"
|
||||||
|
|
||||||
[node name="SpriteTilterComponent" type="Node" parent="." node_paths=PackedStringArray("RotationTarget")]
|
[node name="SpriteTilterComponent" type="Node" parent="." unique_id=1433925210 node_paths=PackedStringArray("RotationTarget")]
|
||||||
script = ExtResource("43_xuhvf")
|
script = ExtResource("43_xuhvf")
|
||||||
RotationTarget = NodePath("../Graphics/Root/Base")
|
RotationTarget = NodePath("../Graphics/Root/Base")
|
||||||
metadata/_custom_type_script = "uid://ceoxet1nqws8w"
|
metadata/_custom_type_script = "uid://ceoxet1nqws8w"
|
||||||
|
|
||||||
[node name="PlayerSfxComponent" type="Node" parent="." node_paths=PackedStringArray("JumpSfx")]
|
[node name="PlayerSfxComponent" type="Node" parent="." unique_id=1375936690 node_paths=PackedStringArray("JumpSfx")]
|
||||||
script = ExtResource("49_qec3q")
|
script = ExtResource("49_qec3q")
|
||||||
JumpSfx = NodePath("../sfx_jump")
|
JumpSfx = NodePath("../sfx_jump")
|
||||||
metadata/_custom_type_script = "uid://b1h8r5irryxcx"
|
metadata/_custom_type_script = "uid://b1h8r5irryxcx"
|
||||||
|
|
||||||
[node name="PlayerGraphicsComponent" type="Node" parent="." node_paths=PackedStringArray("DefaultSprite", "SpaceshipSprite")]
|
[node name="PlayerGraphicsComponent" type="Node" parent="." unique_id=443649347 node_paths=PackedStringArray("DefaultSprite", "SpaceshipSprite")]
|
||||||
script = ExtResource("50_dhjci")
|
script = ExtResource("50_dhjci")
|
||||||
DefaultSprite = NodePath("../Graphics/Root")
|
DefaultSprite = NodePath("../Graphics/Root")
|
||||||
SpaceshipSprite = NodePath("../Graphics/Ship")
|
SpaceshipSprite = NodePath("../Graphics/Ship")
|
||||||
metadata/_custom_type_script = "uid://b2aanqykvdnev"
|
metadata/_custom_type_script = "uid://b2aanqykvdnev"
|
||||||
|
|
||||||
[node name="ProgressiveDamageComponent" type="Node" parent="." node_paths=PackedStringArray("HealthComponent", "Sprite")]
|
[node name="ProgressiveDamageComponent" type="Node" parent="." unique_id=1763641730 node_paths=PackedStringArray("HealthComponent", "Sprite")]
|
||||||
script = ExtResource("38_dhjci")
|
script = ExtResource("38_dhjci")
|
||||||
HealthComponent = NodePath("../HealthComponent")
|
HealthComponent = NodePath("../HealthComponent")
|
||||||
Sprite = NodePath("../Graphics/Root/Base")
|
Sprite = NodePath("../Graphics/Root/Base")
|
||||||
metadata/_custom_type_script = "uid://dupnaark1f7gm"
|
metadata/_custom_type_script = "uid://dupnaark1f7gm"
|
||||||
|
|
||||||
[node name="PacXonGridInteractor" type="Node" parent="."]
|
[node name="PacXonGridInteractor" type="Node" parent="." unique_id=1036082136]
|
||||||
script = ExtResource("42_xuhvf")
|
script = ExtResource("42_xuhvf")
|
||||||
metadata/_custom_type_script = "uid://c00siqtssccr6"
|
metadata/_custom_type_script = "uid://c00siqtssccr6"
|
||||||
|
|
||||||
[node name="PacXonTrailComponent" type="Line2D" parent="."]
|
[node name="PacXonTrailComponent" type="Line2D" parent="." unique_id=100969687]
|
||||||
script = ExtResource("44_uno3u")
|
script = ExtResource("44_uno3u")
|
||||||
metadata/_custom_type_script = "uid://cmk4m7mplqnrm"
|
metadata/_custom_type_script = "uid://cmk4m7mplqnrm"
|
||||||
|
|
||||||
[node name="Feet" type="Marker2D" parent="."]
|
[node name="Feet" type="Marker2D" parent="." unique_id=1308840991]
|
||||||
position = Vector2(0, 16)
|
position = Vector2(0, 16)
|
||||||
|
|
||||||
[node name="FootstepGfx" type="Node2D" parent="." node_paths=PackedStringArray("_controller", "_marker")]
|
[node name="FootstepGfx" type="Node2D" parent="." unique_id=362059837 node_paths=PackedStringArray("_controller", "_marker")]
|
||||||
script = ExtResource("46_6n1ss")
|
script = ExtResource("46_6n1ss")
|
||||||
_particles = ExtResource("45_bl1gx")
|
_particles = ExtResource("45_bl1gx")
|
||||||
_controller = NodePath("..")
|
_controller = NodePath("..")
|
||||||
@@ -327,7 +288,7 @@ _minMoveSpeed = 4.0
|
|||||||
_randomOffsetRange = 0.3
|
_randomOffsetRange = 0.3
|
||||||
metadata/_custom_type_script = "uid://d3ksrjt1ek4gi"
|
metadata/_custom_type_script = "uid://d3ksrjt1ek4gi"
|
||||||
|
|
||||||
[node name="JumpGfxComponent" type="Node2D" parent="." node_paths=PackedStringArray("Controller")]
|
[node name="JumpGfxComponent" type="Node2D" parent="." unique_id=1468920790 node_paths=PackedStringArray("Controller")]
|
||||||
script = ExtResource("47_oefns")
|
script = ExtResource("47_oefns")
|
||||||
ParticleScene = ExtResource("48_bnap0")
|
ParticleScene = ExtResource("48_bnap0")
|
||||||
Controller = NodePath("..")
|
Controller = NodePath("..")
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ radius = 4.0
|
|||||||
|
|
||||||
[node name="Bullet" type="Area2D"]
|
[node name="Bullet" type="Area2D"]
|
||||||
collision_layer = 64
|
collision_layer = 64
|
||||||
collision_mask = 21
|
collision_mask = 85
|
||||||
|
|
||||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
shape = SubResource("CircleShape2D_txsw8")
|
shape = SubResource("CircleShape2D_txsw8")
|
||||||
@@ -47,7 +47,7 @@ VisibilityNotifier = NodePath("../VisibleOnScreenNotifier2D")
|
|||||||
|
|
||||||
[node name="LifetimeComponent" type="Node" parent="."]
|
[node name="LifetimeComponent" type="Node" parent="."]
|
||||||
script = ExtResource("4_aniyw")
|
script = ExtResource("4_aniyw")
|
||||||
LifeTime = 10.0
|
LifeTime = 3.0
|
||||||
|
|
||||||
[node name="LaunchComponent" type="Node2D" parent="."]
|
[node name="LaunchComponent" type="Node2D" parent="."]
|
||||||
script = ExtResource("5_3ks47")
|
script = ExtResource("5_3ks47")
|
||||||
|
|||||||
48
objects/entities/cannon_down.tscn
Normal file
48
objects/entities/cannon_down.tscn
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
[gd_scene load_steps=7 format=3 uid="uid://qrbiu1qo4tt5"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_0efvn"]
|
||||||
|
[ext_resource type="Script" uid="uid://bnaxy8cw3wrko" path="res://scripts/components/PeriodicShootingComponent.cs" id="2_4ycs1"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://chetx6gmnwbxi" path="res://objects/entities/cannon_bullet.tscn" id="3_ab2uh"]
|
||||||
|
[ext_resource type="Script" uid="uid://b3j23e7b7x8ro" path="res://scripts/components/RecoilComponent.cs" id="4_tynaf"]
|
||||||
|
[ext_resource type="Script" uid="uid://c707c53k7c5ae" path="res://scripts/components/SquashAndStretchComponent.cs" id="5_0xigu"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_j5sus"]
|
||||||
|
size = Vector2(16, 16)
|
||||||
|
|
||||||
|
[node name="Cannon" type="StaticBody2D"]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
rotation = 3.14159
|
||||||
|
texture = ExtResource("1_0efvn")
|
||||||
|
hframes = 12
|
||||||
|
vframes = 12
|
||||||
|
frame = 42
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
shape = SubResource("RectangleShape2D_j5sus")
|
||||||
|
|
||||||
|
[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("BulletSpawnPointRight")]
|
||||||
|
script = ExtResource("2_4ycs1")
|
||||||
|
BulletScene = ExtResource("3_ab2uh")
|
||||||
|
ShootInterval = 3.0
|
||||||
|
ShootDirection = Vector2(0, 1)
|
||||||
|
BulletSpawnPointRight = NodePath("../Bullet spawn")
|
||||||
|
ShootingIntervalVariation = 0.61
|
||||||
|
metadata/_custom_type_script = "uid://bnaxy8cw3wrko"
|
||||||
|
|
||||||
|
[node name="Bullet spawn" type="Marker2D" parent="."]
|
||||||
|
position = Vector2(0, 12)
|
||||||
|
|
||||||
|
[node name="RecoilComponent" type="Node" parent="." node_paths=PackedStringArray("RecoilTarget")]
|
||||||
|
script = ExtResource("4_tynaf")
|
||||||
|
RecoilTarget = NodePath("../Sprite2D")
|
||||||
|
RecoilDistance = 4.0
|
||||||
|
RecoilDuration = 0.12
|
||||||
|
metadata/_custom_type_script = "uid://b3j23e7b7x8ro"
|
||||||
|
|
||||||
|
[node name="SquashAndStretchComponent" type="Node" parent="." node_paths=PackedStringArray("TargetNode")]
|
||||||
|
script = ExtResource("5_0xigu")
|
||||||
|
TargetNode = NodePath("../Sprite2D")
|
||||||
|
metadata/_custom_type_script = "uid://c707c53k7c5ae"
|
||||||
48
objects/entities/cannon_left.tscn
Normal file
48
objects/entities/cannon_left.tscn
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
[gd_scene load_steps=7 format=3 uid="uid://cm0rd70wafif1"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_hnnrt"]
|
||||||
|
[ext_resource type="Script" uid="uid://bnaxy8cw3wrko" path="res://scripts/components/PeriodicShootingComponent.cs" id="2_cho7n"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://chetx6gmnwbxi" path="res://objects/entities/cannon_bullet.tscn" id="3_kgmtj"]
|
||||||
|
[ext_resource type="Script" uid="uid://b3j23e7b7x8ro" path="res://scripts/components/RecoilComponent.cs" id="4_nfk87"]
|
||||||
|
[ext_resource type="Script" uid="uid://c707c53k7c5ae" path="res://scripts/components/SquashAndStretchComponent.cs" id="5_43fqe"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_j5sus"]
|
||||||
|
size = Vector2(16, 16)
|
||||||
|
|
||||||
|
[node name="Cannon" type="StaticBody2D"]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
rotation = -1.5708
|
||||||
|
texture = ExtResource("1_hnnrt")
|
||||||
|
hframes = 12
|
||||||
|
vframes = 12
|
||||||
|
frame = 42
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
shape = SubResource("RectangleShape2D_j5sus")
|
||||||
|
|
||||||
|
[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("BulletSpawnPointRight")]
|
||||||
|
script = ExtResource("2_cho7n")
|
||||||
|
BulletScene = ExtResource("3_kgmtj")
|
||||||
|
ShootInterval = 3.0
|
||||||
|
ShootDirection = Vector2(-1, 0)
|
||||||
|
BulletSpawnPointRight = NodePath("../Bullet spawn")
|
||||||
|
ShootingIntervalVariation = 0.61
|
||||||
|
metadata/_custom_type_script = "uid://bnaxy8cw3wrko"
|
||||||
|
|
||||||
|
[node name="Bullet spawn" type="Marker2D" parent="."]
|
||||||
|
position = Vector2(-12, 0)
|
||||||
|
|
||||||
|
[node name="RecoilComponent" type="Node" parent="." node_paths=PackedStringArray("RecoilTarget")]
|
||||||
|
script = ExtResource("4_nfk87")
|
||||||
|
RecoilTarget = NodePath("../Sprite2D")
|
||||||
|
RecoilDistance = 4.0
|
||||||
|
RecoilDuration = 0.12
|
||||||
|
metadata/_custom_type_script = "uid://b3j23e7b7x8ro"
|
||||||
|
|
||||||
|
[node name="SquashAndStretchComponent" type="Node" parent="." node_paths=PackedStringArray("TargetNode")]
|
||||||
|
script = ExtResource("5_43fqe")
|
||||||
|
TargetNode = NodePath("../Sprite2D")
|
||||||
|
metadata/_custom_type_script = "uid://c707c53k7c5ae"
|
||||||
47
objects/entities/cannon_right.tscn
Normal file
47
objects/entities/cannon_right.tscn
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
[gd_scene load_steps=7 format=3 uid="uid://dr6srln4mckwn"]
|
||||||
|
|
||||||
|
[ext_resource type="Texture2D" uid="uid://djifxc5x0dyrw" path="res://sprites/ppc_tileset.png" id="1_r5bjs"]
|
||||||
|
[ext_resource type="Script" uid="uid://bnaxy8cw3wrko" path="res://scripts/components/PeriodicShootingComponent.cs" id="2_gd3be"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://chetx6gmnwbxi" path="res://objects/entities/cannon_bullet.tscn" id="3_1gufj"]
|
||||||
|
[ext_resource type="Script" uid="uid://b3j23e7b7x8ro" path="res://scripts/components/RecoilComponent.cs" id="4_hwc6x"]
|
||||||
|
[ext_resource type="Script" uid="uid://c707c53k7c5ae" path="res://scripts/components/SquashAndStretchComponent.cs" id="5_o674s"]
|
||||||
|
|
||||||
|
[sub_resource type="RectangleShape2D" id="RectangleShape2D_j5sus"]
|
||||||
|
size = Vector2(16, 16)
|
||||||
|
|
||||||
|
[node name="Cannon" type="StaticBody2D"]
|
||||||
|
collision_layer = 0
|
||||||
|
collision_mask = 0
|
||||||
|
|
||||||
|
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||||
|
rotation = 1.5708
|
||||||
|
texture = ExtResource("1_r5bjs")
|
||||||
|
hframes = 12
|
||||||
|
vframes = 12
|
||||||
|
frame = 42
|
||||||
|
|
||||||
|
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
|
||||||
|
shape = SubResource("RectangleShape2D_j5sus")
|
||||||
|
|
||||||
|
[node name="PeriodicShootingComponent" type="Node" parent="." node_paths=PackedStringArray("BulletSpawnPointRight")]
|
||||||
|
script = ExtResource("2_gd3be")
|
||||||
|
BulletScene = ExtResource("3_1gufj")
|
||||||
|
ShootInterval = 3.0
|
||||||
|
BulletSpawnPointRight = NodePath("../Bullet spawn")
|
||||||
|
ShootingIntervalVariation = 0.61
|
||||||
|
metadata/_custom_type_script = "uid://bnaxy8cw3wrko"
|
||||||
|
|
||||||
|
[node name="Bullet spawn" type="Marker2D" parent="."]
|
||||||
|
position = Vector2(12, 0)
|
||||||
|
|
||||||
|
[node name="RecoilComponent" type="Node" parent="." node_paths=PackedStringArray("RecoilTarget")]
|
||||||
|
script = ExtResource("4_hwc6x")
|
||||||
|
RecoilTarget = NodePath("../Sprite2D")
|
||||||
|
RecoilDistance = 4.0
|
||||||
|
RecoilDuration = 0.12
|
||||||
|
metadata/_custom_type_script = "uid://b3j23e7b7x8ro"
|
||||||
|
|
||||||
|
[node name="SquashAndStretchComponent" type="Node" parent="." node_paths=PackedStringArray("TargetNode")]
|
||||||
|
script = ExtResource("5_o674s")
|
||||||
|
TargetNode = NodePath("../Sprite2D")
|
||||||
|
metadata/_custom_type_script = "uid://c707c53k7c5ae"
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user