feat(database): refine weapon registry cards
This commit is contained in:
@@ -1217,6 +1217,13 @@ use the stacked fact treatment. Brand Set bonuses are shown vertically in
|
||||
two-, three-, then four-piece order; do not add a redundant “3/3 bonuses”
|
||||
badge when all three rows are already visible.
|
||||
|
||||
Weapon cards also use stacked facts. Their level-50 Damage range is the primary
|
||||
fact: a taller orange-railed row with enlarged values and the Damage Type in
|
||||
the label. Public damage is calculated from the stored level-1 endpoints using
|
||||
the same `weaponBaseRange` curve as the builder; never label the configured
|
||||
base endpoints as level-50 values. Weapon Talent 1 and Talent 2 pools use
|
||||
separate clickable icon rails and the standard Talent quick view.
|
||||
|
||||
Record cards may end with contextual actions that connect related public
|
||||
registries. Actions use the compact orange tactical-button treatment and
|
||||
encode their destination filter in the URL. Brand cards provide a “find Gear”
|
||||
@@ -1253,6 +1260,13 @@ 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.
|
||||
|
||||
Public database command headers are content-sized and must explicitly override
|
||||
the global 76-pixel site-header rule. Their eyebrow, title, description, count,
|
||||
and index action remain inside the bordered command surface at every scale.
|
||||
Children use `min-width: 0`, long titles may wrap, and category navigation
|
||||
tracks use zero-minimum grid columns so translated or enlarged text cannot
|
||||
force horizontal page overflow.
|
||||
|
||||
### Build persistence and sharing
|
||||
|
||||
The builder distinguishes device persistence from account persistence. `SAVE
|
||||
@@ -1335,3 +1349,25 @@ into per-item storage. A later contributor replaces it through the normal icon
|
||||
upload proposal, preserving the canonical record and audit history. The upload
|
||||
itself retains all transparent-format, size, SVG-safety, focus, drag/drop, and
|
||||
paste validation requirements.
|
||||
|
||||
### Public database performance facts
|
||||
|
||||
Use the shared `DatabaseFacts` renderer for registry-card performance data and
|
||||
quick views. Weapon identity headers keep the native tier beside the category
|
||||
identity as a compact textual `T0`, `T1`, or `T2` marker. Place tier and the
|
||||
smaller damage-type icon in a dedicated top-right status cluster; do not
|
||||
interrupt the readable `category // damage type` metadata with icons. Reserve
|
||||
the emphasized first fact for the weapon's damage range, pair it with the
|
||||
canonical damage-type icon, and render the calculated level as a subdued
|
||||
annotation rather than part of the primary label. Public level-50 damage must
|
||||
be calculated from the stored level-1 endpoints with `weaponBaseRange`; never
|
||||
relabel raw source values.
|
||||
|
||||
Represent bounded handling values such as Accuracy and Stability with compact
|
||||
horizontal meters. Always retain the numeric value and an accessible
|
||||
`value out of 100` label so the graphic does not carry meaning alone. Use the
|
||||
stable sequence `label → meter → exact value`, with the exact number aligned in
|
||||
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
|
||||
neutral. Stack meter labels and tracks on narrow cards rather than shrinking
|
||||
them below a reliably readable width.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import GameIcon from './GameIcon.svelte';
|
||||
import type { DatabaseFact } from '$lib/database/items';
|
||||
|
||||
let {
|
||||
facts,
|
||||
layout = 'grid'
|
||||
}: {
|
||||
facts: DatabaseFact[];
|
||||
layout?: 'grid' | 'stack';
|
||||
} = $props();
|
||||
|
||||
const meterValue = (value: number) => Math.max(0, Math.min(100, value));
|
||||
</script>
|
||||
|
||||
<div class:stacked={layout === 'stack'} class="database-item-facts">
|
||||
{#each facts as fact}
|
||||
<div class:primary={fact.emphasis === 'primary'}>
|
||||
<span>{fact.label}</span>
|
||||
<div class="database-fact-value">
|
||||
{#if fact.gameIcon}<GameIcon name={fact.gameIcon}/>{/if}
|
||||
<b>{fact.value}</b>
|
||||
{#if fact.note}<small>{fact.note}</small>{/if}
|
||||
</div>
|
||||
{#if fact.meters?.length}
|
||||
<div class="database-stat-meters">
|
||||
{#each fact.meters as meter}
|
||||
<div>
|
||||
<i>{meter.label}</i>
|
||||
<progress max="100" value={meterValue(meter.value)} aria-label={`${meter.label}: ${meterValue(meter.value)} out of 100`}></progress>
|
||||
<output aria-label={`${meter.label} exact value`}>{meterValue(meter.value)}</output>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -2,6 +2,7 @@
|
||||
import GameIcon from './GameIcon.svelte';
|
||||
import ItemIcon from './ItemIcon.svelte';
|
||||
import ItemIconRail from './ItemIconRail.svelte';
|
||||
import DatabaseFacts from './DatabaseFacts.svelte';
|
||||
import type { GameIconName } from '$lib/game-icons';
|
||||
import type { DatabaseItem } from '$lib/database/items';
|
||||
|
||||
@@ -17,12 +18,19 @@
|
||||
<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>
|
||||
<div class="database-item-identity">
|
||||
<small>{item.kicker}</small>
|
||||
<h2>{item.name}</h2>
|
||||
</div>
|
||||
{#if item.headerBadge || item.headerMetaIcon}
|
||||
<div class="database-header-status">
|
||||
{#if item.headerMetaIcon}<GameIcon name={item.headerMetaIcon}/>{/if}
|
||||
{#if item.headerBadge}<span class="database-header-badge">{item.headerBadge}</span>{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</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>
|
||||
<DatabaseFacts facts={item.facts} layout={item.factLayout}/>
|
||||
{#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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ItemIcon from './ItemIcon.svelte';
|
||||
import DatabaseFacts from './DatabaseFacts.svelte';
|
||||
import type { DatabaseRelatedItem } from '$lib/database/items';
|
||||
|
||||
let {
|
||||
@@ -38,7 +39,7 @@
|
||||
<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>
|
||||
<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.description}<p>{selected.preview.description}</p>{/if}
|
||||
{#if selected.preview.secondaryDescription}<p class="secondary-description">{selected.preview.secondaryDescription}</p>{/if}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { GameIconName } from '$lib/game-icons';
|
||||
|
||||
export type DatabaseBadge = { label: string; tone?: string };
|
||||
export type DatabaseFact = { label: string; value: string };
|
||||
export type DatabaseMeter = { label: string; value: number };
|
||||
export type DatabaseFact = {
|
||||
label: string;
|
||||
value: string;
|
||||
emphasis?: 'primary';
|
||||
gameIcon?: GameIconName;
|
||||
note?: string;
|
||||
meters?: DatabaseMeter[];
|
||||
};
|
||||
|
||||
export type DatabaseItem = {
|
||||
id: string;
|
||||
@@ -7,6 +17,8 @@ export type DatabaseItem = {
|
||||
kicker: string;
|
||||
iconUrl?: string;
|
||||
gameIcon?: string;
|
||||
headerBadge?: string;
|
||||
headerMetaIcon?: GameIconName;
|
||||
badges?: DatabaseBadge[];
|
||||
facts: DatabaseFact[];
|
||||
factLayout?: 'grid' | 'stack';
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -247,6 +248,17 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
||||
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 (
|
||||
@@ -340,21 +352,69 @@ export const load: PageServerLoad = async ({ params, url }) => {
|
||||
};
|
||||
}
|
||||
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),
|
||||
badges: [{ label: `T${row.native_tier}` }, ...(Number(row.is_exotic) ? [{ label: 'EXOTIC', tone: 'exotic' }] : [])],
|
||||
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 RANGE', value: row.damage_min === null ? 'NOT CONFIGURED' : `${range(row.damage_min,row.damage_max)} · L${row.reference_level}` },
|
||||
{
|
||||
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: 'ACCURACY / STABILITY', value: `${number(row.accuracy)} / ${number(row.stability)}` }
|
||||
{
|
||||
label: 'HANDLING',
|
||||
value: '',
|
||||
meters: [
|
||||
{ label: 'ACCURACY', value: Number(row.accuracy) || 0 },
|
||||
{ label: 'STABILITY', value: Number(row.stability) || 0 }
|
||||
]
|
||||
}
|
||||
],
|
||||
lines: [
|
||||
{ label: 'TALENT POOLS', value: `${row.talent_1_count} TOP · ${row.talent_2_count} BOTTOM` },
|
||||
{ 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`,
|
||||
|
||||
Reference in New Issue
Block a user