feat: consume selected mechanic revisions

This commit is contained in:
jb
2026-09-02 23:32:42 -04:00
parent 7e392a1f37
commit 8991682e59
31 changed files with 6921 additions and 469 deletions
+38 -6
View File
@@ -7,15 +7,20 @@
import type { DatabaseItem } from '$lib/database/items';
import MarkdownContent from './MarkdownContent.svelte';
import WeaponModSlotIcon from './WeaponModSlotIcon.svelte';
import DatabaseMechanicGroupCard from './database/DatabaseMechanicGroup.svelte';
import DatabaseMechanicStatusBadge from './database/DatabaseMechanicStatusBadge.svelte';
import { databaseMechanicGroupStatus, type DatabaseMechanicGroup } from '$lib/database/items';
let {
item,
hideDiscussionUtility = false,
anchor = true
anchor = true,
detail = false
}: {
item: DatabaseItem & { gameIcon?: GameIconName };
hideDiscussionUtility?: boolean;
anchor?: boolean;
detail?: boolean;
} = $props();
let protocolLevel = $state(1);
let talentUpgrades = $state(0);
@@ -29,6 +34,14 @@
const effect = item.protocolPreview?.talentEffects[Number(raw) - 1];
return effect ? valueWithUnit(effect.baseValue * (1 + talentUpgrades / 5), effect.unit) : placeholder;
});
const mechanicGroups = () => item.mechanicGroups ?? [];
const hasMechanicGroups = () => mechanicGroups().length > 0;
const isSpecializationMechanic = (group: DatabaseMechanicGroup) => group.ownerType === 'specialization_tree_rank';
const hasSpecializationMechanics = () => mechanicGroups().some(isSpecializationMechanic);
const primaryMechanicGroup = () => {
const groups = mechanicGroups();
return groups.length === 1 && !isSpecializationMechanic(groups[0]) ? groups[0] : null;
};
</script>
<article id={anchor ? `database-item-${item.id}` : undefined} class="database-item-card" class:talent-card={item.variant === 'talent'} class:protocol-quality-standard={item.topBorderTone === 'standard'} class:protocol-quality-specialized={item.topBorderTone === 'specialized'} class:protocol-quality-superior={item.topBorderTone === 'superior'} class:protocol-quality-highend={item.topBorderTone === 'highend'}>
@@ -39,7 +52,12 @@
</div>
<div class="database-item-identity">
<small>{item.kicker}</small>
<h2>{item.name}</h2>
<div class="database-item-name-line">
<h2>{item.name}</h2>
{#if primaryMechanicGroup()}
<DatabaseMechanicStatusBadge status={databaseMechanicGroupStatus(primaryMechanicGroup()!)} />
{/if}
</div>
</div>
{#if item.headerBadges?.length || item.headerBadge || item.headerMetaIcon}
<div class="database-header-status">
@@ -53,7 +71,7 @@
{#if item.protocolPreview}
<section class="database-protocol-preview">
<div class="database-protocol-primary"><span>{item.protocolPreview.primaryStat}</span><b>{Number.isFinite(protocolPrimaryValue()) ? valueWithUnit(protocolPrimaryValue(), item.protocolPreview.primaryUnit) : 'NOT CONFIGURED'}</b></div>
{#if item.protocolPreview.talentDescription}
{#if item.protocolPreview.talentDescription && !hasMechanicGroups()}
<div class="database-protocol-talent"><p>{talentDescription()}</p></div>
{/if}
<div class="database-protocol-controls"><label class="database-protocol-level"><span>LVL {protocolLevel}</span><input type="range" min="1" max="50" step="1" bind:value={protocolLevel} aria-label={`${item.name} level`}/></label>{#if item.protocolPreview.talentDescription}<div class="database-protocol-diamonds" role="group" aria-label={`${item.name} talent upgrades`}>{#each [1, 2, 3, 4, 5] as upgrade}<button type="button" class:active={talentUpgrades >= upgrade} aria-label={`Set talent upgrades to ${upgrade}`} aria-pressed={talentUpgrades === upgrade} onclick={() => talentUpgrades = talentUpgrades === upgrade ? 0 : upgrade}>◆</button>{/each}</div>{/if}</div>
@@ -86,15 +104,29 @@
{/each}
</div>
{/if}
{#if item.variant === 'talent'}
{#if hasMechanicGroups() && !hasSpecializationMechanics()}
<div class="database-mechanic-groups">
{#each mechanicGroups() as group (group.id)}
<DatabaseMechanicGroupCard {group} {detail}/>
{/each}
</div>
{/if}
{#if item.variant === 'talent' && !hasMechanicGroups()}
<div class="database-talent-tiers">
<section><span>TIER I</span><p>{item.description ?? 'Description is not available.'}</p></section>
<section><span>TIER II</span><p>{item.secondaryDescription ?? 'Description is not available.'}</p></section>
{#if item.secondaryDescription}<section><span>TIER II</span><p>{item.secondaryDescription}</p></section>{/if}
</div>
{:else if !item.protocolPreview}
{:else if !item.protocolPreview && (!hasMechanicGroups() || hasSpecializationMechanics())}
{#if item.description}<p class:data-description={item.descriptionStyle === 'data'}>{item.description}</p>{/if}
{#if item.secondaryDescription}<p class="secondary-description" class:data-description={item.descriptionStyle === 'data'}>{item.secondaryDescription}</p>{/if}
{/if}
{#if hasSpecializationMechanics()}
<div class="database-mechanic-groups database-specialization-effect-groups">
{#each mechanicGroups().filter(isSpecializationMechanic) as group (group.id)}
<DatabaseMechanicGroupCard {group} {detail} showIdentity/>
{/each}
</div>
{/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}
+4 -3
View File
@@ -18,6 +18,7 @@
let closeButton = $state<HTMLButtonElement | null>(null);
let dialog = $state<HTMLDivElement | null>(null);
const openPreview = async (item: DatabaseRelatedItem, button: HTMLButtonElement) => {
if (!item.preview) return;
selected = item;
trigger = button;
await tick();
@@ -32,8 +33,8 @@
};
const trapDialogFocus = (event: KeyboardEvent) => {
if (event.key !== 'Tab' || !dialog) return;
const focusable = [...dialog.querySelectorAll<HTMLElement>('a[href],button:not(:disabled),[tabindex]:not([tabindex="-1"])')]
.filter((element) => !element.hasAttribute('hidden'));
const focusable = [...dialog.querySelectorAll<HTMLElement>('a[href],button:not(:disabled),input:not([type="hidden"]):not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex]:not([tabindex="-1"])')]
.filter((element) => !element.hasAttribute('hidden') && element.getClientRects().length > 0);
const first = focusable[0];
const last = focusable.at(-1);
if (!first || !last) return;
@@ -65,7 +66,7 @@
<div class="database-preview-backdrop" role="presentation" onclick={(event) => { if (event.target === event.currentTarget) void closePreview(); }}>
<div class="database-preview-dialog" bind:this={dialog} role="dialog" aria-modal="true" aria-labelledby={`database-preview-${selected.id}`} tabindex="-1" onkeydown={trapDialogFocus}>
<header><div><small>DATABASE QUICK VIEW</small><strong id={`database-preview-${selected.id}`}>{selected.preview.name}</strong></div><button type="button" bind:this={closeButton} aria-label="Close details" onclick={() => void closePreview()}>×</button></header>
<DatabaseItemCard item={selected.preview}/>
<DatabaseItemCard item={selected.preview} anchor={false} detail/>
</div>
</div>
{/if}
@@ -0,0 +1,60 @@
<script lang="ts">
import {
databaseMechanicGroupDisplaySourceLabel,
databaseMechanicGroupStatus,
databaseMechanicStatusLabel,
type DatabaseMechanicGroup
} from '$lib/database/items';
import DatabaseMechanicStatusBadge from './DatabaseMechanicStatusBadge.svelte';
let {
group,
detail = false,
showIdentity = false
}: {
group: DatabaseMechanicGroup;
detail?: boolean;
showIdentity?: boolean;
} = $props();
const status = $derived(databaseMechanicGroupStatus(group));
const displaySource = $derived(databaseMechanicGroupDisplaySourceLabel(group));
</script>
<section
class="database-mechanic-group"
class:detail
class:specialization-effect={showIdentity}
data-status={status}
aria-label={`${group.label}: ${databaseMechanicStatusLabel(status)}`}
>
{#if showIdentity}
<header class="database-mechanic-group-identity">
<div>
<small>{group.label}</small>
{#if group.context}<strong>{group.context}</strong>{/if}
</div>
<DatabaseMechanicStatusBadge {status}/>
</header>
{:else if group.context}
<p class="database-mechanic-group-context">{group.context}</p>
{/if}
<div class="database-mechanic-variants">
{#each group.variants as variant (variant.label)}
<section class="database-mechanic-variant" data-status={variant.status}>
<div class="database-mechanic-variant-label"><span>{variant.label}</span></div>
<div class="database-mechanic-behavior">
<small>{variant.status === 'modeled' ? 'MODELED BEHAVIOR' : 'AUTHORED DESCRIPTION'}</small>
<p aria-label={variant.behaviorText} title={detail ? undefined : variant.behaviorText}>{variant.behaviorText}</p>
</div>
</section>
{/each}
</div>
{#if detail}
<dl class="database-mechanic-display-source">
<div><dt>DISPLAY SOURCE</dt><dd>{displaySource}</dd></div>
</dl>
{/if}
</section>
@@ -0,0 +1,12 @@
<script lang="ts">
import {
databaseMechanicStatusLabel,
type DatabaseMechanicStatus
} from '$lib/database/items';
let { status }: { status: DatabaseMechanicStatus } = $props();
</script>
<strong class="database-mechanic-status" class:not-modeled={status === 'not_modeled'}>
{databaseMechanicStatusLabel(status)}
</strong>