feat(database): refine weapon registry cards
This commit is contained in:
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