feat: show item renders in public database

This commit is contained in:
jb
2026-07-31 23:33:58 -04:00
parent aaf9e0ce97
commit 9ac2fe47cf
6 changed files with 21 additions and 5 deletions
+4 -1
View File
File diff suppressed because one or more lines are too long
@@ -15,6 +15,7 @@
<article class="database-item-card" class:talent-card={item.variant === 'talent'}>
<header>
{#if item.renderUrl}<img class="database-item-render" src={item.renderUrl} alt="" loading="lazy"/>{/if}
<div class="database-item-icon">
{#if item.gameIcon}<GameIcon name={item.gameIcon} decorative/>{:else}<ItemIcon src={item.iconUrl} alt=""/>{/if}
</div>
+1
View File
@@ -17,6 +17,7 @@ export type DatabaseItem = {
kicker: string;
variant?: 'talent';
iconUrl?: string;
renderUrl?: string;
gameIcon?: GameIconName;
headerBadge?: string;
headerMetaIcon?: GameIconName;
@@ -3,6 +3,7 @@ import type { DatabaseFact, DatabaseItem } from '$lib/database/items';
import type { GameIconName } from '$lib/game-icons';
import { weaponBaseRange } from '$lib/builder/loadout';
import { gameDataIconUrl, type GameDataIconType } from '$lib/server/game-data-icons';
import { managedImageUrl } from '$lib/server/managed-images';
import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
import { error } from '@sveltejs/kit';
@@ -146,7 +147,10 @@ export const load: PageServerLoad = async ({ params, url }) => {
}
const where = conditions.join(' AND ');
countSql = `SELECT COUNT(*) AS total FROM gear item WHERE ${where}`;
rowsSql = `SELECT item.* FROM gear item WHERE ${where}
rowsSql = `SELECT item.*,render.id AS render_id,render.updated_at AS render_updated_at
FROM gear item
LEFT JOIN managed_images render ON render.source_entity_type='gear_render' AND render.source_entity_id=item.id
WHERE ${where}
ORDER BY item.slot,item.tier,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
{
@@ -208,13 +212,14 @@ export const load: PageServerLoad = async ({ params, url }) => {
}
const where = conditions.join(' AND ');
countSql = `SELECT COUNT(*) AS total FROM weapons item WHERE ${where}`;
rowsSql = `SELECT item.*,
rowsSql = `SELECT item.*,render.id AS render_id,render.updated_at AS render_updated_at,
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_category_attribute_bonuses bonuses WHERE bonuses.weapon_category=item.category) AS bonus_count
FROM weapons item
LEFT JOIN weapon_tier_ranges native ON native.weapon_id=item.id AND native.tier=item.native_tier
LEFT JOIN managed_images render ON render.source_entity_type='weapon_render' AND render.source_entity_id=item.id
WHERE ${where}
ORDER BY item.is_exotic DESC,item.category,item.name COLLATE NOCASE LIMIT ? OFFSET ?`;
filters = [
@@ -481,6 +486,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
];
return {
id: String(row.id), name: String(row.name), kicker: `${row.slot} // T${row.tier}`,
renderUrl: row.render_id ? managedImageUrl(String(row.render_id), row.render_updated_at) : '',
gameIcon: gearIcon(row.slot),
badges: [{ label: `T${row.tier}` }, { label: `${Number(row.tier) === 0 ? 1 : 2} MOD SLOT${Number(row.tier) === 0 ? '' : 'S'}` }],
facts: [
@@ -507,6 +513,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
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}`,
renderUrl: row.render_id ? managedImageUrl(String(row.render_id), row.render_updated_at) : '',
gameIcon: weaponIcon(row.category),
headerBadge: `T${row.native_tier}`,
headerMetaIcon: weaponIcon(row.damage_type),
+3 -1
View File
@@ -21,6 +21,7 @@
let editing = $state<Editor | null>(null);
let augmentEditing = $state<any | null>(null);
let handledResultToken = $state('');
let renderResetToken = $state(0);
const compatibleBrands = (ids: string[]) => ids.map((id) => data.brands.find((brand) => brand.id === id)).filter(Boolean);
const stat = (id: string) => data.stats.find((item) => item.id === id);
const format = (value: number | string, statId: string) => {
@@ -63,6 +64,7 @@
}
if (form?.gearSaved && form.resultToken && form.resultToken !== handledResultToken) {
handledResultToken = form.resultToken;
if (form.gearMode === 'add') renderResetToken += 1;
editing = form.gearMode === 'add' ? blank() : null;
}
});
@@ -79,7 +81,7 @@
<SegmentedChoice legend="SLOT" name="slot" options={slotOptions} bind:value={editing.slot} onchange={(values) => changeSlot(values[0])} required/>
<SegmentedChoice legend="TIER" name="tier" options={tierOptions} bind:value={editing.tier} required/>
</div>
<GameDataRenderUploadField currentUrl={editing.renderUrl} label="GEAR RENDER"/>
<GameDataRenderUploadField currentUrl={editing.renderUrl} label="GEAR RENDER" resetToken={renderResetToken}/>
<div class="gear-ranges">
<div class="effect-band range-band"><strong>HP<small>BASE HEALTH</small></strong><label class="field">MINIMUM<input name="healthMin" type="number" min="0" step="any" bind:value={editing.base_health_min} required /></label><label class="field">MAXIMUM<input name="healthMax" type="number" min="0" step="any" bind:value={editing.base_health_max} required /></label></div>
<div class="effect-band range-band" class:unavailable={!armoredSlots.includes(editing.slot)}><strong>AR<small>BASE ARMOR</small></strong><label class="field">MINIMUM<input name="armorMin" type="number" min="0" step="any" bind:value={editing.base_armor_min} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label><label class="field">MAXIMUM<input name="armorMax" type="number" min="0" step="any" bind:value={editing.base_armor_max} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label></div>
+3 -1
View File
@@ -28,6 +28,7 @@
let editing = $state<any | null>(null);
let bonusEditing = $state<any | null>(null);
let handledResultToken = $state('');
let renderResetToken = $state(0);
let openedRequestedWeapon = $state(false);
const edit = (weapon: any) => {
const tiers = [0, 1, 2].map((tier) => {
@@ -94,6 +95,7 @@
}
if (form?.weaponSaved && form.resultToken && form.resultToken !== handledResultToken) {
handledResultToken = form.resultToken;
if (form.weaponMode === 'add') renderResetToken += 1;
editing = form.weaponMode === 'add' ? blank() : null;
}
if (form?.weaponBonusProfileSaved && form.resultToken && form.resultToken !== handledResultToken) {
@@ -147,7 +149,7 @@
<SegmentedChoice legend="DAMAGE TYPE" name="damageType" options={damageTypeOptions} bind:value={editing.damage_type} required/>
<label class="field exotic-toggle">EXOTIC STATUS<span title="Exotic weapon"><input name="isExotic" type="checkbox" value="1" checked={Number(editing.is_exotic) === 1} onchange={(event) => setExotic(event.currentTarget.checked)}/><GameIcon name="exotic" decorative/><b class="sr-only">EXOTIC WEAPON</b></span></label>
</div>
<GameDataRenderUploadField currentUrl={editing.renderUrl} label="WEAPON RENDER"/>
<GameDataRenderUploadField currentUrl={editing.renderUrl} label="WEAPON RENDER" resetToken={renderResetToken}/>
{#if Number(editing.is_exotic)}
<section class="weapon-exotic-talent">
<div class="selection-head"><div><span>DEDICATED EXOTIC TALENT</span><small>SELECT ONE TALENT FROM THE EXOTIC WEAPON REGISTRY</small></div><b>{editing.exotic_talent_id ? '1 / 1 ASSIGNED' : 'REQUIRED'}</b></div>