feat(database): add public game data registries

This commit is contained in:
JB
2026-07-26 00:39:09 -04:00
parent ae6013d9d6
commit 317e5b39bd
12 changed files with 702 additions and 20 deletions
+9
View File
File diff suppressed because one or more lines are too long
@@ -0,0 +1,12 @@
<script lang="ts">
import { databaseCategories } from '$lib/database/categories';
let { active = '' }: { active?: string } = $props();
</script>
<nav class="database-category-nav" aria-label="Game database categories">
{#each databaseCategories as category}
<a class:active={active === category.slug} aria-current={active === category.slug ? 'page' : undefined} href={`/database/${category.slug}`}>
<small>{category.code}</small><span>{category.label}</span><b>›</b>
</a>
{/each}
</nav>
@@ -0,0 +1,56 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/state';
import GameIcon from './GameIcon.svelte';
import ItemIcon from './ItemIcon.svelte';
import type { GameIconName } from '$lib/game-icons';
type Option = { value: string; label: 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<Record<string, string>>({});
const change = (key: string, value: string) => {
const params = new URLSearchParams(page.url.searchParams);
const selected = params.getAll(key);
params.delete(key);
for (const entry of selected.includes(value) ? selected.filter((entry) => entry !== value) : [...selected, value]) params.append(key, entry);
params.delete('page');
void goto(`${page.url.pathname}?${params}`, { keepFocus: true, noScroll: true });
};
const clear = () => {
const params = new URLSearchParams(page.url.searchParams);
for (const group of groups) params.delete(group.key);
params.delete('page');
void goto(`${page.url.pathname}?${params}`, { noScroll: true });
};
const visible = (group: Group) => {
const query = (queries[group.key] ?? '').normalize('NFKC').trim().toLocaleLowerCase();
return group.options.filter((option) => !query || option.label.toLocaleLowerCase().includes(query));
};
const selectedCount = $derived(groups.reduce((total, group) => total + group.selected.length, 0));
</script>
<section class="database-facet-panel" aria-label="Database filters">
<header><div><small>TACTICAL FILTER MATRIX</small><strong>MATCH ANY WITHIN A GROUP · MATCH EVERY ACTIVE GROUP</strong></div>{#if selectedCount}<button type="button" onclick={clear}>CLEAR {selectedCount} FILTER{selectedCount === 1 ? '' : 'S'}</button>{/if}</header>
<div class="database-facet-groups">
{#each groups as group}
<fieldset class:searchable={group.kind === 'searchable'} class="database-facet-group">
<legend>{group.label}<span>{group.selected.length ? `${group.selected.length} ACTIVE` : 'ANY'}</span></legend>
{#if group.kind === 'searchable'}
<label class="database-facet-search"><span class="sr-only">Search {group.label}</span><input type="search" value={queries[group.key] ?? ''} oninput={(event) => queries[group.key] = event.currentTarget.value} placeholder={`FILTER ${group.label}`}/></label>
{/if}
<div>
{#each visible(group) as option}
<button type="button" class:selected={group.selected.includes(option.value)} aria-pressed={group.selected.includes(option.value)} onclick={() => change(group.key, option.value)}>
{#if option.gameIcon}<GameIcon name={option.gameIcon} decorative/>{:else if option.iconUrl !== undefined}<ItemIcon src={option.iconUrl} alt=""/>{:else}<i aria-hidden="true"></i>{/if}
<span>{option.label}</span>
</button>
{:else}
<small>NO MATCHING OPTIONS</small>
{/each}
</div>
</fieldset>
{/each}
</div>
</section>
@@ -0,0 +1,41 @@
<script lang="ts">
import GameIcon from './GameIcon.svelte';
import ItemIcon from './ItemIcon.svelte';
import ItemIconRail from './ItemIconRail.svelte';
import type { GameIconName } from '$lib/game-icons';
import type { DatabaseItem } from '$lib/database/items';
let {
item
}: {
item: DatabaseItem & { gameIcon?: GameIconName }
} = $props();
</script>
<article class="database-item-card">
<header>
<div class="database-item-icon">
{#if item.gameIcon}<GameIcon name={item.gameIcon} decorative/>{:else}<ItemIcon src={item.iconUrl} alt=""/>{/if}
</div>
<div><small>{item.kicker}</small><h2>{item.name}</h2></div>
</header>
{#if item.badges?.length}<div class="database-item-badges">{#each item.badges as badge}<span class={badge.tone ?? ''}>{badge.label}</span>{/each}</div>{/if}
<div class:stacked={item.factLayout === 'stack'} class="database-item-facts">
{#each item.facts as fact}<div><span>{fact.label}</span><b>{fact.value}</b></div>{/each}
</div>
{#if item.lines?.length}
<div class="database-item-lines">{#each item.lines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>
{/if}
{#if item.iconGroups?.length}
<div class="database-item-icon-groups">
{#each item.iconGroups as group}
<div><span>{group.label}</span><ItemIconRail label={group.label} items={group.items} emptyLabel={group.emptyLabel}/></div>
{/each}
</div>
{/if}
{#if item.description}<p>{item.description}</p>{/if}
{#if item.secondaryDescription}<p class="secondary-description">{item.secondaryDescription}</p>{/if}
{#if item.actions?.length}
<footer class="database-item-actions">{#each item.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>
{/if}
</article>
+49
View File
@@ -0,0 +1,49 @@
<script lang="ts">
import ItemIcon from './ItemIcon.svelte';
import type { DatabaseRelatedItem } from '$lib/database/items';
let {
label,
items,
emptyLabel = 'NONE ASSIGNED'
}: {
label: string;
items: DatabaseRelatedItem[];
emptyLabel?: string;
} = $props();
let selected = $state<DatabaseRelatedItem | null>(null);
</script>
<svelte:window onkeydown={(event) => { if (event.key === 'Escape') selected = null; }}/>
<div class="item-icon-rail" aria-label={`${label}: ${items.length ? items.map((item) => item.name).join(', ') : emptyLabel}`}>
{#if items.length}
{#each items as item}
<button type="button" title={`View ${item.name}`} aria-label={`View ${item.name} details`} onclick={() => selected = item}>
<ItemIcon src={item.iconUrl} alt=""/>
</button>
{/each}
{:else}
<small>{emptyLabel}</small>
{/if}
</div>
{#if selected?.preview}
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) selected = null; }}>
<div class="database-preview-dialog" role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`}>
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" aria-label="Close details" onclick={() => selected = null}>×</button></header>
<article class="database-item-card">
<header>
<div class="database-item-icon"><ItemIcon src={selected.preview.iconUrl} alt=""/></div>
<div><small>{selected.preview.kicker}</small><h2>{selected.preview.name}</h2></div>
</header>
{#if selected.preview.badges?.length}<div class="database-item-badges">{#each selected.preview.badges as badge}<span class={badge.tone ?? ''}>{badge.label}</span>{/each}</div>{/if}
<div class:stacked={selected.preview.factLayout === 'stack'} class="database-item-facts">{#each selected.preview.facts as fact}<div><span>{fact.label}</span><b>{fact.value}</b></div>{/each}</div>
{#if selected.preview.lines?.length}<div class="database-item-lines">{#each selected.preview.lines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/if}
{#if selected.preview.description}<p>{selected.preview.description}</p>{/if}
{#if selected.preview.secondaryDescription}<p class="secondary-description">{selected.preview.secondaryDescription}</p>{/if}
{#if selected.preview.actions?.length}<footer class="database-item-actions">{#each selected.preview.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>{/if}
</article>
</div>
</div>
{/if}
+14
View File
@@ -0,0 +1,14 @@
export const databaseCategories = [
{ slug: 'gear', label: 'Gear', code: 'GEAR', description: 'Equipment slots, base health and armor, tiers, Brand Sets, and talents.' },
{ slug: 'weapons', label: 'Weapons', code: 'WPN', description: 'Weapon families, damage types, tier ranges, handling, talents, and bonuses.' },
{ slug: 'weapon-mods', label: 'Weapon Mods', code: 'WMOD', description: 'Slot and weapon compatibility, quality rolls, secondary pools, and Mod Effects.' },
{ slug: 'gear-mods', label: 'Gear Mods', code: 'GMOD', description: 'Quality-scaled primary rolls and selectable secondary attribute pools.' },
{ slug: 'os-protocols', label: 'OS Protocols', code: 'OSPR', description: 'Quality, core profile, primary effect, and unique upgradeable talent.' },
{ slug: 'talents', label: 'Talents', code: 'TLNT', description: 'Equipment and weapon talents with tiered descriptions and combat mechanics.' },
{ slug: 'brands', label: 'Brands', code: 'BRND', description: 'Brand Set identities and their two-, three-, and four-piece bonuses.' },
{ slug: 'skill-chips', label: 'Skill Chips', code: 'CHIP', description: 'Set bonuses, primary and secondary pools, and quality-scaled unique talents.' }
] as const;
export type DatabaseCategorySlug = typeof databaseCategories[number]['slug'];
export const databaseCategory = (slug: string) => databaseCategories.find((category) => category.slug === slug);
+31
View File
@@ -0,0 +1,31 @@
export type DatabaseBadge = { label: string; tone?: string };
export type DatabaseFact = { label: string; value: string };
export type DatabaseItem = {
id: string;
name: string;
kicker: string;
iconUrl?: string;
gameIcon?: string;
badges?: DatabaseBadge[];
facts: DatabaseFact[];
factLayout?: 'grid' | 'stack';
actions?: Array<{ label: string; href: string }>;
lines?: DatabaseFact[];
iconGroups?: DatabaseIconGroup[];
description?: string;
secondaryDescription?: string;
};
export type DatabaseRelatedItem = {
id: string;
name: string;
iconUrl?: string;
preview?: DatabaseItem;
};
export type DatabaseIconGroup = {
label: string;
items: DatabaseRelatedItem[];
emptyLabel?: string;
};
+14 -14
View File
@@ -1,23 +1,23 @@
import { getDatabase } from '$lib/server/database';
import { databaseCategories } from '$lib/database/categories';
import type { PageServerLoad } from './$types';
const registries = [
{ key: 'attributes', label: 'Attributes', table: 'stat_attributes', detail: 'Stats, units, source ranges, caps, and calculation types.' },
{ key: 'talents', label: 'Talents', table: 'talents', detail: 'Tiered effects, triggers, durations, cooldowns, and stacks.' },
{ key: 'brands', label: 'Brand Sets', table: 'brand_sets', detail: 'Two-, three-, and four-piece equipment bonuses.' },
{ key: 'gear', label: 'Gear', table: 'gear', detail: 'Equipment slots, base ranges, brands, talents, and tiers.' },
{ key: 'weapons', label: 'Weapons', table: 'weapons', detail: 'Weapon families, damage ranges, talents, upgrades, and bonuses.' },
{ key: 'weaponmods', label: 'Weapon Mods', table: 'weapon_mods', detail: 'Compatibility, quality rolls, secondary pools, and effects.' },
{ key: 'gearmods', label: 'Gear Mods', table: 'gear_mods', detail: 'Primary scaling and secondary attribute pools by quality.' },
{ key: 'protocols', label: 'OS Protocols', table: 'os_protocols', detail: 'Primary effects, shared profiles, and unique talents.' },
{ key: 'skillchips', label: 'Skill Chips', table: 'skill_chip_sets', detail: 'Quality-scaled set bonuses, rolls, and chip talents.' }
] as const;
const tables = {
gear: 'gear',
weapons: 'weapons',
'weapon-mods': 'weapon_mods',
'gear-mods': 'gear_mods',
'os-protocols': 'os_protocols',
talents: 'talents',
brands: 'brand_sets',
'skill-chips': 'skill_chip_sets'
} as const;
export const load: PageServerLoad = async () => {
const database = await getDatabase();
const counts = await Promise.all(registries.map(async (registry) => {
const row = await (await database.prepare(`SELECT COUNT(*) AS count FROM ${registry.table} WHERE active=1`)).get();
return { ...registry, count: Number(row?.count ?? 0) };
const counts = await Promise.all(databaseCategories.map(async (category) => {
const row = await (await database.prepare(`SELECT COUNT(*) AS count FROM ${tables[category.slug]} WHERE active=1`)).get();
return { ...category, count: Number(row?.count ?? 0) };
}));
return {
registries: counts,
+6 -6
View File
@@ -13,17 +13,17 @@
<div class="catalogue-count"><strong>{data.total}</strong><span>ACTIVE RECORDS</span><a class="primary" href="/isac">CONTRIBUTE DATA →</a></div>
</header>
<section class="community-database-intro">
<div><span>DATABASE STATUS</span><strong>PUBLIC INDEX ONLINE</strong><small>Detailed public record pages will arrive incrementally. Current counts reflect active, approved data.</small></div>
<div><span>DATABASE STATUS</span><strong>PUBLIC INDEX ONLINE</strong><small>Select a registry to inspect active, approved records and their resolved gameplay attributes.</small></div>
<div><span>DATA STEWARDSHIP</span><strong>COMMUNITY + OVERSIGHT</strong><small>Registered agents can inspect source data and propose verified corrections through ISAC.</small></div>
</section>
<section class="database-registry-grid">
{#each data.registries as registry, index}
<article>
<header><small>{String(index + 1).padStart(2, '0')} // {registry.key.toUpperCase()}</small><b>{registry.count}</b></header>
<a href={`/database/${registry.slug}`}>
<header><small>{String(index + 1).padStart(2, '0')} // {registry.code}</small><b>{registry.count}</b></header>
<h2>{registry.label}</h2>
<p>{registry.detail}</p>
<span>{registry.count ? 'ACTIVE DATA AVAILABLE' : 'AWAITING CONTRIBUTIONS'}</span>
</article>
<p>{registry.description}</p>
<span>{registry.count ? 'OPEN ACTIVE DATA →' : 'AWAITING CONTRIBUTIONS →'}</span>
</a>
{/each}
</section>
</main>
@@ -0,0 +1,376 @@
import { databaseCategory } from '$lib/database/categories';
import type { GameIconName } from '$lib/game-icons';
import { gameDataIconUrl, type GameDataIconType } from '$lib/server/game-data-icons';
import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
const number = (value: unknown) => {
const numeric = Number(value);
return Number.isFinite(numeric) ? (Number.isInteger(numeric) ? String(numeric) : numeric.toFixed(2).replace(/\.?0+$/, '')) : '—';
};
const valueWithUnit = (value: unknown, unit: unknown) => `${number(value)}${unit ? (unit === '%' ? '%' : ` ${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 iconUrl = (type: GameDataIconType, id: unknown, updatedAt: unknown) =>
updatedAt ? gameDataIconUrl(type, String(id), updatedAt) : '';
export const load: PageServerLoad = async ({ params, url }) => {
const category = databaseCategory(params.category);
if (!category) error(404, 'Database category not found');
const database = await getDatabase();
const { search, pattern } = getRegistrySearch(url);
const searchArgs = search ? [pattern] : [];
const searchClause = search ? " AND item.name LIKE ? ESCAPE '\\' COLLATE NOCASE" : '';
let queryArgs: unknown[] = searchArgs;
let filters: Array<{
key: string;
label: string;
kind: 'segmented' | 'searchable';
selected: string[];
options: Array<{ value: string; label: string; iconUrl?: string; gameIcon?: GameIconName }>;
}> = [];
let countSql = '';
let rowsSql = '';
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
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
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()
]);
const slots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'];
const validBrands = new Set(brandRows.map((row) => String(row.id)));
const validTalents = new Set(talentRows.map((row) => String(row.id)));
const selectedSlots = url.searchParams.getAll('slot').filter((value) => slots.includes(value));
const selectedBrands = url.searchParams.getAll('brand').filter((value) => validBrands.has(value));
const selectedTalents = url.searchParams.getAll('talent').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 (selectedSlots.length) {
conditions.push(`item.slot IN (${selectedSlots.map(() => '?').join(',')})`);
queryArgs.push(...selectedSlots);
}
if (selectedBrands.length) {
conditions.push(`EXISTS (SELECT 1 FROM gear_brand_sets filter_brands
WHERE filter_brands.gear_id=item.id AND filter_brands.brand_set_id IN (${selectedBrands.map(() => '?').join(',')}))`);
queryArgs.push(...selectedBrands);
}
if (selectedTalents.length) {
conditions.push(`EXISTS (SELECT 1 FROM gear_talents filter_talents
WHERE filter_talents.gear_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 gear item WHERE ${where}`;
rowsSql = `SELECT item.* FROM gear item WHERE ${where}
ORDER BY item.slot,item.tier,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
{
key: 'slot', label: 'GEAR SLOT', kind: 'segmented', selected: selectedSlots,
options: slots.map((slot) => ({ value: slot, label: slot, gameIcon: gearIcon(slot) }))
},
{
key: 'brand', label: 'BRAND SETS', kind: 'searchable', selected: selectedBrands,
options: brandRows.map((row) => ({
value: String(row.id), label: String(row.name),
iconUrl: iconUrl('brand_set', row.id, row.updated_at)
}))
},
{
key: 'talent', label: 'TALENTS', kind: 'searchable', selected: selectedTalents,
options: talentRows.map((row) => ({
value: String(row.id), label: String(row.name),
iconUrl: iconUrl('talent', row.id, row.updated_at)
}))
}
];
break;
}
case 'weapons':
countSql = `SELECT COUNT(*) AS total FROM weapons item WHERE item.active=1${searchClause}`;
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,
(SELECT COUNT(*) FROM weapon_talent_options options WHERE options.weapon_id=item.id AND options.talent_slot=2) AS talent_2_count,
(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}
ORDER BY item.is_exotic DESC,item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
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,
GROUP_CONCAT(DISTINCT categories.weapon_category) AS categories,
(SELECT COUNT(*) FROM weapon_mod_secondary_options secondary WHERE secondary.weapon_mod_id=item.id) AS secondary_count,
(SELECT COUNT(*) FROM weapon_mod_effects effects WHERE effects.weapon_mod_id=item.id) AS effect_count
FROM weapon_mods item
LEFT JOIN weapons ON weapons.id=item.exotic_weapon_id
LEFT JOIN weapon_mod_compatible_categories categories ON categories.weapon_mod_id=item.id
WHERE item.active=1${searchClause}
GROUP BY item.id ORDER BY item.slot_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'gear-mods':
countSql = `SELECT COUNT(*) AS total FROM gear_mods item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,
(SELECT COUNT(*) FROM gear_mod_secondary_options secondary WHERE secondary.gear_mod_id=item.id) AS secondary_count,
(SELECT MIN(primary_level_1_min) FROM gear_mod_quality_values quality WHERE quality.gear_mod_id=item.id) AS level_1_floor,
(SELECT MAX(primary_level_20_max) FROM gear_mod_quality_values quality WHERE quality.gear_mod_id=item.id) AS level_20_ceiling
FROM gear_mods item WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'os-protocols':
countSql = `SELECT COUNT(*) AS total FROM os_protocols item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,attributes.name AS primary_stat,attributes.unit AS primary_unit,
primary_effect.base_value,talents.name AS talent_name,icons.updated_at AS icon_updated_at
FROM os_protocols item
LEFT JOIN os_protocol_primary_effects primary_effect ON primary_effect.protocol_id=item.id
LEFT JOIN stat_attributes attributes ON attributes.id=primary_effect.stat_attribute_id
LEFT JOIN os_protocol_talents talents ON talents.protocol_id=item.id
LEFT JOIN game_data_icons icons ON icons.entity_type='os_protocol' AND icons.entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.quality,item.primary_attribute_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'talents':
countSql = `SELECT COUNT(*) AS total FROM talents item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='I') AS tier_i,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='II') AS tier_ii,
(SELECT slot FROM weapon_talent_slots slots WHERE slots.talent_id=item.id) AS weapon_slot,
(SELECT scope FROM talent_weapon_scopes scopes WHERE scopes.talent_id=item.id) AS weapon_scope,
icons.updated_at AS icon_updated_at
FROM talents item
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'brands':
countSql = `SELECT COUNT(*) AS total FROM brand_sets item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,icons.updated_at AS icon_updated_at
FROM brand_sets item
LEFT JOIN game_data_icons icons ON icons.entity_type='brand_set' AND icons.entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
case 'skill-chips':
countSql = `SELECT COUNT(*) AS total FROM skill_chip_sets item WHERE item.active=1${searchClause}`;
rowsSql = `SELECT item.*,bonus.min_value,bonus.max_value,attributes.name AS bonus_stat,attributes.unit AS bonus_unit,
talents.name AS talent_name,
(SELECT COUNT(*) FROM skill_chip_primary_options options WHERE options.skill_chip_set_id=item.id) AS primary_count,
(SELECT COUNT(*) FROM skill_chip_secondary_options options WHERE options.skill_chip_set_id=item.id) AS secondary_count,
icons.updated_at AS icon_updated_at
FROM skill_chip_sets item
LEFT JOIN skill_chip_two_piece_bonuses bonus ON bonus.skill_chip_set_id=item.id
LEFT JOIN stat_attributes attributes ON attributes.id=bonus.stat_attribute_id
LEFT JOIN skill_chip_talents talents ON talents.skill_chip_set_id=item.id
LEFT JOIN game_data_icons icons ON icons.entity_type='skill_chip' AND icons.entity_id=item.id
WHERE item.active=1${searchClause}
ORDER BY item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
break;
}
const total = Number((await (await database.prepare(countSql)).get(...queryArgs))?.total ?? 0);
const pagination = getPagination(url, total);
const rows = await (await database.prepare(rowsSql)).all(...queryArgs, pagination.perPage, pagination.offset);
const gearRelations = category.slug === 'gear' && rows.length
? await (await database.prepare(`SELECT relation.*,icons.updated_at
FROM (
SELECT links.gear_id,'brand' AS kind,brands.id,brands.name,
NULL AS category,NULL AS cooldown_seconds,NULL AS duration_seconds,NULL AS max_stacks,
NULL AS tier_i,NULL AS tier_ii
FROM gear_brand_sets links JOIN brand_sets brands ON brands.id=links.brand_set_id
WHERE brands.active=1
UNION ALL
SELECT links.gear_id,'talent' AS kind,talents.id,talents.name,
talents.category,talents.cooldown_seconds,talents.duration_seconds,talents.max_stacks,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='I') AS tier_i,
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=talents.id AND tiers.tier='II') AS tier_ii
FROM gear_talents links JOIN talents ON talents.id=links.talent_id
WHERE talents.active=1
) relation
LEFT JOIN game_data_icons icons ON icons.entity_id=relation.id
AND icons.entity_type=CASE relation.kind WHEN 'brand' THEN 'brand_set' ELSE 'talent' END
WHERE relation.gear_id IN (${rows.map(() => '?').join(',')})
ORDER BY relation.kind,relation.name COLLATE NOCASE`)).all(...rows.map((row) => row.id))
: [];
const gearBrandEffects = category.slug === 'gear' && gearRelations.some((relation) => relation.kind === 'brand')
? await (await database.prepare(`SELECT effects.brand_set_id,effects.pieces_required,effects.value,
attributes.name,attributes.unit FROM brand_set_effects effects
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id IN (${gearRelations.filter((relation) => relation.kind === 'brand').map(() => '?').join(',')})
ORDER BY effects.pieces_required`)).all(...gearRelations.filter((relation) => relation.kind === 'brand').map((relation) => relation.id))
: [];
const brandEffects = category.slug === 'brands' && rows.length
? await (await database.prepare(`SELECT effects.brand_set_id,effects.pieces_required,effects.value,
attributes.name,attributes.unit FROM brand_set_effects effects
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id IN (${rows.map(() => '?').join(',')}) ORDER BY effects.pieces_required`)).all(...rows.map((row) => row.id))
: [];
const items = rows.map((row) => {
switch (category.slug) {
case 'gear':
{
const relations = gearRelations.filter((relation) => relation.gear_id === row.id);
const relatedItems = (kind: 'brand' | 'talent', type: GameDataIconType) => relations
.filter((relation) => relation.kind === kind)
.map((relation) => {
const relationIcon = iconUrl(type, relation.id, relation.updated_at);
const effects = kind === 'brand' ? gearBrandEffects.filter((effect) => effect.brand_set_id === relation.id) : [];
return {
id: String(relation.id),
name: String(relation.name),
iconUrl: relationIcon,
preview: kind === 'brand' ? {
id: String(relation.id), name: String(relation.name), kicker: 'BRAND SET // EQUIPMENT',
iconUrl: relationIcon,
factLayout: 'stack' as const,
actions: [{ label: 'FIND GEAR WITH THIS BRAND', href: `/database/gear?brand=${encodeURIComponent(String(relation.id))}` }],
facts: effects.map((effect) => ({
label: `${effect.pieces_required} PIECES`,
value: `${effect.name} · +${valueWithUnit(effect.value,effect.unit)}`
}))
} : {
id: String(relation.id), name: String(relation.name), kicker: `${relation.category} // GEAR TALENT`,
iconUrl: relationIcon,
badges: [
...(relation.cooldown_seconds ? [{ label: `${number(relation.cooldown_seconds)}s COOLDOWN` }] : []),
...(relation.duration_seconds ? [{ label: `${number(relation.duration_seconds)}s DURATION` }] : []),
...(relation.max_stacks ? [{ label: `${relation.max_stacks} STACKS` }] : [])
],
facts: [
{ label: 'TIER I', value: relation.tier_i ? 'CONFIGURED' : 'MISSING' },
{ label: 'TIER II', value: relation.tier_ii ? 'CONFIGURED' : 'MISSING' }
],
description: relation.tier_i ? `TIER I · ${relation.tier_i}` : 'Tier I description is not configured.',
secondaryDescription: relation.tier_ii ? `TIER II · ${relation.tier_ii}` : 'Tier II description is not configured.'
}
};
});
const iconGroups = [
{ label: 'BRAND OPTIONS', items: relatedItems('brand', 'brand_set'), emptyLabel: 'NO BRANDS ASSIGNED' },
...(['Backpack', 'Body Armor'].includes(String(row.slot))
? [{ label: 'AVAILABLE TALENTS', items: relatedItems('talent', 'talent'), emptyLabel: 'NO TALENTS ASSIGNED' }]
: [])
];
return {
id: String(row.id), name: String(row.name), kicker: `${row.slot} // T${row.tier}`,
gameIcon: gearIcon(row.slot),
badges: [{ label: `T${row.tier}` }, { label: `${Number(row.tier) === 0 ? 1 : 2} MOD SLOT${Number(row.tier) === 0 ? '' : 'S'}` }],
facts: [
{ label: 'BASE HEALTH', value: range(row.base_health_min,row.base_health_max) },
...(row.base_armor_min === null ? [] : [{ label: 'BASE ARMOR', value: range(row.base_armor_min,row.base_armor_max) }])
],
iconGroups
};
}
case 'weapons':
return {
id: String(row.id), name: String(row.name), kicker: `${row.category} // ${row.damage_type}`,
gameIcon: weaponIcon(row.category),
badges: [{ label: `T${row.native_tier}` }, ...(Number(row.is_exotic) ? [{ label: 'EXOTIC', tone: 'exotic' }] : [])],
facts: [
{ label: 'DAMAGE RANGE', value: row.damage_min === null ? 'NOT CONFIGURED' : `${range(row.damage_min,row.damage_max)} · L${row.reference_level}` },
{ label: 'RATE / MAG', value: `${number(row.rpm)} RPM · ${number(row.magazine)} MAG` },
{ label: 'RELOAD', value: valueWithUnit(row.reload_speed,'s') },
{ label: 'ACCURACY / STABILITY', value: `${number(row.accuracy)} / ${number(row.stability)}` }
],
lines: [
{ label: 'TALENT POOLS', value: `${row.talent_1_count} TOP · ${row.talent_2_count} BOTTOM` },
{ label: 'BASE BONUSES', value: String(row.bonus_count) }
]
};
case 'weapon-mods':
return {
id: String(row.id), name: String(row.name), kicker: `${row.slot_type} // WEAPON MOD`,
badges: row.exotic_weapon_id ? [{ label: 'EXOTIC', tone: 'exotic' }] : [{ label: 'STANDARD → HIGH-END' }],
facts: [
{ label: 'LEVELS', value: '01–20' },
{ label: 'SECONDARY POOL', value: row.exotic_weapon_id ? 'FIXED ATTRIBUTES' : `${row.secondary_count} OPTIONS` },
{ label: 'MOD EFFECTS', value: row.exotic_weapon_id ? 'FIXED' : String(row.effect_count) }
],
lines: [{ label: row.exotic_weapon_id ? 'ASSIGNED WEAPON' : 'WEAPON CATEGORIES', value: row.exotic_weapon_id ? list(row.exotic_weapon) : list(row.categories).toUpperCase() }]
};
case 'gear-mods':
return {
id: String(row.id), name: String(row.name), kicker: 'GEAR MOD // STANDARD QUALITIES',
badges: [{ label: 'STANDARD → HIGH-END' }],
facts: [
{ label: 'LEVELS', value: '01–20' },
{ label: 'PRIMARY OPTIONS', value: '5' },
{ label: 'SECONDARY POOL', value: `${row.secondary_count} OPTIONS` }
],
lines: [{ label: 'OBSERVED PRIMARY ENVELOPE', value: `${number(row.level_1_floor)} → ${number(row.level_20_ceiling)}` }]
};
case 'os-protocols':
return {
id: String(row.id), name: String(row.name), kicker: `${row.primary_attribute_type} // OS PROTOCOL`,
iconUrl: iconUrl('os_protocol',row.id,row.icon_updated_at),
badges: [{ label: String(row.quality), tone: String(row.quality).toLowerCase().replace('-','') }],
facts: [
{ label: 'LEVELS', value: '01–50' },
{ label: 'PRIMARY EFFECT', value: row.primary_stat ? `${row.primary_stat} · ${valueWithUnit(row.base_value,row.primary_unit)}` : 'NOT CONFIGURED' },
{ label: 'TALENT UPGRADES', value: '0–5' }
],
lines: [{ label: 'UNIQUE TALENT', value: list(row.talent_name,'NOT CONFIGURED') }]
};
case 'talents':
return {
id: String(row.id), name: String(row.name), kicker: `${row.category}${row.weapon_slot ? ` // SLOT ${row.weapon_slot}` : ''}`,
iconUrl: iconUrl('talent',row.id,row.icon_updated_at),
badges: [
...(row.weapon_scope === 'Exotic' ? [{ label: 'EXOTIC', tone: 'exotic' }] : []),
...(row.cooldown_seconds ? [{ label: `${number(row.cooldown_seconds)}s COOLDOWN` }] : []),
...(row.duration_seconds ? [{ label: `${number(row.duration_seconds)}s DURATION` }] : []),
...(row.max_stacks ? [{ label: `${row.max_stacks} STACKS` }] : [])
],
facts: [
{ label: 'TIER I', value: row.tier_i ? 'CONFIGURED' : 'MISSING' },
{ label: 'TIER II', value: row.tier_ii ? 'CONFIGURED' : 'MISSING' }
],
description: row.tier_i ? `TIER I · ${row.tier_i}` : 'Tier I description is not configured.',
secondaryDescription: row.tier_ii ? `TIER II · ${row.tier_ii}` : 'Tier II description is not configured.'
};
case 'brands': {
const effects = brandEffects.filter((effect) => effect.brand_set_id === row.id);
return {
id: String(row.id), name: String(row.name), kicker: 'BRAND SET // EQUIPMENT',
iconUrl: iconUrl('brand_set',row.id,row.icon_updated_at),
factLayout: 'stack' as const,
actions: [{ label: 'FIND GEAR WITH THIS BRAND', href: `/database/gear?brand=${encodeURIComponent(String(row.id))}` }],
facts: effects.map((effect) => ({
label: `${effect.pieces_required} PIECES`,
value: `${effect.name} · +${valueWithUnit(effect.value,effect.unit)}`
}))
};
}
case 'skill-chips':
return {
id: String(row.id), name: String(row.name), kicker: 'SKILL CHIP SET // 2–3 PIECES',
iconUrl: iconUrl('skill_chip',row.id,row.icon_updated_at),
badges: [{ label: 'SUPERIOR' }, { label: 'HIGH-END' }],
facts: [
{ label: '2-PIECE BONUS', value: row.bonus_stat ? `${row.bonus_stat} · ${range(row.min_value,row.max_value,row.bonus_unit)}` : 'NOT CONFIGURED' },
{ label: 'PRIMARY POOL', value: `${row.primary_count} OPTIONS` },
{ label: 'SECONDARY POOL', value: `${row.secondary_count} OPTIONS` }
],
lines: [{ label: '3-PIECE TALENT', value: list(row.talent_name,'NOT CONFIGURED') }]
};
}
});
return { category, items, pagination, search, filters };
};
@@ -0,0 +1,32 @@
<script lang="ts">
import DatabaseCategoryNav from '$lib/components/DatabaseCategoryNav.svelte';
import DatabaseItemCard from '$lib/components/DatabaseItemCard.svelte';
import DatabaseFacetFilters from '$lib/components/DatabaseFacetFilters.svelte';
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
let { data } = $props();
</script>
<svelte:head>
<title>{data.category.label} database — SHD.build</title>
<meta name="description" content={`${data.category.description} Community-maintained data for The Division Resurgence.`}/>
</svelte:head>
<main class="page database-category-page">
<header class="database-category-command">
<div><p class="eyebrow"><span></span> ISAC PUBLIC ARCHIVE / {data.category.code}</p><h1>{data.category.label}</h1><p>{data.category.description}</p></div>
<div><strong>{data.pagination.total}</strong><span>ACTIVE RECORDS</span><a href="/database">DATABASE INDEX →</a></div>
</header>
<DatabaseCategoryNav active={data.category.slug}/>
{#if data.filters?.length}<DatabaseFacetFilters groups={data.filters}/>{/if}
<RegistryPagination pagination={data.pagination} search={data.search} searchPlaceholder={`SEARCH ${data.category.label.toUpperCase()} BY NAME`}/>
<section class="database-item-grid">
{#each data.items as item}
<DatabaseItemCard item={item as any}/>
{:else}
<div class="database-empty">
<span>◇</span><strong>{data.search ? 'NO MATCHING RECORDS' : 'NO ACTIVE RECORDS'}</strong>
<p>{data.search ? `No ${data.category.label.toLocaleLowerCase()} match “${data.search}”.` : `This registry is awaiting approved ${data.category.label.toLocaleLowerCase()} data.`}</p>
</div>
{/each}
</section>
</main>