add initial project files and configurations, including EventBus, systems, and resources

This commit is contained in:
2026-01-24 02:47:23 +01:00
commit bba82f64fd
110 changed files with 2735 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
public abstract partial class BaseLiftSystem : Node
{
[Export] protected float PowerPerClick = 5f;
[Export] protected float Gravity = 2f;
[Export] protected float TargetValue = 100f; // Distance OR Time
protected float CurrentProgress = 0f;
protected bool IsLiftComplete = false;
protected int ActiveHazardCount = 0;
public override void _Ready()
{
EventBus.OnLiftEffortApplied += ApplyPower;
EventBus.OnHazardSpawned += OnHazardSpawned;
EventBus.OnHazardResolved += OnHazardResolved;
EventBus.OnLiftCompleted += OnLiftCompleted;
}
public override void _ExitTree()
{
EventBus.OnLiftEffortApplied -= ApplyPower;
EventBus.OnHazardSpawned -= OnHazardSpawned;
EventBus.OnHazardResolved -= OnHazardResolved;
EventBus.OnLiftCompleted -= OnLiftCompleted;
}
public virtual void Initialize(float target, float gravity)
{
TargetValue = target;
Gravity = gravity;
CurrentProgress = 0f;
IsLiftComplete = false;
ActiveHazardCount = 0;
}
private void OnLiftCompleted(bool success)
{
IsLiftComplete = true;
}
private void OnHazardResolved(HazardType _)
{
ActiveHazardCount--;
if (ActiveHazardCount < 0) ActiveHazardCount = 0;
}
private void OnHazardSpawned(HazardType _)
{
ActiveHazardCount++;
}
private void ApplyPower(float effortDelta)
{
if (IsLiftComplete || ActiveHazardCount > 0) return;
HandleEffort(effortDelta);
}
protected abstract void HandleEffort(float effortDelta);
public abstract override void _Process(double delta);
}

View File

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

View File

@@ -0,0 +1,38 @@
using Godot;
using MaxEffort.Code.Core;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class BenchPressSystem : BaseLiftSystem
{
protected override void HandleEffort(float effortDelta)
{
CurrentProgress += PowerPerClick * effortDelta;
CheckWin();
}
public override void _Process(double delta)
{
if (IsLiftComplete) return;
if (CurrentProgress > 0)
{
CurrentProgress -= Gravity * (float)delta;
CurrentProgress = Mathf.Max(0, CurrentProgress);
}
var ratio = CurrentProgress / TargetValue;
EventBus.PublishLiftProgress(ratio);
EventBus.PublishLiftVisualHeight(ratio);
}
private void CheckWin()
{
if (CurrentProgress >= TargetValue)
{
EventBus.PublishLiftCompleted(true);
}
}
}

View File

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

View File

@@ -0,0 +1,72 @@
using Godot;
using MaxEffort.Code.Core;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class CameraShakeSystem : Node
{
[Export] private Camera2D _camera;
[Export] private float _decayRate = 0.8f; // How fast shaking stops
[Export] private float _maxOffset = 20.0f; // Max pixels to shake
[Export] private float _maxRoll = 0.1f; // Max rotation (radians)
private float _trauma = 0f; // Current shake intensity (0 to 1)
private float _currentFocus = 0f;
private RandomNumberGenerator _rng = new();
public override void _Ready()
{
EventBus.OnCameraTrauma += AddTrauma;
EventBus.OnFocusChanged += OnFocusChanged;
}
public override void _ExitTree()
{
EventBus.OnCameraTrauma -= AddTrauma;
EventBus.OnFocusChanged -= OnFocusChanged;
}
public override void _Process(double delta)
{
if (_camera == null) return;
if (_currentFocus > 0.75f)
{
AddTrauma(1.5f * (float)delta);
}
if (_trauma > 0)
{
_trauma -= _decayRate * (float)delta;
_trauma = Mathf.Max(0, _trauma);
var shake = _trauma * _trauma;
var offsetX = _maxOffset * shake * _rng.RandfRange(-1, 1);
var offsetY = _maxOffset * shake * _rng.RandfRange(-1, 1);
var rotation = _maxRoll * shake * _rng.RandfRange(-1, 1);
_camera.Offset = new Vector2(offsetX, offsetY);
_camera.Rotation = rotation;
}
else
{
if (_camera.Offset != Vector2.Zero)
{
_camera.Offset = _camera.Offset.Lerp(Vector2.Zero, (float)delta * 5f);
_camera.Rotation = Mathf.Lerp(_camera.Rotation, 0, (float)delta * 5f);
}
}
}
private void OnFocusChanged(float focus)
{
_currentFocus = focus;
}
private void AddTrauma(float amount)
{
_trauma = Mathf.Clamp(_trauma + amount, 0f, 1f);
}
}

View File

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

View File

@@ -0,0 +1,64 @@
using Godot;
using MaxEffort.Code.Core;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class DeadliftSystem : BaseLiftSystem
{
[Export] private float _barHeight = 100f; // Visual height of the lift
[Export] private float _holdZoneThreshold = 0.8f; // Must be above 80% to count
[Export] private Node2D _barVisual;
[Export] private Vector2 _startPos;
[Export] private Vector2 _endPos;
private float _currentBarHeight = 0f;
private float _holdTimer = 0f;
public override void Initialize(float targetTime, float gravity)
{
base.Initialize(targetTime, gravity);
_holdTimer = 0f;
}
protected override void HandleEffort(float effortDelta)
{
_currentBarHeight += PowerPerClick * effortDelta;
_currentBarHeight = Mathf.Min(_currentBarHeight, _barHeight);
}
public override void _Process(double delta)
{
if (IsLiftComplete) return;
var dt = (float)delta;
if (_currentBarHeight > 0)
{
_currentBarHeight -= Gravity * dt;
_currentBarHeight = Mathf.Max(0, _currentBarHeight);
}
var visualRatio = _currentBarHeight / _barHeight; // 0.0 to 1.0 (Height)
if (visualRatio >= _holdZoneThreshold && ActiveHazardCount == 0)
{
_holdTimer += dt;
EventBus.PublishCameraTrauma(0.15f * dt);
}
EventBus.PublishLiftVisualHeight(visualRatio);
EventBus.PublishLiftProgress(_holdTimer / TargetValue);
if (_barVisual != null)
{
_barVisual.Position = _startPos.Lerp(_endPos, visualRatio);
}
if (_holdTimer >= TargetValue)
{
EventBus.PublishCameraTrauma(1.0f);
EventBus.PublishLiftCompleted(true);
}
}
}

View File

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

View File

@@ -0,0 +1,96 @@
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class GameManager : Node
{
[Export] private Godot.Collections.Array<DayConfig> _days;
[Export] private HazardSystem _hazardSystem;
[Export] private Node _minigameContainer;
[Export] private Control _winScreen;
[Export] private Control _loseScreen;
[Export] private Label _dayLabel;
private int _currentDayIndex = 0;
private Node _currentMiniGame;
public override void _Ready()
{
EventBus.OnLiftCompleted += HandleLiftResult;
StartDay(_currentDayIndex);
}
public override void _ExitTree()
{
EventBus.OnLiftCompleted -= HandleLiftResult;
}
private void StartDay(int index)
{
if (index >= _days.Count)
{
GD.Print("YOU BEAT THE GYM! ALL DAYS COMPLETE.");
// TODO: Show "Game Complete" screen
return;
}
var config = _days[index];
_currentMiniGame?.QueueFree();
_hazardSystem.ClearHazards();
if (config.MiniGameScene != null)
{
_currentMiniGame = config.MiniGameScene.Instantiate();
_minigameContainer.AddChild(_currentMiniGame);
var liftSystem = _currentMiniGame.GetNode<BaseLiftSystem>("System");
liftSystem?.Initialize(config.TargetWeight, config.Gravity);
}
_hazardSystem.SetAvailableHazards(config.AvailableHazards);
if (_dayLabel != null) _dayLabel.Text = config.DayTitle;
if (_winScreen != null) _winScreen.Visible = false;
if (_loseScreen != null) _loseScreen.Visible = false;
GD.Print($"Started {config.DayTitle}");
}
private void HandleLiftResult(bool success)
{
if (success)
{
GD.Print("Day Complete! Next day loading...");
if (_winScreen != null) _winScreen.Visible = true;
// Wait for player input to proceed, or auto-load after delay
// For now, let's auto-advance after 3 seconds for the prototype
GetTree().CreateTimer(3.0f).Timeout += () => NextDay();
}
else
{
GD.Print("FAIL! Go home.");
if (_loseScreen != null) _loseScreen.Visible = true;
// Restart day after delay
GetTree().CreateTimer(3.0f).Timeout += () => RestartDay();
}
}
private void NextDay()
{
_currentDayIndex++;
StartDay(_currentDayIndex);
}
private void RestartDay()
{
GetTree().ReloadCurrentScene();
}
}

View File

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

View File

@@ -0,0 +1,115 @@
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class HazardController : Node2D
{
[Export] private AnimatedSprite2D _animSprite;
[Export] private Area2D _clickArea;
[Export] private CollisionShape2D _clickShape; // Reference to resize it
[Export] private Label _nameLabel; // Can still use UI nodes inside Node2D
private HazardDef _data;
private float _timeActive = 0f;
private bool _isResolved = false;
private int _currentHealth = 1;
public void Initialize(HazardDef data)
{
_data = data;
_currentHealth = data.ClicksToResolve;
if (_animSprite != null && data.Animations != null)
{
_animSprite.SpriteFrames = data.Animations;
var texture = data.Animations.GetFrameTexture(data.IdleAnimName, 0);
if (texture != null && _clickShape != null)
{
var rect = new RectangleShape2D();
rect.Size = texture.GetSize();
_clickShape.Shape = rect;
}
_animSprite.Play(data.IdleAnimName);
}
if (_nameLabel != null) _nameLabel.Text = data.DisplayName;
if (_clickArea != null)
{
_clickArea.InputEvent += OnInputEvent;
}
Scale = Vector2.Zero;
var tween = CreateTween();
tween.SetTrans(Tween.TransitionType.Back).SetEase(Tween.EaseType.Out);
tween.TweenProperty(this, "scale", Vector2.One, 0.4f);
if (!string.IsNullOrEmpty(data.WalkAnimName))
{
_animSprite?.Play(data.WalkAnimName);
tween.TweenCallback(Callable.From(() => _animSprite?.Play(data.IdleAnimName)));
}
}
public override void _Process(double delta)
{
if (_isResolved) return;
_timeActive += (float)delta;
if (_timeActive >= _data.TimeToFail)
{
EventBus.PublishLiftCompleted(false);
QueueFree();
}
}
public override void _ExitTree()
{
if (_clickArea != null) _clickArea.InputEvent -= OnInputEvent;
}
private void OnInputEvent(Node viewport, InputEvent @event, long shapeIdx)
{
if (_isResolved) return;
if (@event is InputEventMouseButton mouseEvent && mouseEvent.Pressed && mouseEvent.ButtonIndex == MouseButton.Left)
{
TakeDamage();
}
}
private void TakeDamage()
{
_currentHealth--;
var tween = CreateTween();
tween.TweenProperty(this, "scale", new Vector2(1.2f, 0.8f), 0.05f);
tween.TweenProperty(this, "scale", Vector2.One, 0.05f);
if (_animSprite != null)
{
_animSprite.Modulate = Colors.Red;
tween.Parallel().TweenProperty(_animSprite, "modulate", Colors.White, 0.1f);
}
if (_currentHealth <= 0)
{
Resolve();
}
}
private void Resolve()
{
_isResolved = true;
EventBus.PublishHazardResolved(_data.Type);
var tween = CreateTween();
tween.TweenProperty(this, "scale", Vector2.Zero, 0.2f);
tween.TweenCallback(Callable.From(QueueFree));
}
}

View File

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

View File

@@ -0,0 +1,114 @@
using System.Linq;
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class HazardSystem : Node
{
[Export] private Godot.Collections.Array<HazardDef> _possibleHazards;
[Export] private Godot.Collections.Array<Node2D> _spawnLocations;
[Export] private PackedScene _hazardPrefab; // The visual object to spawn
[Export] private float _checkInterval = 1.0f; // How often to try spawning
private float _currentFocus = 0f;
private double _timer = 0;
private RandomNumberGenerator _rng = new();
public override void _Ready()
{
EventBus.OnFocusChanged += OnFocusChanged;
EventBus.OnHazardResolved += OnHazardResolved;
}
public override void _ExitTree()
{
EventBus.OnFocusChanged -= OnFocusChanged;
EventBus.OnHazardResolved -= OnHazardResolved;
}
public override void _Process(double delta)
{
_timer += delta;
if (_timer >= _checkInterval)
{
_timer = 0;
TrySpawnHazard();
}
}
public void SetAvailableHazards(Godot.Collections.Array<HazardDef> hazards)
{
if (_possibleHazards == null) _possibleHazards = [];
_possibleHazards.Clear();
foreach(var h in hazards) _possibleHazards.Add(h);
}
public void ClearHazards()
{
if (_spawnLocations == null) return;
// Loop through all locations and destroy active hazards
foreach (var loc in _spawnLocations)
{
foreach (Node child in loc.GetChildren())
{
child.QueueFree();
}
}
_timer = 0;
}
private void OnFocusChanged(float focus)
{
_currentFocus = focus;
}
private void TrySpawnHazard()
{
if (_currentFocus < 0.2f) return;
var spawnChance = _currentFocus * 0.5f;
if (_rng.Randf() < spawnChance)
{
SpawnRandomHazard();
}
}
private void SpawnRandomHazard()
{
if (_possibleHazards.Count == 0 || _hazardPrefab == null || _spawnLocations.Count == 0) return;
var emptyLocations = _spawnLocations.Where(loc => loc.GetChildCount() == 0).ToArray();
if (emptyLocations.Length == 0) return;
var targetLoc = emptyLocations[_rng.Randi() % emptyLocations.Length];
var validHazards = _possibleHazards
.Where(h => h.MinFocusToSpawn <= _currentFocus)
.ToArray();
if (validHazards.Length == 0) return;
var selected = validHazards[_rng.Randi() % validHazards.Length];
if (_hazardPrefab.Instantiate() is HazardController instance)
{
instance.Position = Vector2.Zero;
targetLoc.AddChild(instance);
instance.Initialize(selected);
EventBus.PublishHazardSpawned(selected.Type);
}
}
private void OnHazardResolved(HazardType type)
{
GD.Print($"Resolved {type}!");
}
}

View File

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

View File

@@ -0,0 +1,23 @@
using Godot;
using MaxEffort.Code.Core;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class PlayerInputSystem : Node
{
private const string LiftAction = "lift_action";
public override void _Process(double delta)
{
if (Input.IsActionPressed(LiftAction))
{
EventBus.PublishLiftEffortApplied((float)delta);
}
if (Input.IsActionJustReleased(LiftAction))
{
EventBus.PublishFocusRelease();
}
}
}

View File

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

View File

@@ -0,0 +1,195 @@
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class SoundManager : Node
{
[Export] private SoundBank _bank;
[Export] private int _poolSize = 8;
private AudioStreamPlayer[] _sfxPool;
private AudioStreamPlayer _strainPlayer;
private AudioStreamPlayer _heartbeatPlayer;
private AudioStreamPlayer _musicPlayer;
private bool _isLifting = false;
private float _currentLiftProgress = 0f;
private int _masterBus;
private int _musicBus;
private int _sfxBus;
public override void _Ready()
{
_masterBus = AudioServer.GetBusIndex("Master");
_musicBus = AudioServer.GetBusIndex("Music");
_sfxBus = AudioServer.GetBusIndex("Sfx");
InitializePool();
EventBus.OnLiftEffortApplied += HandleLiftEffort;
EventBus.OnFocusRelease += HandleFocusRelease;
EventBus.OnFocusChanged += OnFocusChanged;
EventBus.OnLiftCompleted += HandleLiftComplete;
EventBus.OnLiftProgress += OnLiftProgress;
EventBus.OnHazardSpawned += OnOnHazardSpawned;
EventBus.OnHazardResolved += OnHazardResolved;
EventBus.OnCameraTrauma += OnCameraTrauma;
PlayMusic(_bank.GameMusic);
}
public override void _ExitTree()
{
EventBus.OnLiftEffortApplied -= HandleLiftEffort;
EventBus.OnFocusRelease -= HandleFocusRelease;
EventBus.OnLiftCompleted -= HandleLiftComplete;
EventBus.OnLiftProgress -= OnLiftProgress;
EventBus.OnHazardSpawned -= OnOnHazardSpawned;
EventBus.OnHazardResolved -= OnHazardResolved;
EventBus.OnCameraTrauma -= OnCameraTrauma;
}
public void PlayMusic(AudioStream clip)
{
if (clip == null || _musicPlayer.Stream == clip) return;
_musicPlayer.Stream = clip;
_musicPlayer.Play();
}
public void ToggleMasterMute(bool isMuted)
{
AudioServer.SetBusMute(_masterBus, isMuted);
}
public void ToggleMusicMute(bool isMuted)
{
AudioServer.SetBusMute(_musicBus, isMuted);
}
private void InitializePool()
{
_sfxPool = new AudioStreamPlayer[_poolSize];
for (var i = 0; i < _poolSize; i++)
{
var p = new AudioStreamPlayer();
p.Bus = "Sfx";
AddChild(p);
_sfxPool[i] = p;
}
_strainPlayer = new AudioStreamPlayer { Bus = "Sfx" };
AddChild(_strainPlayer);
_heartbeatPlayer = new AudioStreamPlayer { Bus = "Sfx" };
AddChild(_heartbeatPlayer);
_musicPlayer = new AudioStreamPlayer { Bus = "Music" };
AddChild(_musicPlayer);
if (_bank != null && _bank.HeartbeatLoop != null)
{
_heartbeatPlayer.Stream = _bank.HeartbeatLoop;
_heartbeatPlayer.VolumeDb = -80f;
_heartbeatPlayer.Play();
}
}
private void PlaySfx(AudioStream clip, float pitch = 1.0f)
{
if (clip == null) return;
AudioStreamPlayer bestCandidate = null;
var longestPlaybackPosition = -1f;
foreach (var player in _sfxPool)
{
if (!player.Playing)
{
bestCandidate = player;
break;
}
if (player.GetPlaybackPosition() > longestPlaybackPosition)
{
longestPlaybackPosition = player.GetPlaybackPosition();
bestCandidate = player;
}
}
if (bestCandidate != null)
{
bestCandidate.Stream = clip;
bestCandidate.PitchScale = pitch;
bestCandidate.Play();
}
}
private void HandleLiftEffort(float _)
{
if (!_isLifting)
{
_isLifting = true;
if (_bank.StrainLoop != null)
{
_strainPlayer.Stream = _bank.StrainLoop;
_strainPlayer.Play();
}
}
_strainPlayer.PitchScale = 1.0f + (_currentLiftProgress * 0.3f);
}
private void HandleFocusRelease()
{
if (_isLifting)
{
_isLifting = false;
_strainPlayer.Stop();
PlaySfx(_bank.EffortExhale);
}
}
private void HandleLiftComplete(bool success)
{
_strainPlayer.Stop();
PlaySfx(success ? _bank.WinStinger : _bank.FailStinger);
}
private void OnCameraTrauma(float amount)
{
if (amount > 0.5f) PlaySfx(_bank.CameraTrauma);
}
private void OnHazardResolved(HazardType _)
{
PlaySfx(_bank.HazardClear);
}
private void OnOnHazardSpawned(HazardType _)
{
PlaySfx(_bank.HazardSpawn);
}
private void OnLiftProgress(float progress)
{
_currentLiftProgress = progress;
}
private void OnFocusChanged(float focus)
{
if (focus < 0.1f)
{
_heartbeatPlayer.VolumeDb = -80f;
}
else
{
var t = (focus - 0.1f) / 0.9f;
_heartbeatPlayer.VolumeDb = Mathf.Lerp(-30f, 5f, t);
_heartbeatPlayer.PitchScale = Mathf.Lerp(1.0f, 1.4f, t);
}
}
}

View File

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

View File

@@ -0,0 +1,65 @@
using Godot;
using MaxEffort.Code.Core;
using MaxEffort.Code.Data;
namespace MaxEffort.Code.Systems;
[GlobalClass]
public partial class TunnelSystem : Node
{
[Export] private TunnelConfig _config;
[Export] private ColorRect _vignetteOverlay;
private float _currentFocus = 0f;
private bool _isEfforting = false;
public override void _Ready()
{
EventBus.OnLiftEffortApplied += HandleEffort;
EventBus.OnFocusRelease += HandleRelease;
}
public override void _ExitTree()
{
EventBus.OnLiftEffortApplied -= HandleEffort;
EventBus.OnFocusRelease -= HandleRelease;
}
public override void _Process(double delta)
{
var dt = (float)delta;
if (_isEfforting)
{
_currentFocus += _config.VisionNarrowRate * dt;
}
else
{
_currentFocus -= _config.VisionRecoverRate * dt;
}
_currentFocus = Mathf.Clamp(_currentFocus, 0f, _config.MaxTunnelIntensity);
var visualValue = _config.VisionCurve?.Sample(_currentFocus) ?? _currentFocus;
if (_vignetteOverlay != null)
{
var mat = _vignetteOverlay.Material as ShaderMaterial;
mat?.SetShaderParameter("vignette_intensity", visualValue);
}
EventBus.PublishFocusChanged(_currentFocus);
_isEfforting = false;
}
private void HandleRelease()
{
_isEfforting = false;
}
private void HandleEffort(float effortDelta)
{
_isEfforting = true;
}
}

View File

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