Strip Health, Mana, Food, Stamina, Oxygen, Energy — only Gold and Blood remain as they serve as adventure gates (Contemporary ≥30, DarkFantasy ≥20). Remove 22 orphaned items, 5 recipes, and the AlchemyTable workstation. Replace energy_cell in rocket_boots recipe with cosmic_shard. Change box_endgame condition from AllResourcesVisible to BoxesOpenedAbove:500. Add ItemUtilitySnapshot test that maps every item to its usage contexts (loot sources, crafting, interactions, adventures) and generates a report. DEBUG overwrites the snapshot; RELEASE asserts no changes. Update specifications.md and CLAUDE.md to reflect resource cleanup. Remove obsolete bugs.md and refactoring_plan.md.
711 lines
25 KiB
C#
711 lines
25 KiB
C#
using OpenTheBox.Core;
|
|
using OpenTheBox.Core.Crafting;
|
|
using OpenTheBox.Core.Enums;
|
|
using OpenTheBox.Core.Items;
|
|
using OpenTheBox.Data;
|
|
using OpenTheBox.Rendering.Panels;
|
|
using OpenTheBox.Simulation;
|
|
using OpenTheBox.Simulation.Events;
|
|
|
|
namespace OpenTheBox.Tests;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// CraftingEngine Tests
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
public class CraftingEngineTests
|
|
{
|
|
private static ContentRegistry CreateRegistryWithRecipe(
|
|
string recipeId = "refine_wood",
|
|
WorkstationType station = WorkstationType.Foundry,
|
|
string ingredientId = "material_wood_raw",
|
|
int ingredientQty = 2,
|
|
string resultId = "material_wood_refined",
|
|
int resultQty = 1)
|
|
{
|
|
var registry = new ContentRegistry();
|
|
|
|
registry.RegisterItem(new ItemDefinition(
|
|
ingredientId, $"item.{ingredientId}", ItemCategory.Material, ItemRarity.Common,
|
|
["Material"], MaterialType: MaterialType.Wood, MaterialForm: MaterialForm.Raw));
|
|
|
|
registry.RegisterItem(new ItemDefinition(
|
|
resultId, $"item.{resultId}", ItemCategory.Material, ItemRarity.Common,
|
|
["Material"], MaterialType: MaterialType.Wood, MaterialForm: MaterialForm.Refined));
|
|
|
|
registry.RegisterRecipe(new Recipe(
|
|
recipeId, $"recipe.{recipeId}", station,
|
|
[new RecipeIngredient(ingredientId, ingredientQty)],
|
|
new RecipeResult(resultId, resultQty)));
|
|
|
|
return registry;
|
|
}
|
|
|
|
private static GameState CreateState(bool craftingPanel = true, bool unlockFoundry = true)
|
|
{
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
if (craftingPanel)
|
|
state.UnlockedUIFeatures.Add(UIFeature.CraftingPanel);
|
|
if (unlockFoundry)
|
|
state.UnlockedWorkstations.Add(WorkstationType.Foundry);
|
|
return state;
|
|
}
|
|
|
|
// ── FindCraftableRecipes ──────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void FindCraftable_NoCraftingPanel_ReturnsEmpty()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState(craftingPanel: false);
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCraftable_NoWorkstation_ReturnsEmpty()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState(unlockFoundry: false);
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCraftable_InsufficientIngredients_ReturnsEmpty()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 1)); // Need 2
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCraftable_AllConditionsMet_ReturnsRecipe()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Single(result);
|
|
Assert.Equal("refine_wood", result[0].Id);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCraftable_WorkstationBusy_ReturnsEmpty()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
// Add an in-progress job using Foundry
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "some_other",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Empty(result);
|
|
}
|
|
|
|
[Fact]
|
|
public void FindCraftable_CompletedJobOnStation_AllowsNew()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
// A completed job doesn't block the station
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "some_other",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddHours(-2),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
});
|
|
|
|
var result = engine.FindCraftableRecipes(state, registry);
|
|
|
|
Assert.Single(result);
|
|
}
|
|
|
|
// ── StartCraftingJob ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void StartJob_ConsumesIngredients_CreatesJob()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var recipe = registry.Recipes["refine_wood"];
|
|
var events = engine.StartCraftingJob(recipe, state, registry);
|
|
|
|
// Ingredients consumed
|
|
Assert.Equal(0, state.CountItems("material_wood_raw"));
|
|
// Job created
|
|
Assert.Single(state.ActiveCraftingJobs);
|
|
Assert.Equal("refine_wood", state.ActiveCraftingJobs[0].RecipeId);
|
|
Assert.Equal(CraftingJobStatus.InProgress, state.ActiveCraftingJobs[0].Status);
|
|
// Events emitted
|
|
Assert.Contains(events, e => e is CraftingStartedEvent);
|
|
Assert.Contains(events, e => e is ItemConsumedEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void StartJob_DurationBasedOnResultRarity()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = new ContentRegistry();
|
|
registry.RegisterItem(new ItemDefinition(
|
|
"input", "item.input", ItemCategory.Material, ItemRarity.Common, ["Material"]));
|
|
registry.RegisterItem(new ItemDefinition(
|
|
"output", "item.output", ItemCategory.Material, ItemRarity.Rare, ["Material"]));
|
|
registry.RegisterRecipe(new Recipe(
|
|
"test", "recipe.test", WorkstationType.Foundry,
|
|
[new RecipeIngredient("input", 1)],
|
|
new RecipeResult("output", 1)));
|
|
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("input", 1));
|
|
|
|
engine.StartCraftingJob(registry.Recipes["test"], state, registry);
|
|
|
|
Assert.Equal(TimeSpan.FromSeconds(60), state.ActiveCraftingJobs[0].Duration);
|
|
}
|
|
|
|
// ── TickJobs ──────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void TickJobs_InProgressNotElapsed_NoEvent()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var state = CreateState();
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var events = engine.TickJobs(state);
|
|
|
|
Assert.Empty(events);
|
|
Assert.Equal(CraftingJobStatus.InProgress, state.ActiveCraftingJobs[0].Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void TickJobs_InProgressElapsed_MarksCompleted()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var state = CreateState();
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var events = engine.TickJobs(state);
|
|
|
|
Assert.Single(events);
|
|
Assert.IsType<CraftingCompletedEvent>(events[0]);
|
|
Assert.Equal(CraftingJobStatus.Completed, state.ActiveCraftingJobs[0].Status);
|
|
}
|
|
|
|
[Fact]
|
|
public void TickJobs_AlreadyCompleted_NoDoubleEvent()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var state = CreateState();
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
});
|
|
|
|
var events = engine.TickJobs(state);
|
|
|
|
Assert.Empty(events);
|
|
}
|
|
|
|
// ── CollectCompleted ──────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Collect_CompletedJob_CreatesResultItem()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "refine_wood",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
});
|
|
|
|
var events = engine.CollectCompleted(state, registry);
|
|
|
|
// Result item added
|
|
Assert.True(state.HasItem("material_wood_refined"));
|
|
// Job removed
|
|
Assert.Empty(state.ActiveCraftingJobs);
|
|
// Events: ItemReceivedEvent + CraftingCollectedEvent
|
|
Assert.Contains(events, e => e is ItemReceivedEvent);
|
|
Assert.Contains(events, e => e is CraftingCollectedEvent);
|
|
}
|
|
|
|
[Fact]
|
|
public void Collect_NoCompletedJobs_NoEvents()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
|
|
var events = engine.CollectCompleted(state, registry);
|
|
|
|
Assert.Empty(events);
|
|
}
|
|
|
|
[Fact]
|
|
public void Collect_InProgressJob_NotCollected()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "refine_wood",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var events = engine.CollectCompleted(state, registry);
|
|
|
|
Assert.Empty(events);
|
|
Assert.Single(state.ActiveCraftingJobs);
|
|
}
|
|
|
|
// ── AutoCraftCheck ────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void AutoCraft_MatchingRecipeExists_StartsJob()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var events = engine.AutoCraftCheck(state, registry);
|
|
|
|
Assert.Contains(events, e => e is CraftingStartedEvent);
|
|
Assert.Single(state.ActiveCraftingJobs);
|
|
Assert.Equal(0, state.CountItems("material_wood_raw"));
|
|
}
|
|
|
|
[Fact]
|
|
public void AutoCraft_NoMatchingRecipe_NoAction()
|
|
{
|
|
var engine = new CraftingEngine();
|
|
var registry = CreateRegistryWithRecipe();
|
|
var state = CreateState();
|
|
// No materials
|
|
|
|
var events = engine.AutoCraftCheck(state, registry);
|
|
|
|
Assert.Empty(events);
|
|
Assert.Empty(state.ActiveCraftingJobs);
|
|
}
|
|
|
|
// ── CraftingJob properties ────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void CraftingJob_IsComplete_TrueWhenTimeElapsed()
|
|
{
|
|
var job = new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
};
|
|
|
|
Assert.True(job.IsComplete);
|
|
}
|
|
|
|
[Fact]
|
|
public void CraftingJob_IsComplete_FalseWhenNotElapsed()
|
|
{
|
|
var job = new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
};
|
|
|
|
Assert.False(job.IsComplete);
|
|
}
|
|
|
|
[Fact]
|
|
public void CraftingJob_ProgressPercent_CompletedIs100()
|
|
{
|
|
var job = new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "test",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddMinutes(-5),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
};
|
|
|
|
Assert.Equal(100.0, job.ProgressPercent);
|
|
}
|
|
|
|
// ── GetCraftDuration ──────────────────────────────────────────────
|
|
|
|
[Theory]
|
|
[InlineData(ItemRarity.Common, 15)]
|
|
[InlineData(ItemRarity.Uncommon, 30)]
|
|
[InlineData(ItemRarity.Rare, 60)]
|
|
[InlineData(ItemRarity.Epic, 90)]
|
|
[InlineData(ItemRarity.Legendary, 120)]
|
|
[InlineData(ItemRarity.Mythic, 180)]
|
|
public void GetCraftDuration_CorrectPerRarity(ItemRarity rarity, int expectedSeconds)
|
|
{
|
|
Assert.Equal(TimeSpan.FromSeconds(expectedSeconds), CraftingEngine.GetCraftDuration(rarity));
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// CraftingPanel Tests
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
public class CraftingPanelTests
|
|
{
|
|
[Fact]
|
|
public void Render_EmptyJobs_ShowsEmptyMessage()
|
|
{
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
var output = RenderHelper.RenderToString(CraftingPanel.Render(state));
|
|
|
|
Assert.NotEmpty(output);
|
|
// Should contain the empty workshop message
|
|
Assert.Contains("active", output, StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_InProgressJob_ShowsProgressBar()
|
|
{
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "refine_wood",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var output = RenderHelper.RenderToString(CraftingPanel.Render(state));
|
|
|
|
Assert.NotEmpty(output);
|
|
Assert.Contains("Foundry", output);
|
|
Assert.Contains("%", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_CompletedJob_ShowsDone()
|
|
{
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "refine_wood",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
});
|
|
|
|
var output = RenderHelper.RenderToString(CraftingPanel.Render(state));
|
|
|
|
Assert.NotEmpty(output);
|
|
Assert.Contains("Foundry", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_WithRegistry_ResolvesRecipeName()
|
|
{
|
|
var registry = new ContentRegistry();
|
|
registry.RegisterRecipe(new Recipe(
|
|
"refine_wood", "recipe.refine_wood", WorkstationType.Foundry,
|
|
[new RecipeIngredient("material_wood_raw", 2)],
|
|
new RecipeResult("material_wood_refined", 1)));
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "refine_wood",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
|
|
var output = RenderHelper.RenderToString(CraftingPanel.Render(state, registry));
|
|
|
|
Assert.NotEmpty(output);
|
|
// Should show recipe name key (or localized version)
|
|
Assert.Contains("recipe.refine_wood", output);
|
|
}
|
|
|
|
[Fact]
|
|
public void Render_MixedJobs_DoesNotThrow()
|
|
{
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "job1",
|
|
Workstation = WorkstationType.Foundry,
|
|
StartedAt = DateTime.UtcNow,
|
|
Duration = TimeSpan.FromHours(1),
|
|
Status = CraftingJobStatus.InProgress
|
|
});
|
|
state.ActiveCraftingJobs.Add(new CraftingJob
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
RecipeId = "job2",
|
|
Workstation = WorkstationType.Furnace,
|
|
StartedAt = DateTime.UtcNow.AddSeconds(-10),
|
|
Duration = TimeSpan.FromSeconds(1),
|
|
Status = CraftingJobStatus.Completed
|
|
});
|
|
|
|
var output = RenderHelper.RenderToString(CraftingPanel.Render(state));
|
|
|
|
Assert.NotEmpty(output);
|
|
Assert.Contains("Foundry", output);
|
|
Assert.Contains("Furnace", output);
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// Recipe Loading Tests
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
public class RecipeLoadingTests
|
|
{
|
|
private static string ContentPath(string file) =>
|
|
Path.Combine("content", "data", file);
|
|
|
|
[Fact]
|
|
public void LoadRecipes_AllRecipesLoaded()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
Assert.NotEmpty(registry.Recipes);
|
|
// recipes.json has 18 recipes after removing alchemy/supply/energy/oxygen recipes
|
|
Assert.True(registry.Recipes.Count >= 15, $"Expected >= 15 recipes, got {registry.Recipes.Count}");
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_AllRecipesHaveIngredients()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
foreach (var recipe in registry.Recipes.Values)
|
|
{
|
|
Assert.NotEmpty(recipe.Ingredients);
|
|
foreach (var ingredient in recipe.Ingredients)
|
|
{
|
|
Assert.NotNull(ingredient.ItemDefinitionId);
|
|
Assert.True(ingredient.Quantity > 0, $"Recipe {recipe.Id} has ingredient with qty <= 0");
|
|
}
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_AllRecipesHaveResult()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
foreach (var recipe in registry.Recipes.Values)
|
|
{
|
|
Assert.NotNull(recipe.Result);
|
|
Assert.NotNull(recipe.Result.ItemDefinitionId);
|
|
Assert.True(recipe.Result.Quantity > 0, $"Recipe {recipe.Id} has result with qty <= 0");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_AllRecipesHaveValidWorkstation()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
var validStations = Enum.GetValues<WorkstationType>().ToHashSet();
|
|
|
|
foreach (var recipe in registry.Recipes.Values)
|
|
{
|
|
Assert.Contains(recipe.Workstation, validStations);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_NullPath_Succeeds()
|
|
{
|
|
// Without recipes path, registry should still work
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"));
|
|
|
|
Assert.Empty(registry.Recipes);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_AllBlueprintItemsExist()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
// Every blueprint item should have a WorkstationType set
|
|
var blueprints = registry.Items.Values
|
|
.Where(i => i.Tags.Contains("Blueprint"))
|
|
.ToList();
|
|
|
|
Assert.NotEmpty(blueprints);
|
|
foreach (var bp in blueprints)
|
|
{
|
|
Assert.True(bp.WorkstationType.HasValue, $"Blueprint {bp.Id} missing WorkstationType");
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void LoadRecipes_EachUsedWorkstation_HasBlueprint()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
var usedStations = registry.Recipes.Values
|
|
.Select(r => r.Workstation)
|
|
.Distinct()
|
|
.ToHashSet();
|
|
|
|
var blueprintStations = registry.Items.Values
|
|
.Where(i => i.WorkstationType.HasValue)
|
|
.Select(i => i.WorkstationType!.Value)
|
|
.ToHashSet();
|
|
|
|
foreach (var station in usedStations)
|
|
{
|
|
Assert.Contains(station, blueprintStations);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// GameSimulation Crafting Integration Tests
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
public class SimulationCraftingTests
|
|
{
|
|
private static string ContentPath(string file) =>
|
|
Path.Combine("content", "data", file);
|
|
|
|
[Fact]
|
|
public void OpenBox_WithMaterials_TriggerAutoCraft()
|
|
{
|
|
var registry = ContentRegistry.LoadFromFiles(
|
|
ContentPath("items.json"),
|
|
ContentPath("boxes.json"),
|
|
ContentPath("interactions.json"),
|
|
ContentPath("recipes.json"));
|
|
|
|
var state = GameState.Create("Test", Locale.EN);
|
|
state.UnlockedUIFeatures.Add(UIFeature.CraftingPanel);
|
|
state.UnlockedWorkstations.Add(WorkstationType.Foundry);
|
|
|
|
// Pre-add materials so that after any box opening, auto-craft can check
|
|
state.AddItem(ItemInstance.Create("material_wood_raw", 2));
|
|
|
|
var engine = new CraftingEngine();
|
|
var events = engine.AutoCraftCheck(state, registry);
|
|
|
|
// Should start a crafting job for refine_wood
|
|
Assert.Contains(events, e => e is CraftingStartedEvent cs && cs.RecipeId == "refine_wood");
|
|
Assert.Single(state.ActiveCraftingJobs);
|
|
}
|
|
}
|