diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 1169c11..0354ab1 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -1245,6 +1245,13 @@ sets such as Brands and Talents. Always expose selected counts, a single clear action, `aria-pressed` state, keyboard focus, and explicit text explaining the combination rule. At intermediate width, the bounded facet spans the row above two searchable facets; on narrow screens all facet groups stack vertically. +Weapon discovery applies this pattern to weapon-family icons, damage-type +icons, and the searchable pool of compatible Talents. Talent facet rows show +the rendered Tier I description beneath the name. Talent searches match both +fields case-insensitively so mechanical phrases can discover talents even when +the talent name is unknown. Brand facets similarly index the names of all +set-bonus Attributes, allowing players to search by the desired mechanic +without exposing that metadata as extra visual clutter. ### Build persistence and sharing diff --git a/src/app.css b/src/app.css index 5d28cea..3cbe6f2 100644 --- a/src/app.css +++ b/src/app.css @@ -204,6 +204,8 @@ .database-category-page>.registry-pagination{margin:10px 0}.database-item-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:9px}.database-item-card{min-width:0;display:flex;flex-direction:column;border:1px solid var(--shd-border);border-top:2px solid #404748;background:#111415}.database-item-card>header{height:auto;min-height:78px;display:grid;grid-template-columns:58px minmax(0,1fr);align-items:center;gap:12px;padding:11px 13px;border-bottom:1px solid var(--shd-border)}.database-item-icon{width:58px;height:58px;display:grid;place-items:center;border:1px solid #343a3b;background:#0c0f10}.database-item-icon :is(img,.game-icon){width:52px;height:52px;object-fit:contain}.database-item-card>header small{display:block;margin-bottom:5px;color:#697174;font-size:6px;font-weight:900;letter-spacing:.8px;text-transform:uppercase}.database-item-card h2{overflow-wrap:anywhere;margin:0;font-size:14px;line-height:1.1}.database-item-badges{display:flex;flex-wrap:wrap;gap:5px;padding:8px 12px 0}.database-item-badges span{padding:5px 7px;border:1px solid #424849;background:#171b1c;color:#a5acae;font-size:6px;font-weight:900;letter-spacing:.6px;text-transform:uppercase}.database-item-badges .exotic{border-color:#74552a;background:#1a160d;color:var(--quality-high-end)}.database-item-badges :is(.standard,.specialized,.superior,.highend){border-left-width:2px}.database-item-badges .standard{border-left-color:var(--quality-standard)}.database-item-badges .specialized{border-left-color:var(--quality-specialized)}.database-item-badges .superior{border-left-color:var(--quality-superior)}.database-item-badges .highend{border-left-color:var(--quality-high-end)}.database-item-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(90px,1fr));margin:9px 12px 0;border:1px solid var(--shd-border)}.database-item-facts>div{min-width:0;display:grid;align-content:center;gap:4px;min-height:53px;padding:7px;border-left:1px solid var(--shd-border)}.database-item-facts>div:first-child{border-left:0}.database-item-facts span,.database-item-lines span{color:#626a6c;font-size:5px;font-weight:900;letter-spacing:.65px}.database-item-facts b{overflow-wrap:anywhere;color:#d2d6d3;font-size:8px;line-height:1.35}.database-item-lines{display:grid;gap:1px;margin:7px 12px 0;border-top:1px solid var(--shd-border)}.database-item-lines>div{display:grid;grid-template-columns:110px 1fr;gap:9px;padding:8px 0;border-bottom:1px solid var(--shd-border)}.database-item-lines b{overflow-wrap:anywhere;font-size:7px;line-height:1.4}.database-item-card>p{margin:9px 12px 0;padding:9px;border-left:2px solid var(--shd-orange);background:#0d1011;color:#a2a8aa;font:10px/1.55 Georgia,serif}.database-item-card>p.secondary-description{margin-top:5px;border-left-color:#5c6466}.database-item-card>:last-child{margin-bottom:12px}.database-empty{grid-column:1/-1;display:grid;justify-items:center;gap:9px;padding:70px 24px;border:1px solid var(--shd-border);background:#101314;text-align:center}.database-empty>span{color:var(--shd-orange);font-size:30px}.database-empty strong{font-size:10px;letter-spacing:1px}.database-empty p{margin:0;color:#7e8688;font:11px/1.5 Georgia,serif} .database-item-facts.stacked{grid-template-columns:1fr}.database-item-facts.stacked>div{grid-template-columns:82px minmax(0,1fr);align-items:center;min-height:43px;border-left:0;border-top:1px solid var(--shd-border)}.database-item-facts.stacked>div:first-child{border-top:0}.database-item-facts.stacked b{color:var(--shd-orange)} .database-item-actions{display:flex;flex-wrap:wrap;gap:6px;margin:10px 12px 12px;padding-top:9px;border-top:1px solid var(--shd-border)}.database-item-actions>a{min-height:34px;display:inline-flex;align-items:center;justify-content:center;padding:0 11px;border:1px solid #74552a;background:#1b150c;color:var(--shd-orange);font-size:6px;font-weight:900;letter-spacing:.65px}.database-item-actions>a:hover,.database-item-actions>a:focus-visible{border-color:var(--shd-orange);background:#261b0b;color:#ffd19b}.database-item-actions>a:active{transform:translateY(1px);filter:brightness(.82)} +.database-facet-group>div>button i{width:6px;height:6px;border:1px solid #596164;background:#171b1c;transform:rotate(45deg)}.database-facet-group>div>button.selected i{border-color:var(--shd-orange);background:var(--shd-orange);box-shadow:0 0 6px color-mix(in srgb,var(--shd-orange) 65%,transparent)} +.database-facet-group.searchable>div{max-height:220px}.database-facet-group.searchable>div>button:has(span small){min-height:58px;align-items:flex-start}.database-facet-group.searchable>div>button>span{display:grid;gap:3px;white-space:normal;text-align:left}.database-facet-group.searchable>div>button>span>small{display:-webkit-box;overflow:hidden;color:#747c7e;font:8px/1.35 Georgia,serif;-webkit-box-orient:vertical;-webkit-line-clamp:2}.database-facet-group.searchable>div>button>span>small>b{margin-right:5px;color:var(--shd-orange);font:5px/1 monospace;letter-spacing:.5px} .database-item-icon-groups{display:grid;gap:8px;margin:8px 12px 0}.database-item-icon-groups>div{display:grid;grid-template-columns:110px minmax(0,1fr);align-items:center;gap:9px;padding-top:8px;border-top:1px solid var(--shd-border)}.database-item-icon-groups>div>span{color:#626a6c;font-size:5px;font-weight:900;letter-spacing:.65px}.item-icon-rail{min-width:0;display:flex;flex-wrap:wrap;gap:5px}.item-icon-rail>button{width:34px;height:34px;display:grid;place-items:center;padding:0;border:1px solid #394041;background:#0d1011;cursor:pointer}.item-icon-rail>button:hover,.item-icon-rail>button:focus-visible{border-color:var(--shd-orange);background:#211a0e}.item-icon-rail>button:active{transform:translateY(1px);filter:brightness(.82)}.item-icon-rail img{width:30px;height:30px;object-fit:contain}.item-icon-rail small{color:#687073;font-size:6px;font-weight:900;letter-spacing:.55px}.database-preview-backdrop{position:fixed;z-index:1200;inset:0;display:grid;place-items:center;padding:20px;background:rgba(3,5,5,.82);backdrop-filter:blur(3px)}.database-preview-dialog{width:min(520px,100%);max-height:calc(100vh - 40px);overflow:auto;border:1px solid #4b5254;border-top:2px solid var(--shd-orange);background:#0b0e0f;box-shadow:0 18px 60px #000}.database-preview-dialog>header{height:auto;display:flex;align-items:center;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--shd-border);background:#15191a}.database-preview-dialog>header>div{display:grid;gap:3px}.database-preview-dialog>header small{color:#697174;font-size:5px;letter-spacing:.8px}.database-preview-dialog>header strong{font-size:10px;letter-spacing:.45px}.database-preview-dialog>header button{width:32px;height:32px;padding:0;border:1px solid #434a4c;background:#101314;color:#aab0b2;font-size:20px;line-height:1;cursor:pointer}.database-preview-dialog>header button:hover,.database-preview-dialog>header button:focus-visible{border-color:var(--shd-orange);color:var(--shd-orange)}.database-preview-dialog>.database-item-card{margin:10px;border-top-color:#404748} @media(max-width:1200px){.database-category-nav{grid-template-columns:repeat(4,1fr)}.database-category-nav>a:nth-child(5){border-left:0}.database-category-nav>a:nth-child(n+5){border-top:1px solid var(--shd-border)}.database-facet-groups{grid-template-columns:1fr 1fr}.database-facet-group:first-child{grid-column:1/-1}.database-item-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.database-category-command{align-items:stretch;flex-direction:column}.database-category-command>div:last-child{justify-items:start}.database-category-nav{grid-template-columns:repeat(2,1fr)}.database-category-nav>a:nth-child(odd){border-left:0}.database-category-nav>a:nth-child(n+3){border-top:1px solid var(--shd-border)}.database-facet-panel>header{align-items:flex-start;flex-direction:column}.database-facet-groups{grid-template-columns:1fr}.database-facet-group:first-child{grid-column:auto}.database-facet-group>div{grid-template-columns:repeat(2,minmax(0,1fr))}.database-item-grid{grid-template-columns:1fr}.database-item-card>header{grid-template-columns:52px minmax(0,1fr)}.database-item-icon{width:52px;height:52px}.database-item-icon :is(img,.game-icon){width:46px;height:46px}}@media(prefers-reduced-motion:reduce){.database-registry-grid>a{transition:none}} .community-metrics{gap:38px}.community-metrics strong{color:var(--shd-orange);font-size:15px;letter-spacing:1px}.community-preview .gear-node{width:98px;color:var(--shd-text)}.community-preview .gear-node:hover,.community-preview .gear-node:focus-visible{border-color:var(--shd-orange);background:#211a0e}.community-preview .gear-node:active{transform:translateY(1px);filter:brightness(.86)}.community-preview .gear-node b{font-size:7px;letter-spacing:.55px}.community-channels{padding:58px max(24px,5vw) 70px;border-top:1px solid var(--shd-border);background:#0c0f10}.community-channels>.section-heading{max-width:1240px}.community-channels>.section-heading>p{max-width:310px;margin:0;color:var(--shd-text-muted);font:12px/1.5 Georgia,serif;text-align:right}.community-channel-grid{max-width:1240px;display:grid;grid-template-columns:repeat(4,1fr);margin:auto;border:1px solid var(--shd-border)}.community-channel-grid>a{min-height:205px;display:flex;flex-direction:column;padding:20px;border-left:1px solid var(--shd-border);background:#111415;color:var(--shd-text)}.community-channel-grid>a:first-child{border-left:0}.community-channel-grid>a:hover,.community-channel-grid>a:focus-visible{background:#171b1c;box-shadow:inset 0 2px var(--shd-orange)}.community-channel-grid>a:active{filter:brightness(.84)}.community-channel-grid small{color:#626a6c;font-size:6px;letter-spacing:1px}.community-channel-grid strong{margin:18px 0 9px;font-size:14px;letter-spacing:.6px}.community-channel-grid span{color:#858d8f;font:11px/1.55 Georgia,serif}.community-channel-grid b{margin-top:auto;padding-top:17px;color:var(--shd-orange);font-size:7px;letter-spacing:.8px}.community-index-page{padding-top:34px}.community-index-hero{margin-bottom:12px}.community-database-intro{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px}.community-database-intro>div{display:grid;grid-template-columns:150px 1fr 1.4fr;align-items:center;gap:12px;min-height:65px;padding:10px 13px;border:1px solid var(--shd-border);background:#101314}.community-database-intro span{color:#666e70;font-size:6px;letter-spacing:1px}.community-database-intro strong{color:var(--shd-orange);font-size:8px;letter-spacing:.7px}.community-database-intro small{color:#7f8789;font:10px/1.45 Georgia,serif}.database-registry-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:9px}.database-registry-grid article{min-height:180px;display:flex;flex-direction:column;padding:17px;border:1px solid var(--shd-border);border-top:2px solid #43494a;background:#111415}.database-registry-grid article:hover{border-top-color:var(--shd-orange);background:#15191a}.database-registry-grid header{height:auto;display:flex;justify-content:space-between}.database-registry-grid header small{color:#596164;font-size:6px;letter-spacing:.8px}.database-registry-grid header b{color:var(--shd-orange);font-size:20px}.database-registry-grid h2{margin:18px 0 8px;font-size:16px}.database-registry-grid p{color:#858d8f;font:11px/1.55 Georgia,serif}.database-registry-grid>article>span{margin-top:auto;color:#687073;font-size:6px;font-weight:900;letter-spacing:.8px}.news-channel-grid{display:grid;grid-template-columns:minmax(0,1fr) 330px;gap:10px}.news-primary{min-height:390px;padding:30px;border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:linear-gradient(135deg,#151819,#0d1011)}.news-primary>div:first-child{display:flex;justify-content:space-between;padding-bottom:13px;margin-bottom:30px;border-bottom:1px solid var(--shd-border);color:#626a6c;font-size:6px;letter-spacing:1px}.news-primary>div:first-child b{color:var(--shd-orange)}.news-primary h2{max-width:720px;margin:19px 0;font-size:clamp(30px,4vw,52px);line-height:.95}.news-primary>p:not(.eyebrow){max-width:760px;color:#969d9f;font:14px/1.7 Georgia,serif}.news-primary footer{display:flex;gap:7px;margin-top:35px}.news-primary footer span{display:grid;padding:8px 10px;border:1px solid var(--shd-border);color:#626a6c;font-size:6px;letter-spacing:.8px}.news-primary footer b{margin-top:4px;color:#d5d8d5;font-size:8px}.news-channel-grid>aside{padding:19px;border:1px solid var(--shd-border);background:#111415}.news-channel-grid>aside>div{min-height:58px;display:grid;grid-template-columns:30px 1fr auto;align-items:center;border-top:1px solid var(--shd-border)}.news-channel-grid>aside small{color:#596164;font-size:7px}.news-channel-grid>aside span{font-size:8px;letter-spacing:.7px}.news-channel-grid>aside b{color:#6e7678;font-size:6px}@media(max-width:1100px){.community-channel-grid{grid-template-columns:1fr 1fr}.community-channel-grid>a:nth-child(3){border-left:0;border-top:1px solid var(--shd-border)}.community-channel-grid>a:nth-child(4){border-top:1px solid var(--shd-border)}.community-database-intro{grid-template-columns:1fr}.database-registry-grid{grid-template-columns:1fr 1fr}}@media(max-width:750px){.community-channels>.section-heading>p{display:none}.community-channel-grid,.database-registry-grid,.news-channel-grid{grid-template-columns:1fr}.community-channel-grid>a{min-height:155px;border-left:0;border-top:1px solid var(--shd-border)}.community-channel-grid>a:first-child{border-top:0}.community-database-intro>div{grid-template-columns:1fr}.news-primary{padding:22px}.news-primary footer{flex-wrap:wrap}}@media(max-width:430px){.site-header .brand-name small{display:block;font-size:4px;letter-spacing:.7px}} diff --git a/src/lib/components/DatabaseFacetFilters.svelte b/src/lib/components/DatabaseFacetFilters.svelte index 4d7a4de..2b746b0 100644 --- a/src/lib/components/DatabaseFacetFilters.svelte +++ b/src/lib/components/DatabaseFacetFilters.svelte @@ -5,7 +5,7 @@ import ItemIcon from './ItemIcon.svelte'; import type { GameIconName } from '$lib/game-icons'; - type Option = { value: string; label: string; iconUrl?: string; gameIcon?: GameIconName }; + type Option = { value: string; label: string; description?: string; searchText?: string; iconUrl?: string; gameIcon?: GameIconName }; type Group = { key: string; label: string; kind: 'segmented' | 'searchable'; options: Option[]; selected: string[] }; let { groups }: { groups: Group[] } = $props(); let queries = $state>({}); @@ -26,7 +26,10 @@ }; const visible = (group: Group) => { const query = (queries[group.key] ?? '').normalize('NFKC').trim().toLocaleLowerCase(); - return group.options.filter((option) => !query || option.label.toLocaleLowerCase().includes(query)); + return group.options.filter((option) => !query + || option.label.toLocaleLowerCase().includes(query) + || option.description?.toLocaleLowerCase().includes(query) + || option.searchText?.toLocaleLowerCase().includes(query)); }; const selectedCount = $derived(groups.reduce((total, group) => total + group.selected.length, 0)); @@ -44,7 +47,7 @@ {#each visible(group) as option} {:else} NO MATCHING OPTIONS diff --git a/src/routes/database/[category]/+page.server.ts b/src/routes/database/[category]/+page.server.ts index c85685a..87d3e24 100644 --- a/src/routes/database/[category]/+page.server.ts +++ b/src/routes/database/[category]/+page.server.ts @@ -14,7 +14,7 @@ const valueWithUnit = (value: unknown, unit: unknown) => `${number(value)}${unit const range = (min: unknown, max: unknown, unit: unknown = '') => `${valueWithUnit(min, unit)}–${valueWithUnit(max, unit)}`; const list = (value: unknown, fallback = 'NONE') => typeof value === 'string' && value ? value : fallback; const gearIcon = (slot: unknown) => String(slot).toLowerCase().replaceAll(' ', '_') as GameIconName; -const weaponIcon = (category: unknown) => String(category).toLowerCase(); +const weaponIcon = (category: unknown) => String(category).toLowerCase() as GameIconName; const iconUrl = (type: GameDataIconType, id: unknown, updatedAt: unknown) => updatedAt ? gameDataIconUrl(type, String(id), updatedAt) : ''; @@ -31,7 +31,7 @@ export const load: PageServerLoad = async ({ params, url }) => { label: string; kind: 'segmented' | 'searchable'; selected: string[]; - options: Array<{ value: string; label: string; iconUrl?: string; gameIcon?: GameIconName }>; + options: Array<{ value: string; label: string; description?: string; searchText?: string; iconUrl?: string; gameIcon?: GameIconName }>; }> = []; let countSql = ''; let rowsSql = ''; @@ -39,10 +39,17 @@ export const load: PageServerLoad = async ({ params, url }) => { switch (category.slug) { case 'gear': { const [brandRows, talentRows] = await Promise.all([ - (await database.prepare(`SELECT brands.id,brands.name,icons.updated_at FROM brand_sets brands + (await database.prepare(`SELECT brands.id,brands.name, + (SELECT GROUP_CONCAT(DISTINCT attributes.name) + FROM brand_set_effects effects + JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id + WHERE effects.brand_set_id=brands.id) AS bonus_attributes, + icons.updated_at FROM brand_sets brands LEFT JOIN game_data_icons icons ON icons.entity_type='brand_set' AND icons.entity_id=brands.id WHERE brands.active=1 ORDER BY brands.name COLLATE NOCASE`)).all(), - (await database.prepare(`SELECT talents.id,talents.name,icons.updated_at FROM talents + (await database.prepare(`SELECT talents.id,talents.name, + (SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i_description, + icons.updated_at FROM talents LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=talents.id WHERE talents.active=1 AND talents.category IN ('Backpack','Body Armor','Vest') ORDER BY talents.name COLLATE NOCASE`)).all() @@ -86,6 +93,7 @@ export const load: PageServerLoad = async ({ params, url }) => { key: 'brand', label: 'BRAND SETS', kind: 'searchable', selected: selectedBrands, options: brandRows.map((row) => ({ value: String(row.id), label: String(row.name), + searchText: String(row.bonus_attributes ?? ''), iconUrl: iconUrl('brand_set', row.id, row.updated_at) })) }, @@ -93,14 +101,49 @@ export const load: PageServerLoad = async ({ params, url }) => { key: 'talent', label: 'TALENTS', kind: 'searchable', selected: selectedTalents, options: talentRows.map((row) => ({ value: String(row.id), label: String(row.name), + description: String(row.tier_i_description ?? ''), iconUrl: iconUrl('talent', row.id, row.updated_at) })) } ]; break; } - case 'weapons': - countSql = `SELECT COUNT(*) AS total FROM weapons item WHERE item.active=1${searchClause}`; + case 'weapons': { + const talentRows = await (await database.prepare(`SELECT DISTINCT talents.id,talents.name, + (SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i_description, + icons.updated_at + FROM weapon_talent_options options + JOIN weapons source_weapons ON source_weapons.id=options.weapon_id AND source_weapons.active=1 + JOIN talents ON talents.id=options.talent_id AND talents.active=1 + LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=talents.id + ORDER BY talents.name COLLATE NOCASE`)).all(); + const weaponTypes = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG']; + const damageTypes = ['Blast', 'Pierce', 'Shredding']; + const validTalents = new Set(talentRows.map((row) => String(row.id))); + const selectedTypes = url.searchParams.getAll('weaponType').filter((value) => weaponTypes.includes(value)); + const selectedDamageTypes = url.searchParams.getAll('damageType').filter((value) => damageTypes.includes(value)); + const selectedTalents = url.searchParams.getAll('weaponTalent').filter((value) => validTalents.has(value)); + const conditions = ['item.active=1']; + queryArgs = []; + if (search) { + conditions.push("item.name LIKE ? ESCAPE '\\' COLLATE NOCASE"); + queryArgs.push(pattern); + } + if (selectedTypes.length) { + conditions.push(`item.category IN (${selectedTypes.map(() => '?').join(',')})`); + queryArgs.push(...selectedTypes); + } + if (selectedDamageTypes.length) { + conditions.push(`item.damage_type IN (${selectedDamageTypes.map(() => '?').join(',')})`); + queryArgs.push(...selectedDamageTypes); + } + if (selectedTalents.length) { + conditions.push(`EXISTS (SELECT 1 FROM weapon_talent_options filter_talents + WHERE filter_talents.weapon_id=item.id AND filter_talents.talent_id IN (${selectedTalents.map(() => '?').join(',')}))`); + queryArgs.push(...selectedTalents); + } + const where = conditions.join(' AND '); + countSql = `SELECT COUNT(*) AS total FROM weapons item WHERE ${where}`; rowsSql = `SELECT item.*, native.damage_min,native.damage_max,native.reference_level, (SELECT COUNT(*) FROM weapon_talent_options options WHERE options.weapon_id=item.id AND options.talent_slot=1) AS talent_1_count, @@ -108,9 +151,28 @@ export const load: PageServerLoad = async ({ params, url }) => { (SELECT COUNT(*) FROM weapon_base_attribute_bonuses bonuses WHERE bonuses.weapon_id=item.id) AS bonus_count FROM weapons item LEFT JOIN weapon_tier_ranges native ON native.weapon_id=item.id AND native.tier=item.native_tier - WHERE item.active=1${searchClause} + WHERE ${where} ORDER BY item.is_exotic DESC,item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`; + filters = [ + { + key: 'weaponType', label: 'WEAPON TYPE', kind: 'segmented', selected: selectedTypes, + options: weaponTypes.map((type) => ({ value: type, label: type, gameIcon: weaponIcon(type) })) + }, + { + key: 'damageType', label: 'DAMAGE TYPE', kind: 'segmented', selected: selectedDamageTypes, + options: damageTypes.map((type) => ({ value: type, label: type, gameIcon: weaponIcon(type) })) + }, + { + key: 'weaponTalent', label: 'TALENT POOL', kind: 'searchable', selected: selectedTalents, + options: talentRows.map((row) => ({ + value: String(row.id), label: String(row.name), + description: String(row.tier_i_description ?? ''), + iconUrl: iconUrl('talent', row.id, row.updated_at) + })) + } + ]; break; + } case 'weapon-mods': countSql = `SELECT COUNT(*) AS total FROM weapon_mods item WHERE item.active=1${searchClause}`; rowsSql = `SELECT item.*,weapons.name AS exotic_weapon,