Table of Contents

Namespace GrindFest

Classes

Ability
AbstractPrefabs
AbyssBehaviour
Achievement
AchievementData
AchievementsManager
ActiveOverworldMonsterSpawner
ActiveSkill

A skill that is executed: it owns the equipment swap protocol, animation helpers and combat targeting that used to live on the SkillBehaviour MonoBehaviour.

The coroutine is hosted on SkillUserBehaviour — the skill never needs a GameObject.

AddressableReference
AddressableScriptableObject
AgentTuning

Gives every character's NavMeshAgent the avoidance settings the game actually relies on.

Alchemy
AlchemyCauldron
AlchemyLadleBehaviour
AlchemyTests

Alchemy system tests — brewing and recipe validation. Liquid transfer tests (pouring, filling) are in LiquidTests. Each public IEnumerator method is a separate test case.

AnimatingState
AnimatorMoveListener
AnvilBehaviour
AnvilItemContainerTests

Anvil item container migration tests. These tests use ONLY bot API (DropInto, InteractWith) to place items and craft. They verify observable outcomes (items produced, items consumed) — NOT internal storage.

Purpose: safety net for migrating CraftingStationBehaviour.Items → InventoryContainerBehaviour. All tests should pass BEFORE and AFTER the migration.

AnyItemAffix
ArcLash
AreaAffix
AreaBasedBehaviour
AreaBasedLevelBehaviour
AreaBehaviour
AreaExitBehaviour
AreaFloorBaker

Keeps a NavMesh baked around each hero, because the world is generated and the mesh has to follow.

AreaLevelBehaviour
ArmageddonSpell
ArmorAffix
ArmorBehaviour
ArmorDefenseAffix
ArmorMastery
Assets
AsyncWait

Player-loop-driven waits — the platform-correct replacement for blocking on a task.

Why this exists: on WebGL there are no threads, so task.Wait(), task.Result and GetAwaiter().GetResult() cannot ever complete there. They are not a slow path, they are a hang — which is how DungeonGenerator froze a browser and how OnApplicationQuit could stop a build from exiting. Everything below is driven by Unity's own player loop (NextFrameAsync(CancellationToken)), so it completes wherever the player runs.

Deliberately not built on Task.Delay: a delay is a timer, and whether the runtime can fire one without threads is exactly the platform question this type exists to avoid.

AsynchronousMultiplayerManager
AsynchronousMultiplayerManager.ChatMessage
AsynchronousMultiplayerManager.CodeUploadResponse
AsynchronousMultiplayerManager.EquippedItem
AsynchronousMultiplayerManager.HeroData
AsynchronousMultiplayerManager.SkillData
AttackEvent
AutoScrollDropTable
AutoSkillBookDropTable
AutoWeaponDropTable
AutomaticHero

Base class for all bots

AutomaticIdentifyToolBehaviour

Tool that automatically identifies unidentified items when mousing over them. Attach to any item that should have auto-identify capability when equipped.

AutomaticParty
AxeBehaviour
AxeMastery
BalanceProbe

Base class for all balance probe tests. Provides:

  • Stat logging (hero + monster equipment, damage, armor, HP)
  • Combat event tracking (hits, crits, blocks, damage dealt/taken)
  • RunFight() for standard 1v1 fights with consistent output format

Naming conventions: [What]Vs[What] — HeroVsZombie, StrBuildVsDexBuild [Area]Probe — CrimsonMeadowsProbe [System]Probe — DropTableProbe, XpCurveProbe

Output format (every probe logs these): Hero (LvN, STR:N DEX:N INT:N) Dmg:N-N Armor:N HP:N/N [weapon / shield] Monster (LvN) Dmg:N-N Armor:N HP:N/N [weapon / shield] RESULT: WON/DIED | TTK:N.Ns | Hero HP:N/N COMBAT: Hits:N Crits:N Blocks:N DmgOut:N DmgIn:N MBlocks:N

Bash
BattleOrders
BehaviourTreeExtensions
Berserk
Blacksmithing
BlacksmithingTests

Blacksmithing system tests — anvil repair, socketing, forge smelting/salvage. Each public IEnumerator method is a separate test case.

BlessSpell
Block
BlockSkill
BloodMagic
BookBehaviour
BookTests

Tests for the book system — verifies OneJS rendering pipeline puts correct content on the 3D book via shared RenderTextures.

Uses TEST_COLOR_BOOK which has solid-color pages using the leaf model: Cover: Red (1,0,0) — separate, via RenderCover content[0]: Green (0,1,0) — OpenFront right (leaf 1 front) content[1]: Blue (0,0,1) — OpenMiddle left (leaf 1 back) content[2]: Yellow (1,1,0) — OpenMiddle right (leaf 2 front) content[3]: Magenta (1,0,1) — after turn 1 left (leaf 2 back) content[4]: Cyan (0,1,1) — after turn 1 right (leaf 3 front) content[5]: Orange (1,0.5,0) — after turn 2 left (leaf 3 back) content[6]: White (1,1,1) — after turn 2 right (leaf 4 front)

BookcaseBehaviour
BootClockGuard

Remembers what the game clock was when the session started, says so loudly when it was stopped, and puts it back so the rest of the session is usable.

BotAttribute
BotProfile

Serializable bot configuration profile. Stored per-profile. Feature unlock state is NOT stored here — it's read from Grimoire inscribed nodes (persistent across prestiges). Only toggle states (enabled/disabled) are stored per-profile.

BotProfileStore

Holds the player's bot configuration profiles and tracks which automation features are unlocked. Features are unlocked via Grimoire node IDs — this class reads the party's inscribed nodes list. The actual toggle states are stored per-profile.

JS callers reach this via Puerts as CS.GrindFest.BotProfileStore.Instance.

It lives nowhere in particular and cannot be missing. There is nothing to wire up in the inspector, so there is no reason to keep an object for it in the scene: Instance creates the one instance on first access (play mode only, never a second copy). Callers just read BotProfileStore.Instance and use it.

This used to be an override on this class that built its own GameObject whenever no live one could be found, with no play-mode guard - the OneJS edit-mode preview mounts the UI outside play mode, the UI reads the store, and every editor tick that found no live one added another object to the open scene (hundreds of them, saved with the scene). The behaviour was right; the guard that makes it safe belongs in the base class, where it cannot be forgotten.

Buff

A temporary effect. Ticked centrally by SkillUserBehaviour — there is no per-buff Update() and no GameObject.

BuffShrineBehaviour
BugReportService

Stateless helpers extracted from the legacy UIReportBug MonoBehaviour so the OneJS Bug Report panel can capture screenshots and submit reports without depending on the legacy scene panel being instantiated.

Same SendGrid endpoint, same recipient, same payload shape — just the network/screenshot plumbing without the UI bits.

BurnableBehaviour

Unified fire/fuel component. Handles burning, fuel consumption, fire VFX, fire spread, damage, light flicker, crafting station attachment, and tooltip. Replaces the old FuelSystem, FuelBehaviour, and CampfireBehaviour.

BurnableBehaviourEditor
ButtonMasherAchievement

Button Masher - Manually use skills by clicking. Bots can't help you here.

CampingSkill
CarpentryBenchBehaviour
CarpentrySkill
CartographySkill
ChangeTimeOfDaySpell
ChangeTimeToDaySpell
ChangeTimeToNightSpell
ChangeWeatherSpell
CharacterBehaviour
CharacterClass
CharacterCreationUiTests

The engine half of the character-creation screen: opening the panel spawns a preview hero, closing it takes that hero away again, and nothing is left behind.

This earned a test on 2026-09-17 because the flow failed silently inside the panel: the TSX spawned the preview with Object.Instantiate(Object, Vector3, Quaternion), which OneJS v3 cannot bind at all - the dispatcher answers "Method not found: UnityEngine.Object.Instantiate" because that name also carries generic overloads (measured: the 1-arg and the Transform overload do bind). The panel looked exactly the same whether the spawn worked or threw, and "create a hero" is not something a player can work around. The spawn now lives in UICharacterCreation.ShowPreviewForClass, so a bridge call that throws fails right here instead of in someone's playtest.

Setup uses backdoors (closing whatever was open, asking C# to drop a preview); the behaviour under test - show and hide the preview - is driven through the game's own entry points, the same calls the character-selection screen and the panel's Back button make.

CharacterHealthBehaviour
CharacterManaBehaviour
CharacterScreenUiTests

Spec (positive behaviour) tests for the Character screen: these describe what the UI is supposed to do for a player, independent of any past bug. Prefer this style - a test that states the intended behaviour keeps its value after every rewrite, whereas a regression test that guards one past mistake can be deleted once the cause is gone.

Setup uses backdoors (handing out a stat point, opening the panel through the game's own entry point); the behaviour under test - spending a stat point - is done the way a player does it: by clicking the "+" that sits next to the "Strength" label. LevelAndStatsTests covers where the points come from in the first place.

CharacterStaminaBehaviour
CharacterState
CharacterState.StateMachine
Charge
ChestBehaviour
ChiselBehaviour
ClassRequirement
Cleave
CloseableUIStack

Manages a stack of closeable UI elements so ESC closes the most recently opened item.

CloudCodePushManager
CoffinBehaviour
Colors
CombatBehaviour
CombatStatsTracker

Tracks combat events during a balance probe and produces structured output comparable across runs. Subscribe to hero + monster GetHit events (firing AFTER damage calculation, so counters are accurate).

Output format: Hits:N Crits:N Blocks:N DmgOut:N DmgIn:N MBlocks:N

CombatTests

Combat tests — attacking, killing, damage dealing.

Compiler
CompilerTests

The bot-script compiler - the feature the whole game is built around.

ComponentPayload

The payload of a saveable component (ISerializableComponent). Opaque bytes on purpose: a component - ours or a mod's - may use any encoding it likes (this helper uses MemoryPack) instead of being tied to a JSON string.

Layout: one version byte, then the body. There is deliberately no magic and no legacy format - a payload is written and read by the component that owns it and by nobody else, so there is nothing to disambiguate. A payload that does not read back (an unknown version, corrupt bytes, or the JSON text left in a save from before the binary format) simply makes TryRead return false, which callers already treat as "nothing to restore".

The version belongs to the component, not to the game: a component whose payload shape changes in a way the serializer cannot tolerate (a member removed or reordered - adding one is fine) writes a higher version and switches on the value from TryRead<T>(byte[], out T, out byte).

ConfiguredItemDrop
ConsumableBehaviour
ConsumableTests

Consumable tests — drinking potions, eating food.

ContentHotReloadTests

P5 gate (plan §6): a .json write inside a mod folder must re-merge the content database on its own — the file watcher, not a manual reload call — and everything resolved from the previous merge must see the new state (definitions re-resolve; MonsterPool's name caches are invalidated).

Writes its own temporary content mods, so it runs anywhere; restores the database in the finally blocks.

ContentPlatformTests

Boot-time gates for the content platform (GrindFest.Workspace/CONTENT_PLATFORM_PLAN.md).

These can only be checked in a running game, which is the point: the EditMode gates prove the generated data is correct, these prove the game uses it instead of loading everything.

CoreTests

Core framework tests — isolation, CleanSlate, runner behavior.

CraftingRecipe
CraftingStationBehaviour
CreateHeroData
CropBehaviour

Drives a crop's lifecycle on a single GameObject.

Two modes:

  • Multi-mesh stage mode – for harvestable individual plants (wheat stalks, tomato bushes). Add child GameObjects (one per visual age) to VisualStages. The component cycles through them by timer. When all stages are done the crop is harvestable. Combine with HealthBehaviour + DropTableLootBehaviour — the plant is destroyed by normal attacks.
  • Spawn-grid mode – for the seedling row prefab that spawns many individual plant objects when it matures. Set NextStagePrefab + SpawnCount. When the timer expires the seedling is destroyed and N plant prefabs are spawned in a grid, each carrying their own CropBehaviour in multi-mesh mode.
Wheat setup:
WheatSeedling.prefab → CropStageBehaviour { NextStagePrefab=WheatStalk, Duration=120s, SpawnCount=16, GridSpacing=0.4f }
WheatStalk.prefab → CropStageBehaviour { VisualStages=[S,M,L], StageDurations=[30,30,0], IsHarvestable=true } + HealthBehaviour + DropTable

Tomato setup:
TomatoSprout.prefab → CropStageBehaviour { NextStagePrefab=TomatoBush, Duration=60s, SpawnCount=4, GridSpacing=1.2f }
TomatoBush.prefab → CropStageBehaviour { VisualStages=[S,M,Mature], StageDurations=[20,20,0], IsHarvestable=true } + HealthBehaviour + DropTable

CropContainerBehaviour

State machine and crop lifecycle manager for any object that can hold a planted crop: tilled farm plots, plant pots, planter boxes, greenhouse benches, etc.

Call Initialize(Vector3) (done automatically by TillingSkill) to mark the container as a tilled terrain plot. Set Rows and Columns in the Inspector on the prefab. Use standalone on pot/planter prefabs without calling Initialize.

After planting, this component instantiates the FirstStagePrefab as a child. That child carries a CropBehaviour which drives the growth timer, visual transitions, and tooltip display autonomously. Growth state is derived on-demand via IsEmpty, IsMature, and IsOvergrown.

CropData

Which growth stage a CropBehaviour had reached, and how far into it.

CurePoisonSpell
CurseSpell
CursedCoinBehaviour
CursorDuringRecording
DamageEvent
DamageOverTimeDebuff

Repeated damage over time. The applier fills in the payload (damage + element) on the instance it creates — the old version carried it on a shared prefab and had to patch it after spawning.

DeathEvent
DeathState
DebugDraw
DebugExtension
DefaultAutomaticParty
DelegatePriorityComparer<T>
DelegateWithPriority<T>
DevTest

Base class for in-game developer tests. Tests run as coroutines in the live game — no NUnit, no scene reload. Editor-only (uses Addressables, Cheats patterns).

Each public method returning IEnumerator is discovered as a test. Group related tests in one class, each method is a separate test case.

DevTestCli

Command-line front door for the DevTests, so the SAME test suite that gates the editor can also gate a player build (a Steam build, a CI artefact, or a locally built exe):

GrindFest.exe -devtests [-devtests-filter=Monster] [-devtests-report="C:\path\report.txt"]
http://host/?devtests[=Monster]      (WebGL: the page URL is the command line)

The runner starts the filtered run once the game's systems exist, and when the run finishes this class writes a small machine-readable report file and quits the process with exit code 0 (all passed) or 1 (failures) — so a caller can simply wait for the process and read the code, plus the full log path from the report. Without -devtests the runner removes itself in players; nothing ships active.

Caveats worth knowing before pointing this at a real install: • DevTests are destructive by design (they clear the party mid-run) — run against a build profile whose persistent data you don't mind losing, or override the data folder where the platform allows it. • The bundle E2E gate needs an editor-built fixture and skips itself in players; the bundle BUILD gate is EditMode-only by nature (Addressables refuses to build in play mode). • Tests are discovered by reflection; the project runs with managed stripping Disabled, which is what keeps them discoverable in players. If stripping ever gets enabled, DevTest types need preserving.

DevTestException
DevTestRunner

In-game developer test runner. Discovers all DevTest subclasses and runs their public IEnumerator methods as test cases in the live game. No NUnit, no scene reload.

Run all tests: Ctrl+Shift+T or DevTestRunner.Instance.RunAllTests()
Run filtered:DevTestRunner.Instance.RunAllTests("PourWater") (substring match on ClassName.MethodName)

DevTestSkipException

Thrown by a test that cannot run in this build at all - as opposed to one that ran and failed.

DiggableBehaviour
DiggingSkill
DirectionExtensions
DirectionMarker

Editor-only gizmo to visualize a transform's direction. Useful for projectile attach points, spawn points, aim directions, etc.

DisableInEditor
DiscordManager
DisplayTooltip
DoorBehaviour
DropEquipmentOnDeathBehavior
DropItemTests

Simple drop tests to verify DropInto works correctly with different item types. Isolates the drop mechanism from crafting recipes.

DropLootBehaviour
DropRandomLootOnDeathBehaviour
DropSkill
DropTableChild
DropTableLootBehaviour
DungeonAffix
DungeonBasedLevelBehaviour
DungeonBehaviour
DungeonCrawlerAchievement

Dungeon Crawler - Enter 1,000 dungeons. The abyss files a restraining order.

DungeonMonsterSpawner
DungeonPortal
DungeonRoom
DungeonRoomSpawner
DuplicateCatchAffix
DurabilityBehaviour
DurabilityLifetimeTests

How much time a durability pool actually buys, measured - the unit the whole ladder is authored in (DURABILITY_DESIGN.md).

DwarvenTotemEffect
EatFoodSkill
ElementExtensions
ElementalBehaviour
ElementalMagic
EnableDisableEventBehaviour
EnableRandomGameObject
EquipPrefabWeaponBehaviour
EquipRandomWeaponFromTableBehaviour
EquipableBehaviour
EquipmentBehaviour
EquipmentBehaviour.BodyPartTransform
EquipmentSlotToBodyPartTransformDictionary
EventAggregator
EventfulPropertyAttribute

Marks a field whose property and change event are generated at compile time by Tools/EventfulProperties (shipped as a Roslyn analyzer in Assets/_GrindFest/Plugins, labelled RoslynAnalyzer). The containing class must be declared partial.

[EventfulProperty] private long _gold;

generates

public long Gold { get => _gold; set { ...OnGoldChanged?.Invoke(value); } }
public event System.Action<long> OnGoldChanged;

which is exactly the shape the UI's useEventSync(source, "Gold") subscribes to. This is the v2 OneJS codegen (OneJS.Codegen.dll); the v3 package does not ship it, so this project keeps its own copy rather than hand-writing the property and the event for every value the UI watches.

ExplodeBarrelSkill
ExplodingBarrelBehaviour
ExplorerCartographerAchievement

Explorer's Cartographer - The world writes itself. You just walk through it.

ExplosionSkill
ExternalAchievementService

Platform-agnostic service for unlocking external achievements (Steam, Kongregate, etc). Supports both progress updates (stat-based) and direct unlocks.

How it works with Steam:

  • Each achievement defines a Steam Stat name (e.g. "stat_kills") and an achievement ID.
  • Progress calls update the stat value. Steam shows progress in the overlay.
  • When the stat reaches the threshold configured in the Steamworks dashboard, Steam auto-unlocks the achievement. We also call Trigger() as a safety net.
ExtraMonsterAffix
FMOD_AnimationEvents
Faction
FactionBehaviour
FactionMemberBehaviour
FactionRelationship
FarmerAI

AI for a Farmer NPC that autonomously cycles through all four farming actions: Till → Plant → Water → Harvest → repeat.

The farmer inserts its farming loop before the wander behaviour (via BeforeWander(BehaviorTreeBuilder)), so it only wanders when there's nothing to farm.

Players can observe this NPC to learn the bot API:

// What this NPC does — you can do the same!
while (IsBotting)
{
    if (Hero.Till()  == ActionResult.Done) continue;
    if (Hero.Plant() == ActionResult.Done) continue;
    if (Hero.Water() == ActionResult.Done) continue;
    Hero.Harvest();
}
FarmingTests

Farming system dev tests. Written from the perspective of a player learning to write a farming bot. Each test follows the three-phase pattern: Setup → Action (bot API) → Verify.

FearBuff

Marks the victim as terrified — the monster AI sees this buff and flees. It carries no payload of its own, which is why it was the first skill converted: the entire definition is its duration.

FieldSetupWithShape
FillWithSocketsOnGameStartBehaviour
FindMissingScriptsRecursively
FireTrap
Fireball

Casts a fireball that explodes on impact, dealing 1.2x weapon damage as fire damage to all enemies in a 2m radius

FireworksSkill
FishBehaviour
FishStoryAchievement

The Fisher King - By the waters of Leman I sat down and wept.

FishingRodAffix
FishingRodBehaviour
FishingSkill
FishingSpotBehaviour
FlagBehaviour
FlagData
FoodBehaviour
ForgeBehaviour
FrameProbe

Logs one measurement line every couple of seconds while a development build runs: frame time, garbage collection, and what FMOD is costing.

FreezeSunSpell
Frenzy
FuelableRecipe
GameManager
GameNetworkManager
GameObjectExtensions
GemBehaviour
GenerateDungeonOnEnter
GeneratedAreaOverworldMonsterSpawner
GetHitEvent
GetHitState
GlobetrotterAchievement

Globetrotter - Your bot has wanderlust.

GoblinVillageSpawner
GoldAchievement

Gold Goes Brrr - Lifetime gold earned. Gold forever.

GolfSkill
GunRangedAttack
HammerBehaviour
Handle
HealEvent
HealSpell
HealthBehaviour
HealthShrineBehaviour
HerbalMagic
Hero
HeroController
HeroData
HeroLevelBehaviour
HeroLooksBehaviour
HeroSpawnPointBehaviour
HeroVsSkeleton

Balance probes against Crimson Meadows enemies. Skeleton (Lv3 Common, weapon) — bread-and-butter enemy after Village. Skeleton Archer (Lv4 Common) — ranged, kites the player. Skeleton Warrior (Lv6 Elite) — weapon + shield, uses Block skill.

HeroVsZombie

Balance probes vs Zombie (Lv1) — the tutorial enemy.

HoeBehaviour

Marker component identifying an item as a Hoe tool. Required by TillingSkill to till terrain and create farm plots.

HolyMagic
Howl
IdleState
ImmortalityPotionAffix
InfiniteWaveSpawner
InitialEquipment
InkFileHandler
InkFunctionAttribute
InscriptionSkill
InscriptionTable
InteractiveBehaviour
InventoryContainerBehaviour
InventoryController
InvokeOnReloadAttribute

Marks a static method to be automatically called when your bot script is recompiled. This is useful for destroying instantiated classes of your types that will stop being valid (mono behaviours are destroyed automatically) or storing state before your bot code is reloaded.

ItemAffix
ItemAffixRestrictionsAttribute
ItemBehaviour
ItemBehaviourEditor
ItemData
ItemDexterityAffix
ItemExperienceAffix

Adds experience bonus when item is equipped. Follows same pattern as ItemStatAffix for consistency.

ItemIntelligenceAffix
ItemLevelBehaviour
ItemPickupDropTests

Simple tests for core item pick-up and drop mechanics. Tests the building blocks that DropInto relies on.

ItemStatAffix
ItemStrengthAffix
Journal
JsonDescriptionAttribute
JsonEnumAttribute
KeyBehaviour
Kill10000EnemiesTest
Kill100EnemiesTest
KillAchievement

Mass Extinction - Kill monsters. Lots of monsters. ALL the monsters.

KillTrigger
KillableExplodingBarrelBehaviour
LastStandSkill
Leap
LevelAndStatsTests

Spec (positive behaviour) tests for hero progression: what levelling up is supposed to give.

These describe the intended rule rather than guarding a past mistake, so they keep their value after a rewrite (compare the ESC test in SettingsUiTests, which is explicitly temporary).

LevelBehaviour
LifeDrainSpell

Channels to continuously drain life from target enemy and converts it to mana or health based on the spell variant

LifeRegeneration
LightBuff

Marks the character as glowing — a pure marker buff, no payload at all.

LightSpell
LiquidContainerBehaviour
LiquidContent
LiquidTests

Liquid transfer tests — filling and pouring between containers (buckets, cauldrons, rivers). Each public IEnumerator method is a separate test case.

LockpickingSkill
LogBehaviour
Logger

Captures Unity log messages to Logs/alchemy_log.txt. Errors/exceptions always captured. Regular Debug.Log captured when from bot/alchemy code. Only active in Unity Editor.

LookAtCamera
LootCategoryBehaviour
LootGenerator
LuckyAffix
LumberjackingSkill
LumberjackingTests

Lumberjacking tests — chopping trees, gathering wood. Each public IEnumerator method is a separate test case.

LureBehaviour
MMHierarchyBlink
MaceMastery
MagicRunes
MagpieAchievement

Magpie - If it's on the floor, it's yours.

MainPathBehaviour
MainRoadBehaviour

Marks a Road spline as part of the main road network. At Start(), reads spline knots and registers them as waypoints in MainPathBehaviour.Waypoints so GoToArea() navigation works.

Add this component to Road SplineMarker GOs (via +MainRoadBehaviour in ink). The road spline endpoints must align with neighboring area road splines for the waypoint chain to connect properly.

ManaBehaviour
ManaShieldBuff
ManaShieldSpell
MapMagicPerformanceTests

Performance probes for MapMagic terrain generation. These tests pin a tile far enough from the player that it is guaranteed unloaded, then time the full Refresh → OnTileApplied roundtrip.

Wrapped in Profiler samples so a Deep Profile capture (Window → Analysis → Profiler, "Deep Profile" enabled, hit "Record") will show a breakdown per generator inside the "MapMagic:Generate Tile" marker.

Each test logs:

  • wall time from Refresh() to OnTileApplied (background generation)
  • wall time from OnTileApplied to next frame end (main-thread apply)
MapMarkerMover
MasonrySkill
MassTransmuteIntoGold
MaterialComponentRecipe
MaterialRecipe
MaterializerHydrationTests

P5 gate (plan §8): data must win over the prefab. A patch changes one number in the definition's data; the spawn path must produce instances carrying the patched value while the archetype asset stays untouched — and a live content reload must make the next spawn see the new value, with no restart.

Writes its own temporary content mod, so it runs anywhere; restores the database in the finally block.

MaterializerSpikeTests

P5 phase-0 spike (CONTENT_PLATFORM_PLAN.md §11): before the materialiser is built for real, measure what it costs. For a representative item, monster and structure: plain clone vs clone+apply, field coverage, managed bytes per materialise, runtime size of a clone — and the part that must not get lost in the numbers: a value changed in data must land on the spawned instance.

The numbers are the deliverable; the assertions are loose sanity bounds only.

MaxLevelAchievement

Number Go Up - Reach higher and higher levels. Brain happy.

Meditate
MeleeAttack
MemoryPackSetup

Registers the formatters MemoryPack cannot discover by itself (see ResistanceDictionaryFormatter). In a player the hook calls this before the first scene loads; outside play mode nothing calls it, so a check or an editor tool that touches a SaveData has to - which is a caveat the round-trip test exists to keep honest.

MeshBoundaryGenerator
Mining
MiningToolBehaviour
ModAssetBundleTests

P6 gate (Content category): a mod's own prefab and icon, shipped as a content directory, must load through the platform exactly like a base asset — the mod folder is discovered, its directory registered and its keys namespaced, and the resolved prefab still carries its sprite.

The fixture is published by the EditMode gate GrindFest.Tests.ModAssetBuildTests, which builds it straight into the mod folder (Mods/__dt_modbundle/Content/content-directory/). So this test publishes only the mod's manifest and its content patch, then activates the mod through the production refresh path (ModManager.RefreshAndReload), exactly like a subscribed mod would be.

What it used to do: copy a catalog*.json + bundles/ fixture out of Library/GrindFestModBundleFixture and load it through the Addressables catalogue path. Both the Addressables build and the catalogue are gone (2026-09-20) — and with them the SBP Error that made this gate fail on a machine whose Library had been rebuilt, which is why it spent its last days reporting "fixture missing" instead of testing anything.

ModAssetCatalogs

Runtime side of the mod asset pipeline: a mod's assets are a built content directory, scoped mod:<mod folder name>.

Mods/<Mod>/Content/content-directory/     ← GrindFest ▸ Mods ▸ Build Mod Assets

It is registered by RegisterAllAsync(CancellationToken) at boot, after the base game — which is what makes a mod's version of a key win, with no id remapping and no bundle rewriting. What is left here is the policy check that mechanism cannot express: every key a mod contributes has to live under mod/<folder name>/, because otherwise a mod can shadow a base-game asset by accident and the override looks like the game being broken rather than like a mod doing its job.

The Addressables catalogue path is gone (2026-09-20). It needed a catalogue to load, bundle internal ids to rewrite and an InternalIdTransformFunc to make paths machine-independent; a content directory is self-describing and addressed by path, so none of that exists any more. A mod that still ships only a catalogue is now a red error naming the command to run — not a silently empty mod, and not a second code path that has to keep working forever.

Failures are per-mod by design: a broken mod is a red error naming the mod, never a dead boot.

ModDefinition

Mod manifest deserialized from mod.json. Required in every mod folder for Workshop publishing and mod browser display.

ModInventoryUiTests

The InventoryList example mod's overlay: does it follow the hero's inventory?

This exists because it did not. The overlay read Character.Inventory.Items during render and never subscribed to anything, so it only redrew when some unrelated poll happened to hand React a different object - and when that stopped happening, taking an item out of the inventory left the old contents on screen (reported 2026-09-24: "it doesn't react reactively"). The game's own UI panels subscribe instead (useObservableList, and [EventfulProperty] + useEventSync for single values), which is what the mod now does too.

The assertions are the player's view: the strings on the overlay, read out of the UI Toolkit tree with the framework's Ui helpers - the same ones the panel tests use. The action is the player's too (BotHero.Drop), not a poke at the list.

Skips itself when the mod is not installed: the overlay belongs to a MOD, and a project without it has nothing to assert (a skip, not a failure - see DevTestSkipException).

ModManager

Central mod manager. Handles discovery, deployment, and hot-reload of all mod types.

Currently supports:

  • Ink Story mods (.ink files → runtime compilation → world regeneration)
  • UI mods (JavaScript/TypeScript → evaluated by the OneJS JSRunner)

Future:

  • Prefab mods (Addressable prefab overrides)
  • Script mods (C# bot scripts)

Folder structure: persistentDataPath/Mods/GrindFest/Story/ — base story mod (smart-copied from StreamingAssets) persistentDataPath/Mods/[ModName]/Story/ — additional story mods persistentDataPath/Mods/[ModName]/UI/dist/index.js — UI mod entry point (eval'd after app.js) persistentDataPath/Mods/[ModName]/Prefabs/ — .prefab DSL files defining custom prefabs (resolved by InkWorldGenerator) persistentDataPath/Mods/[ModName]/Scripts/ — (future) script mods

In Editor, can optionally use Assets/_GrindFest/Story/ directly (toggle via UseEditorStoryFiles).

ModOrderingTests

The P4 ordering gate that was never written as a test.

CONTENT_PLATFORM_PLAN.md §11 lists P4's gate as "mod DevTest: add/patch/remove item + monster, ordering test". The patch half became ContentDatabaseTests.ModPatchesAddPatchAndRemoveDefinitions and PatchOrderFollowsPackageOrder (EditMode). The ordering and entry-point-resilience half was only ever verified by hand, in play, with two fixture mods copied into the live Mods folder -- and left there. Two consequences, both worth avoiding next time: the leftovers produced a red log line on every boot for four days, and the verification cannot be repeated by anyone, because the fixtures exist on one machine and nowhere in the repo.

This covers what that run covered, is repeatable, and owns its fixtures the way ContentHotReloadTests does: __dt_modorder* folders are written, asserted against, and deleted in a finally block (with a clean-up of leftovers first, because a timeout aborts before finally).

ModScriptCompiler

Compiles C# scripts from mods into in-memory assemblies, with security scanning.

Security model:

  1. Parse all .cs files into Roslyn syntax trees.
  2. Walk the syntax trees looking for dangerous API usage (not string matching — avoids false positives from comments and string literals).
  3. If dangerous APIs found and mod is not trusted → block compilation.
  4. Trust is hash-based: if scripts change, trust is revoked.
  5. First-party mods (author "GrindFest Team") are always trusted.
MonsterAffix
MonsterAffixExtraDamage
MonsterAffixExtraFast
MonsterAffixVitality
MonsterBehaviour
MonsterLevelBehaviour
MonsterPool
MonsterRankBehaviour
MonsterRankExtensions
MonsterSpawner
MortarBehaviour
MovementState
MovementTests

Movement tests — verifying hero can walk, navigate, and reach destinations.

MusicManager
MusicOrchestrator
MusicSkill
MusicalInstrumentBehaviour

Provides MIDI instrument functionality for playable musical items.

MyPreciousAchievement

My Precious - Hold 1 million gold at once. Dragon cosplay.

NameplateBasedOnWorldTile
NameplateTests

The nameplate layer: where a label is drawn relative to its item, which items share one label, and which label the pointer picks.

These are the behaviours that kept regressing by hand (2026-09-23 and 24): labels ending up hundreds of pixels from their items, heaps of the same kind never collapsing so a dozen plates fought over the same pixels, a Ctrl comparison that only appeared if Ctrl was held before the pointer arrived, and a pointer scan that picked the first label in pool order instead of the one drawn on top. Every one of those was found with a throwaway script in GrindFest.Workspace and nothing kept it fixed. This is that knowledge as tests.

What is asserted is observable: pixel distances between what is on screen and the item's own projection, how many labels a heap costs, whether the two halves of a comparison touch, and which label a point resolves to. Not asserted: the label's root RectTransform size, which is a layout artefact that reports the plate wrongly (measured (140, 10) for a full plate, and (10, 35) after a forced rebuild) - the geometry the game lays out with is PlateSize(UINameplate) and the tests read the same one.

Alt and Ctrl are forced by disabling PlayerInput, which rewrites both flags from the keyboard every frame; that also means the pointer scan does not run, so a test that needs a hover calls OnPlayerHover the way PlayerInput does. The input is always restored (try/finally), because the rest of the suite needs it.

NamespaceRewriter

A syntax rewriter that transforms all references to GrindFest namespace to GrindFest.Isolated namespace.

NativeFileWatcher

Drop-in replacement for FileSystemWatcher that avoids Mono's DefaultWatcher polling (which allocates ~300 KB/frame of GC).

On Windows: Uses ReadDirectoryChangesW (event-based, zero-GC steady state). On other platforms: Falls back to periodic polling with controlled interval.

Events fire on a background thread, same as FileSystemWatcher.

NavMeshAgentExtensions
NavMeshVisualizer
Necromancy
ObservableList<T>

A minimal observable wrapper around List<T>. Fires Changed on every mutation (Add, Remove, Clear, indexer set). Zero-allocation event — uses Action with no args.

Designed for OneJS bridge: JS subscribes via onejs.subscribe(list, 'Changed', cb).

Unity-serializable: the inner List is serialized by Unity.

OfflineHeroBehaviour
OneJSGlobals

Exposes a runtime-created C# object to JavaScript as a global on the live OneJS runner.

The runner's own globals are an inspector-assigned list on OneJS.JSRunner (_globals), which cannot serve objects that only come into existence while the game is running. This helper does exactly what JSRunner's own InjectGlobals does for its inspector entries: hand the object to QuickJS to get a handle, then wrap that handle on the JS side so scripts can use the object directly (call methods, read properties, subscribe to events with add_OnXChanged).

OrderedDelegateHandler
OrkHordeSpawner
OverworldMonsterSpawner

Checks the player movement and after few steps spawns a monsterrs

PageContentBridge

Bridge between BookController and OneJS for rendering book page content. Creates 4 RenderTexture-targeted UIDocuments matching EndlessBook's material slots:

  • Left: BookPageLeft (left page in OpenMiddle)
  • Right: BookPageRight (right page in OpenMiddle)
  • Front: BookPageFront (front of turning page / right side in OpenFront / cover when closed)
  • Back: BookPageBack (back of turning page)
PanelIds

Element ids of the OneJS panel roots, as set on the JSX side (OneJS maps a JSX id to VisualElement.name, which is what FindByName(string) reads).

Why ids instead of text: a test that wants to know "is the main menu open?" cannot answer that from text reliably. The same words appear in several panels - the Settings panel carries a "Settings" title, so the menu's own "Settings" entry is not unique - and any copy change (or localization) breaks the probe. An id is one stable handle per panel, and it survives a panel rewrite as long as the id is kept.

Text keeps its own job: clicking what a player reads, and proving the CONTENT rendered. A panel can be open while its content renders nothing, which is exactly the bug SettingsUiTests guards, so a panel-id check alone would not have caught it.

Keep in sync with the TSX: panel-main-menu App/src/components/MainMenu.tsx panel-settings App/src/components/Settings/SettingsPanel.tsx panel-character-screen App/src/components/CharacterScreen/CharacterScreenView.tsx panel-prestige-tree App/src/components/PrestigeTree/PrestigeTreeView.tsx panel-achievements App/src/components/AchievementsPanel/AchievementsPanel.tsx panel-mod-browser App/src/components/ModBrowser/ModBrowser.tsx panel-report-bug App/src/components/ReportBug/ReportBugPanel.tsx panel-bot-config App/src/components/BotConfig/BotConfigPanel.tsx panel-character-selection App/src/components/CharacterSelection.tsx panel-character-creation App/src/components/CharacterCreation.tsx

ParentToBone
Party

Must be partial � the OneJS [EventfulProperty] source generator only processes partial classes. This allows useEventfulState(party, 'Gold') in TSX components for reactive currency display.

PartyAnimalAchievement

Strength in Numbers - Build your army.

PartyController
PersistedAreaBehaviour

Stores all items in this area in save data, and restores them on load. Checks if player has modified/entered area, so it doesn't save unnecessary data.

PersistentAreaData
PersistentItemBehaviour
PestleBehaviour
PetBehaviour
PhaseDoorSpell
PickUpApproachTests

Walking to an item before picking it up - the half of pick-up that no other test exercises.

PickUpSkill
PlantingSkill

Plants seeds in the nearest empty CropContainerBehaviour within range. Requires SeedsBehaviour in inventory or equipped.

Bot API:

Hero.Plant("Wheat");
PlayerInput

Item comparison dialog — shows when an equipable item is picked up and the corresponding equipment slot is already occupied. Works for both bot auto-loot and manual controller pickup.

PlayerInput.RayCastHitDistanceComparer
PlayerKillerAttribute
PlayerStory
PlaytestInfo

Text-based game state for AI playtesting. Call via MCP unity_execute: return GrindFest.PlaytestInfo.GetState();

PortalBehaviour
PortalGateBehaviour
PortalViewBehaviour

Renders a see-through portal effect on a Moon Gate. Creates a secondary camera at the destination, renders to a RenderTexture, and projects it onto a wide fan/cone mesh extending from the gate opening toward the hero. The fan direction tracks the hero's position for parallax.

PotionAffix
PourWaterTest

Test: Hero pours water from bucket into cauldron using DropInto (bot API). Setup: Spawn cauldron + bucket with water, give bucket to hero. Action: BotHero.DropInto(bucket, cauldron) — same as player bot script. Verify: Cauldron contains water, total conserved.

PrestigeAddictAchievement

Prestige Addict - Purchase 100 prestige upgrades. The meta-game IS the game.

PrestigeNode

Bot-facing snapshot of a single Grimoire (Prestige Tree) node. Returned by GetPrestigeNodes(); bots use this to decide what to inscribe via InscribePrestigeNode(string).

Field semantics:

  • Id is the stable identifier (use this when calling InscribePrestigeNode); Name is the human-facing label and may be re-themed in future updates.
  • Kind is one of: "Sigil", "Bridge", "Aux", "Continuation" (and a couple of specials).
  • Parents is the prerequisite list. The Grimoire only allows inscribing a node when at least one parent is already inscribed (the gateway node has no parents).
  • IsInscribed reflects whether this node is currently inscribed on the local party.
  • GoldCost / SoulCost / GemCost / AchievementPointCost are the per-currency costs to inscribe (unspecified currencies report 0). Real-money nodes (Patron) report 0 across the board — they're paid via Steam, not via in-game wallets, and bots cannot purchase them.
PrestigeSystem

Single source of truth for the Grimoire (prestige tree). Holds the static bonus fields the rest of the game reads (LevelCap, HeroSlot, etc.), AND the JS bridge that the OneJS / Preact UI calls into to query state, register the node registry, and inscribe nodes.

History: this used to be split across PrestigeSystem (fields + dormant legacy shop stubs) and PrestigeTreeBridge (registry + purchases + bot API projection). The split was an artifact of the migration off the old gold shop \u2014 they're now a single class so callers don't have to remember which side owns what.

JS callers reach this class via Puerts as CS.GrindFest.PrestigeSystem.{GetState, TryInscribe, RegisterNodes}.

PrestigeSystem.CostEntry
PrestigeSystem.EffectEntry
PrestigeSystem.NodeEffectsEntry
PrestigeSystem.NodeRegistryEntry
PrestigeSystem.StateDto
Projectile
ProximityTriggeredMonsterSpawner
ProximityTriggeredMonsterSpawner.MonsterSpawnProbability
PublishResult
PumpkinApocalypseAchievement

Pumpkin Apocalypse - Smash 666 pumpkins. The Great Pumpkin weeps.

PumpkinBehaviour
PumpkinLeaderboardHUD

Real-time leaderboard HUD display for pumpkin smashing. Shows: Top N entries, and Your Position (if not in top N). Supports both IMGUI (legacy) and TextMeshPro (zero-allocation) rendering. Requires Steam integration - automatically disabled when DISABLESTEAMWORKS is defined.

QualityBasedWeaponLootBehaviour
QuillBehaviour
RandomLootBehavior
RandomNameBehaviour
RangedAttack

Ranged attack skill for bows and crossbows. Two modes through skill system:

  • If no arrow nocked: plays nock animation (allows movement), spawns arrow
  • If arrow nocked: shoots the arrow
RankExtensions
ReadingSkill
ReagentBehaviour
ReagentRecipe

Alchemy recipe that matches reagents by their Category field. Carries conversion parameters for gradual liquid transformation while stirring.

ReagentSpawnerBehaviour
RemoveCurseSpell
Rend
RendDebuff
RepairRecipe
Requirements
ResistanceDictionary
ResistanceDictionaryFormatter

MemoryPack cannot handle a custom collection subclass, and ResistanceDictionary is exactly that: a UnitySerializedDictionary<TKey, TValue> deriving from Dictionary<Element, float>. Without this formatter the type is simply unknown to MemoryPack and every resistance on every item and hero would vanish on save - silently, read back as all zeroes rather than failing. The data does live in the base dictionary, so this reuses the built-in DictionaryFormatter instead of re-implementing the encoding.

It cannot be attached with an attribute: MemoryPackCustomFormatterAttribute<T> is abstract, and a generic attribute needs C# 11 while Unity compiles at C# 9. So it is registered at runtime by MemoryPackSetup - and anything that (de)serializes a SaveData outside play mode (editor tooling, an edit-mode test) has to call that itself, because the RuntimeInitializeOnLoadMethod hook does not run there. That is how a plain round-trip failed the first time this was tested.

ResizeObjectToFitTheSpline
ResourcePotionData

What a LiquidContainerBehaviour holds.

ResourcePotionSkill
ResourceTypeExtensions
Resurrect
ReturnParticleSystemToPool
RitualBehaviour
RoadSignBehaviour
RoadSignManager
Roar
RuneBehaviour
RunicMagic
SafeAreaMarker
SalvageRecipe
SaveFormatChecks

The assertions for the binary save format, as plain static methods so they can be run from outside the test runner as well - the editor can call these through reflection without entering play mode or building a player, which is how they were written in the first place. Each returns null when it passes, or the reason it failed.

SaveFormatTests

The runner-side wrappers for SaveFormatChecks. They are thin on purpose: all the assertions live in the static methods so they can be run without the runner (and without play mode).

SaveLoadTests

Save & load at the level SkillSaveRoundTripTests does not reach: the file, the path it goes to, and whether a written save is the one Load() will look for.

Why the path assertion is the important one here: SaveManager.saveFile was a serialized field filled only by OnValidate, which runs in the editor alone, so every build carried the build machine's absolute path (measured 2026-09-20: …/C:/Users/<name>/AppData/…/Saves/savedata.json). On WebGL that is outside Application.persistentDataPath — the only tree Unity syncs to IndexedDB — so the save silently did not survive a reload while the console still said "Game saved to …". A path assertion catches that in any build, including one nobody is debugging.

The write test restores the original save bytes afterwards. DevTests are destructive by design, but a save test that eats the player's party would be destructive in the worst possible place.

SaveManager
SawBehaviour
SchemaGenerator
ScytheBehaviour

Marker component identifying an item as a Scythe tool. Scythes are used to attack and harvest crop objects (Wheat, Flax, Rye). HarvestEfficiency applies a yield multiplier to the crop's loot drop.

SeedsBehaviour

Marker component identifying an item as Seeds. Required by PlantingSkill to plant a crop in any CropContainerBehaviour.

Set FirstStagePrefab to the plant prefab (must have a CropBehaviour with VisualStages — the first stage should be invisible to represent the seed in soil).

The container's Rows × Columns determines the grid shape (1×1 for pots, 1×9 for a single farm row, etc.). GridSpacing controls the per-species spread.

SettableRandom
SettingsUiTests

Player-like tests for the Settings panel and the ESC/menu flow.

Why these exist: a UI rewrite (e.g. the planned OneJS upgrade) can break the UI in ways nothing else catches - see the note on the ESC test below. Clicks go through real UI Toolkit events (L2, see Ui.cs); only ESC is invoked directly, because PlayerInput reads ESC from the legacy Input Manager and UI events never feed that.

The UI helpers (ClickText, ClickTextWithin, WaitForText, AssertTextVisible, PressEscape, ...) live on DevTest so every test can use them.

ShieldBash
ShieldBehaviour
ShortScale

Parse a numeric value into a short scale string representation.

ShovelToolBehaviour
ShowTextTooltip
ShowTooltip
Singleton<T>
Skill

Base class for every skill — a plain C# object, one instance per character, created with new. Unity never sees it: the type name is the identity and the design values live in content data.

Lifecycle (driven by SkillUserBehaviour): AddSkill -> OnAdded() Update -> Tick(dt) (cooldowns; buffs also count their duration down here) Remove -> OnRemoved() UseSkill -> Use() coroutine, hosted on the SkillUserBehaviour MonoBehaviour

Partial because the OneJS source generator only processes partial classes — the generated Level property and OnLevelChanged event are what the UI binds to.

SkillAssetTests

Every asset a skill asks for by address must actually resolve, whether or not any test happens to use that skill.

Why this exists: on 2026-09-16 the Brew animation had never played. Alchemy asked a model file for "Villager@Cooking01 - Start" and got null, because Addressables exposes exactly ONE AnimationClip per model file and it was not that one - so WaitForAnimation(null) logged a warning, brewing ran without an animation, and every test stayed green. The same held for Shield@Block01_Hold/_Hit, Praying Start (the clip is named "mixamo.com" in that file) and any other address whose asset is not the one a model file happens to expose. Nothing failed, because a null clip is not an exception and no test asserted on an animation.

This test does not try to use the skills - that needs a station, reagents, a target or a level per skill, and it is what the per-family tests are for. It asks the cheaper question that would have caught all five: does every address the skill code names resolve? It reads the call sites out of the source, so a NEW skill's addresses are covered the moment it is written, with no list to keep in step.

Editor-only, because the call sites are read from Assets/ source files.

SkillAssets

Shared assets that every skill may need, addressed by their Addressables address.

Only genuinely shared, character-level assets belong here (masks, generic consumable clips). Anything skill-specific is read from the skill's own data asset, so a mod can replace it.

Nothing here blocks and nothing here loads synchronously: a read returns what is resident, and on a miss it starts the acquire and returns null, so the next read is a hit. That is what keeps 58 skill properties non-async without a synchronous door — and the one-time warning names the cold address, so the set a hero scope should acquire is discoverable from a play session instead of by reading 27 files.

The honest limitation: the first use of an asset that nothing acquired yet does without it (the same behaviour a WebGL build already had). The fix is to acquire at setup, and the warning is the list.

SkillBookBehaviour
SkillBookConfiguration
SkillBookData

The skill a SkillBookBehaviour teaches.

SkillBookRecipe
SkillCatalog

One shared, read-only instance per skill type — the skill's definition, kept alive so that consumers (loot generator, drop tables, UI, save, character creation) can read a skill's metadata without a prefab and without allocating anything per query.

Three layers, deliberately separate: • this catalog — one prototype per type, design values only, never mutated (User is null) • AssetLibrary — the actual Unity assets, cached by address; a prototype only holds addresses • per character — a real instance created when the skill is learned, holding Level/CooldownTimer/…

Building it is cheap: a POCO is a few dozen bytes, not a GameObject.

SkillData
SkillFactory

Creates skill instances by CLR type. Skills are plain classes now, so "learning" a skill is a new — the type is what a save / drop table / book stores.

Type names are resolved against every loaded assembly once, so both the simple name ("Fireball") and the full name ("GrindFest.Fireball") work without an assembly qualifier — the failure mode the previous attempt hit with Type.GetType.

SkillOwnershipTests

Locks the rules of the skill system that the rest of the game walks over: what a character owns, what a repeated add/use does, and what removing or clearing has to clean up. These are the rules whose violation is invisible until much later — the duplicate-AddSkill regression left a hero holding 271 skills (267 identical MeleeAttacks) without failing anything.

On identity. Today a character owns at most one instance per skill type and the type name is what a save stores. That is a convention, not a law of the design — skills are meant to carry levels (and later experience), and identity may well move to a definition object so that variants can coexist. So these tests assert the part that must survive any such change: nothing accumulates. Where the convention shows up (adding the same type twice, adding twice through a use) they assert the resulting count and that a usable instance is there — not that the very same object came back.

Monsters are used as the character under test so the hero's own skills stay untouched; the one test that has to drive the hero's real use path snapshots and restores them.

SkillSaveRoundTripTests

The save format is what a skill rename breaks silently: a save stores the skill's type name, so a renamed or deleted skill comes back as nothing at all, with no error anywhere. These tests round-trip the real save and load code (SaveHero(HeroController) → MakeHero(Party, HeroData, Vector3, Quaternion)) rather than a copy of it, so a change to either side has to keep working.

The hero that happens to be selected is used as the source but is left exactly as it was found: its skills are snapshotted and restored.

SkillStoneBehaviour
SkillUserBehaviour
SkipTestAttribute

Skip a test class or individual test method during DevTestRunner discovery. Skipped tests are logged but not executed.

Slash
SocketFillerBehaviour
SocketableBehaviour
SocketingRecipe
SongSkillBehaviour
SpearMastery
SpeechBehaviour
Spell

A castable spell: adds cast time, mana (or rune/reagent) cost and the spellbook requirement.

SpellBookBehaviour
SpellScroll
SpellScrollConfiguration
SpellScrollData

The payload types written by ISerializableComponent implementations - one per component, holding exactly the fields that component stores.

SpellScrollRecipe
StackExtensions

Extension methods for Stack to support Remove operation

StaminaBehaviour
StashTests

Tests for stash / chest container interactions. Verifies items can be placed in, retrieved from, and properly tracked in container-based storage (InventoryController + InventoryContainerBehaviour).

StatJunkieAchievement

Stat Junkie - Allocate 10,000 stat points. Min-maxing taken to its logical extreme.

StatModifySpell
StatPotionBehaviour
StatPotionSkill
StatueShrineEffect
SteamWorkshopService

Steam Workshop integration for GrindFest mods. Handles publishing, querying, subscribing, and installing Workshop items.

Uses Facepunch.Steamworks UGC API.

Well-known content folders included when publishing: UI/ (full source + dist/), Scripts/, Story/, Assets/, mod.json, preview.*

Excluded directories: node_modules, .git, .vs, .vscode, .idea, obj, bin

SteamWorkshopService.WorkshopMeta

Auto-generated metadata stored alongside the mod. Keeps mod.json clean (author-edited only).

StoryAssetTests

Every asset the story content names by hand must be registered in the content manifest.

Why this exists: the world generator resolves a prefab name with a dictionary lookup over ContentDB.AllAssets — the generated manifest — and never asks Addressables for a name. An asset that exists on disk but never made it into the manifest therefore cannot be reached at all, and the failure is invisible until the area that names it tries to build: 'Rock' (CanyonOfDeath.ink) logged an InvalidKeyException from the fallback path and left a mining cluster with an unresolved node prefab (measured 2026-09-19). Nothing about that is specific to Addressables — it is a gap between "named by content" and "registered as an asset", which is why the check reads the manifest and not the provider.

An audit run by hand found 3 of 83 references in that state; BP_City and Rock were real content gaps and were registered, while RoadSign_Post turned out to be an object inside the Road Sign Post prefab that this test was collecting as if it were an asset. This test is that audit, so it runs on every suite instead of when somebody notices a missing prop.

Editor-only, because it reads .ink sources out of Assets/.

StoryFlowBehaviour
StoryManager
StoryManager.FlowChoice
StoryManager.StoryFlow
StoryTriggerBehaviour
StunDebuff
SummonSkeleton
SwordMastery
TalkativeBotAchievement

Hello World - Your bot said 10,000 things. Does it ever shut up?

Taunt
TauntSkill
TeleportSpell
TestAlign
TestAttribute
TestCase
TestCharacterReactive

Test component that exercises the OneJS (v3) reactive contract with game-like properties.

Each backing field has a public property and an OnXChanged event, which is what TSX binds to with useEventSync(obj, 'Strength') (it calls obj.add_OnStrengthChanged and re-reads obj.Strength). The one exception is HealthPercent: a calculated property has no setter to hang an event on, so it only updates when its inputs do.

TheyWereBillionsAchievement
ThrowCollisionHandler
ThrowSkill
TillingSkill

Tills terrain at the hero's current position and spawns a CropContainerBehaviour. Requires a HoeBehaviour in inventory or equipped.

Bot API:

Hero.Till();
TimePlayedAchievement

Time is Just a Number - Total play time. Your bot doesn't need sleep.

TorchBehaviour

Handles torch-specific behavior when equipped/unequipped. Uses BurnableBehaviour for fire VFX management.

TotalPartyKillAchievement

Total Party Kill - Wipe your entire party 100 times. Teamwork makes the dream die.

TouchedByDeathAchievement

Touched by Death - Die again. And again. And again.

TownPortalSpell
TransmuteIntoGold
TreasureMapBehaviour
TreeBehaviour
TwitchManager
TwitchManager.WebSocketMessage
TwitchManager.WebSocketMessage.Metadata
TwitchManager.WebSocketMessage.Payload
TwitchManager.WebSocketMessage.Payload.Event
TwitchManager.WebSocketMessage.Payload.Event.Message
TwitchManager.WebSocketMessage.Payload.Session
TwitchPubSubListener
UIAchievementItem
UIAchievementNotification
UIAchievementTooltip
UIAllowedGitRepository
UIAsyncMultiplayerSettings
UIDiscordSettings
UIFastPath

FastPath registrations for the C# members the OneJS UI reads through the plain CS proxy.

The proxy resolves every property through reflection and allocates on each read, which the profiler shows up as RuntimeType.GetPropertiesByName / GetFields_internal on the JS -> C# interop path. FastPath replaces the reflection with a typed delegate, so a registered member costs nothing on the C# side.

Only register what the UI actually reads per tick, and only PRIMITIVES or Unity structs: strings and reference types allocate regardless of FastPath (UTF8 marshalling / handle + type hint), so registering those buys nothing. Values that are already driven by events (useEventSync / useObservableList) never go through the proxy either.

Registration is per TARGET TYPE, so one entry covers every instance of that type.

UIShopTooltip
UISkillCategory
UISkillList
UISkillListItem
UISkillTooltip
UITwichSettings
UIUnitTestItem
UIUnitTests
Ui

Player-like helpers for UI dev tests (OneJS / UI Toolkit).

The point is to behave like a player, so tests survive a UI rewrite:

  • locate elements by the text a player can SEE (not by internal field names),
  • interact by dispatching real UI Toolkit pointer events, so the whole dispatcher + bubbling + OneJS (RegisterCallback) handler + JS logic runs,
  • assert on what is visible on screen, not on internal state (visible means laid out with a real rect - see HasGeometry),

Which level to use: L2 (this class) UI panels - real events through the real dispatcher. L1 (call the JS API directly, e.g. ScriptEngine.Eval) for setup only, or for entry points that cannot be reached from a UI event at all (e.g. ESC: PlayerInput reads the legacy Input Manager, which UI events never feed - so the ESC tests start at handleEscape).

Positions matter: UI Toolkit hit-tests pointer events by position, so an event with a bogus position (0,0) is retargeted and never reaches the element you meant. All the helpers below aim at the target's worldBound centre for that reason.

UiClickThroughTests

Clicks through the whole UI the way a player does, so "the UI works" is a measurement instead of an impression.

Why this exists: every UI bug found on 2026-09-17 was invisible to the tests that existed. The harness asks whether a panel is displayed (display:flex, non-zero rect), which a DraggablePanel parked at its x=9999 fallback satisfies - six panels were off-screen and every check said they were open. It also sends pointer events straight to an element, so it never exercises the picking and hover paths a mouse uses, and it cannot see a bar that hangs below the panel. These tests answer the three questions that were missing: is it ON SCREEN, does the click reach it, and is the console still clean afterwards (the runner fails any test that logs an error it did not declare).

Clicks go through the same helpers as the other UI tests (Ui.Click / ClickText - real UI Toolkit pointer events on a real element), so a handler that only reacts to a mouse event type the v3 bridge never forwards fails here.

UiReloadTests

A UI reload must not cost the session its menu.

This guards a mechanism rather than a feature, because that mechanism is what makes two other tests red: ModManager.RefreshAndReload() ends with _uiModDirty = true, so ANY mod refresh - a player subscribing one, or a DevTest calling it - tears the OneJS UI down and re-evaluates app.js in the middle of a session.

Measured 2026-09-16: a full suite run in which that happens logs TWO [index.tsx] ... Initializing lines instead of one, and SettingsUiTests - both of them, the ESC one and the Settings-button one - fail from that point on with "main menu never opened after 6 ESC attempts". The same two tests pass in a session where nothing refreshed the UI (2 passed in 5,2 s), and they passed in every suite run before the reload happened twice. So the reload is the cause, and this is the smallest reproduction of it: open the menu, reload the UI, open the menu again.

UiSaveStats

Answers the character-creation screen's "have you ever reached X?" questions.

The UI used to reach into CS.GrindFest.SaveData.Current.TotalStatistics.<dictionary> and read a Dictionary key by key. OneJS v3's CS proxy resolves that kind of chained static member access badly: every lookup logged [QuickJS] Type not found: GrindFest.SaveData.Current.TotalStatistics.<member>, and the dev-test harness fails any test that logs an error - so the whole character-creation screen could not be verified at all. A plain static method call on a type is the shape the proxy handles well, so the questions are answered here instead.

Every method answers -1 when nothing is recorded. That is the honest equivalent of the JS tryGet(...).ok == false it replaces, and it cannot be mistaken for a satisfied requirement whose target happens to be 0.

UiSteam

The Steam facts the UI displays, read in a way that cannot throw.

Steamworks throws when it is not running - the editor, a build launched outside Steam, any dev machine - and the OneJS bridge logs every failed property read across the QuickJS boundary as an ERROR, even when the JavaScript catches it. The dev-test harness fails a test that logs an error, so simply showing the beta name was enough to fail every UI test here. These accessors answer an empty value instead, and nothing above them has to care whether Steam is up.

UnStreamArea
UniStormIntegration

Rebuilds UniStorm's weather effects and sounds after the hero they hang under has been destroyed.

Why this is not a patch inside "UniStorm Weather System": that folder is asset-store code, replaced wholesale on upgrade, so every line written into it is a merge conflict waiting for the next version. This file touches only the asset's public surface - the two holder fields, the five effect/sound/prefab lists and WeatherType.CreateWeatherEffect / CreateAdditionalWeatherEffect / CreateWeatherSound - and it leaves the asset's own scripts byte-identical to the shipped version (git diff on them is empty).

The behaviour it works around is inside the asset: InitializeUniStorm() parents the effect and sound holders under PlayerTransform, so destroying the player - a new hero, a party change, a save load - takes the holders and everything under them, while WeatherEffectsList / WeatherSoundsList keep the destroyed objects (Unity's fake null) and UniStormInitialized stays true, so nothing ever rebuilds them. The next weather change or hourly update then reads .name on those objects and throws MissingReferenceException, and the weather stays dead for the rest of the session. Measured in play 2026-09-16 after one hero swap: 12/12 effects and 17/17 sounds destroyed, both holders gone, UniStorm still enabled and still updating.

Everything the game does to UniStorm's player binding goes through this class: FollowHero for the hero that is now selected, StopFollowing for the other direction. Party.SelectHero and Party.ClearHeroes are the only callers, so no other file has to know that UniStorm keeps a player transform, a camera, two holders, a lightning copy and an enabled flag - or the order they have to be touched in.

A hero swap is normally just a change of target: ClearHeroes stops following before it destroys the hero, which detaches the holders so the destruction cannot take them, and SelectHero hands them to the new hero. The 12 effects and 17 sounds are never re-created for it. That matters for a Tab through the party as much as for speed: a rebuild would cut every sound mid-fade and restart every particle. Rebuilding is only the fallback for holders that were gone before anything could detach them - the first initialisation, a scene load or a save load.

The asset has its own version of the first method, UniStormManager.ChangeCameraSource, and it does the same assignments plus the lightning copy. It is not used because it also re-initialises the camera and the cloud shadows, and it returns early until UniStormInitialized - neither of which belongs in a hero swap in this game, where the party camera is shared and only the target changes.

One thing that must NOT be worked around here, because it cannot be: the orientation of the effects. WeatherType.CreateWeatherEffect instantiates with Quaternion.AngleAxis(-90, Vector3.right), and that is not cosmetic - every effect prefab is authored with its root rotated that way (Rain, Drizzle, Hail and Rain Fire are additionally scaled 1.81), and an emitter's shape is transformed together with its transform. Replacing that with Quaternion.identity and forcing transform.localScale = Vector3.one - which this project did in 2024-11, patched the same week with a localRotation copy, and then lost that patch again in 2026-04 - leaves the Hemisphere of Rain pointing up, so rain is emitted into the sky and reads as a flat sheet above the player, and turns the zero-thickness Box of Hail and Rain Fire from a horizontal plane into a vertical one. Box snow keeps looking right, which is why nobody noticed for a year. If the weather ever looks wrong again, check those two lines first - they must stay identical to the shipped asset, and the fix is to restore them, never to rotate or rescale the prefabs.

UnitTestManager
UnitySerializedDictionary<TKey, TValue>
UnlockSpell
UsableItemBehaviour
VBCompiler

Visual Basic compiler that inherits from the base Compiler class. Does not support top-level scripts or isolated execution.

VSCodeManager

Manages VSCode extraction from StreamingAssets zip file with version checking.

The bundled zip is the official VS Code archive downloaded from Microsoft: https://update.code.visualstudio.com/latest/win32-x64-archive/stable No customizations are applied — it's the vanilla VS Code zip. To update it, use the Unity editor menu: GrindFest > VS Code Bundle > Download Latest.

Platform support:

  • Windows: VSCode-win32-x64.zip → extracts → Code.exe (fully working)
  • Linux: VSCode-linux-x64.tar.gz → extracts via system tar → ./code

The extracted folder lives in persistentDataPath (LocalLow/GrindFest) — always writable, survives game updates, and sits next to Saves, Scripts and Mods. Only the archive ships with the build; StreamingAssets is read-only on most platforms and is replaced by updates, so nothing is ever extracted there.

WarCry
WarDrumSkill
WateringCanBehaviour

Marker component identifying an item as a Watering Can. Required by FarmingSkill to water planted farm plots.

WaveScaling
WeaponAirDamageAffix
WeaponAnimationSet
WeaponAttackSpeedAffix
WeaponBehaviour
WeaponDamageAffix
WeaponDeathDamageAffix
WeaponEarthDamageAffix
WeaponElementalDamageAffix
WeaponFireDamageAffix
WeaponIceDamageAffix
WeaponLightDamageAffix
WeaponLightningDamageAffix
WeaponMastery

Boosts one weapon type. The weapon type used to be data on four prefab variants (Axe / Mace / Spear / Sword Mastery), so each variant is a small subclass now.

WeaponPoisonDamageAffix
WeaponSoundSet
WeaponTypeExtensions
WetBuff

Marks the character as covered in a liquid. The payload used to live on a prefab and is now plain data on the instance the applier creates.

Whirlwind
WorkshopItemInfo
WorkshopQueryResult
WorldGeneratorTests

Tests for InkWorldGenerator + ContentParser. TODO: Re-implement — previous tests had issues with inactive renderers and missing MapMarker components.

WorldTileOverworldMonsterSpawner

Structs

ActionResult

Result of a bot action like PickUp, Equip, DropInto, Attack, or InteractWith.

Three core states:

  • Failed — The action could not be performed (item is null, out of range, etc.)
  • InProgress — The hero is working on it (walking to target, animating, etc.)
  • Done — The action completed this frame

Implicitly converts to bool: true if InProgress or Done, false if Failed. This makes it easy to use in priority loops:

if (PickUp(item)) return; // hero is busy picking up — don't start another action

For precise checks, compare with ==:

var result = PickUp(item);
    if (result == ActionResult.Done) Say("Got it!");
    if (result == ActionResult.InProgress) Say("Walking there...");
AsynchronousMultiplayerManager.ColorData
AsynchronousMultiplayerManager.InventoryItem
ComponentPayload.Reader

The reading half of ComponentPayload.Writer: TryRead returning false means this component has no payload in this save (or none that is readable), which callers treat as "nothing to restore".

ComponentPayload.Writer

One component's slot in the save: the payload dictionary plus the key that component owns. Handing this to Serialize(Writer) and Deserialize(Reader) is what keeps a component from having to know about keys, byte arrays or a missing entry - and it makes it impossible for the two directions to disagree about the key, which is exactly how the crop component's state silently stopped round-tripping.

DamageEntry
ModAssetCatalogs.LoadedCatalog

What a loaded mod contributed, for the browser and tests.

ModScriptCompiler.CompileResult

Result of compiling a single mod's scripts.

ModScriptCompiler.SecurityFinding

Represents a single security finding from the syntax tree scan.

PersistedItemData
Price
ShortScaleData

Allows for the value to be separated from the symbol; both can be styled separately.

TooltipItem

Interfaces

ICharacterResource
ICloseableUI

Interface for UI elements that can be closed with ESC

IItemDropConfiguration
ILockable
IMod

Entry point for native C# mods (D12). The game compiles every mod's Scripts/*.cs into an in-memory assembly, then instantiates each IMod implementation and calls OnLoad(ModDefinition) once, in mod load order, before the story/world is generated.

Mod code references the whole game assembly and runs in the same process — a mod has the game's full power (scripts are vetted by the security scan / trust store before compilation). An entry point that throws is logged and skipped; it never takes the boot down with it.

ISerializableComponent

A component that stores its own state in part of the save. It is handed its own slot (ComponentPayload.Writer / ComponentPayload.Reader) and deals only in its own typed payload - the key, the byte array and the missing-entry case all live in that slot.

Enums

AchievementCategory
AnchorPosition
AttackShape
BodyPart
CharacterBehaviour.GroundType
ClassRequirement.ClassRequirementType
DifficultyNames
Direction
DirectionMarker.MarkerStyle
Element
EquipmentSlot
FarmState

Growth state of a CropContainerBehaviour. Used as a filter parameter in FindNearest(Vector3, FarmState, float) and FindNearestFarmPlot(Vector3, FarmState, float).

FishSize
FishingSpotType
GameManager.ParentType
GameMode
GoToResult
HeroSex
InteractionKind
ItemRank
ItemTooltipOrder
LootCategory
ModContentFlags

Flags indicating which well-known content folders a mod contains.

MonsterCategory
MonsterRank
MoveResult
PlayerInput.ItemComparisonDecision
Price.GrowthType
Race
Relationship
ResourceType
SpawnTime
Stat
StatFlags
TargetType
TreeState
WeaponType
WorkshopSortOrder

Delegates

ActionRef<T>