Add initial implementation of game mechanics and resources management

This commit is contained in:
2025-08-23 00:38:46 +02:00
commit e12acb0238
91 changed files with 2018 additions and 0 deletions

View File

@@ -0,0 +1,29 @@
using System;
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class AddResourceEffect : Effect
{
[Export] public ResourceType TargetResource { get; set; }
[Export] public double Value { get; set; }
public override void Execute(GameState state)
{
switch (TargetResource)
{
case ResourceType.Faith:
state.Faith += Value;
break;
case ResourceType.Followers:
state.Followers += (long)Value;
break;
case ResourceType.Corruption:
state.Corruption += Value;
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}

View File

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

View File

@@ -0,0 +1,24 @@
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class ApplyBuffEffect : Effect
{
public enum BuffTarget { FaithGeneration }
[Export] public BuffTarget TargetStat { get; set; }
[Export] public float Multiplier { get; set; } = 2.0f;
[Export] public double Duration { get; set; } = 30.0;
public override void Execute(GameState gameState)
{
var newBuff = new Buff
{
Multiplier = Multiplier,
Duration = Duration
};
gameState.ActiveBuffs.Add(newBuff);
}
}

View File

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

View File

@@ -0,0 +1,7 @@
namespace ParasiticGod.Scripts.Core.Effects;
public class Buff
{
public float Multiplier { get; set; } = 1.0f;
public double Duration { get; set; }
}

View File

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

View File

@@ -0,0 +1,36 @@
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class ConvertResourceEffect : Effect
{
[Export] public ResourceType FromResource { get; set; }
[Export] public double FromAmount { get; set; }
[Export] public ResourceType ToResource { get; set; }
[Export] public double ToAmount { get; set; }
public override void Execute(GameState gameState)
{
double GetValue(ResourceType type) => type switch {
ResourceType.Faith => gameState.Faith,
ResourceType.Followers => gameState.Followers,
ResourceType.Corruption => gameState.Corruption,
_ => 0
};
void SetValue(ResourceType type, double value) {
switch(type) {
case ResourceType.Faith: gameState.Faith = value; break;
case ResourceType.Followers: gameState.Followers = (long)value; break;
case ResourceType.Corruption: gameState.Corruption = value; break;
}
}
if (GetValue(FromResource) >= FromAmount)
{
SetValue(FromResource, GetValue(FromResource) - FromAmount);
SetValue(ToResource, GetValue(ToResource) + ToAmount);
}
}
}

View File

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

View File

@@ -0,0 +1,11 @@
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class DestroySelfEffect : Effect
{
public override void Execute(GameState gameState)
{
}
}

View File

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

View File

@@ -0,0 +1,8 @@
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
public abstract partial class Effect : Resource
{
public abstract void Execute(GameState gameState);
}

View File

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

View File

@@ -0,0 +1,30 @@
using Godot;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class ModifyStatEffect : Effect
{
public enum Stat { FaithPerFollower }
public enum Operation { Add, Multiply }
[Export] public Stat TargetStat { get; set; }
[Export] public Operation Op { get; set; }
[Export] public double Value { get; set; }
public override void Execute(GameState gameState)
{
if (TargetStat == Stat.FaithPerFollower)
{
switch (Op)
{
case Operation.Add:
gameState.FaithPerFollower += Value;
break;
case Operation.Multiply:
gameState.FaithPerFollower *= Value;
break;
}
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
namespace ParasiticGod.Scripts.Core.Effects;
public enum ResourceType { Faith, Followers, Corruption }

View File

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

View File

@@ -0,0 +1,16 @@
using Godot;
using Godot.Collections;
namespace ParasiticGod.Scripts.Core.Effects;
[GlobalClass]
public partial class UnlockMiracleEffect : Effect
{
[Export]
public Array<string> MiraclesToUnlock { get; set; }
public override void Execute(GameState gameState)
{
}
}

View File

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

43
Scripts/Core/GameLogic.cs Normal file
View File

@@ -0,0 +1,43 @@
using System;
namespace ParasiticGod.Scripts.Core;
public class GameLogic
{
public void UpdateGameState(GameState state, double delta)
{
state.Faith += state.FaithPerSecond * delta;
for (var i = state.ActiveBuffs.Count - 1; i >= 0; i--)
{
var buff = state.ActiveBuffs[i];
buff.Duration -= delta;
if (buff.Duration <= 0)
{
state.ActiveBuffs.RemoveAt(i);
}
}
}
public bool TryToPerformMiracle(GameState state, MiracleDefinition miracle)
{
if (state.Faith < miracle.FaithCost || state.Followers < miracle.FollowersRequired)
{
return false;
}
state.Faith -= miracle.FaithCost;
if (miracle.Effects != null)
{
foreach (var effect in miracle.Effects)
{
effect.Execute(state);
}
}
state.Corruption = Math.Clamp(state.Corruption, 0, 100);
return true;
}
}

View File

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

27
Scripts/Core/GameState.cs Normal file
View File

@@ -0,0 +1,27 @@
using System.Collections.Generic;
using ParasiticGod.Scripts.Core.Effects;
namespace ParasiticGod.Scripts.Core;
public class GameState
{
public double Faith { get; set; } = 50.0;
public double FaithPerFollower { get; set; } = 0.5;
public long Followers { get; set; } = 40;
public double Corruption { get; set; } = 0.0;
public List<Buff> ActiveBuffs { get; } = [];
public double FaithPerSecond
{
get
{
var totalMultiplier = 1.0;
foreach (var buff in ActiveBuffs)
{
totalMultiplier *= buff.Multiplier;
}
return Followers * FaithPerFollower * totalMultiplier;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,18 @@
using Godot;
using Godot.Collections;
using ParasiticGod.Scripts.Core.Effects;
namespace ParasiticGod.Scripts.Core;
[GlobalClass]
public partial class MiracleDefinition : Resource
{
public string Id { get; set; }
public bool UnlockedByDefault { get; set; }
[Export] public string Name { get; set; }
[Export] public double FaithCost { get; set; }
[Export] public long FollowersRequired { get; set; }
[Export] public Array<Effect> Effects { get; set; }
}

View File

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

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using ParasiticGod.Scripts.Core.Effects;
namespace ParasiticGod.Scripts.Core;
public class EffectDto
{
public string Type { get; set; }
// --- For "AddResource" Effect ---
public ResourceType TargetResource { get; set; }
public double Value { get; set; }
// --- For "ApplyBuff" Effect ---
public ApplyBuffEffect.BuffTarget TargetBuffStat { get; set; }
public float Multiplier { get; set; }
public double Duration { get; set; }
// --- For "ConvertResource" Effect ---
public ResourceType FromResource { get; set; }
public double FromAmount { get; set; }
public ResourceType ToResource { get; set; }
public double ToAmount { get; set; }
// --- For "ModifyStat" Effect ---
public ModifyStatEffect.Stat TargetStat { get; set; }
public ModifyStatEffect.Operation Op { get; set; }
public List<string> MiraclesToUnlock { get; set; }
}
public class MiracleDto
{
public string Name { get; set; }
public double FaithCost { get; set; }
public long FollowersRequired { get; set; }
public bool UnlockedByDefault { get; set; }
public List<EffectDto> Effects { get; set; }
}

View File

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

View File

@@ -0,0 +1,113 @@
using System;
using System.Text.Json;
using Godot;
using Godot.Collections;
using Newtonsoft.Json;
using ParasiticGod.Scripts.Core.Effects;
namespace ParasiticGod.Scripts.Core;
public static class MiracleLoader
{
private static readonly System.Collections.Generic.Dictionary<string, Type> EffectRegistry = new()
{
{ "AddResource", typeof(AddResourceEffect) },
{ "ApplyBuff", typeof(ApplyBuffEffect) },
{ "ConvertResource", typeof(ConvertResourceEffect) },
{ "ModifyStat", typeof(ModifyStatEffect) },
{ "UnlockMiracle", typeof(UnlockMiracleEffect) },
{ "DestroySelf", typeof(DestroySelfEffect) }
};
public static System.Collections.Generic.Dictionary<string, MiracleDefinition> LoadMiraclesFromDirectory(string path)
{
var loadedMiracles = new System.Collections.Generic.Dictionary<string, MiracleDefinition>();
using var dir = DirAccess.Open(path);
if (dir == null) return loadedMiracles;
dir.ListDirBegin();
var fileName = dir.GetNext();
while (!string.IsNullOrEmpty(fileName))
{
if (!dir.CurrentIsDir() && fileName.EndsWith(".json"))
{
var filePath = path.PathJoin(fileName);
var fileNameNoExt = fileName.GetBaseName();
var miracle = LoadMiracleFromFile(filePath, fileNameNoExt); // Pass the ID
if (miracle != null)
{
loadedMiracles.Add(fileNameNoExt, miracle);
}
}
fileName = dir.GetNext();
}
GD.Print($"Loaded {loadedMiracles.Count} miracles from {path}");
return loadedMiracles;
}
private static MiracleDefinition LoadMiracleFromFile(string filePath, string miracleId)
{
var fileContent = FileAccess.GetFileAsString(filePath);
if (string.IsNullOrEmpty(fileContent))
{
GD.PushError($"Failed to read file or file is empty: {filePath}");
return null;
}
var miracleDto = JsonConvert.DeserializeObject<MiracleDto>(fileContent);
if (miracleDto == null)
{
GD.PushError($"Failed to deserialize miracle JSON: {filePath}");
return null;
}
var miracleDef = new MiracleDefinition
{
Id = miracleId,
UnlockedByDefault = miracleDto.UnlockedByDefault,
Name = miracleDto.Name,
FaithCost = miracleDto.FaithCost,
FollowersRequired = miracleDto.FollowersRequired,
Effects = []
};
foreach (var effectDto in miracleDto.Effects)
{
if (EffectRegistry.TryGetValue(effectDto.Type, out var effectType))
{
var effectInstance = (Effect)Activator.CreateInstance(effectType);
switch (effectInstance)
{
case AddResourceEffect addResourceEffect:
addResourceEffect.TargetResource = effectDto.TargetResource;
addResourceEffect.Value = effectDto.Value;
break;
case ApplyBuffEffect applyBuffEffect:
applyBuffEffect.TargetStat = effectDto.TargetBuffStat;
applyBuffEffect.Multiplier = effectDto.Multiplier;
applyBuffEffect.Duration = effectDto.Duration;
break;
case ConvertResourceEffect convertResourceEffect:
convertResourceEffect.FromResource = effectDto.FromResource;
convertResourceEffect.FromAmount = effectDto.FromAmount;
convertResourceEffect.ToResource = effectDto.ToResource;
convertResourceEffect.ToAmount = effectDto.ToAmount;
break;
case ModifyStatEffect modifyStatEffect:
modifyStatEffect.TargetStat = effectDto.TargetStat;
modifyStatEffect.Op = effectDto.Op;
modifyStatEffect.Value = effectDto.Value;
break;
case UnlockMiracleEffect unlockMiracleEffect:
unlockMiracleEffect.MiraclesToUnlock = new Array<string>(effectDto.MiraclesToUnlock);
break;
}
miracleDef.Effects.Add(effectInstance);
}
}
return miracleDef;
}
}

View File

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

View File

@@ -0,0 +1,10 @@
using Godot;
namespace ParasiticGod.Scripts.Core;
[GlobalClass]
public partial class TierDefinition : Resource
{
[Export] public PackedScene Scene { get; private set; }
[Export] public long Threshold { get; private set; }
}

View File

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

10
Scripts/Follower.cs Normal file
View File

@@ -0,0 +1,10 @@
using Godot;
namespace ParasiticGod.Scripts;
[GlobalClass]
public partial class Follower : Node2D
{
public enum FollowerTier { Tier1, Tier2, Tier3, Tier4, Tier5 }
[Export] public FollowerTier Tier { get; private set; }
}

1
Scripts/Follower.cs.uid Normal file
View File

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

27
Scripts/FollowerMarker.cs Normal file
View File

@@ -0,0 +1,27 @@
using Godot;
namespace ParasiticGod.Scripts;
[GlobalClass]
public partial class FollowerMarker : Marker2D
{
public bool IsOccupied { get; private set; }
public Follower FollowerInstance { get; private set; }
public void PlaceFollower(Follower followerInstance)
{
if (IsOccupied) return;
AddChild(followerInstance);
followerInstance.Position = Vector2.Zero;
IsOccupied = true;
FollowerInstance = followerInstance;
}
public void RemoveFollower()
{
if (!IsOccupied) return;
FollowerInstance.QueueFree();
FollowerInstance = null;
IsOccupied = false;
}
}

View File

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

View File

@@ -0,0 +1,116 @@
using System.Collections.Generic;
using Godot;
using Godot.Collections;
using ParasiticGod.Scripts.Core;
using ParasiticGod.Scripts.Singletons;
namespace ParasiticGod.Scripts;
[GlobalClass]
public partial class PopulationVisualizer : Node
{
[Export] private Node2D _markersContainer;
[Export] private int _unitsPerMarker = 5;
[Export] private Array<TierDefinition> _tiers;
private readonly List<FollowerMarker> _markers = [];
private long _lastKnownUnitCount = -1;
private int _lastKnownTierIndex = -1;
private bool _isUpdating = false;
public override void _Ready()
{
foreach (var child in _markersContainer.GetChildren())
{
if (child is FollowerMarker marker)
{
_markers.Add(marker);
}
}
GameBus.Instance.StateChanged += OnStateChanged;
}
public override void _ExitTree()
{
GameBus.Instance.StateChanged -= OnStateChanged;
}
private void OnStateChanged(GameState newState)
{
if (_isUpdating) return;
var currentUnitCount = newState.Followers;
var currentMarkersToShow = (int)currentUnitCount / _unitsPerMarker;
var lastMarkersToShow = (int)_lastKnownUnitCount / _unitsPerMarker;
var newTierIndex = GetTierIndex(currentUnitCount);
if (currentMarkersToShow != lastMarkersToShow || newTierIndex != _lastKnownTierIndex)
{
UpdateVisualsProgressively(currentUnitCount, newTierIndex);
}
}
private int GetTierIndex(long currentUnitCount)
{
for (var i = _tiers.Count - 1; i >= 0; i--)
{
if (currentUnitCount >= _tiers[i].Threshold)
{
return i;
}
}
return -1;
}
private async void UpdateVisualsProgressively(long currentUnitCount, int newTierIndex)
{
_isUpdating = true;
if (newTierIndex < 0)
{
_isUpdating = false;
return;
}
var followersToShow = (int)currentUnitCount / _unitsPerMarker;
var currentTier = _tiers[newTierIndex];
for (var i = 0; i < _markers.Count; i++)
{
var marker = _markers[i];
var needsChange = false;
if (i < followersToShow)
{
// Note: The 'Follower' script would need a way to know its tier index or resource path
// to do a perfect comparison. For now, we'll just check for occupancy.
if (!marker.IsOccupied || _lastKnownTierIndex != newTierIndex)
{
if (marker.IsOccupied) marker.RemoveFollower();
var followerInstance = currentTier.Scene.Instantiate<Follower>();
marker.PlaceFollower(followerInstance);
needsChange = true;
}
}
else
{
if (marker.IsOccupied)
{
marker.RemoveFollower();
needsChange = true;
}
}
if (needsChange)
{
await ToSignal(GetTree().CreateTimer(0.1f), SceneTreeTimer.SignalName.Timeout);
}
}
_lastKnownUnitCount = currentUnitCount;
_lastKnownTierIndex = newTierIndex;
_isUpdating = false;
}
}

View File

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

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using Godot;
using ParasiticGod.Scripts.Core;
using ParasiticGod.Scripts.Core.Effects;
namespace ParasiticGod.Scripts.Singletons;
public partial class GameBus : Node
{
public static GameBus Instance { get; private set; }
public Dictionary<string, MiracleDefinition> AllMiracles { get; private set; }
private readonly GameState _gameState = new();
private readonly GameLogic _gameLogic = new();
public event Action<GameState> StateChanged;
public event Action<MiracleDefinition> MiraclePerformed;
public event Action<List<MiracleDefinition>> MiraclesUnlocked;
public event Action<MiracleDefinition> MiracleCompleted;
public override void _EnterTree()
{
Instance = this;
AllMiracles = MiracleLoader.LoadMiraclesFromDirectory("user://Mods/Miracles");
}
public override void _ExitTree()
{
Instance = null;
}
public override void _Process(double delta)
{
_gameLogic.UpdateGameState(_gameState, delta);
StateChanged?.Invoke(_gameState);
if (_gameState.Corruption >= 100)
{
GD.Print("The world has died!");
GetTree().Quit(); // For now
}
}
public void PerformMiracle(MiracleDefinition miracle)
{
if (_gameLogic.TryToPerformMiracle(_gameState, miracle))
{
MiraclePerformed?.Invoke(miracle);
var miraclesToUnlock = new List<MiracleDefinition>();
foreach (var effect in miracle.Effects)
{
if (effect is UnlockMiracleEffect unlockEffect)
{
foreach (var id in unlockEffect.MiraclesToUnlock)
{
if (AllMiracles.TryGetValue(id, out var def))
{
miraclesToUnlock.Add(def);
}
}
}
else if (effect is DestroySelfEffect)
{
MiracleCompleted?.Invoke(miracle);
}
}
if (miraclesToUnlock.Count > 0)
{
MiraclesUnlocked?.Invoke(miraclesToUnlock);
}
}
}
}

View File

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