544 lines
29 KiB
TypeScript
544 lines
29 KiB
TypeScript
import { databaseCategory } from '$lib/database/categories';
|
||
import type { GameIconName } from '$lib/game-icons';
|
||
import { weaponBaseRange } from '$lib/builder/loadout';
|
||
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() as GameIconName;
|
||
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; description?: string; searchText?: 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,
|
||
(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,
|
||
(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()
|
||
]);
|
||
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),
|
||
searchText: String(row.bonus_attributes ?? ''),
|
||
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),
|
||
description: String(row.tier_i_description ?? ''),
|
||
iconUrl: iconUrl('talent', row.id, row.updated_at)
|
||
}))
|
||
}
|
||
];
|
||
break;
|
||
}
|
||
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,
|
||
(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 ${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,
|
||
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': {
|
||
const talentSlots = [
|
||
{ value: 'Backpack', label: 'Backpack', gameIcon: 'backpack' as const },
|
||
{ value: 'Body Armor', label: 'Body Armor', gameIcon: 'body_armor' as const },
|
||
{ value: 'Weapon 1', label: 'Weapon 1 · Top', gameIcon: 'smg' as const },
|
||
{ value: 'Weapon 2', label: 'Weapon 2 · Bottom', gameIcon: 'smg' as const }
|
||
];
|
||
const selectedTalentSlots = url.searchParams.getAll('talentSlot')
|
||
.filter((value) => talentSlots.some((slot) => slot.value === value));
|
||
const conditions = ['item.active=1'];
|
||
queryArgs = [];
|
||
if (search) {
|
||
conditions.push("item.name LIKE ? ESCAPE '\\' COLLATE NOCASE");
|
||
queryArgs.push(pattern);
|
||
}
|
||
if (selectedTalentSlots.length) {
|
||
const slotConditions = selectedTalentSlots.map((slot) => {
|
||
if (slot === 'Backpack') return "item.category='Backpack'";
|
||
if (slot === 'Body Armor') return "item.category IN ('Body Armor','Vest')";
|
||
queryArgs.push(slot === 'Weapon 1' ? 1 : 2);
|
||
return `item.category='Weapon' AND EXISTS (
|
||
SELECT 1 FROM weapon_talent_slots filter_slots
|
||
WHERE filter_slots.talent_id=item.id AND filter_slots.slot=?
|
||
)`;
|
||
});
|
||
conditions.push(`(${slotConditions.map((condition) => `(${condition})`).join(' OR ')})`);
|
||
}
|
||
const where = conditions.join(' AND ');
|
||
countSql = `SELECT COUNT(*) AS total FROM talents item WHERE ${where}`;
|
||
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 ${where}
|
||
ORDER BY item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
|
||
filters = [{
|
||
key: 'talentSlot',
|
||
label: 'TALENT SLOT',
|
||
kind: 'segmented',
|
||
selected: selectedTalentSlots,
|
||
options: talentSlots
|
||
}];
|
||
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,abilities.name AS ability_name,specializations.name AS specialization_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 specialization_abilities abilities ON abilities.id=item.specialization_ability_id
|
||
LEFT JOIN specializations ON specializations.id=abilities.specialization_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 weaponRelations = category.slug === 'weapons' && rows.length
|
||
? await (await database.prepare(`SELECT options.weapon_id,options.talent_slot,talents.*,
|
||
(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,
|
||
icons.updated_at AS icon_updated_at
|
||
FROM weapon_talent_options options
|
||
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
|
||
WHERE options.weapon_id IN (${rows.map(() => '?').join(',')})
|
||
ORDER BY options.talent_slot,talents.name COLLATE NOCASE`)).all(...rows.map((row) => row.id))
|
||
: [];
|
||
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':
|
||
{
|
||
const relatedTalents = weaponRelations.filter((relation) => relation.weapon_id === row.id);
|
||
const talentItems = (slot: number) => relatedTalents
|
||
.filter((relation) => Number(relation.talent_slot) === slot)
|
||
.map((relation) => {
|
||
const talentIcon = iconUrl('talent', relation.id, relation.icon_updated_at);
|
||
return {
|
||
id: String(relation.id),
|
||
name: String(relation.name),
|
||
iconUrl: talentIcon,
|
||
preview: {
|
||
id: String(relation.id), name: String(relation.name), kicker: `WEAPON TALENT // SLOT ${slot}`,
|
||
iconUrl: talentIcon,
|
||
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 level50Damage = row.damage_min === null ? null : weaponBaseRange(row.damage_min, row.damage_max, 50);
|
||
return {
|
||
id: String(row.id), name: String(row.name), kicker: `${row.category} // ${row.damage_type}`,
|
||
gameIcon: weaponIcon(row.category),
|
||
headerBadge: `T${row.native_tier}`,
|
||
headerMetaIcon: weaponIcon(row.damage_type),
|
||
badges: Number(row.is_exotic) ? [{ label: 'EXOTIC', tone: 'exotic' }] : [],
|
||
factLayout: 'stack' as const,
|
||
facts: [
|
||
{
|
||
label: 'DAMAGE',
|
||
value: level50Damage ? `${level50Damage.min.toLocaleString('en-US')}–${level50Damage.max.toLocaleString('en-US')}` : 'NOT CONFIGURED',
|
||
emphasis: 'primary' as const,
|
||
gameIcon: weaponIcon(row.damage_type),
|
||
note: 'AT LEVEL 50'
|
||
},
|
||
{ label: 'RATE / MAG', value: `${number(row.rpm)} RPM · ${number(row.magazine)} MAG` },
|
||
{ label: 'RELOAD', value: valueWithUnit(row.reload_speed,'s') },
|
||
{
|
||
label: 'HANDLING',
|
||
value: '',
|
||
meters: [
|
||
{ label: 'ACCURACY', value: Number(row.accuracy) || 0 },
|
||
{ label: 'STABILITY', value: Number(row.stability) || 0 }
|
||
]
|
||
}
|
||
],
|
||
lines: [
|
||
{ label: 'BASE BONUSES', value: String(row.bonus_count) }
|
||
],
|
||
iconGroups: [
|
||
{ label: 'TALENT 1 POOL', items: talentItems(1), emptyLabel: 'NO TALENTS ASSIGNED' },
|
||
{ label: 'TALENT 2 POOL', items: talentItems(2), emptyLabel: 'NO TALENTS ASSIGNED' }
|
||
]
|
||
};
|
||
}
|
||
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': {
|
||
const talentCategory = String(row.category);
|
||
const isWeaponTalent = talentCategory === 'Weapon';
|
||
const normalizedCategory = talentCategory === 'Vest' ? 'Body Armor' : talentCategory;
|
||
const slotLabel = isWeaponTalent && row.weapon_slot ? `WEAPON ${row.weapon_slot}` : normalizedCategory.toUpperCase();
|
||
return {
|
||
id: String(row.id), name: String(row.name), kicker: `${slotLabel} TALENT`,
|
||
iconUrl: iconUrl('talent',row.id,row.icon_updated_at),
|
||
headerMetaIcon: isWeaponTalent ? 'smg' : gearIcon(normalizedCategory),
|
||
headerBadge: isWeaponTalent && row.weapon_slot
|
||
? `${row.weapon_slot} · ${Number(row.weapon_slot) === 1 ? 'TOP' : 'BOTTOM'}`
|
||
: undefined,
|
||
badges: [
|
||
...(row.weapon_scope === 'Exotic' ? [{ label: 'EXOTIC', tone: 'exotic' }] : []),
|
||
...(row.duration_seconds ? [{ label: `${number(row.duration_seconds)}s DURATION` }] : []),
|
||
...(row.max_stacks ? [{ label: `${row.max_stacks} STACKS` }] : [])
|
||
],
|
||
facts: [],
|
||
description: row.tier_i ? String(row.tier_i) : 'Description is not available.',
|
||
secondaryDescription: row.tier_ii ? String(row.tier_ii) : 'Description is not available.',
|
||
descriptionStyle: 'data' as const,
|
||
afterLines: row.cooldown_seconds ? [{ label: 'TALENT COOLDOWN', value: `${number(row.cooldown_seconds)} SECONDS` }] : []
|
||
};
|
||
}
|
||
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: 'SPECIALIZATION ABILITY', value: row.ability_name ? `${row.ability_name} · ${row.specialization_name}` : 'UNASSIGNED' },
|
||
{ 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 };
|
||
};
|