Compare commits

...

8 Commits

Author SHA1 Message Date
ef4d128869 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 2025-08-12 03:38:23 +02:00
f3aa2631f2 Add new components: HealComponent, HitComponent, HomingMissileMotionComponent, LeverComponent, and TriggerLeverComponent 2025-08-10 22:38:19 +02:00
54ffa8a42c Add new components: ExplosiveComponent, FadeAwayComponent, FireEffectComponent, FlipComponent, GravityMotionComponent, LaunchComponent, and update PlatformMovement with LastDirection property 2025-08-10 17:53:06 +02:00
ac477115c5 Add new components: EnemyDeathComponent, EnemyWaveTriggerComponent, and ExitDoorComponent 2025-08-10 14:50:18 +02:00
99473d1295 Add new components: CanPickUpComponent, CollapsableComponent, DestroyableComponent, EffectInflictorComponent, StatusEffectComponent, and StatusEffectDataResource 2025-08-10 12:57:58 +02:00
5dbf04cc5f cleanup 2025-08-10 01:36:13 +02:00
4098d192af Add core game components including ConfigFileHandler, GameManager, SaveSystem, and UIManager 2025-08-10 01:35:35 +02:00
8467f67090 Implement BeamComponent in C# and enhance marketplace button functionality 2025-07-21 22:02:05 +02:00
71 changed files with 3203 additions and 136 deletions

View File

@@ -0,0 +1,25 @@
using Godot;
namespace Mr.BrickAdventures.Autoloads;
public partial class ConfigFileHandler : Node
{
private ConfigFile _settingsConfig = new();
private const string SettingsPath = "user://settings.ini";
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}");
}
}
}

195
Autoloads/GameManager.cs Normal file
View File

@@ -0,0 +1,195 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.Autoloads;
public partial class GameManager : Node
{
[Export] public Array<PackedScene> LevelScenes { get; set; } = 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 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 GetUnlockedSkills()
{
var unlocked = (Array<SkillData>)PlayerState["unlocked_skills"];
var session = (Array<SkillData>)CurrentSessionState["skills_unlocked"];
if ((((Array)session)!).Count == 0) return (Array)unlocked;
if ((((Array)unlocked)!).Count == 0) return (Array)session;
var joined = new Array();
joined.AddRange((Array)unlocked ?? new Array());
joined.AddRange((Array)session ?? new Array());
return joined;
}
}

46
Autoloads/SaveSystem.cs Normal file
View File

@@ -0,0 +1,46 @@
using Godot;
using Godot.Collections;
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 GM _gm;
public override void _Ready()
{
//_gm = GetNode<GM>("/root/GameManager");
}
public void SaveGame()
{
//TODO: Implement saving logic
}
public bool LoadGame()
{
//TODO: Implement loading logic
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"]);
return true;
}
public bool CheckSaveExists() => FileAccess.FileExists(SavePath);
}

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);
}
}

141
Mr. Brick Adventures.csproj Normal file
View File

@@ -0,0 +1,141 @@
<Project Sdk="Godot.NET.Sdk/4.4.1">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>Mr.BrickAdventures</RootNamespace>
</PropertyGroup>
<ItemGroup>
<Content Include="scripts\components\.idea\.gitignore" />
<Content Include="scripts\components\.idea\encodings.xml" />
<Content Include="scripts\components\.idea\indexLayout.xml" />
<Content Include="scripts\components\.idea\inspectionProfiles\Project_Default.xml" />
<Content Include="scripts\components\.idea\projectSettingsUpdater.xml" />
<Content Include="scripts\components\.idea\vcs.xml" />
<Content Include="scripts\components\.idea\workspace.xml" />
<Content Include="scripts\components\BeamComponent.cs.uid" />
<Content Include="scripts\components\beam_component.gd" />
<Content Include="scripts\components\beam_component.gd.uid" />
<Content Include="scripts\components\brick_throw.gd" />
<Content Include="scripts\components\brick_throw.gd.uid" />
<Content Include="scripts\components\bullet_component.gd" />
<Content Include="scripts\components\bullet_component.gd.uid" />
<Content Include="scripts\components\cage_component.gd" />
<Content Include="scripts\components\cage_component.gd.uid" />
<Content Include="scripts\components\cannot_stomp_component.gd" />
<Content Include="scripts\components\cannot_stomp_component.gd.uid" />
<Content Include="scripts\components\can_be_launched_component.gd" />
<Content Include="scripts\components\can_be_launched_component.gd.uid" />
<Content Include="scripts\components\can_pickup.gd" />
<Content Include="scripts\components\can_pickup.gd.uid" />
<Content Include="scripts\components\charge_throw_component.gd" />
<Content Include="scripts\components\charge_throw_component.gd.uid" />
<Content Include="scripts\components\chase_level_component.gd" />
<Content Include="scripts\components\chase_level_component.gd.uid" />
<Content Include="scripts\components\cleanup_component.gd" />
<Content Include="scripts\components\cleanup_component.gd.uid" />
<Content Include="scripts\components\collapsable.gd" />
<Content Include="scripts\components\collapsable.gd.uid" />
<Content Include="scripts\components\collectable.gd" />
<Content Include="scripts\components\collectable.gd.uid" />
<Content Include="scripts\components\damage_component.gd" />
<Content Include="scripts\components\damage_component.gd.uid" />
<Content Include="scripts\components\destroyable_component.gd" />
<Content Include="scripts\components\destroyable_component.gd.uid" />
<Content Include="scripts\components\effect_inflictor_component.gd" />
<Content Include="scripts\components\effect_inflictor_component.gd.uid" />
<Content Include="scripts\components\enemy_death.gd" />
<Content Include="scripts\components\enemy_death.gd.uid" />
<Content Include="scripts\components\enemy_wave_trigger.gd" />
<Content Include="scripts\components\enemy_wave_trigger.gd.uid" />
<Content Include="scripts\components\exit_door_component.gd" />
<Content Include="scripts\components\exit_door_component.gd.uid" />
<Content Include="scripts\components\explosive_component.gd" />
<Content Include="scripts\components\explosive_component.gd.uid" />
<Content Include="scripts\components\fade_away.gd" />
<Content Include="scripts\components\fade_away.gd.uid" />
<Content Include="scripts\components\fire_effect_component.gd" />
<Content Include="scripts\components\fire_effect_component.gd.uid" />
<Content Include="scripts\components\flashing_component.gd" />
<Content Include="scripts\components\flashing_component.gd.uid" />
<Content Include="scripts\components\flip_player.gd" />
<Content Include="scripts\components\flip_player.gd.uid" />
<Content Include="scripts\components\gravity_motion_component.gd" />
<Content Include="scripts\components\gravity_motion_component.gd.uid" />
<Content Include="scripts\components\health.gd" />
<Content Include="scripts\components\health.gd.uid" />
<Content Include="scripts\components\heal_component.gd" />
<Content Include="scripts\components\heal_component.gd.uid" />
<Content Include="scripts\components\hit_component.gd" />
<Content Include="scripts\components\hit_component.gd.uid" />
<Content Include="scripts\components\homing_missile_motion.gd" />
<Content Include="scripts\components\homing_missile_motion.gd.uid" />
<Content Include="scripts\components\ice_effect_component.gd" />
<Content Include="scripts\components\ice_effect_component.gd.uid" />
<Content Include="scripts\components\invulnerability_component.gd" />
<Content Include="scripts\components\invulnerability_component.gd.uid" />
<Content Include="scripts\components\jump_pad_component.gd" />
<Content Include="scripts\components\jump_pad_component.gd.uid" />
<Content Include="scripts\components\kill_player_out_of_screen.gd" />
<Content Include="scripts\components\kill_player_out_of_screen.gd.uid" />
<Content Include="scripts\components\knockback.gd" />
<Content Include="scripts\components\knockback.gd.uid" />
<Content Include="scripts\components\launch_component.gd" />
<Content Include="scripts\components\launch_component.gd.uid" />
<Content Include="scripts\components\lever_component.gd" />
<Content Include="scripts\components\lever_component.gd.uid" />
<Content Include="scripts\components\lifetime_component.gd" />
<Content Include="scripts\components\lifetime_component.gd.uid" />
<Content Include="scripts\components\magnetic_skill.gd" />
<Content Include="scripts\components\magnetic_skill.gd.uid" />
<Content Include="scripts\components\out_of_screen_component.gd" />
<Content Include="scripts\components\out_of_screen_component.gd.uid" />
<Content Include="scripts\components\periodic_shooting.gd" />
<Content Include="scripts\components\periodic_shooting.gd.uid" />
<Content Include="scripts\components\PlatformMovementComponent.cs.uid" />
<Content Include="scripts\components\PlayerController.cs.uid" />
<Content Include="scripts\components\player_death.gd" />
<Content Include="scripts\components\player_death.gd.uid" />
<Content Include="scripts\components\player_movement.gd" />
<Content Include="scripts\components\player_movement.gd.uid" />
<Content Include="scripts\components\progressive_damage_component.gd" />
<Content Include="scripts\components\progressive_damage_component.gd.uid" />
<Content Include="scripts\components\projectile_component.gd" />
<Content Include="scripts\components\projectile_component.gd.uid" />
<Content Include="scripts\components\projectile_init_component.gd" />
<Content Include="scripts\components\projectile_init_component.gd.uid" />
<Content Include="scripts\components\requirement_component.gd" />
<Content Include="scripts\components\requirement_component.gd.uid" />
<Content Include="scripts\components\score.gd" />
<Content Include="scripts\components\score.gd.uid" />
<Content Include="scripts\components\ship_movement.gd" />
<Content Include="scripts\components\ship_movement.gd.uid" />
<Content Include="scripts\components\ship_shooter.gd" />
<Content Include="scripts\components\ship_shooter.gd.uid" />
<Content Include="scripts\components\side_to_side_movement.gd" />
<Content Include="scripts\components\side_to_side_movement.gd.uid" />
<Content Include="scripts\components\skill_unlocker_component.gd" />
<Content Include="scripts\components\skill_unlocker_component.gd.uid" />
<Content Include="scripts\components\spaceship_enter_component.gd" />
<Content Include="scripts\components\spaceship_enter_component.gd.uid" />
<Content Include="scripts\components\spaceship_exit_component.gd" />
<Content Include="scripts\components\spaceship_exit_component.gd.uid" />
<Content Include="scripts\components\spin_component.gd" />
<Content Include="scripts\components\spin_component.gd.uid" />
<Content Include="scripts\components\status_effect_component.gd" />
<Content Include="scripts\components\status_effect_component.gd.uid" />
<Content Include="scripts\components\stomp_damage_component.gd" />
<Content Include="scripts\components\stomp_damage_component.gd.uid" />
<Content Include="scripts\components\straight_motion_component.gd" />
<Content Include="scripts\components\straight_motion_component.gd.uid" />
<Content Include="scripts\components\terrain_hit_fx.gd" />
<Content Include="scripts\components\terrain_hit_fx.gd.uid" />
<Content Include="scripts\components\tooltip_component.gd" />
<Content Include="scripts\components\tooltip_component.gd.uid" />
<Content Include="scripts\components\trail_component.gd" />
<Content Include="scripts\components\trail_component.gd.uid" />
<Content Include="scripts\components\trigger_lever_component.gd" />
<Content Include="scripts\components\trigger_lever_component.gd.uid" />
<Content Include="scripts\components\unlock_on_requirement_component.gd" />
<Content Include="scripts\components\unlock_on_requirement_component.gd.uid" />
</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>

View File

@@ -19,7 +19,7 @@ config/version="in-dev"
run/main_scene="uid://cl00e2ocomk3m"
config/use_custom_user_dir=true
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
boot_splash/bg_color=Color(0, 0, 0, 1)
boot_splash/show_image=false

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,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum CollectableType
{
Coin,
Kid,
Health,
}

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,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum SkillType
{
Attack,
Throw,
Misc,
}

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,8 @@
namespace Mr.BrickAdventures.scripts.Resources;
public enum StatusEffectType
{
None,
Fire,
Ice
}

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);
}
}

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,109 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
[GlobalClass]
public partial class BeamComponent : Node2D
{
private float _currentLength = 0.0f;
private const float PixelSize = 16.0f; // Assuming 16 pixels per unit for scaling
[Export]
public float ExpansionSpeed { get; set; } = 100.0f;
[Export]
public float MaxLength { get; set; } = 512.0f;
[Export]
public Vector2 Direction { get; set; } = Vector2.Down;
[Export]
public Node2D Root { get; set; }
[Export]
public Sprite2D Sprite { get; set; }
[Export]
public CollisionShape2D CollisionShape { get; set; }
public override void _Ready()
{
if (Root == null)
{
GD.PrintErr("Root node is not set for BeamComponent.");
}
if (Sprite == null)
{
GD.PrintErr("Sprite node is not set for BeamComponent.");
}
if (CollisionShape == null)
{
GD.PrintErr("CollisionShape node is not set for BeamComponent.");
}
var shape = CollisionShape?.Shape as RectangleShape2D;
shape?.SetSize(new Vector2(_currentLength / 2.0f, _currentLength / 2.0f));
Sprite?.SetScale(new Vector2(1f, 1f));
CollisionShape?.SetPosition(Vector2.Zero);
}
public override void _Process(double delta)
{
var newLength = _currentLength + ExpansionSpeed * (float)delta;
if (newLength > MaxLength) newLength = MaxLength;
if (!CheckForObstacle(newLength)) ExpandBeam(newLength);
}
private void ExpandBeam(float newLength)
{
_currentLength = newLength;
if (Direction == Vector2.Up)
{
var pos = Sprite.Position;
var scale = Sprite.Scale;
var shape = CollisionShape?.Shape as RectangleShape2D;
Sprite.SetScale(new Vector2(scale.X, _currentLength / PixelSize));
Sprite.SetPosition(new Vector2(pos.X, -_currentLength / 2.0f));
shape?.SetSize(new Vector2(PixelSize / 2f, _currentLength / 2.0f));
CollisionShape?.SetPosition(new Vector2(CollisionShape.Position.X, -_currentLength / 2.0f));
} else if (Direction == Vector2.Down)
{
var pos = Sprite.Position;
var scale = Sprite.Scale;
var shape = CollisionShape?.Shape as RectangleShape2D;
Sprite.SetScale(new Vector2(scale.X, _currentLength / PixelSize));
Sprite.SetPosition(new Vector2(pos.X, _currentLength / 2.0f));
shape?.SetSize(new Vector2(PixelSize / 2f, _currentLength / 2.0f));
CollisionShape?.SetPosition(new Vector2(CollisionShape.Position.X, _currentLength / 2.0f));
} else if (Direction == Vector2.Left)
{
var pos = Sprite.Position;
var scale = Sprite.Scale;
var shape = CollisionShape?.Shape as RectangleShape2D;
Sprite.SetScale(new Vector2(_currentLength / PixelSize, scale.Y));
Sprite.SetPosition(new Vector2(-_currentLength / 2.0f, pos.Y));
shape?.SetSize(new Vector2(_currentLength / 2.0f, PixelSize / 2f));
CollisionShape?.SetPosition(new Vector2(-_currentLength / 2.0f, CollisionShape.Position.Y));
} else if (Direction == Vector2.Right)
{
var pos = Sprite.Position;
var scale = Sprite.Scale;
var shape = CollisionShape?.Shape as RectangleShape2D;
Sprite.SetScale(new Vector2(_currentLength / PixelSize, scale.Y));
Sprite.SetPosition(new Vector2(_currentLength / 2.0f, pos.Y));
shape?.SetSize(new Vector2(_currentLength / 2.0f, PixelSize / 2f));
CollisionShape?.SetPosition(new Vector2(_currentLength / 2.0f, CollisionShape.Position.Y));
}
}
private bool CheckForObstacle(float newLength)
{
var spaceState = GetWorld2D().DirectSpaceState;
var queryStart = GlobalPosition;
var queryEnd = queryStart + Direction.Normalized() * newLength;
var query = PhysicsRayQueryParameters2D.Create(queryStart, queryEnd);
query.CollideWithAreas = false;
query.CollideWithBodies = true;
var result = spaceState.IntersectRay(query);
return result.Count > 0;
}
}

View File

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

View File

@@ -0,0 +1,8 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class CanBeLaunchedComponent : Node
{
}

View File

@@ -0,0 +1,8 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class CanPickUpComponent : Node
{
}

View File

@@ -0,0 +1,81 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class CollapsableComponent : Node
{
[Export] public Timer ToCollapseTimer { get; set; }
[Export] public Timer ResetTimer { get; set; }
[Export] public Sprite2D Sprite2D { get; set; }
[Export] public CollisionShape2D CollisionShape { get; set; }
[Export] public float CollapseTime { get; set; } = 0.5f;
[Export] public float ResetTime { get; set; } = 0.5f;
[Export] public float AnimationTime { get; set; } = 0.25f;
public override void _Ready()
{
ResetTimers();
ToCollapseTimer.Timeout += OnToCollapseTimerTimeout;
ResetTimer.Timeout += OnResetTimerTimeout;
}
public void OnCollapsableDetectorBodyEntered(Node2D body)
{
ToCollapseTimer.Start();
}
public void OnCollapsableDetectorBodyExited(Node2D body)
{
var collapseTimeLeft = Mathf.Abs(ToCollapseTimer.TimeLeft - CollapseTime);
if (collapseTimeLeft < (0.1f * CollapseTime))
{
ResetTimers();
}
}
private void OnToCollapseTimerTimeout()
{
_ = Collapse();
}
private void OnResetTimerTimeout()
{
_ = Reactivate();
}
private async Task Collapse()
{
ToCollapseTimer.Stop();
ToCollapseTimer.SetWaitTime(CollapseTime);
var tween = CreateTween();
tween.TweenProperty(Sprite2D, "modulate:a", 0f, AnimationTime);
await ToSignal(tween, Tween.SignalName.Finished);
CollisionShape?.CallDeferred("set_disabled", true);
ResetTimer.Start();
}
private async Task Reactivate()
{
ResetTimer.Stop();
ResetTimer.SetWaitTime(ResetTime);
var tween = CreateTween();
tween.TweenProperty(Sprite2D, "modulate:a", 1f, AnimationTime);
await ToSignal(tween, Tween.SignalName.Finished);
CollisionShape?.CallDeferred("set_disabled", false);
}
private void ResetTimers()
{
ToCollapseTimer.Stop();
ToCollapseTimer.SetWaitTime(CollapseTime);
ResetTimer.Stop();
ResetTimer.SetWaitTime(ResetTime);
}
}

View File

@@ -0,0 +1,50 @@
using System;
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class CollectableComponent : Node
{
private bool _hasFadeAway = false;
[Export] public Area2D Area2D { get; set; }
[Export] public CollisionShape2D CollisionShape { get; set; }
[Export] public CollectableResource Data { get; set; }
[Export] public AudioStreamPlayer2D Sfx {get; set; }
[Signal] public delegate void CollectedEventHandler(Variant amount, CollectableType type, Node2D body);
public override void _Ready()
{
if (Area2D != null)
Area2D.BodyEntered += OnArea2DBodyEntered;
else
GD.PushError("Collectable node missing Area2D node.");
if (Owner.HasNode("FadeAwayComponent"))
_hasFadeAway = true;
}
private async void OnArea2DBodyEntered(Node2D body)
{
try
{
if (!body.HasNode("CanPickUpComponent")) return;
EmitSignalCollected(Data.Amount, Data.Type, body);
CollisionShape?.CallDeferred("set_disabled", true);
Sfx?.Play();
if (_hasFadeAway) return;
if (Sfx != null)
await ToSignal(Sfx, AudioStreamPlayer2D.SignalName.Finished);
Owner.QueueFree();
}
catch (Exception e)
{
GD.PushError($"Error in CollectableComponent.OnArea2DBodyEntered: {e.Message}");
}
}
}

View File

@@ -0,0 +1,105 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class DamageComponent : Node
{
[Export] public float Damage { get; set; } = 0.25f;
[Export] public Area2D Area { get; set; }
[Export] public StatusEffectDataResource StatusEffectData { get; set; }
[Export] public Timer DamageTimer { get; set; }
private Node _currentTarget = null;
[Signal] public delegate void EffectInflictedEventHandler(Node2D target, StatusEffectDataResource effect);
public override void _Ready()
{
if (Area == null)
{
GD.PushError($"DamageComponent: Area2D node is not set.");
return;
}
Area.BodyEntered += OnAreaBodyEntered;
Area.BodyExited += OnAreaBodyExited;
Area.AreaEntered += OnAreaAreaEntered;
if (DamageTimer != null)
{
DamageTimer.Timeout += OnDamageTimerTimeout;
}
}
public override void _Process(double delta)
{
if (_currentTarget == null) return;
if (DamageTimer != null) return;
ProcessEntityAndApplyDamage(_currentTarget as Node2D);
}
public void DealDamage(HealthComponent target) => target.DecreaseHealth(Damage);
private void OnAreaAreaEntered(Area2D area)
{
if (!CheckIfProcessingIsOn())
return;
if (area == Area) return;
var parent = area.GetParent();
if (parent.HasNode("DamageComponent"))
ProcessEntityAndApplyDamage(parent as Node2D);
}
private void OnAreaBodyExited(Node2D body)
{
if (body != _currentTarget) return;
_currentTarget = null;
DamageTimer?.Stop();
}
private void OnAreaBodyEntered(Node2D body)
{
_currentTarget = body;
if (!CheckIfProcessingIsOn())
return;
DamageTimer?.Start();
ProcessEntityAndApplyDamage(body);
}
private void OnDamageTimerTimeout()
{
if (_currentTarget == null) return;
ProcessEntityAndApplyDamage(_currentTarget as Node2D);
}
private void ProcessEntityAndApplyDamage(Node2D body)
{
if (body == null) return;
if (!body.HasNode("HealthComponent")) return;
var health = body.GetNode<HealthComponent>("HealthComponent");
var inv = body.GetNodeOrNull<InvulnerabilityComponent>("InvulnerabilityComponent");
if (inv != null && inv.IsInvulnerable())
return;
if (StatusEffectData != null && StatusEffectData.Type != StatusEffectType.None)
EmitSignalEffectInflicted(body, StatusEffectData);
DealDamage(health);
inv?.Activate();
}
private bool CheckIfProcessingIsOn()
{
return ProcessMode is ProcessModeEnum.Inherit or ProcessModeEnum.Always;
}
}

View File

@@ -0,0 +1,35 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class DestroyableComponent : Node2D
{
[Export] public HealthComponent Health { get; set; }
[Export] public PackedScene DestroyEffect { get; set; }
public override void _Ready()
{
if (Health == null)
{
GD.PushError("DestroyableComponent: HealthComponent is not set.");
return;
}
Health.Death += OnHealthDeath;
}
private void OnHealthDeath()
{
if (DestroyEffect == null)
{
Owner.QueueFree();
return;
}
var effect = DestroyEffect.Instantiate<Node2D>();
Health.GetParent().AddChild(effect);
effect.SetGlobalPosition(Health.GlobalPosition);
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,27 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class EffectInflictorComponent : Node
{
[Export] public DamageComponent Damage { get; set; }
public override void _Ready()
{
if (Damage == null)
{
GD.PushError("EffectInflictorComponent requires a DamageComponent to be set.");
return;
}
Damage.EffectInflicted += OnEffectInflicted;
}
private void OnEffectInflicted(Node2D target, StatusEffectDataResource effect)
{
var statusEffect = target.GetNodeOrNull<StatusEffectComponent>("StatusEffectComponent");
statusEffect?.ApplyEffect(effect);
}
}

View File

@@ -0,0 +1,42 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class EnemyDeathComponent : Node
{
[Export] public float TweenDuration { get; set; } = 0.5f;
[Export] public CollisionShape2D CollisionShape { get; set; }
[Export] public HealthComponent Health { get; set; }
public override void _Ready()
{
if (CollisionShape == null)
{
GD.PushError("EnemyDeathComponent: CollisionShape is not set.");
return;
}
if (Health == null)
{
GD.PushError("EnemyDeathComponent: Health is not set.");
return;
}
Health.Death += OnDeath;
}
private void OnDeath()
{
CallDeferred(nameof(Die));
}
private async Task Die()
{
CollisionShape.SetDisabled(true);
var tween = CreateTween();
tween.TweenProperty(Owner, "scale", Vector2.Zero, TweenDuration);
await ToSignal(tween, Tween.SignalName.Finished);
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,51 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class EnemyWaveTriggerComponent : Node
{
[Export] public Area2D Area2D { get; set; }
[Export] public PathFollow2D PathFollowNode { get; set; }
[Export] public float Speed { get; set; } = 100f;
[Export] public bool Loop { get; set; } = false;
[Export] public bool ActivateOnEnter { get; set; } = true;
private bool _isActive = false;
public override void _Ready()
{
Area2D.BodyEntered += OnBodyEntered;
if (PathFollowNode == null) return;
PathFollowNode.SetProgress(0f);
PathFollowNode.SetProcess(false);
}
public override void _Process(double delta)
{
if (!_isActive || PathFollowNode == null) return;
var progress = PathFollowNode.Progress;
progress += (float)(delta * Speed);
PathFollowNode.SetProgress(progress);
if (!(PathFollowNode.ProgressRatio >= 1f) || Loop) return;
_isActive = false;
PathFollowNode.SetProcess(false);
}
private void OnBodyEntered(Node2D body)
{
if (ActivateOnEnter) StartWave();
}
private void StartWave()
{
if (PathFollowNode == null) return;
PathFollowNode.SetProcess(true);
_isActive = true;
}
}

View File

@@ -0,0 +1,51 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.components;
public partial class ExitDoorComponent : Node
{
[Export] public bool Locked { get; set; } = true;
[Export] public Area2D ExitArea { get; set; }
[Export] public Sprite2D DoorSprite { get; set; }
[Export] public AudioStreamPlayer2D OpenDoorSfx { get; set; }
[Export] public int OpenedDoorFrame { get; set; } = 0;
[Signal] public delegate void ExitTriggeredEventHandler();
private GameManager _gameManager;
public override void _Ready()
{
if (ExitArea == null)
{
GD.PushError("ExitDoorComponent: ExitArea is not set.");
return;
}
ExitArea.BodyEntered += OnExitAreaBodyEntered;
_gameManager = GetNode<GameManager>("/root/gameManager");
}
private void OnExitAreaBodyEntered(Node2D body)
{
throw new System.NotImplementedException();
}
private void Unlock()
{
Locked = false;
if (DoorSprite != null)
{
DoorSprite.Frame = OpenedDoorFrame;
}
OpenDoorSfx?.Play();
}
private void GoToNextLevel()
{
_gameManager.OnLevelComplete();
}
}

View File

@@ -0,0 +1,81 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ExplosiveComponent : Node2D
{
[Export] public DamageComponent Damage { get; set; }
[Export] public Area2D Area { get; set; }
[Export] public Area2D ExplodeArea { get; set; }
[Export] public PackedScene ExplosionEffect { get; set; }
[Export] public float TimeToExplode { get; set; } = 9f;
[Signal] public delegate void OnExplosionEventHandler(Node2D body);
private Timer _timer;
public override void _Ready()
{
if (Damage != null)
{
GD.PushError("ExplosiveComponent: DamageComponent is not set.");
return;
}
if (ExplodeArea != null)
{
GD.PushError("ExplosiveComponent: ExplodeArea is not set.");
return;
}
Area.BodyEntered += OnAreaBodyEntered;
Area.AreaEntered += OnAreaAreaEntered;
}
private void OnAreaAreaEntered(Area2D area)
{
Explode();
}
private void OnAreaBodyEntered(Node2D body)
{
Explode();
}
private void PrepareTimer()
{
_timer = new Timer();
_timer.SetWaitTime(TimeToExplode);
_timer.OneShot = true;
_timer.Autostart = true;
_timer.Timeout += Explode;
AddChild(_timer);
}
private void Explode()
{
_timer.Stop();
if (ExplosionEffect != null)
{
var explosionInstance = ExplosionEffect.Instantiate<GpuParticles2D>();
if (Owner is Node2D root) explosionInstance.SetGlobalPosition(root.GlobalPosition);
GetTree().CurrentScene.AddChild(explosionInstance);
explosionInstance.SetEmitting(true);
}
var bodies = ExplodeArea.GetOverlappingBodies();
foreach (var body in bodies)
{
var health = body.GetNodeOrNull<HealthComponent>("HealthComponent");
if (Damage != null && health != null)
{
Damage.DealDamage(health);
}
EmitSignalOnExplosion(body);
}
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,32 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class FadeAwayComponent : Node
{
[Export] public Sprite2D Sprite { get; set; }
[Export] public float FadeDuration { get; set; } = 1f;
[Export] public float Speed { get; set; } = 10f;
[Export] public Vector2 Direction { get; set; } = Vector2.Up;
[Export] public Area2D Area { get; set; }
public override void _Ready()
{
Area.BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node2D body)
{
_ = FadeAway();
}
private async Task FadeAway()
{
var tween = CreateTween().SetParallel(true);
tween.TweenProperty(Sprite, "modulate:a", 0f, FadeDuration);
tween.TweenProperty(Sprite, "position", Sprite.Position + (Direction * Speed), FadeDuration);
await ToSignal(tween, Tween.SignalName.Finished);
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,71 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class FireEffectComponent : Node
{
[Export] public HealthComponent Health { get; set; }
[Export] public StatusEffectComponent StatusEffectComponent { get; set; }
[Export] public GpuParticles2D FireFX { get; set; }
private StatusEffectDataResource _data = null;
private bool _shouldDealDamage = false;
private double _timeElapsed = 0f;
public override void _Ready()
{
if (Health == null)
{
Health = GetNode<HealthComponent>("HealthComponent");
}
if (StatusEffectComponent == null)
{
StatusEffectComponent = GetNode<StatusEffectComponent>("StatusEffectComponent");
}
if (Health == null)
{
GD.PushError("FireEffectComponent: HealthComponent is not set.");
return;
}
if (StatusEffectComponent == null)
{
GD.PushError("FireEffectComponent: StatusEffectComponent is not set.");
return;
}
StatusEffectComponent.EffectApplied += OnEffectApplied;
StatusEffectComponent.EffectRemoved += OnEffectRemoved;
}
public override void _Process(double delta)
{
if (!_shouldDealDamage || _data == null || Health == null) return;
_timeElapsed += delta;
if (_timeElapsed >= 1f)
{
Health.DecreaseHealth(_data.DamagePerSecond);
_timeElapsed = 0f;
}
}
private void OnEffectApplied(StatusEffect statusEffect)
{
if (statusEffect.EffectData.Type != StatusEffectType.Fire) return;
_data = statusEffect.EffectData;
_shouldDealDamage = true;
FireFX?.SetEmitting(true);
}
private void OnEffectRemoved(StatusEffectType type)
{
if (type != StatusEffectType.Fire) return;
_shouldDealDamage = false;
_data = null;
FireFX?.SetEmitting(false);
}
}

View File

@@ -0,0 +1,83 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class FlashingComponent : Node
{
[Export] public Node2D Sprite { get; set; }
[Export] public float FlashDuration { get; set; } = 0.5f;
[Export] public float FlashTime { get; set; } = 0.1f;
[Export] public bool UseModulate { get; set; } = true;
[Export] public HealthComponent HealthComponent { get; set; }
private Tween _tween;
public override void _Ready()
{
if (HealthComponent != null)
{
HealthComponent.HealthChanged += OnHealthChanged;
HealthComponent.Death += OnDeath;
}
if (Sprite == null)
{
GD.PushError("FlashingComponent: Sprite node is not set.");
return;
}
}
public void StartFlashing()
{
if (Sprite == null) return;
_tween?.Kill();
_tween = CreateTween();
_tween.SetParallel(true);
var flashes = (int)(FlashDuration / FlashTime);
for (var i = 0; i < flashes; i++)
{
if (UseModulate)
{
var opacity = i % 2 == 0 ? 1.0f : 0.3f;
_tween.TweenProperty(Sprite, "modulate:a", opacity, FlashTime);
}
else
{
var visible = i % 2 == 0;
_tween.TweenProperty(Sprite, "visible", visible, FlashTime);
}
}
_tween.TweenCallback(Callable.From(StopFlashing));
}
public void StopFlashing()
{
if (UseModulate)
{
var modulateColor = Sprite.GetModulate();
modulateColor.A = 1.0f;
Sprite.SetModulate(modulateColor);
}
else
{
Sprite.SetVisible(true);
}
}
private void OnHealthChanged(float delta, float totalHealth)
{
if (delta < 0f)
{
StartFlashing();
}
}
private void OnDeath()
{
StopFlashing();
}
}

View File

@@ -0,0 +1,36 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class FlipComponent : Node2D
{
[Export] public Sprite2D LeftEye { get; set; }
[Export] public Sprite2D RightEye { get; set; }
[Export] public PlatformMovementComponent PlatformMovement { get; set; }
public override void _Process(double delta)
{
if (PlatformMovement == null) return;
var velocity = PlatformMovement.LastDirection;
switch (velocity.X)
{
case < 0f:
LeftEye.Frame = 1;
RightEye.Frame = 1;
LeftEye.FlipH = true;
RightEye.FlipH = true;
break;
case > 0f:
LeftEye.Frame = 1;
RightEye.Frame = 1;
LeftEye.FlipH = false;
RightEye.FlipH = false;
break;
default:
LeftEye.Frame = 0;
RightEye.Frame = 0;
break;
}
}
}

View File

@@ -0,0 +1,37 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class GravityMotionComponent : Node2D
{
[Export] public CharacterBody2D Body { get; set; }
[Export] public LaunchComponent LaunchComponent { get; set; }
[Export] public Vector2 Gravity { get; set; } = new Vector2(0, 1000f);
[Export] public Vector2 TargetDirection { get; set; } = Vector2.Up;
private Vector2 _velocity = Vector2.Zero;
public override void _Ready()
{
if (LaunchComponent == null) return;
var direction = LaunchComponent.InitialDirection.X > 0f ? TargetDirection : new Vector2(-TargetDirection.X, TargetDirection.Y);
direction = direction.Normalized();
_velocity = direction * LaunchComponent.Speed;
}
public override void _PhysicsProcess(double delta)
{
if (Body == null) return;
_velocity += Gravity * (float)delta;
Body.Velocity = _velocity;
Body.MoveAndSlide();
if (_velocity.LengthSquared() > 0.01f)
{
Body.Rotation = _velocity.Angle();
}
}
}

View File

@@ -0,0 +1,48 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class HealComponent : Node
{
[Export] public GpuParticles2D HealFx { get; set; }
[Export] public CollectableComponent Collectable { get; set; }
public override void _Ready()
{
if (Collectable == null)
{
GD.PushError("HealComponent: Collectable is not set.");
return;
}
Collectable.Collected += OnCollected;
}
private void OnCollected(Variant amount, CollectableType type, Node2D body)
{
if (type != CollectableType.Health) return;
if (Collectable == null) return;
var healthComponent = body.GetNodeOrNull<HealthComponent>("HealthComponent");
if (healthComponent == null) return;
var value = amount.AsSingle();
healthComponent.IncreaseHealth(value);
if (HealFx != null)
{
PlayHealFx();
}
Owner.QueueFree();
}
private void PlayHealFx()
{
if (HealFx == null) return;
HealFx.Restart();
HealFx.Emitting = true;
}
}

View File

@@ -0,0 +1,65 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class HealthComponent : Node2D
{
[Export] public float Health { get; set; } = 1.0f;
[Export] public float MaxHealth { get; set; } = 1.0f;
[Export] public AudioStreamPlayer2D HurtSfx { get; set; }
[Export] public AudioStreamPlayer2D HealSfx { get; set; }
[Signal] public delegate void HealthChangedEventHandler(float delta, float totalHealth);
[Signal] public delegate void DeathEventHandler();
public void SetHealth(float newValue)
{
_ = ApplyHealthChange(newValue);
}
public void IncreaseHealth(float delta)
{
_ = ApplyHealthChange(Health + delta);
}
public void DecreaseHealth(float delta)
{
_ = ApplyHealthChange(Health - delta);
}
public float GetDelta(float newValue) => newValue - Health;
private async Task ApplyHealthChange(float newHealth, bool playSfx = true)
{
newHealth = Mathf.Clamp(newHealth, 0.0f, MaxHealth);
var delta = newHealth - Health;
if (delta == 0.0f)
return;
if (playSfx)
{
if (delta > 0f && HealSfx != null)
{
HealSfx.Play();
}
else if (delta < 0f && HurtSfx != null)
{
HurtSfx.Play();
await HurtSfx.ToSignal(HurtSfx, AudioStreamPlayer2D.SignalName.Finished);
}
}
Health = newHealth;
if (Health <= 0f)
{
EmitSignalDeath();
}
else
{
EmitSignalHealthChanged(delta, Health);
}
}
}

View File

@@ -0,0 +1,75 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class HitComponent : Node
{
[Export] public Sprite2D Sprite { get; set; }
[Export] public HealthComponent Health { get; set; }
[Export] public float HitDuration { get; set; } = 0.1f;
[Export] public GpuParticles2D HitFx { get; set; }
[Export] public bool FlashMode { get; set; } = true;
public override void _Ready()
{
if (Health != null)
{
Health.HealthChanged += OnHealthChange;
Health.Death += OnDeath;
}
if (Sprite == null)
{
GD.PushError("HitComponent: Sprite is null");
return;
}
if (Sprite.Material != null && FlashMode)
{
Sprite.Material = (Material)Sprite.Material.Duplicate();
}
}
private void Activate()
{
if (!FlashMode) return;
Sprite.SetInstanceShaderParameter("enabled", true);
}
private void Deactivate()
{
if (!FlashMode) return;
Sprite.SetInstanceShaderParameter("enabled", false);
}
private async void OnHealthChange(float delta, float totalHealth)
{
if (!(delta < 0f)) return;
Activate();
await ToSignal(GetTree().CreateTimer(HitDuration), Timer.SignalName.Timeout);
Deactivate();
if (totalHealth > 0f && delta < 0f)
{
HandleHitFx();
}
}
private async void OnDeath()
{
Activate();
await ToSignal(GetTree().CreateTimer(HitDuration), Timer.SignalName.Timeout);
Deactivate();
}
private void HandleHitFx()
{
if (HitFx == null) return;
HitFx.Restart();
HitFx.Emitting = true;
}
}

View File

@@ -0,0 +1,64 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class HomingMissileMotionComponent : Node
{
[Export] public LaunchComponent Launch { get; set; }
[Export] public float MaxSpeed { get; set; } = 16f;
[Export] public float Acceleration { get; set; } = 8f;
[Export] public float MaxTurnRate { get; set; } = 180f; // degrees per second
[Export] public float WobbleStrength { get; set; } = 5f; // degrees
[Export] public float Drag { get; set; } = 0.98f;
[Export] public float SteeringLerp { get; set; } = 0.05f; // low = sluggish, high = responsive
[Export] public Area2D DetectionArea { get; set; }
private Vector2 _steeringDirection = Vector2.Zero;
private Vector2 _velocity = Vector2.Zero;
private Node2D _target = null;
public override void _Ready()
{
DetectionArea.BodyEntered += OnBodyEntered;
_velocity = Launch.GetInitialVelocity();
}
public override void _PhysicsProcess(double delta)
{
if (Launch == null) return;
if (Owner is not Node2D owner) return;
if (_target == null)
{
owner.Position += _velocity * (float)delta;
return;
}
var toTarget = (_target.GlobalPosition - owner.GlobalPosition).Normalized();
_steeringDirection = _steeringDirection.Lerp(toTarget, SteeringLerp);
var angleToTarget = _velocity.AngleTo(_steeringDirection);
var maxAngle = Mathf.DegToRad(MaxTurnRate) * (float)delta;
var clampedAngle = Mathf.Clamp(angleToTarget, -maxAngle, maxAngle);
var rng = new RandomNumberGenerator();
var wobble = Mathf.DegToRad(rng.RandfRange(-WobbleStrength, WobbleStrength));
clampedAngle += wobble;
_velocity = _velocity.Rotated(clampedAngle);
_velocity *= Drag;
var desiredSpeed = Mathf.Min(MaxSpeed, _velocity.Length() + Acceleration * (float)delta);
_velocity = _velocity.Normalized() * desiredSpeed;
owner.Position += _velocity * (float)delta;
owner.Rotation = _velocity.Angle();
}
private void OnBodyEntered(Node2D body)
{
if (_target != null) return;
if (body == null) return;
_target = body;
}
}

View File

@@ -0,0 +1,70 @@
using Godot;
using Godot.Collections;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class IceEffectComponent : Node
{
[Export] public Array<Node> ComponentsToDisable { get; set; } = [];
[Export] public StatusEffectComponent StatusEffectComponent { get; set; }
[Export] public Node2D IceFx { get; set; }
private StatusEffectDataResource _data = null;
private int _iceEffectsApplied = 0;
public override void _Ready()
{
StatusEffectComponent.EffectApplied += OnEffectApplied;
StatusEffectComponent.EffectRemoved += OnEffectRemoved;
}
private void OnEffectApplied(StatusEffect statusEffect)
{
if (statusEffect.EffectData.Type != StatusEffectType.Ice) return;
_data = statusEffect.EffectData;
_iceEffectsApplied++;
ApplyFreeze();
}
private void OnEffectRemoved(StatusEffectType type)
{
if (type != StatusEffectType.Ice) return;
_data = null;
_iceEffectsApplied--;
RemoveFreeze();
}
private void ApplyFreeze()
{
if (IceFx != null)
{
IceFx.Visible = true;
}
foreach (var component in ComponentsToDisable)
{
if (component == null || _iceEffectsApplied == 0) continue;
component.ProcessMode = ProcessModeEnum.Disabled;
}
}
private void RemoveFreeze()
{
if (_iceEffectsApplied > 0) return;
if (IceFx != null)
{
IceFx.Visible = false;
}
foreach (var component in ComponentsToDisable)
{
if (component == null) continue;
component.ProcessMode = ProcessModeEnum.Inherit;
}
}
}

View File

@@ -0,0 +1,31 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class InvulnerabilityComponent : Node
{
[Export] public float Duration { get; set; } = 1f;
[Export] public FlashingComponent FlashingComponent { get; set; }
private bool _isInvulnerable = false;
public void Activate()
{
if (_isInvulnerable)
return;
_isInvulnerable = true;
FlashingComponent?.StartFlashing();
var timer = GetTree().CreateTimer(Duration);
timer.Timeout += Deactivate;
}
private void Deactivate()
{
_isInvulnerable = false;
FlashingComponent?.StopFlashing();
}
public bool IsInvulnerable() => _isInvulnerable;
}

View File

@@ -0,0 +1,39 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class JumpPadComponent : Node
{
[Export] public float JumpForce { get; set; } = 10f;
[Export] public Area2D Area { get; set; }
[Export] public Sprite2D Sprite { get; set; }
[Export] public int StartAnimationIndex { get; set; } = 0;
[Export] public float AnimationDuration { get; set; } = 0.5f;
public override void _Ready()
{
Area.BodyEntered += OnBodyEntered;
}
private void OnBodyEntered(Node2D body)
{
var canBeLaunched = body.GetNodeOrNull<CanBeLaunchedComponent>("CanBeLaunchedComponent");
if (canBeLaunched == null) return;
if (body is not PlayerController { CurrentMovement: PlatformMovementComponent movement }) return;
_ = HandleLaunchPadAnimation();
movement.Body.Velocity = new Vector2(movement.Body.Velocity.X, -JumpForce);
movement.JumpSfx?.Play();
}
private async Task HandleLaunchPadAnimation()
{
if (Sprite == null) return;
var timer = GetTree().CreateTimer(AnimationDuration);
Sprite.Frame = StartAnimationIndex + 1;
await ToSignal(timer, Timer.SignalName.Timeout);
Sprite.Frame = StartAnimationIndex;
}
}

View File

@@ -0,0 +1,21 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class KillPlayerOutOfScreenComponent : Node
{
[Export] public VisibleOnScreenNotifier2D ScreenNotifier { get; set; }
[Export] public HealthComponent HealthComponent { get; set; }
private const float Damage = 6000f;
public override void _Ready()
{
ScreenNotifier.ScreenExited += HandleOutOfScreen;
}
private void HandleOutOfScreen()
{
HealthComponent?.DecreaseHealth(Damage);
}
}

View File

@@ -0,0 +1,48 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class KnockbackComponent : Node
{
[Export] public CharacterBody2D Body { get; set; }
[Export] public float KnockbackForce { get; set; } = 25f;
[Export] public HealthComponent HealthComponent { get; set; }
private bool _knockbackMode = false;
private int _knockbackFrames = 0;
public override void _Ready()
{
HealthComponent.HealthChanged += OnHealthChanged;
}
public override void _Process(double delta)
{
if (_knockbackMode) _knockbackFrames++;
if (_knockbackFrames <= 1) return;
_knockbackMode = false;
_knockbackFrames = 0;
}
public override void _PhysicsProcess(double delta)
{
if (_knockbackMode) ApplyKnockback((float)delta);
}
private void OnHealthChanged(float delta, float totalHealth)
{
if (totalHealth <= 0f || delta >= 0f) return;
_knockbackMode = true;
}
private void ApplyKnockback(float delta)
{
var velocity = Body.Velocity.Normalized();
var knockbackDirection = new Vector2(Mathf.Sign(velocity.X), 0.4f);
var knockbackVector = -knockbackDirection * KnockbackForce * delta;
Body.Velocity += knockbackVector;
}
}

View File

@@ -0,0 +1,24 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class LaunchComponent : Node2D
{
[Export] public Vector2 InitialDirection { get; set; } = Vector2.Right;
[Export] public float Speed { get; set; } = 16f;
[Export] public Vector2 SpawnPosition { get; set; } = Vector2.Zero;
[Export] public float SpawnRotation { get; set; } = 0f;
public override void _Ready()
{
if (Owner is not Node2D root) return;
root.GlobalPosition = SpawnPosition;
root.GlobalRotation = SpawnRotation;
}
public Vector2 GetInitialVelocity()
{
return InitialDirection.Normalized() * Speed;
}
}

View File

@@ -0,0 +1,66 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class LeverComponent : Node
{
[Export] public Area2D Area { get; set; }
[Export] public Sprite2D Sprite { get; set; }
[Export] public int StartAnimationIndex { get; set; } = 0;
[Export] public float AnimationDuration { get; set; } = 0.5f;
[Export] public AudioStreamPlayer2D Sfx { get; set; }
[Signal]
public delegate void ActivatedEventHandler();
public override void _Ready()
{
if (Area == null)
{
GD.PushError("LeverComponent: Area is not set.");
return;
}
if (Sprite == null)
{
GD.PushError("LeverComponent: Sprite is not set.");
return;
}
Area.BodyEntered += OnBodyEntered;
Area.AreaEntered += OnAreaEntered;
}
private void OnAreaEntered(Area2D area)
{
HandleTriggerLogic(area);
}
private void OnBodyEntered(Node2D body)
{
HandleTriggerLogic(body);
}
private async Task Activate()
{
EmitSignalActivated();
Sfx?.Play();
Sprite.Frame = StartAnimationIndex + 1;
var timer = GetTree().CreateTimer(AnimationDuration);
await timer.ToSignal(timer, Timer.SignalName.Timeout);
Sprite.Frame = StartAnimationIndex;
}
private void HandleTriggerLogic(Node2D obj)
{
var triggerLever = obj.GetNodeOrNull<TriggerLeverComponent>("TriggerLeverComponent");
if (triggerLever == null)
{
GD.PushWarning("LeverComponent: TriggerLeverComponent not found in body.");
return;
}
_ = Activate();
}
}

View File

@@ -0,0 +1,27 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class LifetimeComponent : Node
{
[Export] public float LifeTime { get; set; } = 5.0f;
private Timer _lifetimeTimer;
public override void _Ready()
{
_lifetimeTimer = new Timer();
_lifetimeTimer.WaitTime = LifeTime;
_lifetimeTimer.OneShot = true;
_lifetimeTimer.Autostart = true;
_lifetimeTimer.Timeout += OnLifetimeTimeout;
AddChild(_lifetimeTimer);
_lifetimeTimer.Start();
}
private void OnLifetimeTimeout()
{
Owner.QueueFree();
}
}

View File

@@ -0,0 +1,78 @@
using System;
using Godot;
using Godot.Collections;
namespace Mr.BrickAdventures.scripts.components;
public partial class MagneticSkillComponent : Node
{
[Export] public Area2D MagneticArea { get; set; }
[Export] public float MagneticMoveDuration { get; set; } = 1.25f;
private Array<Node2D> _collectablesToPickUp = [];
public override void _Ready()
{
MagneticArea.AreaEntered += OnAreaEntered;
MagneticArea.BodyEntered += OnBodyEntered;
}
public override void _Process(double delta)
{
foreach (var collectable in _collectablesToPickUp)
{
if (!IsInstanceValid(collectable))
{
_collectablesToPickUp.Remove(collectable);
continue;
}
MoveCollectableToOwner(collectable);
}
}
private void OnBodyEntered(Node2D body)
{
if (!HasComponentInChildren(body, "Collectable")) return;
if (_collectablesToPickUp.Contains(body)) return;
_collectablesToPickUp.Add(body);
}
private void OnAreaEntered(Area2D area)
{
if (!HasComponentInChildren(area, "Collectable")) return;
if (_collectablesToPickUp.Contains(area)) return;
_collectablesToPickUp.Add(area);
}
private bool HasComponentInChildren(Node node, string componentName)
{
if (node == null) return false;
if (node.HasNode(componentName)) return true;
foreach (var child in node.GetChildren())
{
if (child is { } childNode && HasComponentInChildren(childNode, componentName))
{
return true;
}
}
return false;
}
private void MoveCollectableToOwner(Node2D collectable)
{
if (!IsInstanceValid(collectable)) return;
if (Owner is not Node2D root) return;
var direction = (root.GlobalPosition - collectable.GlobalPosition).Normalized();
var speed = direction.Length() / MagneticMoveDuration;
collectable.GlobalPosition += direction.Normalized() * speed;
}
}

View File

@@ -0,0 +1,18 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class OutOfScreenComponent : Node
{
[Export] public VisibleOnScreenNotifier2D VisibilityNotifier { get; set; }
public override void _Ready()
{
VisibilityNotifier.ScreenExited += OnScreenExited;
}
private void OnScreenExited()
{
Owner?.QueueFree();
}
}

View File

@@ -0,0 +1,71 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class PeriodicShootingComponent : Node
{
[Export] public PackedScene BulletScene { get; set; }
[Export] public float ShootInterval { get; set; } = 1.0f;
[Export] public Vector2 ShootDirection { get; set; } = Vector2.Right;
[Export] public SideToSideMovementComponent SideToSideMovement { get; set; }
[Export] public Node2D BulletSpawnRight { get; set; }
[Export] public Node2D BulletSpawnLeft { get; set; }
[Export] public float ShootingIntervalVariation { get; set; } = 0.0f;
private Timer _timer;
public override void _Ready()
{
SetupTimer();
}
public override void _Process(double delta)
{
if (SideToSideMovement == null) return;
ShootDirection = SideToSideMovement.Direction != Vector2.Zero ? SideToSideMovement.Direction : Vector2.Right;
}
private void SetupTimer()
{
_timer = new Timer();
_timer.WaitTime = GetShootInterval();
_timer.OneShot = false;
_timer.Autostart = true;
_timer.Timeout += OnTimerTimeout;
AddChild(_timer);
}
private void OnTimerTimeout()
{
Shoot();
_timer.Start();
}
private double GetShootInterval()
{
if (ShootingIntervalVariation == 0f) return ShootInterval;
var rng = new RandomNumberGenerator();
return ShootInterval + rng.RandfRange(-ShootingIntervalVariation, ShootingIntervalVariation);
}
private void Shoot()
{
if (ShootDirection == Vector2.Zero) return;
var root = Owner as Node2D;
var bulletInstance = BulletScene.Instantiate<Node2D>();
var launchComponent = bulletInstance.GetNodeOrNull<LaunchComponent>("LaunchComponent");
var spawnPosition = ShootDirection == Vector2.Right ? BulletSpawnRight.GlobalPosition : BulletSpawnLeft.GlobalPosition;
if (launchComponent != null)
{
launchComponent.InitialDirection = ShootDirection;
launchComponent.SpawnPosition = spawnPosition;
if (root != null) launchComponent.SpawnRotation = root.Rotation;
}
bulletInstance.Position = spawnPosition;
GetTree().CurrentScene.AddChild(bulletInstance);
}
}

View File

@@ -0,0 +1,171 @@
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlatformMovementComponent : Node2D, IMovement
{
[Export]
public float Speed { get; set; } = 300.0f;
[Export]
public float JumpHeight { get; set; } = 100f;
[Export]
public float JumpTimeToPeak { get; set; } = 0.5f;
[Export]
public float JumpTimeToDescent { get; set; } = 0.4f;
[Export]
public int CoyoteFrames { get; set; } = 6;
[Export]
public AudioStreamPlayer2D JumpSfx { get; set; }
[Export]
public Node2D RotationTarget { get; set; }
[Export]
public CharacterBody2D Body { get; set; }
private float _gravity;
private bool _wasLastFloor = false;
private bool _coyoteMode = false;
private Timer _coyoteTimer;
private Vector2 _lastDirection = new Vector2(1, 0);
private float _jumpVelocity;
private float _jumpGravity;
private float _fallGravity;
public Vector2 LastDirection => _lastDirection;
public override void _Ready()
{
base._Ready();
if (Body == null)
return;
_gravity = (float)ProjectSettings.GetSetting("physics/2d/default_gravity");
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
_coyoteTimer = new Timer
{
OneShot = true,
WaitTime = CoyoteFrames / 60.0f
};
_coyoteTimer.Timeout += OnCoyoteTimerTimeout;
AddChild(_coyoteTimer);
}
public string MovementType { get; } = "platform";
public bool Enabled { get; set; }
public Vector2 PreviousVelocity { get; set; }
public override void _Process(double delta)
{
base._Process(delta);
if (Body == null || !Enabled)
return;
if (Body.Velocity.X > 0.0f)
RotationTarget.Rotation = Mathf.DegToRad(-10);
else if (Body.Velocity.X < 0.0f)
RotationTarget.Rotation = Mathf.DegToRad(10);
else
RotationTarget.Rotation = 0;
CalculateJumpVars();
}
public override void _PhysicsProcess(double delta)
{
base._PhysicsProcess(delta);
if (Body == null || !Enabled)
return;
if (Body.IsOnFloor())
{
_wasLastFloor = true;
_coyoteMode = false; // Reset coyote mode when back on the floor
_coyoteTimer.Stop(); // Stop timer when grounded
}
else
{
if (_wasLastFloor) // Start coyote timer only once
{
_coyoteMode = true;
_coyoteTimer.Start();
}
_wasLastFloor = false;
}
if (!Body.IsOnFloor())
Body.Velocity += new Vector2(0, CalculateGravity()) * (float)delta;
if (Input.IsActionPressed("jump") && (Body.IsOnFloor() || _coyoteMode))
Jump();
if (Input.IsActionJustPressed("down"))
Body.Position += new Vector2(0, 1);
float direction = Input.GetAxis("left", "right");
if (direction != 0)
_lastDirection = HandleDirection(direction);
if (direction != 0)
Body.Velocity = new Vector2(direction * Speed, Body.Velocity.Y);
else
Body.Velocity = new Vector2(Mathf.MoveToward(Body.Velocity.X, 0, Speed), Body.Velocity.Y);
Body.MoveAndSlide();
}
private void Jump()
{
if (Body == null)
return;
Body.Velocity = new Vector2(Body.Velocity.X, _jumpVelocity);
_coyoteMode = false;
if (JumpSfx != null)
JumpSfx.Play();
}
private float CalculateGravity()
{
return Body.Velocity.Y < 0.0f ? _jumpGravity : _fallGravity;
}
private void OnCoyoteTimerTimeout()
{
_coyoteMode = false;
}
private Vector2 HandleDirection(float inputDir)
{
if (inputDir > 0)
return new Vector2(1, 0);
else if (inputDir < 0)
return new Vector2(-1, 0);
return _lastDirection;
}
public void OnShipEntered()
{
RotationTarget.Rotation = 0;
}
private void CalculateJumpVars()
{
_jumpVelocity = ((2.0f * JumpHeight) / JumpTimeToPeak) * -1.0f;
_jumpGravity = ((-2.0f * JumpHeight) / (JumpTimeToPeak * JumpTimeToPeak)) * -1.0f;
_fallGravity = ((-2.0f * JumpHeight) / (JumpTimeToDescent * JumpTimeToDescent)) * -1.0f;
}
}

View File

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

View File

@@ -0,0 +1,103 @@
using System.Collections.Generic;
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlayerController : Node2D
{
[Export]
public string DefaultMovementType { get; set; } = "platform";
[Export]
public Godot.Collections.Dictionary<string, NodePath> MovementTypes { get; set; }
[Export]
public Sprite2D ShipSprite { get; set; }
public IMovement CurrentMovement = null;
[Signal]
public delegate void MovementSwitchedEventHandler(string movementType);
public override void _Ready()
{
base._Ready();
foreach (var movementType in MovementTypes.Keys)
{
var movementNode = GetNodeOrNull(movementType);
if (movementNode is IMovement playerMovement)
{
playerMovement.Enabled = false;
}
}
SwitchMovement(DefaultMovementType);
}
public override void _UnhandledInput(InputEvent @event)
{
base._UnhandledInput(@event);
if (@event is InputEventKey inputEventKey && inputEventKey.IsActionPressed("switch_movement"))
{
var nextMovementType = GetNextMovementType();
SwitchMovement(nextMovementType);
}
}
private void SwitchMovement(string movementType)
{
if (CurrentMovement != null)
{
CurrentMovement.Enabled = false;
}
if (MovementTypes.TryGetValue(movementType, out var movement))
{
CurrentMovement = GetNodeOrNull<IMovement>(movement);
if (CurrentMovement == null)
{
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
return;
}
CurrentMovement.Enabled = true;
EmitSignalMovementSwitched(movementType);
}
else
{
GD.PushError($"Movement type '{movementType}' not found in MovementTypes.");
}
if (CurrentMovement == null)
{
GD.PushError("No current movement set after switching.");
}
}
private string GetNextMovementType()
{
var keys = new List<string>(MovementTypes.Keys);
var currentIndex = keys.IndexOf(CurrentMovement?.MovementType);
if (currentIndex == -1)
{
return DefaultMovementType;
}
currentIndex = (currentIndex + 1) % keys.Count;
return keys[currentIndex];
}
public void OnSpaceshipEntered()
{
SwitchMovement("ship");
ShipSprite.Visible = true;
}
public void OnSpaceshipExited()
{
SwitchMovement(DefaultMovementType);
ShipSprite.Visible = false;
}
}

View File

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

View File

@@ -0,0 +1,36 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
namespace Mr.BrickAdventures.scripts.components;
public partial class PlayerDeathComponent : Node2D
{
[Export] public AudioStreamPlayer2D DeathSfx { get; set; }
[Export] public PackedScene DeathEffect { get; set; }
[Export] public HealthComponent HealthComponent { get; set; }
[Export] public Vector2 EffectScale { get; set; } = new Vector2(1.5f, 1.5f);
private GameManager _gameManager;
public override void _Ready()
{
_gameManager = GetNode<GameManager>("/root/gameManager");
HealthComponent.Death += OnDeath;
}
private void OnDeath()
{
DeathSfx?.Play();
if (DeathEffect != null)
{
var effect = DeathEffect.Instantiate<Node2D>();
GetParent().AddChild(effect);
effect.GlobalPosition = GlobalPosition;
effect.Scale = EffectScale;
}
_gameManager.RemoveLives(1);
_gameManager.ResetCurrentSessionState();
}
}

View File

@@ -0,0 +1,65 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProgressiveDamageComponent : Node
{
[Export] public HealthComponent HealthComponent { get; set; }
[Export] public Sprite2D Sprite { get; set; }
[Export] public PlatformMovementComponent PlatformMovement { get; set; }
[Export] public float MinJumpHeight { get; set; } = 60f;
[Export] public float JumpReductionPercentage { get; set; } = 0.1f; // this is a percentage of the jump height per hit
private float _maxHealth;
private float _ogJumpHeight;
public override void _Ready()
{
_maxHealth = HealthComponent.MaxHealth;
HealthComponent.HealthChanged += OnHealthChanged;
if (PlatformMovement != null)
{
_ogJumpHeight = PlatformMovement.JumpHeight;
}
}
private void OnHealthChanged(float delta, float totalHealth)
{
var frame = GetDamageFrame();
if (frame < 0 || frame >= Sprite.GetHframes()) return;
Sprite.Frame = frame;
if (PlatformMovement != null)
{
PlatformMovement.JumpHeight = GetJumpHeight();
}
}
private int GetDamageFrame()
{
if (Sprite == null || HealthComponent == null) return 0;
var framesCount = Sprite.GetHframes();
if (framesCount == 0) return 0;
var currentHealth = HealthComponent.Health;
var healthRatio = currentHealth / _maxHealth;
return (int)(framesCount * (1f - healthRatio));
}
private float GetJumpHeight()
{
if (PlatformMovement == null) return 0f;
var jumpHeight = _ogJumpHeight;
if (jumpHeight <= 0f) return 0f;
var damageFrame = GetDamageFrame();
if (damageFrame < 0 || damageFrame >= Sprite.GetHframes()) return jumpHeight;
var reduction = JumpReductionPercentage * jumpHeight;
var calculatedJumpHeight = jumpHeight - (damageFrame * reduction);
return Mathf.Max(calculatedJumpHeight, MinJumpHeight);
}
}

View File

@@ -0,0 +1,26 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProjectileComponent : Node2D
{
[Export] public float Speed { get; set; } = 16f;
[Export] public float AngleDirection { get; set; }
[Export] public Vector2 SpawnPosition { get; set; } = Vector2.Zero;
[Export] public float SpawnRotation { get; set; } = 0f;
[Export] public CharacterBody2D Body { get; set; }
public override void _Ready()
{
GlobalPosition = SpawnPosition;
GlobalRotation = SpawnRotation;
}
public override void _PhysicsProcess(double delta)
{
if (Body == null) return;
Body.Velocity += new Vector2(0f, -Speed).Rotated(AngleDirection);
Body.MoveAndSlide();
}
}

View File

@@ -0,0 +1,37 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ProjectileInitParams
{
public Vector2 Position { get; set; } = Vector2.Zero;
public Vector2 Direction { get; set; } = Vector2.Right;
public float Rotation { get; set; } = 0f;
public float PowerMultiplier { get; set; } = 1f;
}
public partial class ProjectileInitComponent : Node
{
[Export] public LaunchComponent LaunchComponent { get; set; }
public void Initialize(ProjectileInitParams p)
{
var position = p.Position;
var direction = p.Direction;
var rotation = p.Rotation;
var power = p.PowerMultiplier;
if (Owner is Node2D root)
{
root.GlobalPosition = position;
root.GlobalRotation = rotation;
}
if (LaunchComponent == null) return;
LaunchComponent.InitialDirection = direction;
LaunchComponent.SpawnPosition = position;
LaunchComponent.SpawnRotation = rotation;
LaunchComponent.Speed *= power;
}
}

View File

@@ -0,0 +1,44 @@
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class RequirementComponent : Node
{
[Export] public CollectableType RequirementType { get; set; }
[Export] public int RequirementAmount { get; set; } = 1;
private int _currentAmount = 0;
private const string CollectableGroupName = "Collectables";
[Signal]
public delegate void RequirementMetEventHandler(CollectableType requirementType);
public override void _Ready()
{
var collectables = GetTree().GetNodesInGroup(CollectableGroupName);
foreach (var collectable in collectables)
{
var c = collectable.GetNodeOrNull<CollectableComponent>("CollectableComponent");
if (c != null && c.Data.Type == RequirementType)
{
c.Collected += OnCollected;
}
}
}
private void OnCollected(Variant amount, CollectableType type, Node2D body)
{
AddProgress(amount.As<int>());
}
private void AddProgress(int amount = 1)
{
_currentAmount += amount;
if (_currentAmount >= RequirementAmount)
{
EmitSignalRequirementMet(RequirementType);
}
}
}

View File

@@ -0,0 +1,42 @@
using Godot;
using Mr.BrickAdventures.Autoloads;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class ScoreComponent : Node
{
private GameManager _gameManager;
private const string CoinsGroupName = "Coins";
public override async void _Ready()
{
await ToSignal(GetTree(), SceneTree.SignalName.ProcessFrame);
_gameManager = GetNode<GameManager>("/root/GameManager");
if (_gameManager == null)
{
GD.PrintErr("GameManager not found in the scene tree.");
return;
}
var coins = GetTree().GetNodesInGroup("Coins");
foreach (var coin in coins)
{
var c = coin.GetNodeOrNull<CollectableComponent>("CollectableComponent");
if (c != null)
{
c.Collected += OnCollected;
}
}
}
private void OnCollected(Variant amount, CollectableType type, Node2D body)
{
if (type != CollectableType.Coin) return;
var coinAmount = amount.As<int>();
var currentCoins = (int)_gameManager.CurrentSessionState["coins_collected"];
_gameManager.CurrentSessionState["coins_collected"] = currentCoins + coinAmount;
}
}

View File

@@ -0,0 +1,37 @@
using Godot;
using Mr.BrickAdventures.scripts.interfaces;
namespace Mr.BrickAdventures.scripts.components;
public partial class ShipMovementComponent : Node, IMovement
{
[Export] public float MaxSpeed { get; set; } = 200f;
[Export] public float Acceleration { get; set; } = 100f;
[Export] public float Friction { get; set; } = 50f;
[Export] public CharacterBody2D Body { get; set; }
public string MovementType { get; } = "ship";
public bool Enabled { get; set; }
public Vector2 PreviousVelocity { get; set; }
private Vector2 _velocity = Vector2.Zero;
public Vector2 Velocity => _velocity;
public override void _PhysicsProcess(double delta)
{
if (Body == null || !Enabled) return;
var inputVector = new Vector2(
Input.GetActionStrength("right") - Input.GetActionStrength("left"),
Input.GetActionStrength("down") - Input.GetActionStrength("up")
).Normalized();
_velocity = inputVector != Vector2.Zero ? _velocity.MoveToward(inputVector * MaxSpeed, Acceleration * (float)delta) : _velocity.MoveToward(Vector2.Zero, Friction * (float)delta);
_velocity = _velocity.LimitLength(MaxSpeed);
Body.Velocity = _velocity;
PreviousVelocity = Body.Velocity;
Body.MoveAndSlide();
}
}

View File

@@ -0,0 +1,59 @@
using System.Threading.Tasks;
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class ShipShooterComponent : Node
{
[Export] public PackedScene BulletScene { get; set; }
[Export] public float FireRate { get; set; } = 0.2f;
[Export] public Marker2D BulletSpawn { get; set; }
[Export] public AudioStreamPlayer2D ShootSfx { get; set; }
private bool _canShoot = false;
public override void _Ready()
{
SetProcess(false);
}
public override void _Process(double delta)
{
if (Input.IsActionJustPressed("attack") && _canShoot)
{
_ = Shoot();
}
}
private async Task Shoot()
{
if (!_canShoot) return;
var bullet = BulletScene.Instantiate<Node2D>();
var init = bullet.GetNodeOrNull<ProjectileInitComponent>("ProjectileInitComponent");
init?.Initialize(new ProjectileInitParams()
{
Position = BulletSpawn.GlobalPosition,
});
GetTree().CurrentScene.AddChild(bullet);
ShootSfx?.Play();
_canShoot = false;
await ToSignal(GetTree().CreateTimer(FireRate), Timer.SignalName.Timeout);
_canShoot = true;
}
private void OnShipEntered()
{
_canShoot = true;
SetProcess(true);
}
private void OnShipExited()
{
_canShoot = false;
SetProcess(false);
ShootSfx?.Stop();
}
}

View File

@@ -0,0 +1,117 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class SideToSideMovementComponent : Node
{
[Export] public Sprite2D Sprite { get; set; }
[Export] public float Speed { get; set; } = 10.0f;
[Export] public float WaitTime { get; set; } = 1.0f;
[Export] public RayCast2D LeftRay { get; set; }
[Export] public RayCast2D RightRay { get; set; }
[Export] public RayCast2D LeftWallRay { get; set; }
[Export] public RayCast2D RightWallRay { get; set; }
private Vector2 _direction = Vector2.Left;
private Vector2 _newDirection = Vector2.Left;
private Timer _timer;
private bool _triggeredDirectionChange = false;
[Signal]
public delegate void DirectionChangedEventHandler();
public Vector2 Direction => _direction;
public override void _Ready()
{
SetupTimer();
DirectionChanged += OnDirectionChanged;
}
public override void _PhysicsProcess(double delta)
{
HandleDirection();
HandleSpriteFlip();
HandleMovement(delta);
}
private void HandleDirection()
{
// Check if we are colliding with the left wall
if (LeftWallRay.IsColliding())
{
_newDirection = Vector2.Right;
EmitSignalDirectionChanged();
return;
}
// Check if we are colliding with the right wall
if (RightWallRay.IsColliding())
{
_newDirection = Vector2.Left;
EmitSignalDirectionChanged();
return;
}
// We are not colliding with anything, which means we don't have ground to walk on. Stop moving.
if (!LeftRay.IsColliding() && !RightRay.IsColliding())
{
_newDirection = Vector2.Zero;
return;
}
// If the left ray is not colliding and the right ray is colliding, that means we have ground to the right and we should change direction to the right.
if (!LeftRay.IsColliding() && RightRay.IsColliding())
{
_newDirection = Vector2.Right;
EmitSignalDirectionChanged();
return;
}
// If the right ray is not colliding and the left ray is colliding, that means we have ground to the left and we should change direction to the left.
if (!RightRay.IsColliding() && LeftRay.IsColliding())
{
_newDirection = Vector2.Left;
EmitSignalDirectionChanged();
return;
}
}
private void HandleSpriteFlip()
{
Sprite.FlipH = _direction == Vector2.Left;
}
private void HandleMovement(double delta)
{
var root = Owner as Node2D;
if (root == null) return;
root.Position += _direction * Speed * (float)delta;
}
private void OnDirectionChanged()
{
if (_direction == _newDirection || _triggeredDirectionChange)
return;
_triggeredDirectionChange = true;
_direction = Vector2.Zero;
_timer.Start();
}
private void OnTimerTimeout()
{
_timer.Stop();
_direction = _newDirection;
_triggeredDirectionChange = false;
}
private void SetupTimer()
{
_timer = new Timer();
AddChild(_timer);
_timer.WaitTime = WaitTime;
_timer.OneShot = true;
_timer.Timeout += OnTimerTimeout;
}
}

View File

@@ -0,0 +1,55 @@
using System.Collections.Generic;
using Godot;
using Mr.BrickAdventures.scripts.Resources;
namespace Mr.BrickAdventures.scripts.components;
public partial class StatusEffect : GodotObject
{
public StatusEffectDataResource EffectData { get; set; }
public float ElapsedTime { get; set; }
public Timer Timer { get; set; }
}
public partial class StatusEffectComponent : Node
{
private List<StatusEffect> _activeEffects = [];
[Signal] public delegate void EffectAppliedEventHandler(StatusEffect statusEffect);
[Signal] public delegate void EffectRemovedEventHandler(StatusEffectType type);
public void ApplyEffect(StatusEffectDataResource effectData)
{
var data = effectData.Duplicate() as StatusEffectDataResource;
var timer = CreateTimer(effectData.Duration, data);
var statusEffect = new StatusEffect
{
EffectData = data,
ElapsedTime = 0f,
Timer = timer
};
_activeEffects.Add(statusEffect);
EmitSignalEffectApplied(statusEffect);
}
public void RemoveEffect(StatusEffectType type)
{
var effectToRemove = _activeEffects.Find(effect => effect.EffectData.Type == type);
if (effectToRemove.EffectData == null) return;
_activeEffects.Remove(effectToRemove);
effectToRemove.Timer.QueueFree();
EmitSignalEffectRemoved(type);
}
private Timer CreateTimer(float duration, StatusEffectDataResource effectData)
{
var timer = new Timer();
timer.SetWaitTime(duration);
timer.SetOneShot(true);
timer.SetAutostart(true);
timer.Timeout += () => RemoveEffect(effectData.Type);
AddChild(timer);
return timer;
}
}

View File

@@ -0,0 +1,8 @@
using Godot;
namespace Mr.BrickAdventures.scripts.components;
public partial class TriggerLeverComponent : Node
{
}

View File

@@ -1,4 +1,4 @@
class_name BeamComponent
# class_name BeamComponent
extends Node2D
@export var expansion_speed: float = 16.0

View File

@@ -1,118 +0,0 @@
class_name PlatformMovement
extends PlayerMovement
@export var speed: float = 300.0
@export var jump_height: float = 100
@export var jump_time_to_peak: float = 0.5
@export var jump_time_to_descent: float = 0.4
@export var coyote_frames: int = 6
@export var jump_sfx: AudioStreamPlayer2D
@export var rotation_target: Node2D
@export var body: CharacterBody2D
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var was_last_floor := false
var coyote_mode := false
var coyote_timer: Timer
var last_direction := Vector2.RIGHT
@onready var jump_velocity: float = ((2.0 * jump_height) / jump_time_to_peak) * -1.0
@onready var jump_gravity: float = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
@onready var fall_gravity: float = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0
func _ready() -> void:
if not body:
return
coyote_timer = Timer.new()
coyote_timer.one_shot = true
coyote_timer.wait_time = coyote_frames / 60.0
coyote_timer.timeout.connect(on_coyote_timer_timeout)
add_child(coyote_timer)
func _process(_delta: float) -> void:
if not body or not enabled:
return
if body.velocity.x > 0.0:
rotation_target.rotation = deg_to_rad(-10)
elif body.velocity.x < 0.0:
rotation_target.rotation = deg_to_rad(10)
else:
rotation_target.rotation = 0
calculate_jump_vars()
func _physics_process(delta) -> void:
if not body or not enabled:
return
if body.is_on_floor():
was_last_floor = true
coyote_mode = false # Reset coyote mode when back on the floor
coyote_timer.stop() # Stop timer when grounded
else:
if was_last_floor: # Start coyote timer only once
coyote_mode = true
coyote_timer.start()
was_last_floor = false
if not body.is_on_floor():
body.velocity.y += calculate_gravity() * delta
if Input.is_action_pressed("jump") and (body.is_on_floor() or coyote_mode):
jump()
if Input.is_action_just_pressed("down"):
body.position.y += 1
var direction := Input.get_axis("left", "right")
if direction != 0:
last_direction = handle_direction(direction)
if direction:
body.velocity.x = direction * speed
else:
body.velocity.x = move_toward(body.velocity.x, 0, speed)
previous_velocity = body.velocity
body.move_and_slide()
func jump() -> void:
if not body:
return
body.velocity.y = jump_velocity
coyote_mode = false
if jump_sfx:
jump_sfx.play()
func calculate_gravity() -> float:
return jump_gravity if body.velocity.y < 0.0 else fall_gravity
func on_coyote_timer_timeout() -> void:
coyote_mode = false
func handle_direction(input_dir: float) -> Vector2:
if input_dir > 0:
return Vector2.RIGHT
elif input_dir < 0:
return Vector2.LEFT
return last_direction
func on_ship_entered() -> void:
rotation_target.rotation = 0
func calculate_jump_vars() -> void:
jump_velocity = ((2.0 * jump_height) / jump_time_to_peak) * -1.0
jump_gravity = ((-2.0 * jump_height) / (jump_time_to_peak * jump_time_to_peak)) * -1.0
fall_gravity = ((-2.0 * jump_height) / (jump_time_to_descent * jump_time_to_descent)) * -1.0

View File

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

View File

@@ -0,0 +1,13 @@
using Godot;
namespace Mr.BrickAdventures.scripts.interfaces;
public interface IMovement
{
string MovementType { get; }
bool Enabled { get; set; }
Vector2 PreviousVelocity { get; set; }
void _Process(double delta);
void _PhysicsProcess(double delta);
}

View File

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

View File

@@ -13,7 +13,6 @@ extends Node
@onready var game_manager: GM = $"/root/GameManager"
var unlock_buttons: Array[Button] = []
var skill_buttons: Array[SkillButton] = []
@@ -36,6 +35,14 @@ func _ready() -> void:
skill_unlocker.skill_unlocked.connect(on_skill_unlocked)
func _process(_delta: float) -> void:
for btn in skill_buttons:
if not btn.skill_data:
continue
if btn.skill_data.is_active:
btn.activate()
else:
btn.deactivate()
func _input(event: InputEvent) -> void:
if event.is_action_pressed("show_marketplace"):
@@ -53,7 +60,6 @@ func get_button_text(skill: SkillData) -> String:
return tr(skill.name) + " " + str(skill.cost)
func create_upgrade_button(skill: SkillData) -> void:
var button := marketplace_button.instantiate() as MarketplaceButton
button.text = get_button_text(skill)
@@ -102,17 +108,14 @@ func _on_button_pressed(skill: SkillData) -> void:
func on_skill_unlocked(skill: SkillData) -> void:
# need to fix this method
if not skill:
return
if skill_buttons.size() == 0:
create_skill_button(skill)
for button in skill_buttons:
if button.skill_data.is_active:
button.activate()
else:
button.deactivate()
for btn in skill_buttons:
if btn.skill_data and btn.skill_data.name == skill.name:
return
create_skill_button(skill)
func on_skill_button_pressed(button: SkillButton) -> void:
@@ -120,8 +123,4 @@ func on_skill_button_pressed(button: SkillButton) -> void:
return
skill_unlocker.skill_manager.toggle_skill_activation(button.skill_data)
button.activate()
for other_button in skill_buttons:
if other_button != button:
other_button.deactivate()