Add game scene and level catalog interfaces, and implement scene management in AppRoot

This commit is contained in:
2025-08-15 03:04:21 +02:00
parent 406036504a
commit 2cc54f7b37
14 changed files with 194 additions and 21 deletions

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

View File

@@ -1,6 +1,8 @@
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Godot; using Godot;
using Godot.Collections;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.data; using Mr.BrickAdventures.data;
using Mr.BrickAdventures.game.repositories; using Mr.BrickAdventures.game.repositories;
using Mr.BrickAdventures.game.services; using Mr.BrickAdventures.game.services;
@@ -8,27 +10,61 @@ using Mr.BrickAdventures.game.services;
namespace Mr.BrickAdventures.common; namespace Mr.BrickAdventures.common;
[Meta(typeof(IAutoNode))] [Meta(typeof(IAutoNode))]
public partial class AppRoot : Node2D, IProvide<PlayerRepository>, IProvide<LevelRepository>, IProvide<SaveClient>, IProvide<SaveService> 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); 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 SaveClient _save = new("user://savegame.save", version: 2);
private readonly PlayerRepository _players = new(); private readonly PlayerRepository _players = new();
private readonly LevelRepository _levels = new(); private readonly LevelRepository _levelsRepo = new();
private SaveService _saveService = null!; private SaveService _saveService = null!;
private LevelService _levelService = null!;
private ILevelCatalog _catalog = null!;
PlayerRepository IProvide<PlayerRepository>.Value() => _players; PlayerRepository IProvide<PlayerRepository>.Value() => _players;
LevelRepository IProvide<LevelRepository>.Value() => _levels; LevelRepository IProvide<LevelRepository>.Value() => _levelsRepo;
SaveClient IProvide<SaveClient>.Value() => _save; SaveClient IProvide<SaveClient>.Value() => _save;
SaveService IProvide<SaveService>.Value() => _saveService; SaveService IProvide<SaveService>.Value() => _saveService;
LevelService IProvide<LevelService>.Value() => _levelService;
ILevelCatalog IProvide<ILevelCatalog>.Value() => _catalog;
IGameScenes IProvide<IGameScenes>.Value() => this;
public void OnReady() public void OnReady()
{ {
_saveService = new SaveService(_players, _levels, _save); _saveService = new SaveService(_players, _levelsRepo, _save);
_levelService = new LevelService(_levelsRepo);
_catalog = new ExportedLevelCatalog(_levels, _mainMenu);
_saveService.TryLoad(); _saveService.TryLoad();
this.Provide(); 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;
}
} }

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

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

@@ -1,6 +1,7 @@
using Chickensoft.AutoInject; using Chickensoft.AutoInject;
using Chickensoft.Introspection; using Chickensoft.Introspection;
using Godot; using Godot;
using Mr.BrickAdventures.app;
using Mr.BrickAdventures.game.repositories; using Mr.BrickAdventures.game.repositories;
using Mr.BrickAdventures.game.services; using Mr.BrickAdventures.game.services;
@@ -20,10 +21,11 @@ public partial class MainMenu : Control
[Export] public Label VersionLabel { get; set; } = null!; [Export] public Label VersionLabel { get; set; } = null!;
[Export] public Control SettingsControl { get; set; } = null!; [Export] public Control SettingsControl { get; set; } = null!;
[Export] public Control CreditsControl { get; set; } = null!; [Export] public Control CreditsControl { get; set; } = null!;
[Export] public PackedScene FirstLevelScene { get; set; } = null!;
[Dependency] public SaveService Save => this.DependOn<SaveService>(); [Dependency] public SaveService Save => this.DependOn<SaveService>();
[Dependency] public LevelRepository Levels => this.DependOn<LevelRepository>(); [Dependency] public LevelRepository Levels => this.DependOn<LevelRepository>();
[Dependency] public IGameScenes Scenes => this.DependOn<IGameScenes>();
[Dependency] public ILevelCatalog Catalog => this.DependOn<ILevelCatalog>();
public void OnReady() { public void OnReady() {
VersionLabel.Text = $"v. {ProjectSettings.GetSetting("application/config/version")}"; VersionLabel.Text = $"v. {ProjectSettings.GetSetting("application/config/version")}";
@@ -44,15 +46,13 @@ public partial class MainMenu : Control
private void OnNewGamePressed() { private void OnNewGamePressed() {
Save.NewGame(); Save.NewGame();
Levels.SetCurrent(0); Levels.SetCurrent(0);
GetTree().ChangeSceneToPacked(FirstLevelScene); var first = Catalog.First;
Scenes.Load(first);
} }
private void OnContinuePressed() { private void OnContinuePressed() {
if (!Save.TryLoad()) { if (!Save.TryLoad()) { OnNewGamePressed(); return; }
// Fallback: start new game var scene = Catalog.Get(Levels.Current) ?? Catalog.First;
OnNewGamePressed(); Scenes.Load(scene);
return;
}
GetTree().ChangeSceneToPacked(FirstLevelScene);
} }
} }

View File

@@ -1,6 +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; namespace Mr.BrickAdventures.features.ui.menus;
public class PauseMenu [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

@@ -1,6 +1,21 @@
using Mr.BrickAdventures.game.repositories;
namespace Mr.BrickAdventures.game.services; namespace Mr.BrickAdventures.game.services;
public sealed class LevelService 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

@@ -1,11 +1,11 @@
[gd_scene load_steps=3 format=3 uid="uid://i6mnjbjcoqe5"] [gd_scene load_steps=3 format=3 uid="uid://i6mnjbjcoqe5"]
[ext_resource type="Script" uid="uid://cakgxndurgfa3" path="res://scripts/UI/PauseMenu.cs" id="1_aktha"] [ext_resource type="Script" uid="uid://bwgs02wcfnm8u" path="res://features/ui/menus/PauseMenu.cs" id="1_ljtns"]
[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("ResumeButton", "MainMenuButton", "QuitButton", "SettingsButton")] [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
@@ -14,11 +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")
ResumeButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button") ResumeButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Resume Button")
RestartButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button")
MainMenuButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button") MainMenuButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Exit to menu Button")
QuitButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Quit game Button")
SettingsButton = NodePath("PanelContainer/MarginContainer/VBoxContainer/Settings Button")
[node name="PanelContainer" type="PanelContainer" parent="."] [node name="PanelContainer" type="PanelContainer" parent="."]
layout_mode = 1 layout_mode = 1

View File

@@ -1,22 +1,27 @@
[gd_scene load_steps=8 format=3 uid="uid://cl00e2ocomk3m"] [gd_scene load_steps=12 format=3 uid="uid://cl00e2ocomk3m"]
[ext_resource type="PackedScene" uid="uid://8b6ol5sssbgo" path="res://features/ui/menus/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="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://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="AppRoot" type="Node2D"] [node name="AppRoot" type="Node2D"]
script = ExtResource("1_rtw2f") 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="Main menu" type="CanvasLayer" parent="."] [node name="Main menu" type="CanvasLayer" parent="."]
[node name="MainMenu" parent="Main menu" node_paths=PackedStringArray("SettingsControl", "CreditsControl") instance=ExtResource("1_ekxnf")] [node name="MainMenu" parent="Main menu" node_paths=PackedStringArray("SettingsControl", "CreditsControl") instance=ExtResource("1_ekxnf")]
SettingsControl = NodePath("../Settings menu") SettingsControl = NodePath("../Settings menu")
CreditsControl = NodePath("../Credits") CreditsControl = NodePath("../Credits")
FirstLevelScene = ExtResource("3_oa1go")
[node name="Settings menu" parent="Main menu" node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("2_bqqt6")] [node name="Settings menu" parent="Main menu" node_paths=PackedStringArray("input_settings", "audio_settings") instance=ExtResource("2_bqqt6")]
visible = false visible = false