feature/chunk-meshing
feature/inventory-system
fix/chunk-boundary-seam
refactor/stat-modifier-api
test/damage-calculator
Atomic commits. One feature = one branch. Use imperative mood.
# DO
Add greedy mesh builder with face culling
Fix chunk boundary seam artifacts in meshing
Refactor StatContainer to use dirty-flag caching
Add unit tests for LootTable weighted random
# DON'T
Updated stuff
WIP
fix
Changes to meshing and lighting and AI and UI
mainis always buildable — never push broken code- Every feature branch is based on
main - Rebase onto
mainbefore merging (no merge commits) - Delete branches after merge
Before committing any code, verify:
- Solution builds without errors:
dotnet build MineRPG.sln - All tests pass:
dotnet test src/MineRPG.Tests/MineRPG.Tests.csproj - No new warnings (TreatWarningsAsErrors is enabled)
- New public types in pure projects (Core, RPG, World, Entities, Network) have corresponding test files
- File names match type names exactly
- Namespaces mirror folder structure
- No hardcoded data — everything in
Data/files - No
GD.Print()— use logging system - No
GetNode()with string paths — use[Export] - No business logic in Godot bridge nodes
- No cross-project dependency violations
- Create a JSON file in
Data/Blocks/(e.g.,obsidian.json):
{
"id": 42,
"name": "Obsidian",
"flags": ["Solid"],
"hardness": 50.0,
"atlasCoords": { "x": 5, "y": 3 },
"requiredTool": "diamond_pickaxe",
"lootTableRef": "obsidian_loot"
}-
Add the block's texture to the atlas at the specified coords (
Assets/Textures/Atlas/) -
If the block has special interaction behavior, add an
IBlockInteractionimplementation inMineRPG.World/Blocks/ -
No other code changes needed — the block is automatically loaded by
BlockRegistryat startup
- Create a JSON file in
Data/Items/(e.g.,iron_sword.json):
{
"id": 101,
"name": "Iron Sword",
"type": "Weapon",
"rarity": "Common",
"maxStack": 1,
"stats": {
"attackDamage": 6,
"attackSpeed": 1.6
},
"durability": 250,
"equipmentSlot": "MainHand"
}-
Add the item icon to
Assets/Textures/Items/ -
If the item has unique effects, create an effect class in
MineRPG.RPG/Items/ -
If the item is craftable, add a recipe in
Data/Recipes/
- Create a JSON file in
Data/Mobs/(e.g.,skeleton.json):
{
"id": 10,
"name": "Skeleton",
"health": 20,
"damage": 4,
"speed": 1.2,
"aiPreset": "hostile_melee",
"lootTableRef": "skeleton_loot",
"modelKey": "skeleton",
"spawnRules": {
"biomes": ["plains", "forest"],
"minLightLevel": 0,
"maxLightLevel": 7,
"timeOfDay": "night"
}
}-
Create the mob model/animations in
Assets/Models/ -
Create the Godot scene in
Scenes/Entities/usingMobNodeas root -
If the mob needs custom AI behaviors, add them in
MineRPG.Entities/AI/Actions/
Follow these steps to add a system (e.g., a weather system):
- Pure logic (weather state, transitions, effects on gameplay) →
MineRPG.Core,MineRPG.RPG,MineRPG.World, orMineRPG.Entitiesdepending on the domain - Godot rendering (particles, sky, lighting changes) →
MineRPG.Godot.WorldorMineRPG.Godot.UI
Place the interface in the same project as its implementation. Only put interfaces in MineRPG.Core/Interfaces/ if they have zero domain-specific types (e.g., ITickable, ISaveable). If the interface references domain types (e.g., WeatherState), it belongs in the domain project.
// In MineRPG.World/Weather/ (same project as the implementation)
public interface IWeatherSystem : ITickable
{
WeatherState CurrentWeather { get; }
void TransitionTo(WeatherType type, float duration);
}// In MineRPG.World/Weather/ (or appropriate pure project)
namespace MineRPG.World.Weather;
public sealed class WeatherSystem : IWeatherSystem
{
private readonly IEventBus _eventBus;
public WeatherSystem(IEventBus eventBus)
{
_eventBus = eventBus;
}
public void Tick(float deltaTime)
{
// Pure logic: state transitions, timers, gameplay effects
}
}// In MineRPG.Godot.World/
namespace MineRPG.Godot.World;
public partial class WeatherNode : Node3D
{
private IWeatherSystem _weatherSystem = null!;
public override void _Process(double delta)
{
// Render weather effects based on _weatherSystem.CurrentWeather
}
}In Bootstrap/CompositionRoot.cs, register the new system in the DI container:
// In Bootstrap/CompositionRoot.cs
var weatherSystem = new WeatherSystem(eventBus);
serviceLocator.Register<IWeatherSystem>(weatherSystem);// In MineRPG.Core/Events/GameEvents.cs
public readonly struct WeatherChangedEvent
{
public WeatherType OldWeather { get; init; }
public WeatherType NewWeather { get; init; }
}// In MineRPG.Tests/World/WeatherSystemTests.cs
public class WeatherSystemTests
{
[Fact]
public void TransitionTo_WithValidType_ChangesCurrentWeather() { }
[Fact]
public void Tick_WhenTransitionComplete_PublishesWeatherChangedEvent() { }
}- Every public method in pure projects (Core, RPG, World, Entities, Network)
- Every data transformation (damage calculation, stat modifiers, loot generation)
- Every state transition (quest states, AI states, chunk states)
- Edge cases (empty inventory, zero health, max level, full stack)
- Godot bridge nodes (these are thin wrappers, tested manually)
- Private methods (test through public API)
- Data loading (test the logic, not the file I/O)
Mirror the source structure:
Source: src/MineRPG.RPG/Combat/DamageCalculator.cs
Test: src/MineRPG.Tests/RPG/DamageCalculatorTests.cs
Source: src/MineRPG.World/Chunks/ChunkData.cs
Test: src/MineRPG.Tests/World/ChunkDataTests.cs
MethodName_Condition_ExpectedResult
public void Calculate_WithCriticalHit_ReturnsDoubledDamage() { }
public void AddItem_WhenInventoryFull_ReturnsFalse() { }
public void GetBlock_WithOutOfBoundsCoords_ThrowsArgumentException() { }Use FluentAssertions exclusively. Never use raw Assert.*.
// Equality
result.Should().Be(42);
// Collections
items.Should().HaveCount(3);
items.Should().ContainSingle(i => i.Rarity == ItemRarity.Legendary);
items.Should().BeInAscendingOrder(i => i.Id);
// Exceptions
action.Should().Throw<InvalidOperationException>()
.WithMessage("*already registered*");
// Booleans
isAlive.Should().BeTrue();Use NSubstitute for all mocks and stubs.
var eventBus = Substitute.For<IEventBus>();
var registry = Substitute.For<IRegistry<ushort, BlockDefinition>>();
registry.TryGet(Arg.Any<ushort>(), out Arg.Any<BlockDefinition>())
.Returns(x =>
{
x[1] = new BlockDefinition { Id = 1, Name = "Stone" };
return true;
});
// Verify calls
eventBus.Received(1).Publish(Arg.Any<BlockMinedEvent>());- Profile after implementing each major system — do not wait until the end
- Use the Godot Profiler (Debugger → Profiler) for frame analysis
- Monitor: FPS, draw calls, vertices, physics ticks, memory
- Target minimum: 60 FPS stable with render distance 12 chunks and 50+ active entities
- Test on modest hardware, not only high-end GPUs
The in-game debug overlay (toggled with F3) must display:
- Current FPS
- Loaded chunks count
- Active entities count
- Memory usage
- Draw calls
- Current biome
- Player position (chunk + world coords)
These settings apply to ALL projects in the solution. Do not modify without team discussion:
<Nullable>enable</Nullable>— nullable reference types enabled<TreatWarningsAsErrors>true</TreatWarningsAsErrors>— all warnings are errors<LangVersion>latest</LangVersion>— use latest C# features<AllowUnsafeBlocks>false</AllowUnsafeBlocks>— unsafe disabled by default (only enabled in MineRPG.World)
- File-scoped namespaces: error severity (not suggestion)
- Allman brace style on all constructs
- Interface prefix
I: error severity - PascalCase for types: error severity
_camelCasefor private fields: warning severity (promoted to error byTreatWarningsAsErrors)- 4-space indentation for
.csfiles - 2-space indentation for
.csproj,.json,.yamlfiles