Skip to content

Variable item definitions, and a flatter loot table item - #9

Draft
bitwit wants to merge 3 commits into
developfrom
item-definitions
Draft

bitwit wants to merge 3 commits into
developfrom
item-definitions

Conversation

@bitwit

@bitwit bitwit commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Two pieces, both driven by moving game data to CSV.

1. A variable-item descriptor bound to RP.Stats

@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 CSV column understood with no other edit. It's generic over Metadata rather than taking a macro argument, so @StatsStruct keeps its no-argument form.

RPCache.loadItemDefinitions parses the cells and fills the item cache and the new lootTableItems cache. A cell's ! means always applies:

Cell Lands as
1! fixed stat on the RPItem — nothing to vary
5-10! requiredVariations, 5...10
5 optionalVariations, 5...5
1-2 optionalVariations, 1...2

An unknown stat key throws rather than being silently dropped.

Ranges needed no new type in the loot model: RPFragmentVariation already had base stats plus a variableStats ceiling rolled 0...ceiling and added on, so 5-10 is base 5 with ceiling 5, and a degenerate 5...5 is base 5 with a nil ceiling so the roll is skipped entirely. RPItemDefinitionStatVariation exists only at the parsing seam.

2. RPLootTableItem loses Kind

Kind (.fixed/.variant) and RPLootVariation are gone, replaced by requiredVariations, optionalVariations and maxNumberOfOptionalStats. 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 ambiguity maximumFragments had.

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 variation block, so there was nothing to migrate.

Test target restored

Tests/ had not compiled since f564c0d removed the RPSpaceDictionary protocol TestRPSpace conformed to — the same commit that added loot tables — so nothing in RPTrunk had run since. Restoring the conformance (dictionary-backed storage methods, cache, CodingKeys to keep cache out of Codable) brings the suite back, and adds a randomRule hook beside chanceRule so the optional draw is deterministic.

Verification

swift test150 tests, 1 failure. The failure is BodyTests.test_global_cooldown_gates_actions_even_when_the_ability_is_ready, pre-existing and newly visible rather than newly broken; logged in known-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-loot consumes this by local path and must land together — it uses RoleplayingStats.ItemDefinition and loadItemDefinitions.

🤖 Generated with Claude Code

bitwit and others added 2 commits September 2, 2026 23:25
@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>
Comment thread known-issues.md Outdated
`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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

global cooldown deliberately prevents any other action until it is complete

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant