feat(database): redesign talent registry
This commit is contained in:
@@ -1371,3 +1371,24 @@ a dedicated column to the right of the graphic. Clamp display values to
|
|||||||
`0–100`, use orange only for the filled measure, and keep the unfilled track
|
`0–100`, use orange only for the filled measure, and keep the unfilled track
|
||||||
neutral. Stack meter labels and tracks on narrow cards rather than shrinking
|
neutral. Stack meter labels and tracks on narrow cards rather than shrinking
|
||||||
them below a reliably readable width.
|
them below a reliably readable width.
|
||||||
|
|
||||||
|
Talent registry cards treat the two tier descriptions as their primary
|
||||||
|
content; never replace them with configured/missing summary facts. Render Tier
|
||||||
|
I and Tier II as distinct structured data blocks, then place optional cooldown
|
||||||
|
metadata below both descriptions. Do not use the serif, quote-like prose
|
||||||
|
treatment for tier effects. Use compact fixed tier labels, sans-serif effect
|
||||||
|
text, and engineered borders. Tier I is the neutral baseline; Tier II uses the
|
||||||
|
orange rail, label, and subtly elevated surface to communicate that it is the
|
||||||
|
improved version. Duration and stack limits may remain compact badges. The
|
||||||
|
top-right header status uses the applicable Backpack or Body Armor icon for
|
||||||
|
gear talents and the SMG icon as the generic Weapon Talent symbol. Weapon
|
||||||
|
Talent headers pair that icon with an explicit `1 · TOP` or `2 · BOTTOM`
|
||||||
|
status badge; never rely on the slot number alone.
|
||||||
|
|
||||||
|
When a public registry needs only one small bounded filter vocabulary, use the
|
||||||
|
compact facet-panel form: one full-width group with all options visible in a
|
||||||
|
single row on wide screens. Talent slot filtering uses Backpack, Body Armor,
|
||||||
|
Weapon 1 · Top, and Weapon 2 · Bottom. Always include the Top/Bottom context
|
||||||
|
when presenting numbered Weapon Talent slots to users. The filter remains
|
||||||
|
additive within the group and wraps to two columns on narrow screens; do not
|
||||||
|
introduce search or dropdown controls for this four-option set.
|
||||||
|
|||||||
+4
-1
File diff suppressed because one or more lines are too long
@@ -36,7 +36,7 @@
|
|||||||
|
|
||||||
<section class="database-facet-panel" aria-label="Database filters">
|
<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>
|
<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">
|
<div class:compact={groups.length === 1} class="database-facet-groups">
|
||||||
{#each groups as group}
|
{#each groups as group}
|
||||||
<fieldset class:searchable={group.kind === 'searchable'} class="database-facet-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>
|
<legend>{group.label}<span>{group.selected.length ? `${group.selected.length} ACTIVE` : 'ANY'}</span></legend>
|
||||||
|
|||||||
@@ -41,8 +41,11 @@
|
|||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if item.description}<p>{item.description}</p>{/if}
|
{#if item.description}<p class:data-description={item.descriptionStyle === 'data'}>{item.description}</p>{/if}
|
||||||
{#if item.secondaryDescription}<p class="secondary-description">{item.secondaryDescription}</p>{/if}
|
{#if item.secondaryDescription}<p class="secondary-description" class:data-description={item.descriptionStyle === 'data'}>{item.secondaryDescription}</p>{/if}
|
||||||
|
{#if item.afterLines?.length}
|
||||||
|
<div class="database-item-lines after-description">{#each item.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>
|
||||||
|
{/if}
|
||||||
{#if item.actions?.length}
|
{#if item.actions?.length}
|
||||||
<footer class="database-item-actions">{#each item.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>
|
<footer class="database-item-actions">{#each item.actions as action}<a href={action.href}>{action.label} →</a>{/each}</footer>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -41,8 +41,9 @@
|
|||||||
{#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}
|
{#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}
|
||||||
<DatabaseFacts facts={selected.preview.facts} layout={selected.preview.factLayout}/>
|
<DatabaseFacts facts={selected.preview.facts} layout={selected.preview.factLayout}/>
|
||||||
{#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.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.description}<p class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.description}</p>{/if}
|
||||||
{#if selected.preview.secondaryDescription}<p class="secondary-description">{selected.preview.secondaryDescription}</p>{/if}
|
{#if selected.preview.secondaryDescription}<p class="secondary-description" class:data-description={selected.preview.descriptionStyle === 'data'}>{selected.preview.secondaryDescription}</p>{/if}
|
||||||
|
{#if selected.preview.afterLines?.length}<div class="database-item-lines after-description">{#each selected.preview.afterLines as line}<div><span>{line.label}</span><b>{line.value}</b></div>{/each}</div>{/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}
|
{#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>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,9 +24,11 @@ export type DatabaseItem = {
|
|||||||
factLayout?: 'grid' | 'stack';
|
factLayout?: 'grid' | 'stack';
|
||||||
actions?: Array<{ label: string; href: string }>;
|
actions?: Array<{ label: string; href: string }>;
|
||||||
lines?: DatabaseFact[];
|
lines?: DatabaseFact[];
|
||||||
|
afterLines?: DatabaseFact[];
|
||||||
iconGroups?: DatabaseIconGroup[];
|
iconGroups?: DatabaseIconGroup[];
|
||||||
description?: string;
|
description?: string;
|
||||||
secondaryDescription?: string;
|
secondaryDescription?: string;
|
||||||
|
descriptionStyle?: 'prose' | 'data';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DatabaseRelatedItem = {
|
export type DatabaseRelatedItem = {
|
||||||
|
|||||||
@@ -207,8 +207,35 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
|||||||
WHERE item.active=1${searchClause}
|
WHERE item.active=1${searchClause}
|
||||||
ORDER BY item.quality,item.primary_attribute_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
|
ORDER BY item.quality,item.primary_attribute_type,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
|
||||||
break;
|
break;
|
||||||
case 'talents':
|
case 'talents': {
|
||||||
countSql = `SELECT COUNT(*) AS total FROM talents item WHERE item.active=1${searchClause}`;
|
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.*,
|
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='I') AS tier_i,
|
||||||
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='II') AS tier_ii,
|
(SELECT description FROM talent_tiers tiers WHERE tiers.talent_id=item.id AND tiers.tier='II') AS tier_ii,
|
||||||
@@ -217,9 +244,17 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
|||||||
icons.updated_at AS icon_updated_at
|
icons.updated_at AS icon_updated_at
|
||||||
FROM talents item
|
FROM talents item
|
||||||
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=item.id
|
LEFT JOIN game_data_icons icons ON icons.entity_type='talent' AND icons.entity_id=item.id
|
||||||
WHERE item.active=1${searchClause}
|
WHERE ${where}
|
||||||
ORDER BY item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
|
ORDER BY item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
|
||||||
|
filters = [{
|
||||||
|
key: 'talentSlot',
|
||||||
|
label: 'TALENT SLOT',
|
||||||
|
kind: 'segmented',
|
||||||
|
selected: selectedTalentSlots,
|
||||||
|
options: talentSlots
|
||||||
|
}];
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'brands':
|
case 'brands':
|
||||||
countSql = `SELECT COUNT(*) AS total FROM brand_sets item WHERE item.active=1${searchClause}`;
|
countSql = `SELECT COUNT(*) AS total FROM brand_sets item WHERE item.active=1${searchClause}`;
|
||||||
rowsSql = `SELECT item.*,icons.updated_at AS icon_updated_at
|
rowsSql = `SELECT item.*,icons.updated_at AS icon_updated_at
|
||||||
@@ -449,23 +484,30 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
|||||||
],
|
],
|
||||||
lines: [{ label: 'UNIQUE TALENT', value: list(row.talent_name,'NOT CONFIGURED') }]
|
lines: [{ label: 'UNIQUE TALENT', value: list(row.talent_name,'NOT CONFIGURED') }]
|
||||||
};
|
};
|
||||||
case 'talents':
|
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 {
|
return {
|
||||||
id: String(row.id), name: String(row.name), kicker: `${row.category}${row.weapon_slot ? ` // SLOT ${row.weapon_slot}` : ''}`,
|
id: String(row.id), name: String(row.name), kicker: `${slotLabel} TALENT`,
|
||||||
iconUrl: iconUrl('talent',row.id,row.icon_updated_at),
|
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: [
|
badges: [
|
||||||
...(row.weapon_scope === 'Exotic' ? [{ label: 'EXOTIC', tone: 'exotic' }] : []),
|
...(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.duration_seconds ? [{ label: `${number(row.duration_seconds)}s DURATION` }] : []),
|
||||||
...(row.max_stacks ? [{ label: `${row.max_stacks} STACKS` }] : [])
|
...(row.max_stacks ? [{ label: `${row.max_stacks} STACKS` }] : [])
|
||||||
],
|
],
|
||||||
facts: [
|
facts: [],
|
||||||
{ label: 'TIER I', value: row.tier_i ? 'CONFIGURED' : 'MISSING' },
|
description: row.tier_i ? String(row.tier_i) : 'Description is not available.',
|
||||||
{ label: 'TIER II', value: row.tier_ii ? 'CONFIGURED' : 'MISSING' }
|
secondaryDescription: row.tier_ii ? String(row.tier_ii) : 'Description is not available.',
|
||||||
],
|
descriptionStyle: 'data' as const,
|
||||||
description: row.tier_i ? `TIER I · ${row.tier_i}` : 'Tier I description is not configured.',
|
afterLines: row.cooldown_seconds ? [{ label: 'TALENT COOLDOWN', value: `${number(row.cooldown_seconds)} SECONDS` }] : []
|
||||||
secondaryDescription: row.tier_ii ? `TIER II · ${row.tier_ii}` : 'Tier II description is not configured.'
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
case 'brands': {
|
case 'brands': {
|
||||||
const effects = brandEffects.filter((effect) => effect.brand_set_id === row.id);
|
const effects = brandEffects.filter((effect) => effect.brand_set_id === row.id);
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user