Compare commits

...

9 Commits

Author SHA1 Message Date
817bd96433 Refactor HUD and MainMenu components to use Node attribute for better scene integration 2025-08-15 03:15:54 +02:00
2cc54f7b37 Add game scene and level catalog interfaces, and implement scene management in AppRoot 2025-08-15 03:04:21 +02:00
406036504a Implement core game functionality with AppRoot, SaveClient, PlayerRepository, and LevelRepository classes 2025-08-15 02:46:02 +02:00
173f0e5703 Add Chickensoft packages for enhanced functionality and dependency management 2025-08-15 00:58:19 +02:00
d84f7d1740 Csharp rewrite (#4)
* Implement BeamComponent in C# and enhance marketplace button functionality

* Add core game components including ConfigFileHandler, GameManager, SaveSystem, and UIManager

* cleanup

* Add new components: CanPickUpComponent, CollapsableComponent, DestroyableComponent, EffectInflictorComponent, StatusEffectComponent, and StatusEffectDataResource

* Add new components: EnemyDeathComponent, EnemyWaveTriggerComponent, and ExitDoorComponent

* Add new components: ExplosiveComponent, FadeAwayComponent, FireEffectComponent, FlipComponent, GravityMotionComponent, LaunchComponent, and update PlatformMovement with LastDirection property

* Add new components: HealComponent, HitComponent, HomingMissileMotionComponent, LeverComponent, and TriggerLeverComponent

* Refactor GameManager session state handling and add new components: CanBeLaunchedComponent, IceEffectComponent, JumpPadComponent, KillPlayerOutOfScreenComponent, KnockbackComponent, LifetimeComponent, MagneticSkillComponent, OutOfScreenComponent, PeriodicShootingComponent, PlayerDeathComponent, ProgressiveDamageComponent, ProjectileComponent, ProjectileInitComponent, RequirementComponent, ScoreComponent, ShipMovementComponent, ShipShooterComponent, and SideToSideMovementComponent

* Add new components: CannotStompComponent, SkillUnlockedComponent, SpaceshipEnterComponent, SpaceshipExitComponent, SpinComponent, StompDamageComponent, StraightMotionComponent, TerrainHitFx, TooltipComponent, TrailComponent, and UnlockOnRequirementComponent

* Add new components: BrickThrowComponent, BulletComponent, CageComponent, ChaseLevelComponent, CleanupComponent, and ThrowInputResource classes; implement game saving and loading logic in SaveSystem

* Add audio settings management and platform movement component

* Add ChargeProgressBar, Credits, and GameOverScreen components for UI management

* Add UID files for ConfigFileHandler, GameManager, SaveSystem, and UIManager components

* Add README.md file with game description and features; include project license and contribution guidelines

* Add Hud component for UI management; display health, coins, and lives

* Add MainMenu and Marketplace components; implement game management and skill unlocking features

* Add PauseMenu, SettingsMenu, and SkillButton components; enhance game management and UI functionality
2025-08-15 00:45:57 +02:00
2ad0fe26d2 Fix formatting in README.md for improved readability 2025-08-13 00:57:38 +02:00
c4f7be1b10 Remove license badge from README.md for cleaner presentation (#3) 2025-08-13 00:54:21 +02:00
b957b56567 Enhance README.md with project badges and statistics for improved visibility 2025-08-13 00:52:04 +02:00
cce93286be Update README.md with game story, mission, and features; enhance promotional content (#2) 2025-08-13 00:46:19 +02:00
233 changed files with 5274 additions and 70 deletions

View File

@@ -0,0 +1,27 @@
using Godot;
namespace Mr.BrickAdventures.Autoloads;
public partial class ConfigFileHandler : Node
{
private ConfigFile _settingsConfig = new();
public const string SettingsPath = "user://settings.ini";
public ConfigFile SettingsConfig => _settingsConfig;
public override void _Ready()
{
if (!FileAccess.FileExists(SettingsPath))
{
var err = _settingsConfig.Save(SettingsPath);
if (err != Error.Ok)
GD.PushError($"Failed to create settings file at {SettingsPath}: {err}");
}
else
{
var err = _settingsConfig.Load(SettingsPath);
if (err != Error.Ok)
GD.PushError($"Failed to load settings file at {SettingsPath}: {err}");
}
}
}

View File

@@ -0,0 +1 @@
uid://8cyvbeyd13cj

241
Autoloads/GameManager.cs Normal file
View File

@@ -0,0 +1,241 @@
using System.Collections.Generic;
using System.Linq;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.Autoloads;
public partial class GameManager : Node
{
[Export] public Array<PackedScene> LevelScenes { get; set; } = new();
public PlayerController Player { get; set; }
private List<Node> _sceneNodes = new();
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>() }
};
public Dictionary CurrentSessionState { get; private set; } = new()
{
{ "coins_collected", 0 },
{ "skills_unlocked", new Array<SkillData>() }
};
public override void _EnterTree()
{
GetTree().NodeAdded += OnNodeAdded;
GetTree().NodeRemoved += OnNodeRemoved;
}
public override void _ExitTree()
{
GetTree().NodeAdded -= OnNodeAdded;
GetTree().NodeRemoved -= OnNodeRemoved;
_sceneNodes.Clear();
}
private void OnNodeAdded(Node node)
{
_sceneNodes.Add(node);
}
private void OnNodeRemoved(Node node)
{
_sceneNodes.Remove(node);
}
public void AddCoins(int amount)
{
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] + amount);
}
public void SetCoins(int amount) => PlayerState["coins"] = Mathf.Max(0, amount);
public int GetCoins() => (int)PlayerState["coins"] + (int)CurrentSessionState["coins_collected"];
public void RemoveCoins(int amount)
{
var sessionCoins = (int)CurrentSessionState["coins_collected"];
if (amount <= sessionCoins)
{
CurrentSessionState["coins_collected"] = sessionCoins - amount;
}
else
{
var remaining = amount - sessionCoins;
CurrentSessionState["coins_collected"] = 0;
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"] - remaining);
}
PlayerState["coins"] = Mathf.Max(0, (int)PlayerState["coins"]);
}
public void AddLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] + amount;
public void RemoveLives(int amount) => PlayerState["lives"] = (int)PlayerState["lives"] - amount;
public void SetLives(int amount) => PlayerState["lives"] = amount;
public int GetLives() => (int)PlayerState["lives"];
public bool IsSkillUnlocked(SkillData skill)
{
return ((Array)PlayerState["unlocked_skills"]).Contains(skill)
|| ((Array)CurrentSessionState["skills_unlocked"]).Contains(skill);
}
public void UnlockSkill(SkillData skill)
{
if (!IsSkillUnlocked(skill))
((Array)PlayerState["unlocked_skills"]).Add(skill);
}
public void RemoveSkill(string skillName)
{
var arr = (Array)PlayerState["unlocked_skills"];
foreach (SkillData s in arr)
{
if (s.Name != skillName) continue;
arr.Remove(s);
break;
}
}
public void UnlockSkills(Array<SkillData> skills)
{
foreach (var s in skills)
UnlockSkill(s);
}
public void ResetPlayerState()
{
PlayerState = new Dictionary
{
{ "coins", 0 },
{ "lives", 3 },
{ "current_level", 0 },
{ "completed_levels", new Array<int>() },
{ "unlocked_levels", new Array<int>() {0}},
{ "unlocked_skills", new Array<SkillData>() }
};
}
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();
}
public void ContinueGame()
{
var save = GetNode<SaveSystem>("/root/SaveSystem");
if (!save.LoadGame())
{
GD.PrintErr("Failed to load game. Starting a new game instead.");
StartNewGame();
return;
}
var idx = (int)PlayerState["current_level"];
if (idx < LevelScenes.Count)
GetTree().ChangeSceneToPacked(LevelScenes[idx]);
else
GD.PrintErr("No levels unlocked to continue.");
}
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);
ResetCurrentSessionState();
TryToGoToNextLevel();
GetNode<SaveSystem>("/root/SaveSystem").SaveGame();
}
public Array<SkillData> GetUnlockedSkills()
{
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;
}
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.");
return null;
}
}

View File

@@ -0,0 +1 @@
uid://c6eoi3ymefc0x

61
Autoloads/SaveSystem.cs Normal file
View File

@@ -0,0 +1,61 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.Autoloads;
public partial class SaveSystem : Node
{
[Export] public string SavePath { get; set; } = "user://savegame.save";
[Export] public int Version { get; set; } = 1;
private GameManager _gameManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
}
public void SaveGame()
{
var saveData = new Dictionary
{
{ "player_state", _gameManager.PlayerState},
{ "version", Version}
};
using var file = FileAccess.Open(SavePath, FileAccess.ModeFlags.Write);
file.StoreVar(saveData);
GD.Print("Game state saved to: ", SavePath);
}
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"]}");
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"])
{
skills.Add(skill);
}
_gameManager.UnlockSkills(skills);
return true;
}
public bool CheckSaveExists() => FileAccess.FileExists(SavePath);
}

View File

@@ -0,0 +1 @@
uid://bh20fqbyifidc

65
Autoloads/UIManager.cs Normal file
View File

@@ -0,0 +1,65 @@
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.Autoloads;
public partial class UIManager : Node
{
[Export] public Array<Control> UiStack { get; set; } = new();
[Signal] public delegate void ScreenPushedEventHandler(Control screen);
[Signal] public delegate void ScreenPoppedEventHandler(Control screen);
public void PushScreen(Control screen)
{
if (screen == null)
{
GD.PushError($"Cannot push a null screen.");
return;
}
UiStack.Add(screen);
screen.Show();
screen.SetProcessInput(true);
screen.SetFocusMode(Control.FocusModeEnum.All);
screen.GrabFocus();
EmitSignalScreenPushed(screen);
}
public void PopScreen()
{
if (UiStack.Count == 0)
{
GD.PushError($"Cannot pop screen from an empty stack.");
return;
}
var top = (Control)UiStack[^1];
UiStack.RemoveAt(UiStack.Count - 1);
top.Hide();
top.SetProcessInput(false);
EmitSignalScreenPopped(top);
top.AcceptEvent();
if (UiStack.Count > 0) ((Control)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 bool IsVisibleOnStack(Control screen) => UiStack.Contains(screen) && screen.Visible;
public void CloseAll()
{
while (UiStack.Count > 0)
PopScreen();
}
public static void HideAndDisable(Control screen)
{
screen.Hide();
screen.SetProcessInput(false);
}
}

View File

@@ -0,0 +1 @@
uid://c3ldmnrwperr4

View File

@@ -0,0 +1,13 @@
<Project Sdk="Godot.NET.Sdk/4.4.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>Mr.BrickAdventures</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Chickensoft.AutoInject" Version="2.8.21" />
<PackageReference Include="Chickensoft.GodotNodeInterfaces" Version="2.4.31" />
<PackageReference Include="Chickensoft.Introspection" Version="3.0.2" />
<PackageReference Include="Chickensoft.Introspection.Generator" Version="3.0.2" PrivateAssets="all" OutputItemType="analyzer" />
</ItemGroup>
</Project>

19
Mr. Brick Adventures.sln Normal file
View File

@@ -0,0 +1,19 @@
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
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}.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}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{A1D482B9-207B-4D6C-A0A0-D9E6D1AE2356}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,6 @@
<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_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_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_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_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_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>

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# Przygody Pana Cegły
[![Steam Wishlist](https://img.shields.io/badge/🎮%20PLAY%20NOW%20on%20Steam-FF4C4C?style=for-the-badge&logo=steam&logoColor=white)](https://store.steampowered.com/app/3575090/Mr_Brick_Adventures/)
![Made With Godot](https://img.shields.io/badge/Made%20With-Godot-478CBF?style=for-the-badge&logo=godot-engine&logoColor=white)
![Lines of Code](https://tokei.rs/b1/github/GKaszewski/ppc_v2?style=for-the-badge&category=code)
![Last Commit](https://img.shields.io/github/last-commit/GKaszewski/ppc_v2?style=for-the-badge)
![Stars](https://img.shields.io/github/stars/GKaszewski/ppc_v2?style=for-the-badge)
*"The worlds only brick-throwing dad simulator (probably)."*
---
## The Story
Disaster has struck!
Mr. Bricks kids have gone missing, scattered across mysterious lands filled with treacherous traps, cranky critters, and more collapsing bridges than an OSHA nightmare.
Armed with nothing but his legs, his wits, and an infinite supply of throwable bricks (dont ask where he keeps them), Mr. Brick will stop at nothing to bring his children home.
---
## The Mission
Run, jump, and hurl bricks with pinpoint accuracy as you navigate dangerous worlds. Smash enemies, trigger ancient mechanisms, and uncover hidden treasures, all while dodging hazards that seem designed specifically to ruin your day.
---
## Features Thatll Knock Your Bricks Off
* **Tight, Classic Platforming**
Leap over pits, time your jumps, and try not to land in something unpleasant.
* **Brick-Fu Combat**
Toss bricks to flatten foes, flip switches, and solve puzzles in the most dad-like way possible.
* **Secrets & Collectibles**
Hunt for coins, discover hidden rooms, and find upgrades to make you just a *little* less fragile.
* **NES-Inspired Pixel Art**
All the charm of an 8-bit classic, no cartridge blowing required.
---
## Play It Here
[**Wishlist on Steam**](https://store.steampowered.com/app/3575090/Mr_Brick_Adventures/) so youre ready for launch day.
---
## 📊 Project Stats
📦 **Lines of Code:**
![Lines of code](https://tokei.rs/b1/github/GKaszewski/ppc_v2)
📈 **Repo Activity:**
![Commit activity](https://img.shields.io/github/commit-activity/m/GKaszewski/ppc_v2)
---
## License
Check the [LICENSE](./LICENSE) for the legal stuff.
---
## Contributing
Not open to code contributions right now but you *can* help by:
* Reporting bugs
* Suggesting evil new trap ideas
* Spreading the word so Mr. Brick can find his kids faster

10
app/IGameScenes.cs Normal file
View File

@@ -0,0 +1,10 @@
using Godot;
namespace Mr.BrickAdventures.app;
public interface IGameScenes
{
void Load(PackedScene scene);
void Restart();
void ReturnToMain(PackedScene mainMenu);
}

1
app/IGameScenes.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://b7d1ospa5p4nx

11
app/ILevelCatalog.cs Normal file
View File

@@ -0,0 +1,11 @@
using Godot;
namespace Mr.BrickAdventures.app;
public interface ILevelCatalog
{
int Count { get; }
PackedScene? Get(int index);
PackedScene First { get; }
PackedScene MainMenu { get; }
}

1
app/ILevelCatalog.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://b2sxttgvk6nuq

70
common/AppRoot.cs Normal file
View File

@@ -0,0 +1,70 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.data;
using Mr.BrickAdventures.game.repositories;
using Mr.BrickAdventures.game.services;
namespace Mr.BrickAdventures.common;
[Meta(typeof(IAutoNode))]
public partial class AppRoot : Node2D,
IProvide<PlayerRepository>,
IProvide<LevelRepository>,
IProvide<SaveClient>,
IProvide<SaveService>,
IProvide<LevelService>,
IProvide<IGameScenes>,
IProvide<ILevelCatalog>,
IGameScenes
{
public override void _Notification(int what) => this.Notify(what);
[Export] private Array<PackedScene> _levels = [];
[Export] private PackedScene _mainMenu = null!;
private readonly SaveClient _save = new("user://savegame.save", version: 2);
private readonly PlayerRepository _players = new();
private readonly LevelRepository _levelsRepo = new();
private SaveService _saveService = null!;
private LevelService _levelService = null!;
private ILevelCatalog _catalog = null!;
PlayerRepository IProvide<PlayerRepository>.Value() => _players;
LevelRepository IProvide<LevelRepository>.Value() => _levelsRepo;
SaveClient IProvide<SaveClient>.Value() => _save;
SaveService IProvide<SaveService>.Value() => _saveService;
LevelService IProvide<LevelService>.Value() => _levelService;
ILevelCatalog IProvide<ILevelCatalog>.Value() => _catalog;
IGameScenes IProvide<IGameScenes>.Value() => this;
public void OnReady()
{
_saveService = new SaveService(_players, _levelsRepo, _save);
_levelService = new LevelService(_levelsRepo);
_catalog = new ExportedLevelCatalog(_levels, _mainMenu);
_saveService.TryLoad();
this.Provide();
}
public void Load(PackedScene scene) => GetTree().ChangeSceneToPacked(scene);
public void Restart() => GetTree().ReloadCurrentScene();
public void ReturnToMain(PackedScene mainMenu) => GetTree().ChangeSceneToPacked(mainMenu);
private sealed class ExportedLevelCatalog : ILevelCatalog
{
private readonly Array<PackedScene> _levels;
public PackedScene MainMenu { get; }
public ExportedLevelCatalog(Array<PackedScene> levels, PackedScene mainMenu) {
_levels = levels; MainMenu = mainMenu;
}
public int Count => _levels.Count;
public PackedScene? Get(int index) => (index >= 0 && index < _levels.Count) ? _levels[index] : null;
public PackedScene First => _levels.Count > 0 ? _levels[0] : MainMenu;
}
}

1
common/AppRoot.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://dg2l7cw6da4vb

6
data/ConfigClient.cs Normal file
View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.data;
public class ConfigClient
{
}

1
data/ConfigClient.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://d3s774lnoljcu

103
data/SaveClient.cs Normal file
View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.game.repositories;
namespace Mr.BrickAdventures.data;
public sealed class SaveClient
{
private readonly string _path;
private readonly int _version;
public SaveClient(string path, int version) { _path = path; _version = version; }
public bool Exists() => FileAccess.FileExists(_path);
public bool TryLoad(out PlayerState player, out LevelState level) {
player = null!; level = null!;
if (!Exists()) return false;
using var f = FileAccess.Open(_path, FileAccess.ModeFlags.Read);
var dict = (Dictionary)f.GetVar();
if ((int)dict.GetValueOrDefault("version", -1) != _version) return false;
player = ToPlayer((Dictionary)dict["player"]);
level = ToLevel((Dictionary)dict["level"]);
return true;
}
// Strict load: requires version + player_state + level_state.
// If anything is off, delete the file and act as "no save".
public bool TryLoadStrict(out PlayerState player, out LevelState level) {
player = default!;
level = default!;
if (!Exists()) return false;
try {
using var f = FileAccess.Open(_path, FileAccess.ModeFlags.Read);
if (f == null) { Delete(); return false; }
var dictionary = (Dictionary)f.GetVar();
if (!dictionary.TryGetValue("version", out var v) || (int)v != _version) { Delete(); return false; }
if (!dictionary.TryGetValue("player_state", out var pObj) || (Dictionary)pObj is not { } p) { Delete(); return false; }
if (!dictionary.TryGetValue("level_state", out var lObj) || (Dictionary)lObj is not { } l) { Delete(); return false; }
player = ToPlayer(p);
level = ToLevel(l);
return true;
}
catch (Exception e) {
GD.PushWarning($"SaveClient: load failed — deleting bad save. {e.GetType().Name}: {e.Message}");
Delete();
return false;
}
}
public void Save(PlayerState player, LevelState level) {
using var f = FileAccess.Open(_path, FileAccess.ModeFlags.Write);
var dict = new Dictionary {
{ "version", _version },
{ "player", FromPlayer(player) },
{ "level", FromLevel(level) }
};
f.StoreVar(dict);
}
public void Delete() {
if (!Exists()) return;
var abs = ProjectSettings.GlobalizePath(_path);
var ok = DirAccess.RemoveAbsolute(abs);
if (ok != Error.Ok) GD.PushWarning($"SaveClient: failed to delete {_path}: {ok}");
}
private static Dictionary FromPlayer(PlayerState s) => new() { { "coins", s.Coins }, { "lives", s.Lives } };
private static PlayerState ToPlayer(Dictionary d) => new() {
Coins = d.TryGetValue("coins", out var c) ? (int)c : 0,
Lives = d.TryGetValue("lives", out var l) ? (int)l : 3
};
private static Dictionary FromLevel(LevelState s) => new() {
{ "current", s.Current },
{ "unlocked", new Array<int>(s.Unlocked) },
{ "completed", new Array<int>(s.Completed) },
};
private static LevelState ToLevel(Dictionary d) => new() {
Current = d.TryGetValue("current", out var cur) ? (int)cur : 0,
Unlocked = d.TryGetValue("unlocked", out var ul) && (Array<int>)ul is { } a1 ? a1.ToArray() : [0],
Completed = d.TryGetValue("completed", out var cl) && (Array<int>)cl is { } a2 ? a2.ToArray() : [],
};
}
public record SaveSnapshot {
public required PlayerState PlayerState { get; init; }
public required LevelState LevelState { get; init; }
}

1
data/SaveClient.cs.uid Normal file
View File

@@ -0,0 +1 @@
uid://bsf45t7m2wa07

View File

@@ -0,0 +1,40 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.game.services;
namespace Mr.BrickAdventures.features.level;
[Meta(typeof(IAutoNode))]
public partial class ExitDoorComponent : Area2D
{
public override void _Notification(int what) => this.Notify(what);
[Export] public CollisionShape2D UnlockIndicator { get; set; } = null!;
[Export] public bool Unlocked { get; set; } = false;
[Dependency] public LevelService Levels => this.DependOn<LevelService>();
[Dependency] public ILevelCatalog Catalog => this.DependOn<ILevelCatalog>();
[Dependency] public IGameScenes Scenes => this.DependOn<IGameScenes>();
public void OnReady() {
BodyEntered += OnBodyEntered;
UpdateVisuals();
}
private void OnBodyEntered(Node body) {
if (!Unlocked) return;
if (body is not CharacterBody2D) return;
var nextIdx = Levels.CompleteAndAdvance();
var next = Catalog.Get(nextIdx);
if (next != null) Scenes.Load(next); else Scenes.ReturnToMain(Catalog.MainMenu);
}
private void UpdateVisuals() {
if (UnlockIndicator != null) UnlockIndicator.Disabled = !Unlocked;
}
public void SetUnlocked(bool value) { Unlocked = value; UpdateVisuals(); }
}

View File

@@ -0,0 +1 @@
uid://ddh0jm0heqtc3

34
features/ui/hud/Hud.cs Normal file
View File

@@ -0,0 +1,34 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Mr.BrickAdventures.game.repositories;
using Mr.BrickAdventures.scripts.components;
namespace Mr.BrickAdventures.features.ui.hud;
[Meta(typeof(IAutoNode))]
public partial class Hud : Node
{
public override void _Notification(int what) => this.Notify(what);
[Export] public HealthComponent Health { get; set; } = null!;
[Node] public Label CoinsLabel { get; set; } = null!;
[Node] public ProgressBar HealthBar { get; set; } = null!;
[Node] public Label LivesLabel { get; set; } = null!;
[Dependency] public PlayerRepository Player => this.DependOn<PlayerRepository>();
public void OnResolved() {
CoinsLabel.Text = $"{Tr("COINS_LABEL")}: {Player.Coins}";
LivesLabel.Text = $"{Tr("LIVES_LABEL")}: {Player.Lives}";
Player.CoinsChanged += c => CoinsLabel.Text = $"{Tr("COINS_LABEL")}: {c}";
Player.LivesChanged += l => LivesLabel.Text = $"{Tr("LIVES_LABEL")}: {l}";
if (Health != null) {
HealthBar.MaxValue = Health.MaxHealth;
HealthBar.Value = Health.Health;
Health.HealthChanged += (_, total) => { HealthBar.Value = total; };
}
}
}

View File

@@ -0,0 +1 @@
uid://c1uwe5e1cfdxl

View File

@@ -1,7 +1,7 @@
[gd_scene load_steps=8 format=3 uid="uid://byxf45ukq82pe"] [gd_scene load_steps=8 format=3 uid="uid://byxf45ukq82pe"]
[ext_resource type="LabelSettings" uid="uid://rvn5ivivfvv6" path="res://resources/ui/hud_label_settings.tres" id="1_4dsh5"] [ext_resource type="LabelSettings" uid="uid://rvn5ivivfvv6" path="res://resources/ui/hud_label_settings.tres" id="1_4dsh5"]
[ext_resource type="Script" uid="uid://c3pde84b3kdco" path="res://scripts/ui/hud.gd" id="1_ueofj"] [ext_resource type="Script" uid="uid://c1uwe5e1cfdxl" path="res://features/ui/hud/Hud.cs" id="1_m4pq7"]
[ext_resource type="FontFile" uid="uid://xm0vbusjr7b7" path="res://fonts/PressStart2P-Regular.ttf" id="1_ygmwt"] [ext_resource type="FontFile" uid="uid://xm0vbusjr7b7" path="res://fonts/PressStart2P-Regular.ttf" id="1_ygmwt"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mmcdi"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_mmcdi"]
@@ -16,7 +16,7 @@ bg_color = Color(0.47451, 0.47451, 0.47451, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_22dp1"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_22dp1"]
bg_color = Color(0.858824, 0.254902, 0.380392, 1) bg_color = Color(0.858824, 0.254902, 0.380392, 1)
[node name="HUD" type="Control" node_paths=PackedStringArray("coins_label", "health_progressbar", "lives_label")] [node name="HUD" type="Control"]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -25,10 +25,7 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
size_flags_horizontal = 4 size_flags_horizontal = 4
size_flags_vertical = 4 size_flags_vertical = 4
script = ExtResource("1_ueofj") script = ExtResource("1_m4pq7")
coins_label = NodePath("PanelContainer/MarginContainer/HBoxContainer/Coins label")
health_progressbar = NodePath("PanelContainer/MarginContainer/HBoxContainer/ProgressBar")
lives_label = NodePath("PanelContainer/MarginContainer/HBoxContainer/Lives")
[node name="PanelContainer" type="PanelContainer" parent="."] [node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1
@@ -57,7 +54,8 @@ text = "HEALTH_LABEL"
label_settings = ExtResource("1_4dsh5") label_settings = ExtResource("1_4dsh5")
uppercase = true uppercase = true
[node name="ProgressBar" type="ProgressBar" parent="PanelContainer/MarginContainer/HBoxContainer"] [node name="HealthBar" type="ProgressBar" parent="PanelContainer/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
visible = false visible = false
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
@@ -69,7 +67,8 @@ step = 0.1
value = 60.0 value = 60.0
show_percentage = false show_percentage = false
[node name="Lives" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer"] [node name="LivesLabel" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "LIVES_LABEL" text = "LIVES_LABEL"
@@ -77,7 +76,8 @@ label_settings = ExtResource("1_4dsh5")
horizontal_alignment = 1 horizontal_alignment = 1
uppercase = true uppercase = true
[node name="Coins label" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer"] [node name="CoinsLabel" type="Label" parent="PanelContainer/MarginContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
text = "COINS_LABEL" text = "COINS_LABEL"

View File

@@ -0,0 +1,26 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Mr.BrickAdventures.app;
namespace Mr.BrickAdventures.features.ui.menus;
[Meta(typeof(IAutoNode))]
public partial class GameOverScreen : Control
{
public override void _Notification(int what) => this.Notify(what);
[Export] public Button RestartButton { get; set; } = null!;
[Export] public Button MainMenuButton { get; set; } = null!;
[Export] public PackedScene MainMenuScene { get; set; } = null!;
[Dependency] public IGameScenes Scenes => this.DependOn<IGameScenes>();
public void OnReady() {
Visible = false;
RestartButton.Pressed += () => Scenes.Restart();
MainMenuButton.Pressed += () => Scenes.ReturnToMain(MainMenuScene);
}
public void ShowGameOver() => Show();
}

View File

@@ -0,0 +1 @@
uid://b36jv878751ta

View File

@@ -0,0 +1,57 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.game.repositories;
using Mr.BrickAdventures.game.services;
namespace Mr.BrickAdventures.features.ui.menus;
[Meta(typeof(IAutoNode))]
public partial class MainMenu : Control
{
public override void _Notification(int what) => this.Notify(what);
[Node] public Button NewGameButton { get; set; } = null!;
[Node] public Button ContinueButton { get; set; } = null!;
[Node] public Button SettingsButton { get; set; } = null!;
[Node] public Button CreditsButton { get; set; } = null!;
[Node] public Button ExitButton { get; set; } = null!;
[Node] public Label VersionLabel { get; set; } = null!;
[Export] public Control SettingsControl { get; set; } = null!;
[Export] public Control CreditsControl { get; set; } = null!;
[Dependency] public SaveService Save => this.DependOn<SaveService>();
[Dependency] public LevelRepository Levels => this.DependOn<LevelRepository>();
[Dependency] public IGameScenes Scenes => this.DependOn<IGameScenes>();
[Dependency] public ILevelCatalog Catalog => this.DependOn<ILevelCatalog>();
public void OnReady() {
VersionLabel.Text = $"v. {ProjectSettings.GetSetting("application/config/version")}";
NewGameButton.Pressed += OnNewGamePressed;
ContinueButton.Pressed += OnContinuePressed;
SettingsButton.Pressed += () => SettingsControl.Show();
CreditsButton.Pressed += () => CreditsControl.Show();
ExitButton.Pressed += () => GetTree().Quit();
}
public void OnResolved()
{
ContinueButton.Disabled = !Save.Exists();
(ContinueButton.Disabled ? NewGameButton : ContinueButton).GrabFocus();
}
private void OnNewGamePressed() {
Save.NewGame();
Levels.SetCurrent(0);
var first = Catalog.First;
Scenes.Load(first);
}
private void OnContinuePressed() {
if (!Save.TryLoad()) { OnNewGamePressed(); return; }
var scene = Catalog.Get(Levels.Current) ?? Catalog.First;
Scenes.Load(scene);
}
}

View File

@@ -0,0 +1 @@
uid://fcsg0e8s36in

View File

@@ -0,0 +1,33 @@
using Chickensoft.AutoInject;
using Chickensoft.Introspection;
using Godot;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.game.services;
namespace Mr.BrickAdventures.features.ui.menus;
[Meta(typeof(IAutoNode))]
public partial class PauseMenu : Control
{
public override void _Notification(int what) => this.Notify(what);
[Export] public Button ResumeButton { get; set; } = null!;
[Export] public Button RestartButton { get; set; } = null!;
[Export] public Button MainMenuButton { get; set; } = null!;
[Dependency] public IGameScenes Scenes => this.DependOn<IGameScenes>();
[Dependency] public ILevelCatalog Catalog => this.DependOn<ILevelCatalog>();
[Dependency] public LevelService Levels => this.DependOn<LevelService>();
public void OnReady() {
Visible = false;
ResumeButton.Pressed += () => { GetTree().Paused = false; Hide(); };
RestartButton.Pressed += () => { GetTree().Paused = false; Scenes.Restart(); };
MainMenuButton.Pressed += () => { GetTree().Paused = false; Scenes.ReturnToMain(Catalog.MainMenu); };
}
public void Toggle() {
if (Visible) { GetTree().Paused = false; Hide(); }
else { Show(); GetTree().Paused = true; }
}
}

View File

@@ -0,0 +1 @@
uid://bwgs02wcfnm8u

View File

@@ -1,25 +1,18 @@
[gd_scene load_steps=3 format=3 uid="uid://8b6ol5sssbgo"] [gd_scene load_steps=3 format=3 uid="uid://8b6ol5sssbgo"]
[ext_resource type="Script" uid="uid://hyfvthdbgjbc" path="res://scripts/ui/main_menu.gd" id="1_epxpl"] [ext_resource type="Script" uid="uid://fcsg0e8s36in" path="res://features/ui/menus/MainMenu.cs" id="1_q8hru"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qv2q0"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qv2q0"]
bg_color = Color(0, 0, 0, 1) bg_color = Color(0, 0, 0, 1)
[node name="MainMenu" type="Control" node_paths=PackedStringArray("main_menu_control", "new_game_button", "continue_button", "settings_button", "credits_button", "exit_button", "version_label")] [node name="MainMenu" type="Control"]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
grow_horizontal = 2 grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("1_epxpl") script = ExtResource("1_q8hru")
main_menu_control = NodePath(".")
new_game_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/NewGameButton")
continue_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/ContinueButton")
settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/SettingsButton")
credits_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/CreditsButton")
exit_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/QuitButton")
version_label = NodePath("PanelContainer/MarginContainer/VBoxContainer/version")
[node name="PanelContainer" type="PanelContainer" parent="."] [node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1
@@ -53,31 +46,37 @@ layout_mode = 2
size_flags_vertical = 3 size_flags_vertical = 3
[node name="ContinueButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="ContinueButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "CONTINUE_BUTTON" text = "CONTINUE_BUTTON"
flat = true flat = true
[node name="NewGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="NewGameButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "NEW_GAME_BUTTON" text = "NEW_GAME_BUTTON"
flat = true flat = true
[node name="SettingsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="SettingsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "SETTINGS_BUTTON" text = "SETTINGS_BUTTON"
flat = true flat = true
[node name="CreditsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="CreditsButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "CREDITS_BUTTON" text = "CREDITS_BUTTON"
flat = true flat = true
[node name="QuitButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="ExitButton" type="Button" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
text = "QUIT_BUTTON" text = "QUIT_BUTTON"
flat = true flat = true
[node name="version" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"] [node name="VersionLabel" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 0 size_flags_horizontal = 0
size_flags_vertical = 8 size_flags_vertical = 8

View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.features.ui.settings;
public class AudioSettings
{
}

View File

@@ -0,0 +1 @@
uid://bnj36dhskiem

View File

@@ -0,0 +1,34 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Mr.BrickAdventures.game.repositories;
public sealed class LevelRepository
{
public int Current { get; private set; } = 0;
public HashSet<int> Unlocked { get; } = new() { 0 };
public HashSet<int> Completed { get; } = new();
public event Action<int>? CurrentChanged;
public void SetCurrent(int idx) { Current = idx; CurrentChanged?.Invoke(Current); }
public void Unlock(int idx) => Unlocked.Add(idx);
public void Complete(int idx) { Completed.Add(idx); Unlock(idx + 1); }
public LevelState Export() => new() {
Current = Current, Unlocked = [..Unlocked], Completed = [..Completed]
};
public void Load(LevelState s) {
Current = s.Current;
Unlocked.Clear(); foreach (var i in s.Unlocked) Unlocked.Add(i);
Completed.Clear(); foreach (var i in s.Completed) Completed.Add(i);
CurrentChanged?.Invoke(Current);
}
}
public record LevelState {
public int Current;
public int[] Unlocked = [];
public int[] Completed = [];
}

View File

@@ -0,0 +1 @@
uid://d0604lmwpadt5

View File

@@ -0,0 +1,25 @@
using System;
namespace Mr.BrickAdventures.game.repositories;
public sealed class PlayerRepository
{
public int Coins { get; private set; } = 0;
public int Lives { get; private set; } = 3;
public event Action<int>? CoinsChanged;
public event Action<int>? LivesChanged;
public void SetCoins(int value) { Coins = Math.Max(0, value); CoinsChanged?.Invoke(Coins); }
public void AddCoins(int amount) { SetCoins(Coins + amount); }
public void RemoveCoins(int amount){ SetCoins(Coins - amount); }
public void SetLives(int value) { Lives = value; LivesChanged?.Invoke(Lives); }
public void AddLives(int amount) { SetLives(Lives + amount); }
public void RemoveLives(int amount){ SetLives(Lives - amount); }
public PlayerState Export() => new() { Coins = Coins, Lives = Lives };
public void Load(PlayerState s) { SetCoins(s.Coins); SetLives(s.Lives); }
}
public record PlayerState { public int Coins; public int Lives; }

View File

@@ -0,0 +1 @@
uid://d1dijkt574x4b

View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.game.repositories;
public sealed class SessionRepository
{
}

View File

@@ -0,0 +1 @@
uid://b550hcqugygv0

View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.game.repositories;
public class SettingsRepository
{
}

View File

@@ -0,0 +1 @@
uid://bk11iduo7bg2h

View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.game.repositories;
public class SkillsRepository
{
}

View File

@@ -0,0 +1 @@
uid://cymx7mtmdblun

View File

@@ -0,0 +1,21 @@
using Mr.BrickAdventures.game.repositories;
namespace Mr.BrickAdventures.game.services;
public sealed class LevelService
{
private readonly LevelRepository _levels;
public LevelService(LevelRepository levels) => _levels = levels;
public int CompleteAndAdvance() {
var cur = _levels.Current;
_levels.Complete(cur);
var next = cur + 1;
_levels.SetCurrent(next);
return next;
}
public void StartNew() {
_levels.Load(new LevelState { Current = 0, Unlocked = new [] { 0 }, Completed = [] });
}
}

View File

@@ -0,0 +1 @@
uid://b2tsi3cjnh5xq

View File

@@ -0,0 +1,32 @@
using Mr.BrickAdventures.data;
using Mr.BrickAdventures.game.repositories;
namespace Mr.BrickAdventures.game.services;
public sealed class SaveService
{
private readonly PlayerRepository _players;
private readonly LevelRepository _levels;
private readonly SaveClient _save;
public SaveService(PlayerRepository players, LevelRepository levels, SaveClient save) {
_players = players; _levels = levels; _save = save;
}
public bool TryLoad() {
if (!_save.TryLoad(out var p, out var l)) return false;
_players.Load(p);
_levels.Load(l);
return true;
}
public void Save() => _save.Save(_players.Export(), _levels.Export());
public bool Exists() => _save.Exists();
public void NewGame() {
_players.Load(new PlayerState { Coins = 0, Lives = 3 });
_levels.Load(new LevelState { Current = 0, Unlocked = [0], Completed = [] });
Save();
}
}

View File

@@ -0,0 +1 @@
uid://dxqkxlkkivxog

View File

@@ -0,0 +1,6 @@
namespace Mr.BrickAdventures.game.services;
public sealed class SkillService
{
}

View File

@@ -0,0 +1 @@
uid://dim21hs12wdi5

View File

@@ -32,7 +32,7 @@
[ext_resource type="PackedScene" uid="uid://bg76mtpcmfm2j" path="res://objects/ui/charging_bar_layer.tscn" id="28_3f5nm"] [ext_resource type="PackedScene" uid="uid://bg76mtpcmfm2j" path="res://objects/ui/charging_bar_layer.tscn" id="28_3f5nm"]
[ext_resource type="PackedScene" uid="uid://b12tppjkkqpt4" path="res://objects/fxs/hit_particles.tscn" id="28_jh5m0"] [ext_resource type="PackedScene" uid="uid://b12tppjkkqpt4" path="res://objects/fxs/hit_particles.tscn" id="28_jh5m0"]
[ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="29_jh5m0"] [ext_resource type="Script" uid="uid://ceq8n7yw7qxpi" path="res://scripts/components/hit_component.gd" id="29_jh5m0"]
[ext_resource type="Script" uid="uid://c1wtrgw0x77xo" path="res://scripts/components/platform_movement.gd" id="31_xoue7"] [ext_resource type="Script" uid="uid://cflncpa377l8l" path="res://scripts/components/platform_movement.gd" id="31_xoue7"]
[ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="32_x2b7c"] [ext_resource type="AudioStream" uid="uid://dyev46uqusimi" path="res://sfx/shoot.wav" id="32_x2b7c"]
[ext_resource type="Script" uid="uid://d1ctdx52gskv1" path="res://scripts/components/ship_shooter.gd" id="34_gwc8i"] [ext_resource type="Script" uid="uid://d1ctdx52gskv1" path="res://scripts/components/ship_shooter.gd" id="34_gwc8i"]
[ext_resource type="PackedScene" uid="uid://dtem8jgcyoqar" path="res://objects/entities/green_laser.tscn" id="36_oxudy"] [ext_resource type="PackedScene" uid="uid://dtem8jgcyoqar" path="res://objects/entities/green_laser.tscn" id="36_oxudy"]

View File

@@ -62,9 +62,6 @@ process_material = SubResource("ParticleProcessMaterial_lgb3u")
[node name="UI Layer" parent="." instance=ExtResource("2_lbnsn")] [node name="UI Layer" parent="." instance=ExtResource("2_lbnsn")]
[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")]
player_health = NodePath("../../Brick Player/HealthComponent")
[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] [node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")]
current_level = ExtResource("4_c2yv5") current_level = ExtResource("4_c2yv5")
nodes_to_disable = [NodePath("../../Brick Player")] nodes_to_disable = [NodePath("../../Brick Player")]

View File

@@ -1,6 +1,6 @@
[gd_scene load_steps=11 format=3 uid="uid://6foggu31cu14"] [gd_scene load_steps=11 format=3 uid="uid://6foggu31cu14"]
[ext_resource type="PackedScene" uid="uid://byxf45ukq82pe" path="res://objects/ui/hud.tscn" id="1_tgtfe"] [ext_resource type="PackedScene" uid="uid://byxf45ukq82pe" path="res://features/ui/hud/hud.tscn" id="1_tgtfe"]
[ext_resource type="PackedScene" uid="uid://dulkm3ah4tm0u" path="res://objects/ui/death_screen.tscn" id="2_ln68j"] [ext_resource type="PackedScene" uid="uid://dulkm3ah4tm0u" path="res://objects/ui/death_screen.tscn" id="2_ln68j"]
[ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="3_5kt5k"] [ext_resource type="Script" uid="uid://cp68km8bykymb" path="res://scripts/resources/level_resource.gd" id="3_5kt5k"]
[ext_resource type="PackedScene" uid="uid://wmw6gaisyrvx" path="res://objects/ui/game_over_screen.tscn" id="4_11xmk"] [ext_resource type="PackedScene" uid="uid://wmw6gaisyrvx" path="res://objects/ui/game_over_screen.tscn" id="4_11xmk"]
@@ -34,9 +34,8 @@ visible = false
offset_top = 32.0 offset_top = 32.0
components_to_disable = [null] components_to_disable = [null]
[node name="Pause menu" parent="." node_paths=PackedStringArray("settings_menu") instance=ExtResource("6_1q4vn")] [node name="Pause menu" parent="." instance=ExtResource("6_1q4vn")]
visible = false visible = false
settings_menu = NodePath("../Settings menu")
[node name="Settings menu" parent="." node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("7_hkjav")] [node name="Settings menu" parent="." node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("7_hkjav")]
visible = false visible = false

View File

@@ -1,12 +1,11 @@
[gd_scene load_steps=4 format=3 uid="uid://i6mnjbjcoqe5"] [gd_scene load_steps=3 format=3 uid="uid://i6mnjbjcoqe5"]
[ext_resource type="Script" uid="uid://cugifchx6jhuk" path="res://scripts/ui/pause_menu.gd" id="1_aktha"] [ext_resource type="Script" uid="uid://bwgs02wcfnm8u" path="res://features/ui/menus/PauseMenu.cs" id="1_ljtns"]
[ext_resource type="PackedScene" uid="uid://cl00e2ocomk3m" path="res://scenes/main_menu.tscn" id="2_h4pd5"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ivv"] [sub_resource type="StyleBoxFlat" id="StyleBoxFlat_g4ivv"]
bg_color = Color(0, 0, 0, 1) bg_color = Color(0, 0, 0, 1)
[node name="Pause menu" type="Control" node_paths=PackedStringArray("pause_menu_control", "resume_button", "quit_button", "settings_button", "exit_to_menu_button")] [node name="Pause menu" type="Control" node_paths=PackedStringArray("ResumeButton", "RestartButton", "MainMenuButton")]
layout_mode = 3 layout_mode = 3
anchors_preset = 15 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
@@ -15,13 +14,10 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
size_flags_horizontal = 6 size_flags_horizontal = 6
size_flags_vertical = 6 size_flags_vertical = 6
script = ExtResource("1_aktha") script = ExtResource("1_ljtns")
pause_menu_control = NodePath(".") ResumeButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button")
resume_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button") RestartButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button")
quit_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Quit game Button") MainMenuButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button")
settings_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button")
exit_to_menu_button = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button")
exit_to_menu_scene = ExtResource("2_h4pd5")
[node name="PanelContainer" type="PanelContainer" parent="."] [node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1

View File

@@ -19,7 +19,7 @@ config/version="in-dev"
run/main_scene="uid://cl00e2ocomk3m" run/main_scene="uid://cl00e2ocomk3m"
config/use_custom_user_dir=true config/use_custom_user_dir=true
config/custom_user_dir_name="MrBrickAdventures" config/custom_user_dir_name="MrBrickAdventures"
config/features=PackedStringArray("4.4", "GL Compatibility") config/features=PackedStringArray("4.4", "C#", "GL Compatibility")
run/max_fps=180 run/max_fps=180
boot_splash/bg_color=Color(0, 0, 0, 1) boot_splash/bg_color=Color(0, 0, 0, 1)
boot_splash/show_image=false boot_splash/show_image=false

View File

@@ -66,9 +66,6 @@ process_material = SubResource("ParticleProcessMaterial_lgb3u")
[node name="UI Layer" parent="." instance=ExtResource("3_4fsls")] [node name="UI Layer" parent="." instance=ExtResource("3_4fsls")]
[node name="HUD" parent="UI Layer" index="0" node_paths=PackedStringArray("player_health")]
player_health = NodePath("../../Brick Player/HealthComponent")
[node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")] [node name="DeathScreen" parent="UI Layer" index="1" node_paths=PackedStringArray("nodes_to_disable")]
current_level = ExtResource("4_onnch") current_level = ExtResource("4_onnch")
nodes_to_disable = [NodePath("../../Brick Player")] nodes_to_disable = [NodePath("../../Brick Player")]

View File

@@ -1,27 +1,38 @@
[gd_scene load_steps=6 format=3 uid="uid://cl00e2ocomk3m"] [gd_scene load_steps=12 format=3 uid="uid://cl00e2ocomk3m"]
[ext_resource type="PackedScene" uid="uid://8b6ol5sssbgo" path="res://objects/ui/main_menu.tscn" id="1_ekxnf"] [ext_resource type="PackedScene" uid="uid://8b6ol5sssbgo" path="res://features/ui/menus/main_menu.tscn" id="1_ekxnf"]
[ext_resource type="Script" uid="uid://dg2l7cw6da4vb" path="res://common/AppRoot.cs" id="1_rtw2f"]
[ext_resource type="PackedScene" uid="uid://y0ae6e7t70fj" path="res://objects/ui/settings_menu.tscn" id="2_bqqt6"] [ext_resource type="PackedScene" uid="uid://y0ae6e7t70fj" path="res://objects/ui/settings_menu.tscn" id="2_bqqt6"]
[ext_resource type="PackedScene" uid="uid://bwgmrcyj4mvu" path="res://objects/ui/credits.tscn" id="3_bqqt6"] [ext_resource type="PackedScene" uid="uid://bwgmrcyj4mvu" path="res://objects/ui/credits.tscn" id="3_bqqt6"]
[ext_resource type="PackedScene" uid="uid://chqb11pfoqmeb" path="res://scenes/level_village_2.tscn" id="3_lgwnu"]
[ext_resource type="PackedScene" uid="uid://bol7g83v2accs" path="res://scenes/level_village_1.tscn" id="3_oa1go"]
[ext_resource type="PackedScene" uid="uid://b5fx1vdfky307" path="res://objects/ui/audio_settings.tscn" id="4_8ln24"] [ext_resource type="PackedScene" uid="uid://b5fx1vdfky307" path="res://objects/ui/audio_settings.tscn" id="4_8ln24"]
[ext_resource type="PackedScene" uid="uid://h60obxmju6mo" path="res://scenes/level_village_3.tscn" id="4_flqon"]
[ext_resource type="PackedScene" uid="uid://bhad760x3vvco" path="res://scenes/level_village_4.tscn" id="5_rcqid"]
[ext_resource type="PackedScene" uid="uid://cvfsbiy5ggrpg" path="res://objects/ui/input_settings.tscn" id="5_rtw2f"] [ext_resource type="PackedScene" uid="uid://cvfsbiy5ggrpg" path="res://objects/ui/input_settings.tscn" id="5_rtw2f"]
[ext_resource type="PackedScene" uid="uid://dagpmlgvr262d" path="res://scenes/level_forest_5.tscn" id="6_1ajci"]
[node name="Main menu" type="CanvasLayer"] [node name="AppRoot" type="Node2D"]
script = ExtResource("1_rtw2f")
_levels = Array[PackedScene]([ExtResource("3_oa1go"), ExtResource("3_lgwnu"), ExtResource("4_flqon"), ExtResource("5_rcqid"), ExtResource("6_1ajci")])
_mainMenu = null
[node name="MainMenu" parent="." node_paths=PackedStringArray("settings_control", "credits_control") instance=ExtResource("1_ekxnf")] [node name="Main menu" type="CanvasLayer" parent="."]
settings_control = NodePath("../Settings menu")
credits_control = NodePath("../Credits")
[node name="Settings menu" parent="." node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("2_bqqt6")] [node name="MainMenu" parent="Main menu" node_paths=PackedStringArray("SettingsControl", "CreditsControl") instance=ExtResource("1_ekxnf")]
SettingsControl = NodePath("../Settings menu")
CreditsControl = NodePath("../Credits")
[node name="Settings menu" parent="Main menu" node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("2_bqqt6")]
visible = false visible = false
input_settings = NodePath("../Input Settings") input_settings = NodePath("../Input Settings")
audio_settings = NodePath("../Audio settings") audio_settings = NodePath("../Audio settings")
[node name="Credits" parent="." instance=ExtResource("3_bqqt6")] [node name="Credits" parent="Main menu" instance=ExtResource("3_bqqt6")]
visible = false visible = false
[node name="Audio settings" parent="." instance=ExtResource("4_8ln24")] [node name="Audio settings" parent="Main menu" instance=ExtResource("4_8ln24")]
visible = false visible = false
[node name="Input Settings" parent="." instance=ExtResource("5_rtw2f")] [node name="Input Settings" parent="Main menu" instance=ExtResource("5_rtw2f")]
visible = false visible = false

View File

@@ -0,0 +1,70 @@
using Godot;
namespace Mr.BrickAdventures.scripts.Resources;
public partial class ChargeThrowInputResource : ThrowInputResource
{
[Export] public float MinPower { get; set; } = 0.5f;
[Export] public float MaxPower { get; set; } = 2.0f;
[Export] public float MaxChargeTime { get; set; } = 2.0f;
[Export] public float MinChargeDuration { get; set; } = 0.1f;
private bool _isCharging = false;
private float _chargeStartTime = 0f;
[Signal] public delegate void ChargeStartedEventHandler();
[Signal] public delegate void ChargeUpdatedEventHandler(float chargeRatio);
[Signal] public delegate void ChargeStoppedEventHandler();
public override void ProcessInput(InputEvent @event)
{
if (@event.IsActionPressed("attack"))
{
_isCharging = true;
_chargeStartTime = Time.GetTicksMsec() / 1000f;
EmitSignalChargeStarted();
}
if (@event.IsActionReleased("attack") && _isCharging)
{
var power = CalculatePower();
_isCharging = false;
EmitSignalThrowRequested(power);
EmitSignalChargeStopped();
}
}
public override void Update(double delta)
{
if (!_isCharging) return;
var t = Mathf.Clamp(GetChargeRatio(), MinPower, MaxPower);
EmitSignalChargeUpdated(t);
}
public override bool SupportsCharging()
{
return true;
}
private float CalculatePower()
{
var now = Time.GetTicksMsec() / 1000f;
var heldTime = now - _chargeStartTime;
if (heldTime < MinChargeDuration)
return MinPower;
var t = Mathf.Clamp(heldTime / MaxChargeTime, 0f, 1f);
return Mathf.Lerp(MinPower, MaxPower, t);
}
private float GetChargeRatio()
{
if (!_isCharging) return MinPower;
var now = Time.GetTicksMsec() / 1000f;
var heldTime = now - _chargeStartTime;
var t = Mathf.Clamp(heldTime / MaxChargeTime, 0f, 1f);
return Mathf.Lerp(MinPower, MaxPower, t);
}
}

View File

@@ -0,0 +1 @@
uid://dtpdh4jp51jis

View File

@@ -0,0 +1,9 @@
using Godot;
namespace Mr.BrickAdventures.scripts.Resources;
public partial class CollectableResource : Resource
{
[Export] public Variant Amount { get; set; } = 0.0;
[Export] public CollectableType Type { get; set; }
}

View File

@@ -0,0 +1 @@
uid://gptsgaw3agkf

View File

@@ -0,0 +1,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum CollectableType
{
Coin,
Kid,
Health,
}

View File

@@ -0,0 +1 @@
uid://2ql8wj3vfeke

View File

@@ -0,0 +1,19 @@
using System;
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.scripts.Resources;
public partial class SkillData : Resource
{
[Export] public string Name { get; set; } = "New Skill";
[Export] public string Description { get; set; } = "New Skill";
[Export] public Dictionary<string, Variant> Config { get; set; } = new();
[Export] public int Cost { get; set; } = 0;
[Export] public Texture2D Icon { get; set; }
[Export] public bool IsActive { get; set; } = false;
[Export] public int Level { get; set; } = 1;
[Export] public int MaxLevel { get; set; } = 1;
[Export] public SkillType Type { get; set; } = SkillType.Throw;
[Export] public PackedScene Node { get; set; }
}

View File

@@ -0,0 +1 @@
uid://d4crrfmbgxnqf

View File

@@ -0,0 +1,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum SkillType
{
Attack,
Throw,
Misc,
}

View File

@@ -0,0 +1 @@
uid://sma20qug2r0q

View File

@@ -0,0 +1,10 @@
using Godot;
namespace Mr.BrickAdventures.scripts.Resources;
public partial class StatusEffectDataResource : Resource
{
[Export] public float Duration { get; set; } = 1f;
[Export] public float DamagePerSecond { get; set; } = 0.25f;
[Export] public StatusEffectType Type { get; set; }
}

View File

@@ -0,0 +1 @@
uid://pw0pu6gb21y2

View File

@@ -0,0 +1,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum StatusEffectType
{
None,
Fire,
Ice
}

View File

@@ -0,0 +1 @@
uid://b0a7k7mse3l68

View File

@@ -0,0 +1,20 @@
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.Resources;
public partial class TapThrowInputResource : ThrowInputResource
{
public override void Update(double delta)
{
if (Input.IsActionPressed("attack"))
{
EmitSignalThrowRequested(1f);
}
}
public override bool SupportsCharging()
{
return false;
}
}

View File

@@ -0,0 +1 @@
uid://cx7ryqxemgs56

View File

@@ -0,0 +1,24 @@
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.Resources;
public abstract partial class ThrowInputResource : Resource, IThrowInput
{
[Signal] public delegate void ThrowRequestedEventHandler(float powerMultiplier = 1f);
public virtual void ProcessInput(InputEvent @event)
{
throw new System.NotImplementedException();
}
public virtual void Update(double delta)
{
throw new System.NotImplementedException();
}
public virtual bool SupportsCharging()
{
throw new System.NotImplementedException();
}
}

View File

@@ -0,0 +1 @@
uid://mnn5wy5cyr4m

15
scripts/Screenshot.cs Normal file
View File

@@ -0,0 +1,15 @@
using Godot;
namespace Mr.BrickAdventures.scripts;
public partial class Screenshot : Node
{
public override void _Process(double delta)
{
if (!OS.IsDebugBuild() || !Input.IsActionJustPressed("screenshot")) return;
var img = GetViewport().GetTexture().GetImage();
var id = OS.GetUniqueId() + "_" + Time.GetDatetimeStringFromSystem();
var path = "user://screenshots/screenshot_" + id + ".png";
img.SavePng(path);
}
}

View File

@@ -0,0 +1 @@
uid://chrhjch4ymfvr

144
scripts/SkillManager.cs Normal file
View File

@@ -0,0 +1,144 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts;
public partial class SkillManager : Node
{
private GameManager _gameManager;
[Export] public Array<SkillData> AvailableSkills { get; set; } = [];
public Dictionary ActiveComponents { get; private set; } = new();
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
ApplyUnlockedSkills();
}
public void AddSkill(SkillData skillData)
{
if (ActiveComponents.ContainsKey(skillData.Name))
return;
if (skillData.Type == SkillType.Throw)
{
var unlocked = _gameManager.GetUnlockedSkills();
foreach (var skill in unlocked)
{
SkillData data = null;
foreach (var s in AvailableSkills)
{
if (s == (SkillData)skill)
{
data = s;
break;
}
}
if (data != null && data.Type == SkillType.Throw)
RemoveSkill(data.Name);
}
}
var instance = skillData.Node.Instantiate();
foreach (var key in skillData.Config.Keys)
{
if (instance.HasMethod("get")) // rough presence check
{
var value = skillData.Config[key];
var parent = GetParent();
if (value.VariantType == Variant.Type.NodePath)
{
var np = (NodePath)value;
if (parent.HasNode(np))
value = parent.GetNode(np);
else if (instance.HasNode(np))
value = instance.GetNode(np);
else
continue;
}
// Set via property if exists
instance.Set(key, value);
}
}
Owner.AddChild(instance);
ActiveComponents[skillData.Name] = instance;
}
public void RemoveSkill(string skillName)
{
if (!ActiveComponents.TryGetValue(skillName, out var component))
return;
var inst = (Node)component;
if (IsInstanceValid(inst))
inst.QueueFree();
var skills = _gameManager.GetUnlockedSkills();
foreach (SkillData s in skills)
{
if (s.Name == skillName)
{
s.IsActive = false;
break;
}
}
ActiveComponents.Remove(skillName);
}
public void ApplyUnlockedSkills()
{
foreach (var sd in AvailableSkills)
{
if (_gameManager.IsSkillUnlocked(sd))
{
GD.Print("Applying skill: ", sd.Name);
CallDeferred(MethodName.AddSkill, sd);
}
else
{
RemoveSkill(sd.Name);
}
}
}
public SkillData GetSkillByName(string skillName)
{
foreach (var sd in AvailableSkills)
if (sd.Name == skillName) return sd;
return null;
}
public void ActivateSkill(SkillData skill)
{
if (!ActiveComponents.ContainsKey(skill.Name))
{
AddSkill(skill);
skill.IsActive = true;
}
}
public void DeactivateSkill(SkillData skill)
{
if (ActiveComponents.ContainsKey(skill.Name))
{
RemoveSkill(skill.Name);
skill.IsActive = false;
}
}
public void ToggleSkillActivation(SkillData skill)
{
if (skill == null) return;
if (ActiveComponents.ContainsKey(skill.Name))
DeactivateSkill(skill);
else
ActivateSkill(skill);
}
}

View File

@@ -0,0 +1 @@
uid://di572axt0c3s8

102
scripts/UI/AudioSettings.cs Normal file
View File

@@ -0,0 +1,102 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class AudioSettings : Node
{
[Export] public Slider MasterVolumeSlider { get; set; }
[Export] public Slider MusicVolumeSlider { get; set; }
[Export] public Slider SfxVolumeSlider { get; set; }
[Export] public Control AudioSettingsControl { get; set; }
[Export] public float MuteThreshold { get; set; } = -20f;
private UIManager _uiManager;
private ConfigFileHandler _configFileHandler;
public override void _Ready()
{
_uiManager = GetNode<UIManager>("/root/UIManager");
_configFileHandler = GetNode<ConfigFileHandler>("/root/ConfigFileHandler");
Initialize();
MasterVolumeSlider.ValueChanged += OnMasterVolumeChanged;
MusicVolumeSlider.ValueChanged += OnMusicVolumeChanged;
SfxVolumeSlider.ValueChanged += OnSfxVolumeChanged;
}
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionReleased("ui_cancel")) return;
if (!_uiManager.IsScreenOnTop(AudioSettingsControl)) return;
SaveSettings();
_uiManager.PopScreen();
}
private void OnSfxVolumeChanged(double value)
{
AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("sfx"), (float)value);
HandleMute(AudioServer.GetBusIndex("sfx"), (float)value);
}
private void OnMusicVolumeChanged(double value)
{
AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("music"), (float)value);
HandleMute(AudioServer.GetBusIndex("music"), (float)value);
}
private void OnMasterVolumeChanged(double value)
{
AudioServer.SetBusVolumeDb(AudioServer.GetBusIndex("Master"), (float)value);
HandleMute(AudioServer.GetBusIndex("Master"), (float)value);
}
private void Initialize()
{
var volumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("Master"));
MasterVolumeSlider.Value = volumeDb;
MasterVolumeSlider.MinValue = MuteThreshold;
MasterVolumeSlider.MaxValue = 0f;
var musicVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("music"));
MusicVolumeSlider.Value = musicVolumeDb;
MusicVolumeSlider.MinValue = MuteThreshold;
MusicVolumeSlider.MaxValue = 0f;
var sfxVolumeDb = AudioServer.GetBusVolumeDb(AudioServer.GetBusIndex("sfx"));
SfxVolumeSlider.Value = sfxVolumeDb;
SfxVolumeSlider.MinValue = MuteThreshold;
SfxVolumeSlider.MaxValue = 0f;
}
private void HandleMute(int busIndex, float value)
{
AudioServer.SetBusMute(busIndex, value <= MuteThreshold);
}
private void SaveSettings()
{
var settingsConfig = _configFileHandler.SettingsConfig;
settingsConfig.SetValue("audio_settings", "master_volume", MasterVolumeSlider.Value);
settingsConfig.SetValue("audio_settings", "music_volume", MusicVolumeSlider.Value);
settingsConfig.SetValue("audio_settings", "sfx_volume", SfxVolumeSlider.Value);
settingsConfig.SetValue("audio_settings", "mute_threshold", MuteThreshold);
settingsConfig.Save(ConfigFileHandler.SettingsPath);
}
private void LoadSettings()
{
var settingsConfig = _configFileHandler.SettingsConfig;
if (!settingsConfig.HasSection("audio_settings")) return;
var masterVolume = (float)settingsConfig.GetValue("audio_settings", "master_volume", MasterVolumeSlider.Value);
var musicVolume = (float)settingsConfig.GetValue("audio_settings", "music_volume", MusicVolumeSlider.Value);
var sfxVolume = (float)settingsConfig.GetValue("audio_settings", "sfx_volume", SfxVolumeSlider.Value);
var muteThreshold = (float)settingsConfig.GetValue("audio_settings", "mute_threshold", MuteThreshold);
MasterVolumeSlider.Value = masterVolume;
MusicVolumeSlider.Value = musicVolume;
SfxVolumeSlider.Value = sfxVolume;
MuteThreshold = muteThreshold;
}
}

View File

@@ -0,0 +1 @@
uid://g61qqsymqfxd

View File

@@ -0,0 +1,81 @@
using Godot;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.UI;
public partial class ChargeProgressBar : Node
{
[Export] public ProgressBar ProgressBar { get; set; }
[Export] public BrickThrowComponent ThrowComponent { get; set; }
private ChargeThrowInputResource _throwInput;
public override void _Ready()
{
Owner.ChildEnteredTree += OnNodeEntered;
ProgressBar.Hide();
SetupDependencies();
}
private void OnNodeEntered(Node node)
{
if (node is not BrickThrowComponent throwComponent || ThrowComponent != null) return;
ThrowComponent = throwComponent;
SetupDependencies();
}
private void SetupDependencies()
{
if (ThrowComponent.ThrowInputBehavior is ChargeThrowInputResource throwInput)
{
_throwInput = throwInput;
}
else
{
_throwInput = null;
}
if (_throwInput == null)
{
return;
}
if (!_throwInput.SupportsCharging())
{
ProgressBar.Hide();
return;
}
SetupProgressBar();
_throwInput.ChargeStarted += OnChargeStarted;
_throwInput.ChargeStopped += OnChargeStopped;
_throwInput.ChargeUpdated += OnChargeUpdated;
}
private void SetupProgressBar()
{
ProgressBar.MinValue = _throwInput.MinPower;
ProgressBar.MaxValue = _throwInput.MaxPower;
ProgressBar.Value = _throwInput.MinPower;
ProgressBar.Step = 0.01f;
ProgressBar.Hide();
}
private void OnChargeStarted()
{
ProgressBar.Show();
}
private void OnChargeStopped()
{
ProgressBar.Hide();
}
private void OnChargeUpdated(float chargeRatio)
{
ProgressBar.Value = chargeRatio;
ProgressBar.Show();
}
}

View File

@@ -0,0 +1 @@
uid://dev2q1228otm2

23
scripts/UI/Credits.cs Normal file
View File

@@ -0,0 +1,23 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class Credits : Control
{
private UIManager _uiManager;
public override void _Ready()
{
_uiManager = GetNode<UIManager>("/root/UIManager");
}
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("ui_cancel")) return;
if (_uiManager != null && _uiManager.IsScreenOnTop(this))
{
_uiManager.PopScreen();
}
}
}

View File

@@ -0,0 +1 @@
uid://daevj4uootmcw

View File

@@ -0,0 +1,39 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class GameOverScreen : Node
{
[Export] public Control GameOverPanel { get; set; }
[Export] public Button RestartButton { get; set; }
[Export] public Button MainMenuButton { get; set; }
[Export] public PackedScene MainMenuScene { get; set; }
private GameManager _gameManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
RestartButton.Pressed += OnRestartClicked;
MainMenuButton.Pressed += OnMainMenuClicked;
}
private void OnMainMenuClicked()
{
_gameManager.ResetPlayerState();
GetTree().ChangeSceneToPacked(MainMenuScene);
}
private void OnRestartClicked()
{
_gameManager.RestartGame();
}
public void OnPlayerDeath()
{
if (_gameManager == null || _gameManager.GetLives() != 0) return;
GameOverPanel.Show();
}
}

View File

@@ -0,0 +1 @@
uid://u4qfsx4w72dv

67
scripts/UI/MainMenu.cs Normal file
View File

@@ -0,0 +1,67 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class MainMenu : Node
{
[Export] public Control MainMenuControl { get; set; }
[Export] public Button NewGameButton { get; set; }
[Export] public Button ContinueButton { get; set; }
[Export] public Button SettingsButton { get; set; }
[Export] public Button CreditsButton { get; set; }
[Export] public Button ExitButton { get; set; }
[Export] public Label VersionLabel { get; set; }
[Export] public Control SettingsControl { get; set; }
[Export] public Control CreditsControl { get; set; }
private SaveSystem _saveSystem;
private GameManager _gameManager;
private UIManager _uiManager;
public override void _Ready()
{
_saveSystem = GetNode<SaveSystem>("/root/SaveSystem");
_gameManager = GetNode<GameManager>("/root/GameManager");
_uiManager = GetNode<UIManager>("/root/UIManager");
NewGameButton.Pressed += OnNewGamePressed;
ContinueButton.Pressed += OnContinuePressed;
SettingsButton.Pressed += OnSettingsPressed;
CreditsButton.Pressed += OnCreditsPressed;
ExitButton.Pressed += OnExitPressed;
VersionLabel.Text = $"v. {ProjectSettings.GetSetting("application/config/version")}";
ContinueButton.Disabled = !_saveSystem.CheckSaveExists();
if (_saveSystem.CheckSaveExists())
ContinueButton.GrabFocus();
else
NewGameButton.GrabFocus();
}
private void OnExitPressed()
{
_gameManager.QuitGame();
}
private void OnCreditsPressed()
{
_uiManager.PushScreen(CreditsControl);
}
private void OnSettingsPressed()
{
_uiManager.PushScreen(SettingsControl);
}
private void OnContinuePressed()
{
_gameManager.ContinueGame();
}
private void OnNewGamePressed()
{
_gameManager.StartNewGame();
}
}

View File

@@ -0,0 +1 @@
uid://bna3ggr6n7ycr

142
scripts/UI/Marketplace.cs Normal file
View File

@@ -0,0 +1,142 @@
using System.Collections.Generic;
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.UI;
public partial class Marketplace : Node
{
[Export] public Array<SkillData> Skills { get; set; } = [];
[Export] public GridContainer ToUnlockGrid { get; set; }
[Export] public GridContainer UnlockedGrid { get; set; }
[Export] public Font Font { get; set; }
[Export] public SkillUnlockedComponent SkillUnlockedComponent { get; set; }
[Export] public Array<Node> ComponentsToDisable { get; set; } = [];
[Export] public PackedScene MarketplaceButtonScene { get; set; }
[Export] public PackedScene SkillButtonScene { get; set; }
private GameManager _gameManager;
private readonly List<Button> _unlockButtons = [];
private readonly List<SkillButton> _skillButtons = [];
public override void _Ready()
{
var skillsToUnlock = new List<SkillData>();
foreach (var skill in Skills) skillsToUnlock.Add(skill);
foreach (var skill in skillsToUnlock) CreateUpgradeButton(skill);
var unlockedSkills = _gameManager.GetUnlockedSkills();
foreach (var skill in unlockedSkills) CreateSkillButton(skill);
SkillUnlockedComponent.SkillUnlocked += OnSkillUnlocked;
}
public override void _ExitTree()
{
SkillUnlockedComponent.SkillUnlocked -= OnSkillUnlocked;
}
public override void _Input(InputEvent @event)
{
var root = Owner as Control;
if (!@event.IsActionPressed("show_marketplace")) return;
if (root != null && root.IsVisible())
{
root.Hide();
foreach (var c in ComponentsToDisable) c.ProcessMode = ProcessModeEnum.Inherit;
}
else
{
root?.Show();
foreach (var c in ComponentsToDisable) c.ProcessMode = ProcessModeEnum.Disabled;
}
}
private string GetButtonText(SkillData skill)
{
return $"{Tr(skill.Name)} {skill.Cost}";
}
private void OnSkillUnlocked(SkillData skill)
{
if (_skillButtons.Count == 0) CreateSkillButton(skill);
foreach (var btn in _skillButtons)
{
if (btn.Data.IsActive)
btn.Activate();
else
btn.Deactivate();
}
}
private void CreateSkillButton(SkillData skill)
{
var button = SkillButtonScene.Instantiate<SkillButton>();
button.Data = skill;
button.Setup();
button.Pressed += () => OnSkillButtonPressed(button);
button.Activate();
_skillButtons.Add(button);
UnlockedGrid.AddChild(button);
UnlockedGrid.QueueSort();
}
private void CreateUpgradeButton(SkillData skill)
{
var button = MarketplaceButtonScene.Instantiate<MarketplaceButton>();
button.Text = GetButtonText(skill);
button.Data = skill;
button.Icon = skill.Icon;
button.Pressed += () => OnUpgradeButtonPressed(skill);
_unlockButtons.Add(button);
UnlockedGrid.AddChild(button);
UnlockedGrid.QueueSort();
}
private void OnUpgradeButtonPressed(SkillData skill)
{
if (_gameManager.IsSkillUnlocked(skill))
{
if (skill.Level < skill.MaxLevel)
{
SkillUnlockedComponent.TryUpgradeSkill(skill);
if (!skill.IsActive) SkillUnlockedComponent.SkillManager.ToggleSkillActivation(skill);
}
else
{
SkillUnlockedComponent.SkillManager.ToggleSkillActivation(skill);
}
}
else
{
SkillUnlockedComponent.TryUnlockSkill(skill);
}
}
private void RemoveButton(SkillData skill)
{
foreach (var node in ToUnlockGrid.GetChildren())
{
var child = (Button)node;
if (child.Text != GetButtonText(skill)) continue;
child.QueueFree();
break;
}
}
private void OnSkillButtonPressed(SkillButton button)
{
SkillUnlockedComponent.SkillManager.ToggleSkillActivation(button.Data);
}
}

View File

@@ -0,0 +1 @@
uid://bnc16gndpl87i

View File

@@ -0,0 +1,62 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.components;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.UI;
public partial class MarketplaceButton : Button
{
[Export] public SkillData Data { get; set; }
[Export] public Texture2D UnlockedSkillIcon { get; set; }
[Export] public Texture2D LockedSkillIcon { get; set; }
[Export] public Container SkillLevelContainer { get; set; }
private GameManager _gameManager;
private SkillUnlockedComponent _skillUnlockedComponent;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
Setup();
var player = _gameManager.Player;
var skillUnlockerComponent = player?.GetNodeOrNull<SkillUnlockedComponent>("SkillUnlockerComponent");
if (skillUnlockerComponent == null) return;
skillUnlockerComponent.SkillUnlocked += OnSkillUnlock;
}
public override void _ExitTree()
{
_skillUnlockedComponent.SkillUnlocked -= OnSkillUnlock;
}
private void Setup()
{
if (Data == null) return;
for (var i = 0; i < Data.MaxLevel; i++)
{
var icon = new TextureRect()
{
Texture = i < Data.Level ? UnlockedSkillIcon : LockedSkillIcon,
};
SkillLevelContainer.AddChild(icon);
}
}
private void OnSkillUnlock(SkillData skill)
{
if (skill.Name != Data.Name) return;
for (var i = 0; i < Data.MaxLevel; i++)
{
var icon = SkillLevelContainer.GetChildOrNull<TextureRect>(i);
if (icon == null) continue;
icon.Texture = i < Data.Level ? UnlockedSkillIcon : LockedSkillIcon;
Disabled = i >= Data.Level;
}
}
}

View File

@@ -0,0 +1 @@
uid://vokgv56bjpf1

67
scripts/UI/PauseMenu.cs Normal file
View File

@@ -0,0 +1,67 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class PauseMenu : Node
{
[Export] public Control PauseMenuControl { get; set; }
[Export] public Control SettingsControl { get; set; }
[Export] public Button ResumeButton { get; set; }
[Export] public Button MainMenuButton { get; set; }
[Export] public Button QuitButton { get; set; }
[Export] public Button SettingsButton { get; set; }
[Export] public PackedScene MainMenuScene { get; set; }
private GameManager _gameManager;
private UIManager _uiManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/GameManager");
_uiManager = GetNode<UIManager>("/root/UIManager");
ResumeButton.Pressed += OnResumePressed;
MainMenuButton.Pressed += OnMainMenuPressed;
QuitButton.Pressed += OnQuitPressed;
SettingsButton.Pressed += OnSettingsPressed;
PauseMenuControl.Hide();
}
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("pause")) return;
if (_uiManager.IsVisibleOnStack(PauseMenuControl))
OnResumePressed();
else
{
_uiManager.PushScreen(PauseMenuControl);
_gameManager.PauseGame();
}
}
private void OnSettingsPressed()
{
_uiManager.PushScreen(SettingsControl);
_gameManager.PauseGame();
}
private void OnQuitPressed()
{
_gameManager.QuitGame();
}
private void OnMainMenuPressed()
{
_gameManager.ResumeGame();
_gameManager.ResetCurrentSessionState();
GetTree().ChangeSceneToPacked(MainMenuScene);
}
private void OnResumePressed()
{
_uiManager.PopScreen();
_gameManager.ResumeGame();
}
}

View File

@@ -0,0 +1 @@
uid://cakgxndurgfa3

View File

@@ -0,0 +1,60 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.UI;
public partial class SettingsMenu : Node
{
[Export] public Control InputSettingsControl { get; set; }
[Export] public Control AudioSettingsControl { get; set; }
[Export] public Control DisplaySettingsControl { get; set; }
[Export] public Control GameplaySettingsControl { get; set; }
[Export] public Control SettingsMenuControl { get; set; }
[Export] public Button InputSettingsButton { get; set; }
[Export] public Button AudioSettingsButton { get; set; }
[Export] public Button DisplaySettingsButton { get; set; }
[Export] public Button GameplaySettingsButton { get; set; }
private UIManager _uiManager;
public override void _Ready()
{
_uiManager = GetNode<UIManager>("/root/UIManager");
InputSettingsButton.Pressed += OnInputSettingsPressed;
AudioSettingsButton.Pressed += OnAudioSettingsPressed;
DisplaySettingsButton.Pressed += OnDisplaySettingsPressed;
GameplaySettingsButton.Pressed += OnGameplaySettingsPressed;
InputSettingsControl.Hide();
AudioSettingsControl.Hide();
DisplaySettingsControl.Hide();
GameplaySettingsControl.Hide();
}
public override void _UnhandledInput(InputEvent @event)
{
if (!@event.IsActionPressed("ui_cancel")) return;
if (_uiManager.IsScreenOnTop(SettingsMenuControl)) _uiManager.PopScreen();
}
private void OnInputSettingsPressed()
{
_uiManager.PushScreen(InputSettingsControl);
}
private void OnAudioSettingsPressed()
{
_uiManager.PushScreen(AudioSettingsControl);
}
private void OnDisplaySettingsPressed()
{
_uiManager.PushScreen(DisplaySettingsControl);
}
private void OnGameplaySettingsPressed()
{
_uiManager.PushScreen(GameplaySettingsControl);
}
}

View File

@@ -0,0 +1 @@
uid://i8j47qnrytuo

25
scripts/UI/SkillButton.cs Normal file
View File

@@ -0,0 +1,25 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.UI;
public partial class SkillButton : Button
{
[Export] public SkillData Data { get; set; }
public void Setup()
{
Icon = Data?.Icon;
Text = Tr(Data?.Name) ?? string.Empty;
}
public void Activate()
{
Set("theme_override_colors/font_color", new Color("#49aa10"));
}
public void Deactivate()
{
Set("theme_override_colors/font_color", new Color("#ffffff"));
}
}

Some files were not shown because too many files have changed in this diff Show More