diff --git a/database/schema.sql b/database/schema.sql index 7aea0ba..9e8a9c9 100644 --- a/database/schema.sql +++ b/database/schema.sql @@ -42,7 +42,7 @@ CREATE UNIQUE INDEX IF NOT EXISTS stat_attributes_name_nocase_idx ON stat_attrib CREATE TABLE IF NOT EXISTS talents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, - category TEXT NOT NULL CHECK(category IN ('Weapon', 'Backpack', 'Vest')), + category TEXT NOT NULL CHECK(category IN ('Weapon', 'Backpack', 'Body Armor', 'Vest')), active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL @@ -91,3 +91,51 @@ CREATE TABLE IF NOT EXISTS talent_tier_effects ( created_at TEXT NOT NULL, UNIQUE(talent_tier_id, stat_attribute_id) ); +CREATE TABLE IF NOT EXISTS brand_sets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL +); +CREATE UNIQUE INDEX IF NOT EXISTS brand_sets_name_nocase_idx ON brand_sets(name COLLATE NOCASE); +CREATE TABLE IF NOT EXISTS brand_set_effects ( + id TEXT PRIMARY KEY, + brand_set_id TEXT NOT NULL REFERENCES brand_sets(id) ON DELETE CASCADE, + pieces_required INTEGER NOT NULL CHECK(pieces_required IN (2, 3, 4)), + stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), + value REAL NOT NULL CHECK(value >= 0), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + UNIQUE(brand_set_id, pieces_required) +); +CREATE TABLE IF NOT EXISTS gear ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + slot TEXT NOT NULL CHECK(slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), + tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), + level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 50), + base_health_min REAL NOT NULL CHECK(base_health_min >= 0), + base_health_max REAL NOT NULL CHECK(base_health_max >= base_health_min), + base_armor_min REAL, + base_armor_max REAL, + active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL, + CHECK( + (slot IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NOT NULL AND base_armor_max IS NOT NULL AND base_armor_min >= 0 AND base_armor_max >= base_armor_min) + OR + (slot NOT IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NULL AND base_armor_max IS NULL) + ) +); +CREATE UNIQUE INDEX IF NOT EXISTS gear_name_nocase_idx ON gear(name COLLATE NOCASE); +CREATE TABLE IF NOT EXISTS gear_brand_sets ( + gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, + brand_set_id TEXT NOT NULL REFERENCES brand_sets(id), + PRIMARY KEY(gear_id, brand_set_id) +); +CREATE TABLE IF NOT EXISTS gear_talents ( + gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, + talent_id TEXT NOT NULL REFERENCES talents(id), + PRIMARY KEY(gear_id, talent_id) +); diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 28091b0..1fa05cf 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -87,6 +87,15 @@ Use the spacing tokens for new components. Prefer `8px`, `12px`, and `20px` gaps - Editors use `dashboard-editor` and appear before the affected table. - Data collections use `dashboard-table`, `dashboard-row`, and `table-heading`. - Keep tables dense and horizontally scrollable rather than wrapping columns unpredictably. +- Threshold-based set bonuses use compact `effect-band` groups. Each threshold is + displayed as a fixed piece-count marker followed by its mapped stat and value, + keeping progression readable in editors and table summaries. +- Multi-value compatibility fields use a dense `selection-grid` of checkbox + rows with a visible selected-count summary. Keep the entire row clickable and + preserve native checkbox semantics for keyboard and assistive-technology use. +- When compatibility is conditional on another field, show only valid choices, + clear stale selections when the controlling field changes, and state required + minimum selections in both the section label and validation feedback. ### Forms diff --git a/src/app.css b/src/app.css index 45c6c12..1448483 100644 --- a/src/app.css +++ b/src/app.css @@ -62,3 +62,7 @@ .discord-connection{display:grid;grid-template-columns:1fr;gap:9px;padding:10px}.connection-copy{display:flex!important;align-items:center;gap:10px}.connection-copy>img{width:28px;height:21px;padding:4px;background:#5865f2;border-radius:3px}.discord-link-button{min-height:42px;padding:8px 14px;display:flex;align-items:center;justify-content:center;gap:12px;background:#5865f2;color:#fff!important;border-radius:4px;font-size:14px!important;font-weight:600!important;letter-spacing:0;text-transform:none}.discord-link-button img{display:block;width:25px;height:19px;object-fit:contain}.discord-link-button:hover{background:#4752c4;filter:none}.discord-link-button:active{background:#3c45a5;filter:none}.discord-link-button:focus-visible{outline:2px solid #e0e3ff;outline-offset:3px}.discord-link-button[aria-busy="true"]::after{content:"";width:11px;height:11px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite} +.brand-name{display:block;max-width:420px;margin:10px 0}.effect-editor{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}.effect-band{display:grid;grid-template-columns:58px minmax(0,1fr) 100px;align-items:end;gap:8px;padding:9px;background:#101314;border:1px solid #343a3b}.effect-band>strong{align-self:stretch;display:grid;place-content:center;text-align:center;color:#f6a821;font-size:23px;border-right:1px solid #343a3b}.effect-band>strong small{display:block;color:#707779;font-size:6px;letter-spacing:1px}.effect-band .field{margin:0}.split-actions{justify-content:space-between}.split-actions>div{display:flex;gap:8px}.brand-set-row{grid-template-columns:1.1fr 1.2fr 1.2fr 1.2fr .6fr .3fr}.empty-dashboard{min-height:120px;display:grid;place-content:center;text-align:center;gap:6px;color:#626a6c}.empty-dashboard strong{font-size:10px;letter-spacing:1.2px}.empty-dashboard span{font-size:8px} +.gear-identity{display:grid;grid-template-columns:2fr 1fr .65fr .65fr;gap:8px}.gear-ranges{display:grid;grid-template-columns:1fr 1fr;gap:8px}.range-band{grid-template-columns:58px 1fr 1fr}.effect-band.unavailable{opacity:.4}.selection-head{display:flex;justify-content:space-between;align-items:end;margin:14px 0 6px}.selection-head>div{display:grid}.selection-head span{font-size:8px;font-weight:bold;letter-spacing:1.2px}.selection-head small{font-size:7px;color:#697174;margin-top:3px}.selection-head b{color:#f6a821;font-size:8px;letter-spacing:1px}.selection-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));border:1px solid #303536}.selection-grid label{min-height:38px;display:grid;grid-template-columns:20px 1fr auto;align-items:center;gap:5px;padding:6px 8px;border:1px solid transparent;border-right-color:#282d2e;border-bottom-color:#282d2e;cursor:pointer;font-size:9px}.selection-grid label:hover{background:#171b1c;border-color:#4b4f50}.selection-grid input{width:auto;margin:0;accent-color:#f6a821}.selection-grid small{font-size:6px;color:#646c6e;letter-spacing:.8px}.selection-grid input:checked~small{color:#75c08c}.gear-row{min-width:1050px;grid-template-columns:1.2fr .8fr .8fr 1.35fr .8fr .6fr .3fr}.gear-row>span small{font-size:8px;color:#858b8d} +@media(max-width:1100px){.effect-editor{grid-template-columns:1fr}.selection-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.gear-identity,.gear-ranges{grid-template-columns:1fr 1fr}}@media(max-width:560px){.effect-band{grid-template-columns:48px 1fr}.effect-band .field:last-child{grid-column:2}.split-actions{align-items:stretch}.split-actions,.split-actions>div{width:100%}.split-actions>div:last-child button{flex:1}.gear-identity,.gear-ranges,.selection-grid{grid-template-columns:1fr}.range-band{grid-template-columns:48px 1fr}.range-band .field:last-child{grid-column:2}} + diff --git a/src/routes/admin/+layout.svelte b/src/routes/admin/+layout.svelte index 9bf37ff..42dc964 100644 --- a/src/routes/admin/+layout.svelte +++ b/src/routes/admin/+layout.svelte @@ -4,7 +4,8 @@ const sections = [ { href: '/admin/stats', label: 'Stats & attributes', code: 'ATTR' }, { href: '/admin/talents', label: 'Talents & triggers', code: 'TLNT' }, - { href: '/admin/equipment', label: 'Equipment types', code: 'EQPT', pending: true }, + { href: '/admin/brands', label: 'Brand sets', code: 'BRND' }, + { href: '/admin/gear', label: 'Gear', code: 'GEAR' }, { href: '/admin/weapons', label: 'Weapons', code: 'WPN', pending: true } ]; @@ -16,7 +17,7 @@