Refine Gear augments and builder levels

This commit is contained in:
JB
2026-07-25 17:41:10 -04:00
parent e5f0414a8e
commit 1465a8c9b3
9 changed files with 220 additions and 52 deletions
+28 -12
View File
@@ -4,13 +4,14 @@
import KpiFilters from '$lib/components/KpiFilters.svelte';
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
let { data, form } = $props();
type Editor = { id: string; name: string; slot: string; tier: number; level: number; base_health_min: number; base_health_max: number; base_armor_min: number | null; base_armor_max: number | null; brandIds: string[]; talentIds: string[] };
type Editor = { id: string; name: string; slot: string; tier: number; base_health_min: number; base_health_max: number; base_armor_min: number | null; base_armor_max: number | null; brandIds: string[]; talentIds: string[] };
const slots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'];
const armoredSlots = ['Gloves', 'Knee Pads', 'Body Armor'];
const blank = (): Editor => ({ id: '', name: '', slot: 'Holster', tier: 0, level: 1, base_health_min: 0, base_health_max: 0, base_armor_min: null, base_armor_max: null, brandIds: [], talentIds: [] });
const blank = (): Editor => ({ id: '', name: '', slot: 'Holster', tier: 0, base_health_min: 0, base_health_max: 0, base_armor_min: null, base_armor_max: null, brandIds: [], talentIds: [] });
let editing = $state<Editor | null>(null);
let augmentEditing = $state<any | null>(null);
let handledResultToken = $state('');
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) => {
const numeric = Number(value);
@@ -23,12 +24,24 @@
const edit = (item: Editor) => { editing = { ...item, brandIds: [...item.brandIds], talentIds: [...item.talentIds] }; };
const gearTalents = (slot: string) => data.talents.filter((talent) => talent.category === slot);
const changeSlot = (slot: string) => { if (editing) editing = { ...editing, slot, talentIds: [] }; };
const baseTargets = (slot: string) => ['Gloves', 'Knee Pads', 'Body Armor'].includes(slot) ? 'BASE HEALTH + ARMOR' : 'BASE HEALTH';
const fixedPercentage = (slot: string, augmentSlot: number) => ['Backpack', 'Body Armor'].includes(slot)
? [0, 0, 40, 70, 100][augmentSlot]
: [0, 0, 50, 100][augmentSlot];
const editAugments = (profile: any) => {
augmentEditing = {
...profile,
augments: Array.from({ length: profile.requiredSlots }, (_, index) => {
const augment = profile.augments.find((item: any) => Number(item.augment_slot) === index + 1);
return augment ? { statId: augment.stat_attribute_id, value: augment.value, unlockCost: augment.unlock_cost } : { statId: '', value: '', unlockCost: '' };
return augment
? {
effectType: augment.effect_type, statId: augment.stat_attribute_id ?? '', value: augment.value,
costs: Object.fromEntries([0,1,2].map((tier) => [tier, augment.costs.find((cost: any) => Number(cost.gear_tier) === tier)?.unlock_cost ?? '']))
}
: {
effectType: index === 0 ? 'Custom' : 'Base Percentage', statId: '',
value: index === 0 ? '' : fixedPercentage(profile.slot, index + 1), costs: { 0: '', 1: '', 2: '' }
};
})
};
editing = null;
@@ -53,7 +66,7 @@
{#each data.augmentProfiles as profile}
<button type="button" class:configured={profile.augments.length === profile.requiredSlots} onclick={() => editAugments(profile)}>
<header><div><strong>{profile.slot}</strong><small>{profile.requiredSlots} AUGMENT SLOTS</small></div><span>{profile.augments.length === profile.requiredSlots ? 'CONFIGURED' : 'REQUIRED'}</span></header>
<div>{#each Array(profile.requiredSlots) as _, index}{@const augment = profile.augments.find((item) => Number(item.augment_slot) === index + 1)}<span><small>0{index + 1}</small><b>{augment ? `${stat(augment.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(augment.value, augment.stat_attribute_id)}` : 'NOT SET'}</b><em>{augment ? `${augment.unlock_cost} COST` : '—'}</em></span>{/each}</div>
<div>{#each Array(profile.requiredSlots) as _, index}{@const augment = profile.augments.find((item) => Number(item.augment_slot) === index + 1)}<span><small>0{index + 1}</small><b>{index === 0 ? (augment ? `${stat(augment.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(augment.value, augment.stat_attribute_id)}` : 'CUSTOM NOT SET') : `+${fixedPercentage(profile.slot,index + 1)}% ${baseTargets(profile.slot)}`}</b><em>{augment ? [0,1,2].map((tier) => `T${tier} ${augment.costs.find((cost: any) => Number(cost.gear_tier) === tier)?.unlock_cost ?? '—'}`).join(' · ') : 'TIER COSTS NOT SET'}</em></span>{/each}</div>
</button>
{/each}
</div>
@@ -64,14 +77,18 @@
<PendingForm action="?/saveAugmentProfile">
<input type="hidden" name="slot" value={augmentEditing.slot}/>
<input type="hidden" name="name" value={`${augmentEditing.slot} tactical augments`}/>
<p class="console-note">These {augmentEditing.requiredSlots} augment slots apply to every {augmentEditing.slot} item. Each slot grants exactly one attribute and unlocks at its configured tactical-augment cost.</p>
<p class="console-note">Augment 01 grants a custom attribute. Later slots increase {baseTargets(augmentEditing.slot).toLocaleLowerCase()} on the equipped item itself, before those item values enter agent totals.</p>
<div class="gear-augment-slots">
{#each augmentEditing.augments as augment: any, index}
<section>
<header><strong>TACTICAL AUGMENT 0{index + 1}</strong><span>SHARED SLOT</span></header>
<div class="field"><span>ATTRIBUTE</span><SearchableAssignment items={data.stats} fieldName={`statId${index + 1}`} mode="single" compact subjectLabel={`AUGMENT ${index + 1} ATTRIBUTE`} searchPlaceholder="Search attributes…" bind:value={augment.statId}/></div>
<label class="field">VALUE<input name={`value${index + 1}`} type="number" step="any" bind:value={augment.value} required/></label>
<label class="field">UNLOCK COST<input name={`unlockCost${index + 1}`} type="number" min="0" step="1" bind:value={augment.unlockCost} required/></label>
<header><strong>TACTICAL AUGMENT 0{index + 1}</strong><span>{index === 0 ? 'CUSTOM ATTRIBUTE' : 'FIXED BASE UPGRADE'}</span></header>
{#if index === 0}
<div class="field"><span>ATTRIBUTE</span><SearchableAssignment items={data.stats} fieldName="statId1" mode="single" compact subjectLabel="AUGMENT 1 ATTRIBUTE" searchPlaceholder="Search attributes…" bind:value={augment.statId}/></div>
<label class="field">VALUE<input name="value1" type="number" step="any" bind:value={augment.value} required/></label>
{:else}
<div class="gear-fixed-augment"><span>{baseTargets(augmentEditing.slot)}</span><b>+{augment.value}%</b><small>APPLIES TO THIS ITEM'S CONFIGURED BASE VALUE</small></div>
{/if}
<fieldset class="gear-tier-costs"><legend>UNLOCK COST BY ITEM TIER</legend><div>{#each [0,1,2] as tier}<label class="field">TIER {tier}<input name={`unlockCost${index + 1}Tier${tier}`} type="number" min="0" step="1" bind:value={augment.costs[tier]} required/></label>{/each}</div></fieldset>
</section>
{/each}
</div>
@@ -90,7 +107,6 @@
<label class="field">NAME<input name="name" bind:value={editing.name} required maxlength="80" /></label>
<label class="field">SLOT<select name="slot" value={editing.slot} onchange={(event) => changeSlot(event.currentTarget.value)}>{#each slots as slot}<option>{slot}</option>{/each}</select></label>
<label class="field">TIER<select name="tier" bind:value={editing.tier}><option value={0}>0</option><option value={1}>1</option><option value={2}>2</option></select></label>
<label class="field">LEVEL<input name="level" type="number" min="1" max="50" step="1" bind:value={editing.level} required /></label>
</div>
<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>
@@ -111,8 +127,8 @@
{/if}
<RegistryPagination pagination={data.pagination}/>
<div class="dashboard-table">
<div class="dashboard-row gear-row table-heading"><span>GEAR</span><span>SLOT</span><span>PROGRESSION</span><span>BASE RANGES</span><span>BRAND SETS</span><span>STATE</span><span></span></div>
<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}><div><strong>{item.name}</strong><small>{item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span>{item.slot}</span><span>T{item.tier} · LVL {item.level}</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><span title={brandNames(item.brandIds)}>{item.brandIds.length} COMPATIBLE</span><b class="state-dot">{Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(item)}>EDIT</button></div>
<div class="dashboard-row gear-row" class:archived={Number(item.active) !== 1}><div><strong>{item.name}</strong><small>{item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span>{item.slot}</span><span>T{item.tier}</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="state-dot">{Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(item)}>EDIT</button></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>