feat(ui): AttrCombobox declare vs. consume semantics

Attr-string fields in the Custom Modifier Editor now differentiate
between declare-sites (seed-attribute.attr) and consume-sites
(add-to-attribute.attr, multiply-attribute.attr, add-aura.targetAttr,
absorb-damage-with-attribute.attr):

- Consume-sites hide the illustrative 'User-defined examples' group
  (ArmorPlates/BloodStacks/ManaPool) because those names are fiction
  unless something seeds them.
- Consume-sites surface a new 'Seeded in this descriptor' group
  populated from seed-attribute primitives elsewhere in the current
  tree (including inside trigger children via childPrimitives).
- Badge states: emerald 'seeded' when the typed value matches an
  in-tree seed; red 'not seeded' when it's a non-schema name with no
  backing seed; amber 'user-defined' only in declare-mode for
  off-catalog names.

Declare-mode behaviour is unchanged — inventing ShieldCharges there
still works without warnings.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-21 13:07:27 -06:00
commit 8605a22530
No known key found for this signature in database
5 changed files with 349 additions and 25 deletions

View file

@ -1368,11 +1368,94 @@ test.describe('T29 — Custom modifier DSL e2e', () => {
await page.getByTestId('attr-combobox-option-Hp').first().click();
await expect(input).toHaveValue('Hp');
// seed-attribute is a declare-site, so the illustrative examples
// (ArmorPlates, BloodStacks...) SHOULD be present.
await input.click();
await expect(
page.getByTestId('attr-combobox-option-ArmorPlates'),
).toBeVisible();
// Type a user-defined attr name — badge appears, dropdown filters down.
await input.fill('MyStacks');
await expect(combobox).toContainText('user-defined');
});
test('AttrCombobox consume-mode (add-aura.targetAttr) hides user-defined examples and warns on unseeded names', async ({
page,
}) => {
await freshLobby(page);
await openProfileEditor(page);
await openCustomModifierEditor(page);
// Add an add-aura primitive — its targetAttr field is a consume-site.
await page.getByTestId('custom-primitive-palette-add-aura').click();
await page.getByTestId('custom-primitive-node-0').click();
const input = page.getByTestId('primitive-add-aura-targetAttr-input');
await input.click();
const dropdown = page.getByTestId('primitive-add-aura-targetAttr-dropdown');
await expect(dropdown).toBeVisible();
// Illustrative "User-defined examples" group should NOT appear in
// consume-mode — those names (ArmorPlates, BloodStacks, ManaPool)
// are fiction unless a seed-attribute declares them.
await expect(dropdown).not.toContainText('User-defined examples');
await expect(
page.getByTestId('attr-combobox-option-ArmorPlates'),
).toHaveCount(0);
// Typing an unseeded attr raises the red "not seeded" badge.
await input.fill('MyGhostAttr');
const badge = page.getByTestId('primitive-add-aura-targetAttr-badge');
await expect(badge).toHaveText('not seeded');
});
test('AttrCombobox consume-mode surfaces attrs seeded by other primitives in the same descriptor', async ({
page,
}) => {
await freshLobby(page);
await openProfileEditor(page);
await openCustomModifierEditor(page);
// 1. Add a seed-attribute primitive that declares ShieldCharges=3.
await page.getByTestId('custom-primitive-palette-seed-attribute').click();
await page.getByTestId('custom-primitive-node-0').click();
const seedAttrInput = page.getByTestId('primitive-seed-attribute-attr-input');
await seedAttrInput.fill('ShieldCharges');
// Blur the input so the dropdown closes — the open dropdown is
// absolutely positioned and would intercept the next palette click.
await seedAttrInput.blur();
// 2. Add an absorb-damage-with-attribute primitive. Its attr field
// is a consume-site and should now offer ShieldCharges under
// a "Seeded in this descriptor" group.
await page
.getByTestId('custom-primitive-palette-absorb-damage-with-attribute')
.click();
await page.getByTestId('custom-primitive-node-1').click();
const absorbInput = page.getByTestId(
'primitive-absorb-damage-with-attribute-attr-input',
);
await absorbInput.click();
const dropdown = page.getByTestId(
'primitive-absorb-damage-with-attribute-attr-dropdown',
);
await expect(dropdown).toBeVisible();
await expect(dropdown).toContainText('Seeded in this descriptor');
await expect(
page.getByTestId('attr-combobox-option-ShieldCharges'),
).toBeVisible();
// Picking it commits the value and badge shows emerald "seeded".
await page.getByTestId('attr-combobox-option-ShieldCharges').click();
await expect(absorbInput).toHaveValue('ShieldCharges');
const absorbBadge = page.getByTestId(
'primitive-absorb-damage-with-attribute-attr-badge',
);
await expect(absorbBadge).toHaveText('seeded');
});
test('Templates picker loads a recipe into the editor', async ({ page }) => {
await freshLobby(page);
await openProfileEditor(page);