Conversation
@StatsStruct now synthesizes a nested ItemDefinition alongside dynamicKeys: one String? per stat plus code / displayName / tags / equipmentSlotCode / maxNumberOfOptionalStats / metadata. Both come from the same list of stored properties, so the descriptor and the stat keys cannot drift - adding a stat makes its column understood with no other edit. RPCache.loadItemDefinitions parses those cells and fills the item cache and the new lootTableItems cache. A cell's ! means "always applies": with no range there is nothing to vary so it is a fixed stat on the item, with a range it is a required variation. Everything else is optional, a bare number becoming a degenerate range. An unknown stat key throws rather than being dropped. RPLootTableItem loses Kind and RPLootVariation in favour of requiredVariations, optionalVariations and maxNumberOfOptionalStats; an entry with neither array is implicitly fixed. Required variations bypass the chance roll and the cap, and optionals are now drawn as a uniform random subset rather than a declaration-order prefix. Also restores the test target, which had not compiled since f564c0d removed the RPSpaceDictionary protocol TestRPSpace conformed to. 148 tests now run with one pre-existing failure, logged in known-issues.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
testCellsRouteToFixedRequiredAndOptional only asserted optionalVariations.count, so emitting the same stat twice or swapping two of them passed. And nothing checked that a degenerate range leaves variableStats nil rather than zero, which is what makes rolledStats skip it instead of rolling 0...0. Both new tests were verified by mutation: reversing the key iteration order and always emitting a ceiling each turn them red, and the ceiling mutation is caught by nothing else in the suite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| `TestRPSpace` conformed to, so nothing in `Tests/` had run since. Restoring | ||
| that conformance surfaced this assertion, which had drifted from the global | ||
| cooldown's current behaviour in the meantime. | ||
| - **Fix direction:** decide what the global cooldown should do when an ability |
There was a problem hiding this comment.
global cooldown deliberately prevents any other action until it is complete
There was a problem hiding this comment.
Fixed - this was a stale magic number, not a design question. The test hardcoded 2500ms but RPBody.globalCooldown defaults to 500. Now reads rpSpace.bodies[body.id]!.globalCooldown instead of the literal, so it ticks cooldown - 1 then 1 regardless of what the default is. 150/150 green in 1dff6b5, and the known-issues.md entry is removed since it's no longer open.
| } | ||
|
|
||
| public init(parsing cell: String) throws { | ||
| var text = Self.trimmed(Substring(cell)) |
There was a problem hiding this comment.
I've dicided to reverse things where a "?" at the end means optional and if not its required. It feels like a more natural way to express it and more consistent with swift anyway.
There was a problem hiding this comment.
Flipped in 1dff6b5 - isRequired now defaults to true, and a trailing ? sets it false. Only init(parsing:) changed; everything downstream (RPCache+ItemDefinitions.swift, the routing switch) reads isRequired/isFixed and never touched the symbol, so the flip was contained to one file plus its tests.
Per review on #9: a cell's marker is now "?" for optional, unmarked for required - the opposite of the initial "!" convention. No range and no marker is a fixed stat; no range with "?" is an optional degenerate range. Only RPItemDefinitionStatVariation.init(parsing:) and its tests change - nothing downstream of RPItemDefinitionStatVariation cared which symbol or which default it was. Also per review: the global cooldown gate is correct as designed (it deliberately blocks every other action until it completes) - test_global_cooldown_gates_actions_even_when_the_ability_is_ready was failing on a stale magic number, asserting against a hardcoded 2500 while RPBody.globalCooldown defaults to 500. Reading the body's own globalCooldown instead of hardcoding fixes it - 150 tests now run, 0 failures, so the known-issues.md entry is removed rather than updated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Two pieces, both driven by moving game data to CSV.
1. A variable-item descriptor bound to
RP.Stats@StatsStructnow synthesizes a nestedItemDefinitionalongsidedynamicKeys: oneString?per stat, pluscode/displayName/tags/equipmentSlotCode/maxNumberOfOptionalStats/metadata. Both come from the same list of stored properties, so the descriptor and the stat keys cannot drift — adding a stat makes its CSV column understood with no other edit. It's generic overMetadatarather than taking a macro argument, so@StatsStructkeeps its no-argument form.RPCache.loadItemDefinitionsparses the cells and fills the item cache and the newlootTableItemscache. A cell's!means always applies:1!RPItem— nothing to vary5-10!requiredVariations,5...105optionalVariations,5...51-2optionalVariations,1...2An unknown stat key throws rather than being silently dropped.
Ranges needed no new type in the loot model:
RPFragmentVariationalready had base stats plus avariableStatsceiling rolled0...ceilingand added on, so5-10is base 5 with ceiling 5, and a degenerate5...5is base 5 with a nil ceiling so the roll is skipped entirely.RPItemDefinitionStatVariationexists only at the parsing seam.2.
RPLootTableItemlosesKindKind(.fixed/.variant) andRPLootVariationare gone, replaced byrequiredVariations,optionalVariationsandmaxNumberOfOptionalStats. An entry with neither array is implicitly fixed, via initializer defaults. One variation per stat cell, so the cap counts stats and fragments interchangeably — resolving the ambiguitymaximumFragmentshad.Required variations bypass both the chance roll and the cap. Optionals are now drawn as a uniform random subset rather than a declaration-order prefix; a failed chance roll still doesn't consume the cap. No shipping data carried a
variationblock, so there was nothing to migrate.Test target restored
Tests/had not compiled sincef564c0dremoved theRPSpaceDictionaryprotocolTestRPSpaceconformed to — the same commit that added loot tables — so nothing in RPTrunk had run since. Restoring the conformance (dictionary-backed storage methods,cache,CodingKeysto keepcacheout ofCodable) brings the suite back, and adds arandomRulehook besidechanceRuleso the optional draw is deterministic.Verification
swift test— 150 tests, 1 failure. The failure isBodyTests.test_global_cooldown_gates_actions_even_when_the_ability_is_ready, pre-existing and newly visible rather than newly broken; logged inknown-issues.md.13 of those are new, covering cell parsing, the fixed/required/optional routing, an unknown key throwing, and the roll. The two that pin which stat lands in which optional variation and its bounds were verified by mutation: reversing the key iteration order and always emitting a ceiling each turn them red, and the ceiling mutation is caught by nothing else in the suite.
Downstream
dungeon-cleaners#equipment-lootconsumes this by local path and must land together — it usesRoleplayingStats.ItemDefinitionandloadItemDefinitions.🤖 Generated with Claude Code