feat(isac): support deferred item icons
This commit is contained in:
@@ -19,7 +19,7 @@ export const load: PageServerLoad = async ({ params, cookies }) => {
|
||||
const [gear, weapons, chips, protocols, brands] = await Promise.all([
|
||||
(await database.prepare('SELECT id,name,slot,tier FROM gear')).all(),
|
||||
(await database.prepare('SELECT id,name,category,is_exotic FROM weapons')).all(),
|
||||
(await database.prepare('SELECT id,name,quality FROM skill_chip_sets')).all(),
|
||||
(await database.prepare('SELECT id,name FROM skill_chip_sets')).all(),
|
||||
(await database.prepare('SELECT id,name,quality FROM os_protocols')).all(),
|
||||
(await database.prepare('SELECT id,name FROM brand_sets')).all()
|
||||
]);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
|
||||
import KpiFilters from '$lib/components/KpiFilters.svelte';
|
||||
import IconUploadField from '$lib/components/IconUploadField.svelte';
|
||||
import ItemIcon from '$lib/components/ItemIcon.svelte';
|
||||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||||
let { data, form } = $props();
|
||||
type Effect = { pieces_required: number; stat_attribute_id: string; value: number };
|
||||
@@ -58,7 +59,7 @@
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row brand-set-row table-heading"><span>BRAND SET</span><span>2 PIECES</span><span>3 PIECES</span><span>4+ PIECES</span><span>STATE</span><span></span></div>
|
||||
{#each data.brands as brand}
|
||||
<div class="dashboard-row brand-set-row" class:archived={Number(brand.active) !== 1} class:pending-record={brand.pending}><div class="registry-icon-identity">{#if brand.iconUrl}<img src={brand.iconUrl} alt=""/>{:else}<span>◇</span>{/if}<div><strong>{brand.name}</strong><small>{brand.pending ? `PROPOSED BY ${brand.proposerName}` : `${brand.effects.length}/3 EFFECTS CONFIGURED`}</small></div></div>{#each [2, 3, 4] as pieces}{@const effect = effectFor(brand, pieces)}<span class:configured={effect.stat_attribute_id}>{effect.stat_attribute_id ? formatEffect(effect) : 'NOT CONFIGURED'}</span>{/each}<b class:pending={brand.pending} class="state-dot">{brand.pending ? 'PENDING' : Number(brand.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if brand.pending}<span></span>{:else}<button onclick={() => edit(brand)}>EDIT</button>{/if}</div>
|
||||
<div class="dashboard-row brand-set-row" class:archived={Number(brand.active) !== 1} class:pending-record={brand.pending}><div class="registry-icon-identity"><ItemIcon src={brand.iconUrl} alt=""/><div><strong>{brand.name}</strong><small>{brand.pending ? `PROPOSED BY ${brand.proposerName}` : `${brand.effects.length}/3 EFFECTS CONFIGURED`}</small></div></div>{#each [2, 3, 4] as pieces}{@const effect = effectFor(brand, pieces)}<span class:configured={effect.stat_attribute_id}>{effect.stat_attribute_id ? formatEffect(effect) : 'NOT CONFIGURED'}</span>{/each}<b class:pending={brand.pending} class="state-dot">{brand.pending ? 'PENDING' : Number(brand.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if brand.pending}<span></span>{:else}<button onclick={() => edit(brand)}>EDIT</button>{/if}</div>
|
||||
{:else}
|
||||
<div class="empty-dashboard"><strong>NO BRAND SETS REGISTERED</strong><span>Add the first set to begin mapping equipment synergy bonuses.</span></div>
|
||||
{/each}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</section>
|
||||
<section>
|
||||
<span>03</span>
|
||||
<div><h3>APPROPRIATE IMAGES</h3><p>Upload only relevant game-data icons or assets you are permitted to share. Images must be safe for a general audience and must not contain harassment, explicit material, personal information, watermarks, advertisements, or misleading edits. Prefer clean transparent PNG, WebP, or SVG assets that remain legible at builder-icon sizes.</p></div>
|
||||
<div><h3>APPROPRIATE IMAGES</h3><p>Icons are optional when contributing item data. If you do not have appropriate artwork, save the item without one; SHD.build will use a missing-icon placeholder and another contributor can add the icon later. When uploading, use only relevant game-data icons or assets you are permitted to share. Images must be safe for a general audience and must not contain harassment, explicit material, personal information, watermarks, advertisements, or misleading edits. Prefer clean transparent PNG, WebP, AVIF, or SVG assets that remain legible at builder-icon sizes.</p></div>
|
||||
</section>
|
||||
<section>
|
||||
<span>04</span>
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||||
import GameIcon from '$lib/components/GameIcon.svelte';
|
||||
import ItemIcon from '$lib/components/ItemIcon.svelte';
|
||||
import type { GameIconName } from '$lib/game-icons';
|
||||
let { data, form } = $props();
|
||||
type Editor = { id: string; name: string; slot: string; tier: string; base_health_min: number; base_health_max: number; base_armor_min: number | null; base_armor_max: number | null; brandIds: string[]; talentIds: string[] };
|
||||
@@ -135,6 +136,6 @@
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row gear-row table-heading"><span>GEAR</span><span>SLOT</span><span>TIER</span><span>BASE RANGES</span><span>BRAND SETS</span><span>STATE</span><span></span></div>
|
||||
{#each data.gear as item}
|
||||
<div class="dashboard-row gear-row" class:archived={Number(item.active) !== 1} class:pending-record={item.pending}><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span class="gear-slot-label"><GameIcon name={slotIcon(item.slot)} decorative/>{item.slot}</span><span>T{item.tier}<small> · {Number(item.tier) === 0 ? '1 MOD SLOT' : '2 MOD SLOTS'}</small></span><span>HP {item.base_health_min}–{item.base_health_max}{#if item.base_armor_min !== null}<small> · AR {item.base_armor_min}–{item.base_armor_max}</small>{/if}</span><div class="registry-icon-rail" aria-label={`Compatible Brand Sets: ${brandNames(item.brandIds)}`}>{#each compatibleBrands(item.brandIds) as brand}<span title={brand.name}>{#if brand.iconUrl}<img src={brand.iconUrl} alt={brand.name}/>{:else}<i aria-hidden="true">{brand.name.slice(0,2).toUpperCase()}</i><span class="sr-only">{brand.name}</span>{/if}</span>{/each}</div><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if item.pending}<span></span>{:else}<button onclick={() => edit(item)}>EDIT</button>{/if}</div>
|
||||
<div class="dashboard-row gear-row" class:archived={Number(item.active) !== 1} class:pending-record={item.pending}><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span class="gear-slot-label"><GameIcon name={slotIcon(item.slot)} decorative/>{item.slot}</span><span>T{item.tier}<small> · {Number(item.tier) === 0 ? '1 MOD SLOT' : '2 MOD SLOTS'}</small></span><span>HP {item.base_health_min}–{item.base_health_max}{#if item.base_armor_min !== null}<small> · AR {item.base_armor_min}–{item.base_armor_max}</small>{/if}</span><div class="registry-icon-rail" aria-label={`Compatible Brand Sets: ${brandNames(item.brandIds)}`}>{#each compatibleBrands(item.brandIds) as brand}<span title={brand.name}><ItemIcon src={brand.iconUrl} alt={brand.name}/></span>{/each}</div><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if item.pending}<span></span>{:else}<button onclick={() => edit(item)}>EDIT</button>{/if}</div>
|
||||
{:else}<div class="empty-dashboard"><strong>NO GEAR REGISTERED</strong><span>Add the first gear definition after configuring at least two brand sets.</span></div>{/each}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import GameIcon from '$lib/components/GameIcon.svelte';
|
||||
import type { GameIconName } from '$lib/game-icons';
|
||||
import IconUploadField from '$lib/components/IconUploadField.svelte';
|
||||
import ItemIcon from '$lib/components/ItemIcon.svelte';
|
||||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||||
|
||||
let { data, form } = $props();
|
||||
@@ -166,6 +167,6 @@
|
||||
<div class="dashboard-table protocol-dashboard-table">
|
||||
<div class="dashboard-row protocol-row table-heading"><span>OS PROTOCOL</span><span>QUALITY</span><span>TYPE</span><span>UNIQUE PRIMARY</span><span>UNIQUE TALENT</span><span>STATE</span><span></span></div>
|
||||
{#each data.protocols as protocol}
|
||||
<div class="dashboard-row protocol-row" class:archived={Number(protocol.active) !== 1} class:pending-record={protocol.pending}><div class="registry-icon-identity">{#if protocol.iconUrl}<img src={protocol.iconUrl} alt=""/>{:else}<span>◇</span>{/if}<div><strong>{protocol.name}</strong><small>{protocol.pending ? `PROPOSED BY ${protocol.proposerName}` : 'LEVEL 1–50 · PRIMARY 2× MAX'}</small></div></div><span><b class={`quality-chip ${qualityClass(protocol.quality)}`}>{protocol.quality}</b></span><span class="protocol-type-label"><GameIcon name={iconName(protocol.primary_attribute_type)} decorative/>{protocol.primary_attribute_type}</span><span>{protocol.primaryEffect ? `${stat(protocol.primaryEffect.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(protocol.primaryEffect.base_value, protocol.primaryEffect.stat_attribute_id)} → ${format(levelValue(protocol.primaryEffect.base_value, 50), protocol.primaryEffect.stat_attribute_id)}` : 'NOT CONFIGURED'}</span><div><strong>{protocol.talent?.name ?? 'NOT CONFIGURED'}</strong><small><span class="mini-diamonds">◆◆◆◆◆</span> 5 UPGRADES · 2× MAX</small></div><b class:pending={protocol.pending} class="state-dot">{protocol.pending ? 'PENDING' : Number(protocol.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if protocol.pending}<span></span>{:else}<button onclick={() => edit(protocol)}>EDIT</button>{/if}</div>
|
||||
<div class="dashboard-row protocol-row" class:archived={Number(protocol.active) !== 1} class:pending-record={protocol.pending}><div class="registry-icon-identity"><ItemIcon src={protocol.iconUrl} alt=""/><div><strong>{protocol.name}</strong><small>{protocol.pending ? `PROPOSED BY ${protocol.proposerName}` : 'LEVEL 1–50 · PRIMARY 2× MAX'}</small></div></div><span><b class={`quality-chip ${qualityClass(protocol.quality)}`}>{protocol.quality}</b></span><span class="protocol-type-label"><GameIcon name={iconName(protocol.primary_attribute_type)} decorative/>{protocol.primary_attribute_type}</span><span>{protocol.primaryEffect ? `${stat(protocol.primaryEffect.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(protocol.primaryEffect.base_value, protocol.primaryEffect.stat_attribute_id)} → ${format(levelValue(protocol.primaryEffect.base_value, 50), protocol.primaryEffect.stat_attribute_id)}` : 'NOT CONFIGURED'}</span><div><strong>{protocol.talent?.name ?? 'NOT CONFIGURED'}</strong><small><span class="mini-diamonds">◆◆◆◆◆</span> 5 UPGRADES · 2× MAX</small></div><b class:pending={protocol.pending} class="state-dot">{protocol.pending ? 'PENDING' : Number(protocol.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b>{#if protocol.pending}<span></span>{:else}<button onclick={() => edit(protocol)}>EDIT</button>{/if}</div>
|
||||
{:else}<div class="empty-dashboard"><strong>NO OS PROTOCOLS REGISTERED</strong><span>Configure shared secondary profiles, then add the first protocol.</span></div>{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import IconUploadField from '$lib/components/IconUploadField.svelte';
|
||||
import ItemIcon from '$lib/components/ItemIcon.svelte';
|
||||
import KpiFilters from '$lib/components/KpiFilters.svelte';
|
||||
import PendingForm from '$lib/components/PendingForm.svelte';
|
||||
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
|
||||
@@ -106,6 +107,6 @@
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row skill-chip-row table-heading"><span>SKILL CHIP</span><span>QUALITY</span><span>2-PIECE BONUS</span><span>3-PIECE TALENT</span><span>ROLL POOLS</span><span>STATE</span><span></span></div>
|
||||
{#each data.sets as item}
|
||||
<div class="dashboard-row skill-chip-row" class:archived={Number(item.active) !== 1} class:pending-record={item.pending}><div class="registry-icon-identity">{#if item.iconUrl}<img src={item.iconUrl} alt=""/>{:else}<span>◇</span>{/if}<div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : 'SUPERIOR · HIGH-END'}</small></div></div><span class="skill-chip-quality-pair"><b class="quality-superior">SUP</b><b class="quality-highend">HE</b></span><span>{item.twoPiece ? `${stat(item.twoPiece.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(item.twoPiece.min_value, item.twoPiece.stat_attribute_id)}–${format(item.twoPiece.max_value, item.twoPiece.stat_attribute_id)}` : 'NOT CONFIGURED'}</span><div><strong>{item.talent?.name ?? 'NOT CONFIGURED'}</strong><small>{item.talent?.effects.length ?? 0} SCALED EFFECTS</small></div><span>{item.primaryStatIds.length} PRIMARY · {item.secondaryStatIds.length} SECONDARY</span><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : Number(item.active) ? 'ACTIVE' : 'ARCHIVED'}</b>{#if item.pending}<span></span>{:else}<button onclick={() => edit(item)}>EDIT</button>{/if}</div>
|
||||
<div class="dashboard-row skill-chip-row" class:archived={Number(item.active) !== 1} class:pending-record={item.pending}><div class="registry-icon-identity"><ItemIcon src={item.iconUrl} alt=""/><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : 'SUPERIOR · HIGH-END'}</small></div></div><span class="skill-chip-quality-pair"><b class="quality-superior">SUP</b><b class="quality-highend">HE</b></span><span>{item.twoPiece ? `${stat(item.twoPiece.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(item.twoPiece.min_value, item.twoPiece.stat_attribute_id)}–${format(item.twoPiece.max_value, item.twoPiece.stat_attribute_id)}` : 'NOT CONFIGURED'}</span><div><strong>{item.talent?.name ?? 'NOT CONFIGURED'}</strong><small>{item.talent?.effects.length ?? 0} SCALED EFFECTS</small></div><span>{item.primaryStatIds.length} PRIMARY · {item.secondaryStatIds.length} SECONDARY</span><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : Number(item.active) ? 'ACTIVE' : 'ARCHIVED'}</b>{#if item.pending}<span></span>{:else}<button onclick={() => edit(item)}>EDIT</button>{/if}</div>
|
||||
{:else}<div class="empty-dashboard"><strong>NO SKILL CHIP SETS REGISTERED</strong><span>Add the first Superior and High-end Skill Chip definition.</span></div>{/each}
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import RegistryPagination from '$lib/components/RegistryPagination.svelte';
|
||||
import KpiFilters from '$lib/components/KpiFilters.svelte';
|
||||
import IconUploadField from '$lib/components/IconUploadField.svelte';
|
||||
import ItemIcon from '$lib/components/ItemIcon.svelte';
|
||||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||||
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
|
||||
let { data, form } = $props();
|
||||
@@ -98,4 +99,4 @@
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row talent-row table-heading"><span>TALENT</span><span>CATEGORY</span><span>TIER I EFFECT</span><span>TIER II EFFECT</span><span>STATE</span><span></span></div>{#each data.talents as item}<div class="dashboard-row talent-row" class:pending-record={item.pending}><div class="registry-icon-identity">{#if item.iconUrl}<img src={item.iconUrl} alt=""/>{:else}<span>◇</span>{/if}<div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.category === 'Exotic Weapon' ? `OWNED BY ${item.ownerWeaponName ?? 'UNASSIGNED WEAPON'}` : 'Tiered talent'}</small>{#if cooldownLine(item.cooldown_seconds)}<small class="talent-cooldown">{cooldownLine(item.cooldown_seconds)}</small>{/if}</div></div><span>{item.category}</span><p class="talent-description" class:unconfigured={!configured(item.tiers, 'I')}>{tierDescription(item.tiers, 'I')}</p><p class="talent-description" class:unconfigured={!configured(item.tiers, 'II')}>{tierDescription(item.tiers, 'II')}</p><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : 'ACTIVE'}</b>{#if item.pending}<span></span>{:else if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}<a class="row-action" href={`/isac/weapons?weapon=${item.ownerWeaponId}`}>EDIT WEAPON</a>{:else}<span></span>{/if}{:else}<button onclick={() => editTalent(item)}>EDIT</button>{/if}</div>{/each}</div>
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row talent-row table-heading"><span>TALENT</span><span>CATEGORY</span><span>TIER I EFFECT</span><span>TIER II EFFECT</span><span>STATE</span><span></span></div>{#each data.talents as item}<div class="dashboard-row talent-row" class:pending-record={item.pending}><div class="registry-icon-identity"><ItemIcon src={item.iconUrl} alt=""/><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.category === 'Exotic Weapon' ? `OWNED BY ${item.ownerWeaponName ?? 'UNASSIGNED WEAPON'}` : 'Tiered talent'}</small>{#if cooldownLine(item.cooldown_seconds)}<small class="talent-cooldown">{cooldownLine(item.cooldown_seconds)}</small>{/if}</div></div><span>{item.category}</span><p class="talent-description" class:unconfigured={!configured(item.tiers, 'I')}>{tierDescription(item.tiers, 'I')}</p><p class="talent-description" class:unconfigured={!configured(item.tiers, 'II')}>{tierDescription(item.tiers, 'II')}</p><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : 'ACTIVE'}</b>{#if item.pending}<span></span>{:else if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}<a class="row-action" href={`/isac/weapons?weapon=${item.ownerWeaponId}`}>EDIT WEAPON</a>{:else}<span></span>{/if}{:else}<button onclick={() => editTalent(item)}>EDIT</button>{/if}</div>{/each}</div>
|
||||
|
||||
Reference in New Issue
Block a user