Compare commits
88 Commits
afca70e6c6
...
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 | |||
| bc3108ea37 | |||
| 7257242fce | |||
| 03abf91f59 | |||
| e6f8989d16 | |||
| db2a090acc | |||
| dfc9201f62 | |||
| 46553a351a | |||
| a8ff492aed | |||
| aa73e54b3e | |||
| 98b3202361 | |||
| 6e2bdcdf95 | |||
| f229ff5b7d | |||
| f9cb59d182 | |||
| ead52f6d51 | |||
| bd40c797d4 | |||
| 2d520a708f | |||
| 257a492f72 | |||
| fd10e566b3 | |||
| 021e984877 | |||
| 4b7c38397c | |||
| 6d0e337bd6 | |||
| 51aecf7da5 | |||
| 604520cad5 | |||
| 36d1cac284 | |||
| 53b5f968fd | |||
| 88c7a0a055 | |||
| d786ef4c22 |
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -1,2 +1,4 @@
|
||||
# Normalize EOL for all files that Git considers text files.
|
||||
* text=auto eol=lf
|
||||
addons/* linguist-vendored
|
||||
*.gd linguist-vendored
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,4 +1,13 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
/android/
|
||||
builds/
|
||||
builds/
|
||||
.worktrees/
|
||||
|
||||
# IDE / local tooling
|
||||
.idea/
|
||||
.superpowers/
|
||||
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
|
||||
110
Autoloads/AchievementManager.cs
Normal file
110
Autoloads/AchievementManager.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
using Mr.BrickAdventures.scripts.State;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
/// <summary>
|
||||
/// Manages achievements using GameStateStore.
|
||||
/// </summary>
|
||||
public partial class AchievementManager : Node
|
||||
{
|
||||
public static AchievementManager Instance { get; private set; }
|
||||
|
||||
[Export] private string AchievementsFolderPath = "res://achievements/";
|
||||
[Export] private PackedScene AchievementPopupScene { get; set; }
|
||||
|
||||
private System.Collections.Generic.Dictionary<string, AchievementResource> _achievements = new();
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Instance = this;
|
||||
LoadAchievementsFromFolder();
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
private void LoadAchievementsFromFolder()
|
||||
{
|
||||
using var dir = DirAccess.Open(AchievementsFolderPath);
|
||||
if (dir == null)
|
||||
{
|
||||
GD.PrintErr($"AchievementManager: Could not open achievements folder at '{AchievementsFolderPath}'");
|
||||
return;
|
||||
}
|
||||
|
||||
dir.ListDirBegin();
|
||||
var fileName = dir.GetNext();
|
||||
while (fileName != "")
|
||||
{
|
||||
if (!dir.CurrentIsDir() && fileName.EndsWith(".tres"))
|
||||
{
|
||||
var achievement = GD.Load<AchievementResource>(AchievementsFolderPath + fileName);
|
||||
if (achievement != null)
|
||||
{
|
||||
_achievements.TryAdd(achievement.Id, achievement);
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
if (!_achievements.TryGetValue(achievementId, out var achievement))
|
||||
{
|
||||
GD.PrintErr($"Attempted to unlock non-existent achievement: '{achievementId}'");
|
||||
return;
|
||||
}
|
||||
|
||||
var unlockedIds = GetUnlockedIds();
|
||||
if (unlockedIds.Contains(achievementId))
|
||||
{
|
||||
return; // Already unlocked
|
||||
}
|
||||
|
||||
// 1. Mark as unlocked
|
||||
unlockedIds.Add(achievementId);
|
||||
GD.Print($"Achievement Unlocked: {achievement.DisplayName}");
|
||||
EventBus.EmitAchievementUnlocked(achievementId);
|
||||
|
||||
// 2. Show the UI popup
|
||||
if (AchievementPopupScene != null)
|
||||
{
|
||||
var popup = AchievementPopupScene.Instantiate<scripts.UI.AchievementPopup>();
|
||||
GetTree().Root.AddChild(popup);
|
||||
_ = popup.ShowAchievement(achievement);
|
||||
}
|
||||
|
||||
// 3. Call SteamManager if it's available
|
||||
if (SteamManager.IsSteamInitialized)
|
||||
{
|
||||
SteamManager.UnlockAchievement(achievement.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public void LockAchievement(string achievementId)
|
||||
{
|
||||
var unlockedIds = GetUnlockedIds();
|
||||
if (unlockedIds.Contains(achievementId))
|
||||
{
|
||||
unlockedIds.Remove(achievementId);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAchievementUnlocked(string achievementId)
|
||||
{
|
||||
return GetUnlockedIds().Contains(achievementId);
|
||||
}
|
||||
}
|
||||
1
Autoloads/AchievementManager.cs.uid
Normal file
1
Autoloads/AchievementManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c4vvuqnx5y33u
|
||||
@@ -4,13 +4,16 @@ namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
public partial class ConfigFileHandler : Node
|
||||
{
|
||||
public static ConfigFileHandler Instance { get; private set; }
|
||||
|
||||
private ConfigFile _settingsConfig = new();
|
||||
public const string SettingsPath = "user://settings.ini";
|
||||
|
||||
|
||||
public ConfigFile SettingsConfig => _settingsConfig;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Instance = this;
|
||||
if (!FileAccess.FileExists(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}");
|
||||
}
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +1,120 @@
|
||||
using Godot;
|
||||
using Limbo.Console.Sharp;
|
||||
using Mr.BrickAdventures.scripts;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
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 SkillUnlockerComponent _skillUnlockerComponent;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||
|
||||
RegisterConsoleCommands();
|
||||
_skillManager = SkillManager.Instance;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
UnregisterConsoleCommands();
|
||||
}
|
||||
|
||||
[ConsoleCommand("add_coins", "Adds a specified amount of coins to the player's total.")]
|
||||
private void AddCoinsCommand(int amount)
|
||||
{
|
||||
_gameManager.AddCoins(amount);
|
||||
LimboConsole.Info($"Increased coins by {amount}. Total coins: {_gameManager.GetCoins()}");
|
||||
if (Store == null) return;
|
||||
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)
|
||||
{
|
||||
_gameManager.SetCoins(amount);
|
||||
LimboConsole.Info($"Set coins to {amount}. Total coins: {_gameManager.GetCoins()}");
|
||||
if (Store == null) return;
|
||||
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)
|
||||
{
|
||||
_gameManager.SetLives(amount);
|
||||
LimboConsole.Info($"Set lives to {amount}.");
|
||||
if (Store == null) return;
|
||||
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)
|
||||
{
|
||||
_gameManager.AddLives(amount);
|
||||
LimboConsole.Info($"Increased lives by {amount}. Total lives: {_gameManager.GetLives()}");
|
||||
Store?.AddLives(amount);
|
||||
}
|
||||
|
||||
[ConsoleCommand("set_health", "Sets the player's health to a specified amount.")]
|
||||
private void SetHealthCommand(float amount)
|
||||
{
|
||||
var playerHealthComponent = _gameManager.Player.GetNode<HealthComponent>("HealthComponent");
|
||||
var playerHealthComponent = GameManager?.Player?.GetNode<HealthComponent>("HealthComponent");
|
||||
if (playerHealthComponent != null)
|
||||
{
|
||||
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()
|
||||
{
|
||||
_gameManager.ResetCurrentSessionState();
|
||||
LimboConsole.Info("Current session state has been reset.");
|
||||
Store?.ResetSession();
|
||||
}
|
||||
|
||||
[ConsoleCommand("unlock_skill", "Unlocks and activates a skill by its name.")]
|
||||
private void UnlockSkillCommand(string skillName)
|
||||
{
|
||||
if (!GetSkillManagement()) return;
|
||||
if (!EnsureSkillManagement()) return;
|
||||
|
||||
var skill = _skillManager.GetSkillByName(skillName);
|
||||
if (skill == null)
|
||||
{
|
||||
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
||||
return;
|
||||
}
|
||||
if (skill == null) return;
|
||||
|
||||
_gameManager.UnlockSkill(skill);
|
||||
Store?.UnlockSkillPermanently(skill);
|
||||
_skillManager.ActivateSkill(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)
|
||||
{
|
||||
LimboConsole.Warn("Player node not found.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_skillManager ??= player.GetNode<SkillManager>("SkillManager");
|
||||
_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()
|
||||
{
|
||||
if (!GetSkillManagement()) return;
|
||||
|
||||
if (!EnsureSkillManagement()) return;
|
||||
_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)
|
||||
{
|
||||
if (!GetSkillManagement()) return;
|
||||
if (!EnsureSkillManagement()) return;
|
||||
|
||||
var skill = _skillManager.GetSkillByName(skillName);
|
||||
if (skill == null)
|
||||
{
|
||||
LimboConsole.Warn($"Skill '{skillName}' not found.");
|
||||
return;
|
||||
}
|
||||
if (skill == null) return;
|
||||
|
||||
_gameManager.RemoveSkill(skill.Name);
|
||||
Store?.RemoveUnlockedSkill(skill.Name);
|
||||
_skillManager.DeactivateSkill(skill);
|
||||
LimboConsole.Info($"Skill '{skillName}' has been deactivated.");
|
||||
}
|
||||
|
||||
[ConsoleCommand("remove_all_skills", "Deactivates and removes all skills.")]
|
||||
|
||||
private void RemoveAllSkillsCommand()
|
||||
{
|
||||
if (!GetSkillManagement()) return;
|
||||
|
||||
if (!EnsureSkillManagement()) return;
|
||||
|
||||
foreach (var skill in _skillManager.AvailableSkills)
|
||||
{
|
||||
_gameManager.RemoveSkill(skill.Name);
|
||||
Store?.RemoveUnlockedSkill(skill.Name);
|
||||
_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();
|
||||
}
|
||||
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
194
Autoloads/EventBus.cs
Normal file
194
Autoloads/EventBus.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
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
|
||||
{
|
||||
/// <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 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
Autoloads/EventBus.cs.uid
Normal file
1
Autoloads/EventBus.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://bb2yq5wggdw3w
|
||||
66
Autoloads/FloatingTextManager.cs
Normal file
66
Autoloads/FloatingTextManager.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
using Mr.BrickAdventures.scripts.UI;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
public partial class FloatingTextManager : Node
|
||||
{
|
||||
[Export] public PackedScene FloatingTextScene { get; set; }
|
||||
|
||||
[ExportGroup("Colors")]
|
||||
[Export] public Color DamageColor { get; set; } = new Color("#b21030"); // Red
|
||||
[Export] public Color HealColor { get; set; } = new Color("#71f341"); // Green
|
||||
[Export] public Color CoinColor { get; set; } = new Color("#ebd320"); // Gold
|
||||
[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)
|
||||
{
|
||||
var text = Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture);
|
||||
CreateFloatingText(text, position, DamageColor);
|
||||
}
|
||||
|
||||
public void ShowHeal(float amount, Vector2 position)
|
||||
{
|
||||
var text = $"+{Mathf.Round(amount * 100f).ToString(CultureInfo.InvariantCulture)}";
|
||||
CreateFloatingText(text, position, HealColor);
|
||||
}
|
||||
|
||||
public void ShowCoin(int amount, Vector2 position)
|
||||
{
|
||||
var text = $"+{amount}";
|
||||
CreateFloatingText(text, position, CoinColor);
|
||||
}
|
||||
|
||||
public void ShowMessage(string message, Vector2 position)
|
||||
{
|
||||
CreateFloatingText(message, position, MessageColor);
|
||||
}
|
||||
|
||||
private void CreateFloatingText(string text, Vector2 position, Color color)
|
||||
{
|
||||
if (FloatingTextScene == null)
|
||||
{
|
||||
GD.PushError("FloatingTextManager: FloatingTextScene is not set!");
|
||||
return;
|
||||
}
|
||||
|
||||
var popup = FloatingTextScene.Instantiate<FloatingText>();
|
||||
GetTree().CurrentScene.AddChild(popup);
|
||||
popup.Show(text, position, color);
|
||||
}
|
||||
}
|
||||
1
Autoloads/FloatingTextManager.cs.uid
Normal file
1
Autoloads/FloatingTextManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cobgfsr3gw7cn
|
||||
@@ -1,247 +1,191 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
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
|
||||
{
|
||||
[Export] public Array<PackedScene> LevelScenes { get; set; } = [];
|
||||
|
||||
public PlayerController Player {
|
||||
public PlayerController Player
|
||||
{
|
||||
get => GetPlayer();
|
||||
private set => _player = value;
|
||||
}
|
||||
|
||||
private List<Node> _sceneNodes = [];
|
||||
private PlayerController _player;
|
||||
|
||||
[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;
|
||||
GetTree().NodeRemoved += OnNodeRemoved;
|
||||
Instance = this;
|
||||
EventBus.Instance.PlayerSpawned += OnPlayerSpawned;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
GetTree().NodeAdded -= OnNodeAdded;
|
||||
GetTree().NodeRemoved -= OnNodeRemoved;
|
||||
_sceneNodes.Clear();
|
||||
if (Instance == this) Instance = null;
|
||||
if (EventBus.Instance != null)
|
||||
EventBus.Instance.PlayerSpawned -= OnPlayerSpawned;
|
||||
}
|
||||
|
||||
private void OnNodeAdded(Node node)
|
||||
private void OnPlayerSpawned(PlayerController player)
|
||||
{
|
||||
_sceneNodes.Add(node);
|
||||
}
|
||||
|
||||
private void OnNodeRemoved(Node node)
|
||||
{
|
||||
_sceneNodes.Remove(node);
|
||||
_player = player;
|
||||
player.TreeExiting += () => { if (_player == 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);
|
||||
}
|
||||
#region Game Flow
|
||||
|
||||
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>() }
|
||||
};
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
ResetPlayerState();
|
||||
ResetCurrentSessionState();
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[0]);
|
||||
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
||||
if (Progression == null) return;
|
||||
|
||||
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||
SpeedRunManager?.StartTimer();
|
||||
EventBus.EmitGameStarted();
|
||||
|
||||
GoTo(Progression.Campaign.First);
|
||||
}
|
||||
|
||||
public void ContinueGame()
|
||||
{
|
||||
var save = GetNode<SaveSystem>("/root/SaveSystem");
|
||||
if (!save.LoadGame())
|
||||
if (Progression == null) return;
|
||||
|
||||
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();
|
||||
return;
|
||||
}
|
||||
|
||||
var idx = (int)PlayerState["current_level"];
|
||||
if (idx < LevelScenes.Count)
|
||||
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
|
||||
else
|
||||
GD.PrintErr("No levels unlocked to continue.");
|
||||
SpeedRunManager?.StartTimer();
|
||||
EventBus.EmitGameContinued();
|
||||
|
||||
GoTo(Progression.CurrentLevel ?? Progression.Campaign.First);
|
||||
}
|
||||
|
||||
|
||||
public void RestartGame()
|
||||
{
|
||||
if (Progression == null) return;
|
||||
|
||||
Progression.StartPlaythrough(ProgressionRoot.SoleSlot);
|
||||
SpeedRunManager?.StartTimer();
|
||||
|
||||
GoTo(Progression.Campaign.First);
|
||||
}
|
||||
|
||||
public void OnLevelComplete()
|
||||
{
|
||||
var levelIndex = (int)PlayerState["current_level"];
|
||||
MarkLevelComplete(levelIndex);
|
||||
AddCoins((int)CurrentSessionState["coins_collected"]);
|
||||
foreach (var s in (Array)CurrentSessionState["skills_unlocked"])
|
||||
UnlockSkill((SkillData)s);
|
||||
if (Progression?.CurrentLevel is not { } finished) return;
|
||||
|
||||
ResetCurrentSessionState();
|
||||
TryToGoToNextLevel();
|
||||
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
|
||||
var seconds = SpeedRunManager?.GetCurrentLevelTime() ?? 0.0;
|
||||
LevelTime? time = seconds > 0 ? LevelTime.Of(seconds) : null;
|
||||
|
||||
EventBus.EmitLevelCompleted(IndexOf(finished), GetTree().CurrentScene, seconds);
|
||||
|
||||
// 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"];
|
||||
var session = (Array<SkillData>)CurrentSessionState["skills_unlocked"];
|
||||
if (session!.Count == 0) return unlocked;
|
||||
if (unlocked!.Count == 0) return session;
|
||||
var joined = new Array<SkillData>();
|
||||
joined.AddRange(unlocked);
|
||||
joined.AddRange(session);
|
||||
return joined;
|
||||
if (!Progression.CanEnter(level))
|
||||
{
|
||||
GD.PushError($"Level '{level}' is not reachable in this playthrough.");
|
||||
return;
|
||||
}
|
||||
|
||||
var scene = Progression.Catalogue.SceneFor(level);
|
||||
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()
|
||||
{
|
||||
if (_player != null) return _player;
|
||||
|
||||
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.");
|
||||
if (_player != null && IsInstanceValid(_player)) return _player;
|
||||
_player = 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
|
||||
136
Autoloads/GhostManager.cs
Normal file
136
Autoloads/GhostManager.cs
Normal file
@@ -0,0 +1,136 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json;
|
||||
using Godot;
|
||||
using Mr.BrickAdventures.scripts;
|
||||
using Mr.BrickAdventures.scripts.State;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
public partial class GhostManager : Node
|
||||
{
|
||||
public static GhostManager Instance { get; private 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 IsPlaybackEnabled { get; private set; } = true;
|
||||
|
||||
private List<GhostFrame> _currentRecording = [];
|
||||
private double _startTime = 0.0;
|
||||
private int _currentLevelIndex = -1;
|
||||
|
||||
public void StartRecording(int levelIndex)
|
||||
{
|
||||
if (!IsPlaybackEnabled) return;
|
||||
|
||||
_currentLevelIndex = levelIndex;
|
||||
_currentRecording.Clear();
|
||||
_startTime = Time.GetTicksMsec() / 1000.0;
|
||||
IsRecording = true;
|
||||
GD.Print("Ghost recording started.");
|
||||
}
|
||||
|
||||
public void StopRecording(bool levelCompleted, double finalTime)
|
||||
{
|
||||
if (!IsRecording) return;
|
||||
IsRecording = false;
|
||||
|
||||
if (levelCompleted)
|
||||
{
|
||||
var bestTime = LoadBestTime(_currentLevelIndex);
|
||||
if (finalTime < bestTime)
|
||||
{
|
||||
SaveGhostData(_currentLevelIndex, finalTime);
|
||||
GD.Print($"New best ghost saved for level {_currentLevelIndex}. Time: {finalTime}");
|
||||
}
|
||||
}
|
||||
_currentRecording.Clear();
|
||||
}
|
||||
|
||||
public void RecordFrame(Vector2 position)
|
||||
{
|
||||
if (!IsRecording) return;
|
||||
|
||||
_currentRecording.Add(new GhostFrame
|
||||
{
|
||||
Timestamp = (Time.GetTicksMsec() / 1000.0) - _startTime,
|
||||
Position = position
|
||||
});
|
||||
}
|
||||
|
||||
public void SpawnGhostPlayer(int levelIndex, Node parent)
|
||||
{
|
||||
if (!IsPlaybackEnabled || GhostPlayerScene == null) return;
|
||||
|
||||
var ghostData = LoadGhostData(levelIndex);
|
||||
if (ghostData.Count > 0)
|
||||
{
|
||||
var ghostPlayer = GhostPlayerScene.Instantiate<GhostPlayer>();
|
||||
parent.AddChild(ghostPlayer);
|
||||
ghostPlayer.StartPlayback(ghostData);
|
||||
GD.Print($"Ghost player spawned for level {levelIndex}.");
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveGhostData(int levelIndex, double time)
|
||||
{
|
||||
var path = $"user://ghost_level_{levelIndex}.json";
|
||||
var saveData = new GhostSaveData { BestTime = time };
|
||||
foreach (var frame in _currentRecording)
|
||||
saveData.Frames.Add(new GhostFrameDto { Timestamp = frame.Timestamp, X = frame.Position.X, Y = frame.Position.Y });
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(saveData, SaveSystem.JsonOptions);
|
||||
using var file = FileAccess.Open(path, FileAccess.ModeFlags.Write);
|
||||
file.StoreString(json);
|
||||
}
|
||||
catch (System.Exception e)
|
||||
{
|
||||
GD.PrintErr($"GhostManager: Failed to save ghost data: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private List<GhostFrame> LoadGhostData(int levelIndex)
|
||||
{
|
||||
var path = $"user://ghost_level_{levelIndex}.json";
|
||||
if (!FileAccess.FileExists(path)) return [];
|
||||
|
||||
try
|
||||
{
|
||||
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 [];
|
||||
}
|
||||
}
|
||||
|
||||
private double LoadBestTime(int levelIndex)
|
||||
{
|
||||
var path = $"user://ghost_level_{levelIndex}.json";
|
||||
if (!FileAccess.FileExists(path)) return double.MaxValue;
|
||||
|
||||
try
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Autoloads/GhostManager.cs.uid
Normal file
1
Autoloads/GhostManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://cgmuod4p2hg5h
|
||||
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.Collections;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
using Mr.BrickAdventures.scripts.State;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
/// <summary>
|
||||
/// Save system that serializes state to JSON using DTOs.
|
||||
/// </summary>
|
||||
public partial class SaveSystem : Node
|
||||
{
|
||||
[Export] public string SavePath { get; set; } = "user://savegame.save";
|
||||
[Export] public int Version { get; set; } = 1;
|
||||
[Export] public string SavePath { get; set; } = "user://savegame.json";
|
||||
[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()
|
||||
{
|
||||
_gameManager = GetNode<GameManager>("/root/GameManager");
|
||||
Instance = this;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
public void SaveGame()
|
||||
{
|
||||
var saveData = new Dictionary
|
||||
var store = GameStateStore.Instance;
|
||||
if (store == null)
|
||||
{
|
||||
{ "player_state", _gameManager.PlayerState},
|
||||
{ "version", Version}
|
||||
GD.PrintErr("SaveSystem: GameStateStore not available.");
|
||||
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);
|
||||
file.StoreVar(saveData);
|
||||
GD.Print("Game state saved to: ", SavePath);
|
||||
try
|
||||
{
|
||||
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()
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
GD.Print("Game state loaded from: ", SavePath);
|
||||
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"])
|
||||
try
|
||||
{
|
||||
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 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
|
||||
216
Autoloads/SkillManager.cs
Normal file
216
Autoloads/SkillManager.cs
Normal file
@@ -0,0 +1,216 @@
|
||||
using System.Linq;
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
using Mr.BrickAdventures.Progression;
|
||||
using Mr.BrickAdventures.scripts.components;
|
||||
using Mr.BrickAdventures.scripts.interfaces;
|
||||
using Mr.BrickAdventures.scripts.Resources;
|
||||
|
||||
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
|
||||
{
|
||||
private PlayerController _player;
|
||||
|
||||
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
|
||||
|
||||
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]
|
||||
public delegate void ActiveThrowSkillChangedEventHandler(BrickThrowComponent throwComponent);
|
||||
[Signal]
|
||||
public delegate void SkillRemovedEventHandler(SkillData skillData);
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
Instance = this;
|
||||
EventBus.Instance.SkillSetChanged += Reconcile;
|
||||
}
|
||||
|
||||
public override void _ExitTree()
|
||||
{
|
||||
if (EventBus.Instance != null)
|
||||
EventBus.Instance.SkillSetChanged -= Reconcile;
|
||||
|
||||
if (Instance == this) Instance = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the PlayerController from its _Ready method to register itself with the manager.
|
||||
/// </summary>
|
||||
public void RegisterPlayer(PlayerController player)
|
||||
{
|
||||
if (_player == player) return;
|
||||
|
||||
if (_player != null && IsInstanceValid(_player))
|
||||
UnregisterPlayer();
|
||||
|
||||
_player = player;
|
||||
if (_player != null)
|
||||
{
|
||||
_player.TreeExiting += UnregisterPlayer;
|
||||
Reconcile();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterPlayer()
|
||||
{
|
||||
if (_player != null && IsInstanceValid(_player))
|
||||
{
|
||||
_player.TreeExiting -= UnregisterPlayer;
|
||||
RemoveAllActiveSkills();
|
||||
}
|
||||
_player = null;
|
||||
}
|
||||
|
||||
/// <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)
|
||||
{
|
||||
if (_player == null || !IsInstanceValid(_player))
|
||||
{
|
||||
GD.Print("SkillManager: Player not available to add skill.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ActiveComponents.ContainsKey(skillData.Name))
|
||||
return;
|
||||
|
||||
if (skillData.Type == SkillType.Throw)
|
||||
{
|
||||
foreach (var sd in AvailableSkills)
|
||||
{
|
||||
if (sd != skillData && sd?.Type == SkillType.Throw)
|
||||
RemoveSkill(sd.Name);
|
||||
}
|
||||
}
|
||||
|
||||
var instance = skillData.Node.Instantiate();
|
||||
if (instance is ISkill skill)
|
||||
{
|
||||
skill.Initialize(_player, skillData, level);
|
||||
skill.Activate();
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PrintErr($"Skill scene for '{skillData.Name}' does not implement ISkill!");
|
||||
instance.QueueFree();
|
||||
return;
|
||||
}
|
||||
|
||||
_player.AddChild(instance);
|
||||
ActiveComponents[skillData.Name] = instance;
|
||||
_activeLevels[skillData.Name] = level;
|
||||
|
||||
if (instance is BrickThrowComponent btc)
|
||||
EmitSignalActiveThrowSkillChanged(btc);
|
||||
}
|
||||
|
||||
public void RemoveSkill(string skillName)
|
||||
{
|
||||
_activeLevels.Remove(skillName);
|
||||
|
||||
if (!ActiveComponents.TryGetValue(skillName, out var component))
|
||||
return;
|
||||
|
||||
if (component.AsGodotObject() is BrickThrowComponent)
|
||||
EmitSignalActiveThrowSkillChanged(null);
|
||||
|
||||
var inst = (Node)component;
|
||||
if (inst is ISkill skill)
|
||||
skill.Deactivate();
|
||||
|
||||
if (IsInstanceValid(inst))
|
||||
inst.QueueFree();
|
||||
|
||||
ActiveComponents.Remove(skillName);
|
||||
var sd = GetSkillByName(skillName);
|
||||
if (sd != null) EmitSignalSkillRemoved(sd);
|
||||
}
|
||||
|
||||
private void RemoveAllActiveSkills()
|
||||
{
|
||||
foreach (var skillName in ActiveComponents.Keys.Select(k => k.ToString()).ToList())
|
||||
RemoveSkill(skillName);
|
||||
}
|
||||
|
||||
public SkillData GetSkillByName(string skillName)
|
||||
{
|
||||
foreach (var sd in AvailableSkills)
|
||||
if (sd?.Name == skillName) return sd;
|
||||
return null;
|
||||
}
|
||||
|
||||
public bool IsSkillActive(SkillData skill) =>
|
||||
skill != null && ActiveComponents.ContainsKey(skill.Name);
|
||||
|
||||
public void ActivateSkill(SkillData skill)
|
||||
{
|
||||
if (!ActiveComponents.ContainsKey(skill.Name))
|
||||
AddSkill(skill);
|
||||
}
|
||||
|
||||
public void DeactivateSkill(SkillData skill)
|
||||
{
|
||||
if (ActiveComponents.ContainsKey(skill.Name))
|
||||
RemoveSkill(skill.Name);
|
||||
}
|
||||
|
||||
public void ToggleSkillActivation(SkillData skill)
|
||||
{
|
||||
if (skill == null) return;
|
||||
|
||||
if (ActiveComponents.ContainsKey(skill.Name))
|
||||
DeactivateSkill(skill);
|
||||
else
|
||||
ActivateSkill(skill);
|
||||
}
|
||||
}
|
||||
1
Autoloads/SkillManager.cs.uid
Normal file
1
Autoloads/SkillManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://dru77vj07e18s
|
||||
66
Autoloads/SpeedRunManager.cs
Normal file
66
Autoloads/SpeedRunManager.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using Godot;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
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;
|
||||
|
||||
private double _startTime;
|
||||
private double _levelStartTime;
|
||||
private List<double> _splits = [];
|
||||
|
||||
[Signal] public delegate void TimeUpdatedEventHandler(double totalTime, double levelTime);
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (!IsRunning) return;
|
||||
|
||||
EmitSignalTimeUpdated(GetCurrentTotalTime(), GetCurrentLevelTime());
|
||||
}
|
||||
|
||||
public void StartTimer()
|
||||
{
|
||||
_startTime = Time.GetTicksMsec() / 1000.0;
|
||||
_levelStartTime = _startTime;
|
||||
_splits.Clear();
|
||||
IsRunning = true;
|
||||
GD.Print("Speedrun timer started.");
|
||||
}
|
||||
|
||||
public void StopTimer()
|
||||
{
|
||||
if (!IsRunning) return;
|
||||
IsRunning = false;
|
||||
var finalTime = GetCurrentTotalTime();
|
||||
GD.Print($"Speedrun finished. Final time: {FormatTime(finalTime)}");
|
||||
|
||||
// Save personal best if applicable
|
||||
}
|
||||
|
||||
public void Split()
|
||||
{
|
||||
if (!IsRunning) return;
|
||||
|
||||
var now = Time.GetTicksMsec() / 1000.0;
|
||||
var splitTime = now - _levelStartTime;
|
||||
_splits.Add(splitTime);
|
||||
_levelStartTime = now;
|
||||
GD.Print($"Split recorded: {FormatTime(splitTime)}");
|
||||
}
|
||||
|
||||
public double GetCurrentTotalTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _startTime : 0;
|
||||
public double GetCurrentLevelTime() => IsRunning ? (Time.GetTicksMsec() / 1000.0) - _levelStartTime : 0;
|
||||
|
||||
public static string FormatTime(double time)
|
||||
{
|
||||
var span = System.TimeSpan.FromSeconds(time);
|
||||
return $"{(int)span.TotalMinutes:00}:{span.Seconds:00}.{span.Milliseconds:000}";
|
||||
}
|
||||
}
|
||||
1
Autoloads/SpeedRunManager.cs.uid
Normal file
1
Autoloads/SpeedRunManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://c6ohe36xw1h21
|
||||
77
Autoloads/SteamManager.cs
Normal file
77
Autoloads/SteamManager.cs
Normal file
@@ -0,0 +1,77 @@
|
||||
using System;
|
||||
using Godot;
|
||||
using Steamworks;
|
||||
using Steamworks.Data;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
public partial class SteamManager : Node
|
||||
{
|
||||
private const uint AppId = 3575090;
|
||||
|
||||
public static string PlayerName { get; private set; } = "Player";
|
||||
public static bool IsSteamInitialized { get; private set; } = false;
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
try
|
||||
{
|
||||
SteamClient.Init(AppId);
|
||||
IsSteamInitialized = true;
|
||||
|
||||
PlayerName = SteamClient.Name;
|
||||
|
||||
GD.Print($"Steam initialized successfully for user: {PlayerName}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GD.PushError("Failed to initialize Steamworks. Is Steam running?");
|
||||
GD.PushError(e.Message);
|
||||
IsSteamInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
public override void _Process(double delta)
|
||||
{
|
||||
if (IsSteamInitialized) SteamClient.RunCallbacks();
|
||||
}
|
||||
|
||||
public override void _Notification(int what)
|
||||
{
|
||||
if (what == NotificationWMCloseRequest)
|
||||
{
|
||||
if (IsSteamInitialized)
|
||||
{
|
||||
SteamClient.Shutdown();
|
||||
}
|
||||
GetTree().Quit();
|
||||
}
|
||||
}
|
||||
|
||||
public static void UnlockAchievement(string achievementId)
|
||||
{
|
||||
if (!IsSteamInitialized)
|
||||
{
|
||||
GD.Print($"Steam not initialized. Cannot unlock achievement '{achievementId}'.");
|
||||
return;
|
||||
}
|
||||
|
||||
var ach = new Achievement(achievementId);
|
||||
|
||||
if (ach.State)
|
||||
{
|
||||
GD.Print($"Achievement '{achievementId}' is already unlocked.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (ach.Trigger())
|
||||
{
|
||||
SteamUserStats.StoreStats();
|
||||
GD.Print($"Successfully triggered achievement: {ach.Name} ({ach.Identifier})");
|
||||
}
|
||||
else
|
||||
{
|
||||
GD.PrintErr($"Failed to trigger achievement: {achievementId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
1
Autoloads/SteamManager.cs.uid
Normal file
1
Autoloads/SteamManager.cs.uid
Normal file
@@ -0,0 +1 @@
|
||||
uid://b5abjlnbia63q
|
||||
@@ -1,11 +1,22 @@
|
||||
using Godot;
|
||||
using Godot.Collections;
|
||||
|
||||
namespace Mr.BrickAdventures.Autoloads;
|
||||
|
||||
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 ScreenPoppedEventHandler(Control screen);
|
||||
@@ -34,19 +45,19 @@ public partial class UIManager : Node
|
||||
return;
|
||||
}
|
||||
|
||||
var top = (Control)UiStack[^1];
|
||||
var top = UiStack[^1];
|
||||
UiStack.RemoveAt(UiStack.Count - 1);
|
||||
top.Hide();
|
||||
top.SetProcessInput(false);
|
||||
EmitSignalScreenPopped(top);
|
||||
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 bool IsScreenOnTop(Control screen) => UiStack.Count > 0 && (Control)UiStack[^1] == screen;
|
||||
public Control TopScreen() => UiStack.Count > 0 ? UiStack[^1] : null;
|
||||
|
||||
public bool IsScreenOnTop(Control screen) => UiStack.Count > 0 && UiStack[^1] == screen;
|
||||
|
||||
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,10 +1,25 @@
|
||||
<Project Sdk="Godot.NET.Sdk/4.4.1">
|
||||
<Project Sdk="Godot.NET.Sdk/4.7.1">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<EnableDynamicLoading>true</EnableDynamicLoading>
|
||||
<RootNamespace>Mr.BrickAdventures</RootNamespace>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="LimboConsole.Sharp" Version="0.0.1-beta-008" />
|
||||
<PackageReference Include="Facepunch.Steamworks" Version="2.3.3" />
|
||||
<PackageReference Include="Facepunch.Steamworks.Dlls" Version="2.3.2" />
|
||||
<PackageReference Include="Facepunch.Steamworks.Library" Version="2.3.3" />
|
||||
</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>
|
||||
</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
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mr. Brick Adventures", "Mr. Brick Adventures.csproj", "{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}"
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
ExportDebug|Any CPU = ExportDebug|Any CPU
|
||||
ExportRelease|Any CPU = ExportRelease|Any CPU
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Debug|x64 = Debug|x64
|
||||
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
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{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|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.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.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
|
||||
EndGlobal
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
<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_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_003ACharacterBody2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FSourcesCache_003Fbba0bbd7a98ee58286e9484fbe86e01afff6232283f6efd3556eb7116453_003FCharacterBody2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACollisionShape2D_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E1_003Fresharper_002Dhost_003FSourcesCache_003F2ca9b7334678f5c97c7c2a9fbe4837be71cae11b6a30408dd4791b18f997e4a_003FCollisionShape2D_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACSharpInstanceBridge_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F14_003F4b4ade3f_003FCSharpInstanceBridge_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADelegateUtils_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F08_003F45f75e10_003FDelegateUtils_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ADictionary_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003F_002Econfig_003FJetBrains_003FRider2025_002E2_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F94701b444afa4c3d9a7a53ebcaa35fd1583c00_003F47_003Fda1d8d31_003FDictionary_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_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_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>
|
||||
13
achievements/level_complete_1.tres
Normal file
13
achievements/level_complete_1.tres
Normal file
@@ -0,0 +1,13 @@
|
||||
[gd_resource type="Resource" script_class="AchievementResource" load_steps=3 format=3 uid="uid://3odfkm1ig5"]
|
||||
|
||||
[ext_resource type="Texture2D" uid="uid://cebeyr4wnibvk" path="res://sprites/achievement.png" id="1_usw25"]
|
||||
[ext_resource type="Script" uid="uid://duib5phrmpro5" path="res://scripts/Resources/AchievementResource.cs" id="2_n7ktn"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_n7ktn")
|
||||
Id = "level_complete_1"
|
||||
DisplayName = "Complete level 1"
|
||||
Description = ""
|
||||
Icon = ExtResource("1_usw25")
|
||||
IsSecret = false
|
||||
metadata/_custom_type_script = "uid://duib5phrmpro5"
|
||||
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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/icon.svg-17eb5d3e2a3cfbe59852220758c5b7bd.cte
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/responses_menu.svg-87cf63ca685d53616205049572
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/update.svg-f1628866ed4eb2e13e3b81f75443687e.c
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
[configuration]
|
||||
entry_symbol = "godotsteam_init"
|
||||
compatibility_minimum = "4.4"
|
||||
|
||||
[libraries]
|
||||
macos.debug = "res://addons/godotsteam/osx/libgodotsteam.macos.template_debug.framework"
|
||||
macos.release = "res://addons/godotsteam/osx/libgodotsteam.macos.template_release.framework"
|
||||
windows.debug.x86_64 = "res://addons/godotsteam/win64/libgodotsteam.windows.template_debug.x86_64.dll"
|
||||
windows.debug.x86_32 = "res://addons/godotsteam/win32/libgodotsteam.windows.template_debug.x86_32.dll"
|
||||
windows.release.x86_64 = "res://addons/godotsteam/win64/libgodotsteam.windows.template_release.x86_64.dll"
|
||||
windows.release.x86_32 = "res://addons/godotsteam/win32/libgodotsteam.windows.template_release.x86_32.dll"
|
||||
linux.debug.x86_64 = "res://addons/godotsteam/linux64/libgodotsteam.linux.template_debug.x86_64.so"
|
||||
linux.debug.x86_32 = "res://addons/godotsteam/linux32/libgodotsteam.linux.template_debug.x86_32.so"
|
||||
linux.release.x86_64 = "res://addons/godotsteam/linux64/libgodotsteam.linux.template_release.x86_64.so"
|
||||
linux.release.x86_32 = "res://addons/godotsteam/linux32/libgodotsteam.linux.template_release.x86_32.so"
|
||||
|
||||
[dependencies]
|
||||
macos.universal = { "res://addons/godotsteam/osx/libsteam_api.dylib": "" }
|
||||
windows.x86_64 = { "res://addons/godotsteam/win64/steam_api64.dll": "" }
|
||||
windows.x86_32 = { "res://addons/godotsteam/win32/steam_api.dll": "" }
|
||||
linux.x86_64 = { "res://addons/godotsteam/linux64/libsteam_api.so": "" }
|
||||
linux.x86_32 = { "res://addons/godotsteam/linux32/libsteam_api.so": "" }
|
||||
@@ -1 +0,0 @@
|
||||
uid://cbt61wgh4qe5l
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>libgodotsteam.debug</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godotsteam.godotsteam</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>libgodotsteam.debug</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.15</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4.15</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.12</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>libgodotsteam</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>org.godotsteam.godotsteam</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>libgodotsteam</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>4.15</string>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>MacOSX</string>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>4.15</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.12</string>
|
||||
</dict>
|
||||
</plist>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryDefault.png-fcf3696b58
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PhantomCameraBtnPrimaryHover.png-3d2e4d225f6a
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/inventory_container.png-12241277f279bfc4bf7d5
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/level_spritesheet.png-26a44dd21a040a5480d5ccb
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/player_sprite.svg-8862ecb19e12152eb892607676f
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/sign_prompt.png-18d451127e1cd1a16367acd23cec4
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=true
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -19,6 +19,8 @@ dest_files=["res://.godot/imported/target.png-878c5e8d057c8a9a4c2322d4ab88e9ef.s
|
||||
compress/mode=2
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -21,6 +21,7 @@ msdf_pixel_range=8
|
||||
msdf_size=48
|
||||
allow_system_fallback=true
|
||||
force_autohinter=false
|
||||
modulate_color_glyphs=false
|
||||
hinting=1
|
||||
subpixel_positioning=1
|
||||
keep_rounding_remainders=true
|
||||
|
||||
@@ -18,6 +18,8 @@ dest_files=["res://.godot/imported/PriorityOverride.svg-e76e07f4bbd98169f119e17f
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=true
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -26,6 +28,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=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/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
@@ -25,6 +27,10 @@ mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
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/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user