@@ -12,11 +12,12 @@
import LoadoutMatrix , { type MatrixCell } from '$lib/components/builder/LoadoutMatrix.svelte' ;
import LoadoutStatsSummary from '$lib/components/builder/LoadoutStatsSummary.svelte' ;
import PlannedCombatModel from '$lib/components/builder/PlannedCombatModel.svelte' ;
import TalentTierChoice from '$lib/components/builder/TalentTierChoice.svelte' ;
import { summarizeLoadout } from '$lib/builder/loadout-summary' ;
import { toast } from '$lib/toast' ;
import {
blankRoll , clamp , createDraft , gearBaseStatRange , gearSlots , levelScaledRange , parseLoadoutDraft , remapRangeValue , scaled , skillChipQualities , skillChipSecondaryCapacity , weaponBaseRange ,
type LoadoutDraft , type Roll , type SkillChipQuality
blankRoll , clamp , createDraft , gearBaseStatRange , gearSlots , levelScaledRange , migrateLoadoutDraftV5 , parseLoadoutDraft , remapRangeValue , scaled , skillChipQualities , skillChipSecondaryCapacity , weaponBaseRange ,
type LoadoutDraft , type Roll , type SkillChipQuality , type TalentTier
} from '$lib/builder/loadout' ;
let { data , form } = $props ();
@@ -71,6 +72,8 @@
return selectedRank ? [{ node , rank : node.ranks.find (( entry : any ) => Number ( entry . rank ) === selectedRank ) }] : [];
}) ?? []);
const talent = ( id : string ) => data . talents . find (( item ) => item . id === id );
const supportedTalentTier = ( id : string , tier : TalentTier ) : TalentTier =>
talent ( id ) ? . tiers . some (( entry : any ) => entry . tier === ( tier === 2 ? 'II' : 'I' )) ? tier : 1 ;
const brand = ( id : string ) => data . brands . find (( item ) => item . id === id );
const unitValue = ( value : unknown , statId : string ) => {
const numeric = Number ( value );
@@ -207,6 +210,7 @@
selection . level = 50 ;
selection . brandId = item ? . brandIds [ 0 ] ?? '' ;
selection . talentId = item ? . talentIds [ 0 ] ?? '' ;
selection . talentTier = supportedTalentTier ( selection . talentId , Number ( item ? . tier ) >= 2 ? 2 : 1 );
selection . health = Number ( item ? . base_health_min ?? 0 );
selection . armor = Number ( item ? . base_armor_min ?? 0 );
const core = coreAllocationRange ( Number ( item ? . tier ?? 0 ), selection . level , 'gear' );
@@ -255,7 +259,10 @@
const core = coreAllocationRange ( selection . tier , selection . level , 'weapon' );
for ( const key of coreKeys ) selection [ key ] = core . min ;
selection . talent1Id = item ? . talent1Ids [ 0 ] ?? '' ;
selection . talent1Tier = supportedTalentTier ( selection . talent1Id , selection . tier >= 2 ? 2 : 1 );
selection . talent2Id = item ? . talent2Ids [ 0 ] ?? '' ;
selection . talent2Tier = supportedTalentTier ( selection . talent2Id , selection . tier >= 2 ? 2 : 1 );
selection . exoticTalentTier = supportedTalentTier ( item ? . exotic_talent_id ?? '' , selection . tier >= 2 ? 2 : 1 );
selection . rolls = [ blankRoll (), blankRoll ()];
};
const configureChip = ( index : number , id : string ) => {
@@ -375,7 +382,7 @@
}));
const weaponCells = $derived . by (() : MatrixCell [] => draft . weapons . map (( selection , index ) => {
const item = weaponItem ( index );
const talents = [ selection . talent1Id , selection . talent2Id ]. map (( id ) => talent ( id ) ? . name ). filter ( Boolean );
const talents = [[ selection . talent1Id , selection . talent1Tier ], [ selection . talent2Id , selection . talent2Tier ] ].map (([ id , tier ] ) => id ? ` ${ talent ( String ( id )) ? . name ?? 'Talent' } T ${ tier } ` : '' ). filter ( Boolean );
const details = [... talents , rollSummary ( selection . rolls )]. filter ( Boolean ). join ( ' · ' );
return {
id : `weapon: ${ index } ` ,
@@ -392,7 +399,7 @@
const item = gearItem ( slot );
const details = [
brand ( selection . brandId ) ? . name ,
talent ( selection . talentId ) ? . name ,
selection . talentId ? ` ${ talent ( selection . talentId ) ? . name ?? 'Talent' } T ${ selection . talentTier } ` : '' ,
rollSummary ( selection . rolls )
]. filter ( Boolean ). join ( ' · ' );
return {
@@ -429,17 +436,24 @@
localStorage . removeItem ( 'shd-build-loadout-draft-v3' );
localStorage . removeItem ( 'shd-build-loadout-draft-v4' );
if ( data . initialBuild ) return ;
const stored = localStorage . getItem ( 'shd-build-loadout-draft-v5 ' );
const current = localStorage . getItem ( 'shd-build-loadout-draft-v6 ' );
const legacy = current ? null : localStorage . getItem ( 'shd-build-loadout-draft-v5' );
const stored = current ?? legacy ;
if ( ! stored ) return ;
try {
const parsed = parseLoadoutDraft ( JSON . parse ( stored )) ;
const raw = JSON . parse ( stored );
const parsed = current ? parseLoadoutDraft ( raw ) : migrateLoadoutDraftV5 ( raw , data );
if ( parsed ) {
draft = parsed ;
if ( legacy ) {
localStorage . setItem ( 'shd-build-loadout-draft-v6' , JSON . stringify ( parsed ));
localStorage . removeItem ( 'shd-build-loadout-draft-v5' );
}
if ( weaponItem ( 0 ) && weaponItem ( 1 ) && Number ( weaponItem ( 0 ). is_exotic ) && Number ( weaponItem ( 1 ). is_exotic )) draft . weapons [ 1 ]. itemId = '' ;
saved = true ;
} else localStorage . removeItem ( 'shd-build-loadout-draft-v5' );
} else localStorage . removeItem ( current ? 'shd-build-loadout-draft-v6' : 'shd-build-loadout-draft-v5' );
} catch {
localStorage . removeItem ( 'shd-build-loadout-draft-v5' );
localStorage . removeItem ( current ? 'shd-build-loadout-draft-v6' : 'shd-build-loadout-draft-v5' );
}
});
$effect (() => {
@@ -457,7 +471,7 @@
: 'Build published to the SHD archive.' ;
});
function saveDraft() {
localStorage . setItem ( 'shd-build-loadout-draft-v5 ' , JSON . stringify ( draft ));
localStorage . setItem ( 'shd-build-loadout-draft-v6 ' , JSON . stringify ( draft ));
saved = true ;
toast . success ( 'Draft saved locally' , 'This loadout is available on this device.' );
}
@@ -495,7 +509,7 @@
{ #if persistedSlug && savedBuildVisibility !== 'private' } < a class = "builder-share-link" href = { `/builds/ ${ persistedSlug } ` } > VIEW SHARE PAGE →</ a > { /if }
</ div >
{ /if }
< div class = "builder-status" >< span >< i ></ i > KNOWN STATS ONLINE</ span >< span > ADVANCED MODEL · COMING SOON</ span >< span > 12 LOADOUT CELLS + 9 CHIPS </ span >< span > { data . gear . length + data . weapons . length + data . chipSets . length + data . protocols . length } CATALOG ITEMS </ span >< /div >
< div class = "builder-status" >< span >< i ></ i > KNOWN STATS ONLINE</ span >< span > ADVANCED MODEL · COMING SOON</ span ></ div >
</ div >
{ #if message } < p class = "success" role = "status" > { message } </ p > { /if }
{ #if form ? . buildError } < p class = "error" role = "alert" > { form . buildError } </ p > { /if }
@@ -518,29 +532,28 @@
<div class="loadout-editor-head"><div><span>AGENT CLASS</span><h2>SPECIALIZATION FOCUS</h2></div><b>{selectedFocus ? ` $ { selectedSpecializationPoints } / $ { data . specializationProgression . availablePoints } POINTS ` : 'UNASSIGNED' } </b ></ div >
< BuilderPicker items = { specializationItems } value= { draft . specialization . focusId } onchange = { selectFocus } label="SPECIALIZATION + FOCUS " />
{ #if selectedFocus }
< div class = "builder-specialization-identity" >< span class = "focus-composite-icon" >< ItemIcon src = { selectedFocus . specialization . iconUrl } alt= { selectedFocus . specialization . name } / >< GameIcon name = { selectedFocus . primary_attribute . toLocaleLowerCase () as GameIconName } decorative/ ></ span >< div >< small > { selectedFocus . specialization . name } </ small >< h3 > { selectedFocus . name } </ h3 >< p > { selectedFocus . description || 'No focus description has been entered.' } </ p ></ div >< b class:configured = { Number ( selectedFocus . builder_ready ) === 1 } > { Number ( selectedFocus . builder_ready ) === 1 ? 'BUILDER READY' : 'ISAC DATA INCOMPLETE' } </b ></ div >
< div class = "builder-ability-chip-p anels " >
{ #each [ 0 , 1 , 2 ] as abilityIndex }
{ @const ability = selectedFocus . abilities [ abilityIndex ]}
< section >
< header >< ItemIcon src = { ability ? . iconUrl } alt= { ability ? . name ?? `Ability $ { abilityIndex + 1 } `}/><div><small>ABILITY {abilityIndex + 1}</small><strong>{ability?.name ?? 'NOT CONFIGURED'}</strong></div><b>{draft.chips.slice(abilityIndex * 3, abilityIndex * 3 + 3).filter((chip) => chip.setId).length} / 3 CHIPS</b></header>
<p>{ability?.description ?? 'ISAC requires this class ability before compatible Skill Chips can be equipped.'}</p >
<div>{#each [0,1,2] as chipSlot}{@const chipIndex = abilityIndex * 3 + chipSlot}{@const item = chipItem(chipIndex)}<button type="button" onclick={() => active = ` chip :$ { chipIndex } `}><small>CHIP {chipSlot + 1}</small><strong>{item?.name ?? 'EMPTY SLOT'}</strong><span>{item ? draft.chips[chipIndex].quality : 'SELECT CHIP'}</span></button>{/each}</div>
</section>
{/each}
</div>
<div class="builder-specialization-telemetry"><span>POINTS <b>{selectedSpecializationPoints} / {data.specializationProgression.availablePoints}</b></span><span>CREDITS <b>{selectedSpecializationCosts.credits.toLocaleString()}</b></span>{#each selectedSpecializationCosts.resources as resource}<span>{String(resource[0]).replace('resource-electronics-','').replaceAll('-',' ').toUpperCase()} <b>× {resource[1]}</b></span>{/each}</div>
<div class="builder-focus-tree">
<div class="builder-section-title"><div><span>FOCUS UPGRADE TREE</span><small>PREVIOUS ROWS REQUIRE EVERY AVAILABLE RANK 1 + 2 BEFORE ADVANCING</small></div><b>{selectedFocus.nodes.length} NODES</b></div>
{#each [...new Set(selectedFocus.nodes.map((node: any) => Number(node.row_number)))] as row}
<section><header><span>ROW {String(row).padStart(2, '0')}</span><b>{row === 1 ? 'ENTRY ROW' : 'LOCKED BY PREVIOUS ROWS'}</b></header><div>
{#each selectedFocus.nodes.filter((node: any) => Number(node.row_number) === row) as node}
<article><ItemIcon src={node.iconUrl} alt={node.name}/><div><strong>{node.name}</strong><small>{node.description_template}</small></div><SegmentedChoice legend={` $ { node . name } rank ` } name = { `specialization-node- ${ node . id } ` } options= {[{ value : '0' , label : 'OFF' },... Array . from ({ length :Number ( node . max_rank )},( _ , index ) => ({ value :String ( index + 1 ), label : `R$ { index + 1 } `,className:canSelectNodeRank(node,index+1)?'':'unavailable',disabled:!canSelectNodeRank(node,index+1)}))]} value={String(draft.specialization.nodeRanks[node.id] ?? 0)} onchange={(values) => setNodeRank(node,Number(values[0]))}/></article>
< div class = "builder-specialization-layout" >
< div class = "builder-editor-l ane" >
< div class = "builder-specialization-identity" >< span class = "focus-composite-icon" >< ItemIcon src = { selectedFocus . specialization . iconUrl } alt= { selectedFocus . specialization . name } / >< GameIcon name = { selectedFocus . primary_attribute . toLocaleLowerCase () as GameIconName } decorative/ ></ span >< div >< small > { selectedFocus . specialization . name } </ small >< h3 > { selectedFocus . name } </ h3 >< p > { selectedFocus . description || 'No focus description has been entered.' } </ p ></ div >< b class:configured = { Number ( selectedFocus . builder_ready ) === 1 } > { Number ( selectedFocus . builder_ready ) === 1 ? 'BUILDER READY' : 'ISAC DATA INCOMPLETE' } </b ></ div >
< div class = "builder-focus-abilities" aria-label = "Assigned abilities" >
{ #each [ 0 , 1 , 2 ] as abilityIndex }
{ @const ability = selectedFocus . abilities [ abilityIndex ]}
< button type = "button" onclick = {() => active = `ability:$ { abilityIndex } `}><ItemIcon src={ability?.iconUrl} alt=""/><span><small>ABILITY {abilityIndex + 1}</small><strong>{ability?.name ?? 'NOT CONFIGURED'}</strong></span><b>{draft.chips.slice(abilityIndex * 3, abilityIndex * 3 + 3).filter((chip) => chip.setId).length} / 3 CHIPS</b></button >
{/each}
</div></section>
{/each}
</div>
<div class="builder-specialization-telemetry"><span>POINTS <b>{selectedSpecializationPoints} / {data.specializationProgression.availablePoints}</b></span><span>CREDITS <b>{selectedSpecializationCosts.credits.toLocaleString()}</b></span>{#each selectedSpecializationCosts.resources as resource}<span>{String(resource[0]).replace('resource-electronics-','').replaceAll('-',' ').toUpperCase()} <b>× {resource[1]}</b></span> {/each}</div>
</div>
<div class="builder-focus-tree builder-editor-lane">
<div class="builder-section-title"><div><span>FOCUS UPGRADE TREE</span><small>COMPLETE EARLIER ROWS TO ADVANCE</small></div><b>{selectedFocus.nodes.length} NODES</b></div>
{#each [...new Set(selectedFocus.nodes.map((node: any) => Number(node.row_number)))] as row}
<section><header><span>ROW {String(row).padStart(2, '0')}</span><b>{row === 1 ? 'ENTRY ROW' : 'PREVIOUS ROWS REQUIRED'}</b></header><div>
{#each selectedFocus.nodes.filter((node: any) => Number(node.row_number) === row) as node}
<article><ItemIcon src={node.iconUrl} alt={node.name}/><div><strong>{node.name}</strong><small>{node.description_template}</small></div><SegmentedChoice legend={` $ { node . name } rank ` } name= { `specialization-node- ${ node . id } ` } options = {[{ value : '0' , label : 'OFF' },... Array . from ({ length :Number ( node . max_rank )},( _ , index ) => ({ value :String ( index + 1 ), label : `R$ { index + 1 } `,className:canSelectNodeRank(node,index+1)?'':'unavailable',disabled:!canSelectNodeRank(node,index+1)}))]} value={String(draft.specialization.nodeRanks[node.id] ?? 0)} onchange={(values) => setNodeRank(node,Number(values[0]))}/></article>
{/each}
</div></section>
{/each}
</div>
</div>
<PlannedCombatModel headingId="builder-combat-model-heading" />
{/if}
{:else if activeParts[0] === 'ability'}
{@const abilityIndex = Number(activeParts[1])}
@@ -565,20 +578,27 @@
{#if item}
{@const healthRange = gearBaseRange(item, 'health', selection.level)}
{@const armorRange = item.base_armor_min !== null ? gearBaseRange(item, 'armor', selection.level) : null}
<div class="builder-equipment-controls">
<RangeValueControl label="GEAR LEVEL" min={Number(item.tier) === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeGearLevel(slot,value)}/>
<BuilderRadioGrid label="BRAND SET" name={` gear - $ { slot } - brand ` } items = { item . brandIds . map (( id : string ) => brand ( id )). filter ( Boolean )} bind :value = { selection . brandId } columns = { Math . min ( item . brandIds . length , 4 )} iconOnly required />
{ #if item . talentIds . length }< BuilderRadioGrid label = "EQUIPPED TALENT" name = { `gear- ${ slot } -talent` } items = { item . talentIds . map (( id : string ) => talent ( id )). filter ( Boolean )} bind :value = { selection . talentId } columns = { Math . min ( item . talentIds . length , 4 )} searchable = { item . talentIds . length > 6 } required /> { /if }
</ div >
< p class : provisional = { healthRange . provisional } class = "builder-scaling-note" > { healthRange . provisional ? 'Estimated range: levels 1– 9 remain provisional while additional game samples are collected.' : 'Base Health and Armor are calculated from the stored gear base ranges at the selected item level. Display values are truncated to match the game.' }</ p >
< div class = "builder-range-grid" >< RangeValueControl label = "BASE HEALTH" min = { healthRange . min } max = { healthRange . max } provisional = { healthRange . provisional } bind :value = { selection . health } /> { #if armorRange }< RangeValueControl label = "BASE ARMOR" min = { armorRange . min } max = { armorRange . max } provisional = { armorRange . provisional } bind :value = { selection . armor } /> { /if }</ div >
{@const gearCore = coreAllocationRange(Number(item.tier), selection.level, 'gear')}
< div class = "builder-section-title" >< div >< span > CORE ATTRIBUTE ALLOCATION </ span >< small > SHARED T { item . tier } POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES </ small ></ div >< b class : restricted = { coreTotal ( selection ) < gearCore . poolMin || coreTotal ( selection ) > gearCore . poolMax } > { coreTotal ( selection )} / { gearCore . poolMin } – { gearCore . poolMax }</ b ></ div >
< div class = "builder-core-grid" >{ #each coreKeys as key }< RangeValueControl label = { key . toUpperCase ()} min = { 0 } max = { gearCore . poolMax } bind :value = { selection [ key ]} /> { /each }</ div >
< div class = "builder-section-title" >< div >< span > ROLLED ATTRIBUTES </ span >< small > TWO REQUIRED SLOTS · VALUES ARE LIMITED TO GEAR RANGES </ small ></ div ></ div >
< div class = "builder-rolls required" >{ #each selection . rolls as roll , index }{ @const rollRange = attributeRange ( attribute ( roll . statId ), 'gear' , selection . level )}< div >< BuilderPicker items = { gearAttributes } bind :value = { roll . statId } onchange = {( id ) => setRollStat ( roll , id , 'gear' , selection . level )} disabledIds = { selection . rolls . filter (( _ , itemIndex ) => itemIndex !== index ). map (( entry ) => entry . statId ). filter ( Boolean )} label = { `ATTRIBUTE ${ index + 1 } ` } /><RangeValueControl label="VALUE" min= { rollRange . min } max = { rollRange . max } provisional = { rollRange . provisional } bind :value = { roll . value } /></div> { /each }</ div >
{@const augments = data.gearAugments.filter((entry) => entry.gear_slot === slot)}
{ #if augments . length }< div class = "builder-section-title" >< div >< span > TACTICAL AUGMENTS </ span >< small > ENABLE COLLECTED UPGRADES · BASE BONUSES APPLY TO THIS ITEM ONLY </ small ></ div >< b >{ selection . augmentSlots . length } / { augments . length } ACTIVE </ b ></ div >< div class = "builder-augment-list" >{ #each augments as augment }< label >< input type = "checkbox" checked = { selection . augmentSlots . includes ( Number ( augment . augment_slot ))} onchange = {() => selection . augmentSlots = selection . augmentSlots . includes ( Number ( augment . augment_slot )) ? selection . augmentSlots . filter (( value ) => value !== Number ( augment . augment_slot )) : [... selection . augmentSlots , Number ( augment . augment_slot )]} /><span><b>AUGMENT 0 { augment . augment_slot }</ b >< small >{ augment . effect_type === 'Custom' ? `$ { attribute ( augment . stat_attribute_id ) ? . name } · $ { unitValue ( augment . value , augment . stat_attribute_id )} ` : ` + $ { augment . value } % BASE HEALTH$ { item . base_armor_min !== null ? ' + ARMOR' : '' } `}</small></span><em>{augment.costs.find((cost: any) => Number(cost.gear_tier) === Number(item.tier))?.unlock_cost ?? '—'} COST · T{item.tier}</em></label>{/each}</div>{/if}
<div class="builder-editor-columns">
<section class="builder-editor-lane" aria-label="Gear identity and base values">
<div class="builder-equipment-controls">
<RangeValueControl label="GEAR LEVEL" min={Number(item.tier) === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeGearLevel(slot,value)}/>
<BuilderRadioGrid label="BRAND SET" name={` gear - $ { slot } - brand ` } items = { item . brandIds . map (( id : string ) => brand ( id )). filter ( Boolean )} bind :value = { selection . brandId } columns = { Math . min ( item . brandIds . length , 4 )} iconOnly required />
{ #if item . talentIds . length }< BuilderRadioGrid label = "EQUIPPED TALENT" name = { `gear- ${ slot } -talent` } items = { item . talentIds . map (( id : string ) => talent ( id )). filter ( Boolean )} value = { selection . talentId } onchange = {( id ) => { selection . talentId = id ; selection . talentTier = supportedTalentTier ( id , selection . talentTier ); }} columns = { Math . min ( item . talentIds . length , 4 )} searchable = { item . talentIds . length > 6 } required /> { /if }
{ #if selection . talentId && talent ( selection . talentId )}< TalentTierChoice name = { `gear- ${ slot } -talent-tier` } talent = { talent ( selection . talentId )} bind :value = { selection . talentTier } /> { /if }
</ div >
< p class : provisional = { healthRange . provisional } class = "builder-scaling-note" > { healthRange . provisional ? 'Estimated range: levels 1– 9 remain provisional.' : 'Base values scale with item level and use game-matching truncated display values.' }</ p >
< div class = "builder-range-grid" >< RangeValueControl label = "BASE HEALTH" min = { healthRange . min } max = { healthRange . max } provisional = { healthRange . provisional } bind :value = { selection . health } /> { #if armorRange }< RangeValueControl label = "BASE ARMOR" min = { armorRange . min } max = { armorRange . max } provisional = { armorRange . provisional } bind :value = { selection . armor } /> { /if }</ div >
</ section >
< section class = "builder-editor-lane" aria-label = "Gear attributes and augments" >
< div class = "builder-section-title" >< div >< span > CORE ALLOCATION </ span >< small > SHARED T { item . tier } POOL </ small ></ div >< b class : restricted = { coreTotal ( selection ) < gearCore . poolMin || coreTotal ( selection ) > gearCore . poolMax } > { coreTotal ( selection )} / { gearCore . poolMin } – { gearCore . poolMax }</ b ></ div >
< div class = "builder-core-grid" >{ #each coreKeys as key }< RangeValueControl label = { key . toUpperCase ()} min = { 0 } max = { gearCore . poolMax } bind :value = { selection [ key ]} /> { /each }</ div >
< div class = "builder-section-title" >< div >< span > ROLLED ATTRIBUTES </ span >< small > TWO REQUIRED SLOTS </ small ></ div ></ div >
< div class = "builder-rolls required" >{ #each selection . rolls as roll , index }{ @const rollRange = attributeRange ( attribute ( roll . statId ), 'gear' , selection . level )}< div >< BuilderPicker items = { gearAttributes } bind :value = { roll . statId } onchange = {( id ) => setRollStat ( roll , id , 'gear' , selection . level )} disabledIds = { selection . rolls . filter (( _ , itemIndex ) => itemIndex !== index ). map (( entry ) => entry . statId ). filter ( Boolean )} label = { `ATTRIBUTE ${ index + 1 } ` } /><RangeValueControl label="VALUE" min= { rollRange . min } max = { rollRange . max } provisional = { rollRange . provisional } bind :value = { roll . value } /></div> { /each }</ div >
{ #if augments . length }< div class = "builder-section-title" >< div >< span > TACTICAL AUGMENTS </ span >< small > OPTIONAL ITEM UPGRADES </ small ></ div >< b >{ selection . augmentSlots . length } / { augments . length } ACTIVE </ b ></ div >< div class = "builder-augment-list" >{ #each augments as augment }< label >< input type = "checkbox" checked = { selection . augmentSlots . includes ( Number ( augment . augment_slot ))} onchange = {() => selection . augmentSlots = selection . augmentSlots . includes ( Number ( augment . augment_slot )) ? selection . augmentSlots . filter (( value ) => value !== Number ( augment . augment_slot )) : [... selection . augmentSlots , Number ( augment . augment_slot )]} /><span><b>AUGMENT 0 { augment . augment_slot }</ b >< small >{ augment . effect_type === 'Custom' ? `$ { attribute ( augment . stat_attribute_id ) ? . name } · $ { unitValue ( augment . value , augment . stat_attribute_id )} ` : ` + $ { augment . value } % BASE HEALTH$ { item . base_armor_min !== null ? ' + ARMOR' : '' } `}</small></span><em>{augment.costs.find((cost: any) => Number(cost.gear_tier) === Number(item.tier))?.unlock_cost ?? '—'} COST · T{item.tier}</em></label>{/each}</div>{/if}
</section>
</div>
{/if}
{:else if activeParts[0] === 'weapon'}
{@const index = Number(activeParts[1])}{@const selection = draft.weapons[index]}{@const item = weaponItem(index)}{@const other = weaponItem(index ? 0 : 1)}
@@ -586,27 +606,32 @@
<BuilderPicker items={data.weapons} bind:value={selection.itemId} onchange={(id) => configureWeapon(index,id)} disabledIds={other && Number(other.is_exotic) ? data.weapons.filter((entry) => Number(entry.is_exotic)).map((entry) => entry.id) : []} label="WEAPON DEFINITION"/>
{#if item}
{#if Number(item.is_exotic) && other && Number(other.is_exotic)}<p class="error">Only one exotic weapon may be equipped. Choose a standard weapon in either slot.</p>{/if}
<div class="builder-weapon-controls">
<RangeValueControl label="WEAPON LEVEL" min={selection.tier === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeWeaponLevel(index,value)}/>
<SegmentedChoice legend="TIER" name={` weapon - $ { index } - tier ` } options = { item . ranges . map (( range : any ) => ({ value : String ( range . tier ), label : `T$ { range . tier } ` }))} value={String(selection.tier)} onchange={(values) => changeWeaponTier(index,Number(values[0]))} required/>
<div class="builder-fixed"><span>{item.rpm} RPM</span><span>{item.magazine} SHOTS</span><span>{item.reload_speed}s RELOAD</span></div>
</div>
{@const range = item.ranges.find((entry: any) => Number(entry.tier) === selection.tier)}
{#if range}
<p class:provisional={selection.level < 40} class="builder-scaling-note">{selection.level < 40 ? 'Estimated range: the weapon curve below level 40 is provisional and may change as observations are added.' : 'Damage is calculated from the stored weapon base range at the selected item level. Core attributes use the shared level-50 tier allocation pool.'}</p >
{@const damageRange = weaponStatRange(range,'damage',selection.level)}
<div class="builder-range-grid weapon-damage"><RangeValueControl label="DAMAGE" min={damageRange.min} max={damageRange.max} provisional={damageRange.provisional} bind:value={selection.damage}/></div >
{@const weaponCore = coreAllocationRange(selection.tier, selection.level, 'weapon')}
<div class="builder-section-title"><div><span>CORE ATTRIBUTE ALLOCATION</span><small>SHARED T{selection.tier} POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES</small></div><b class:restricted={coreTotal(selection) < weaponCore.poolMin || coreTotal(selection ) > weaponCore.poolMax}>{coreTotal(selection)} / {weaponCore.poolMin}– {weaponCore.poolMax}</b></div >
<div class="builder-core-grid">{#each coreKeys as key}<RangeValueControl label={key.toUpperCase()} min={0} max={weaponCore.poolMax} bind:value={selection[key]}/>{/each} </div>
{#if range}{@const damageRange = weaponStatRange(range,'damage',selection.level)}{@const weaponCore = coreAllocationRange(selection.tier, selection.level, 'weapon')}
<div class="builder-editor-columns" >
<section class="builder-editor-lane" aria-label="Weapon identity and base values">
<div class="builder-weapon-controls" >
<RangeValueControl label="WEAPON LEVEL" min={selection.tier === 2 ? 40 : 1} max={50} value={selection.level} onchange={(value) => changeWeaponLevel(index,value)}/>
<SegmentedChoice legend="TIER" name={` weapon - $ { index } - tier ` } options = { item . ranges . map (( entry : any ) => ({ value : String ( entry . tier ), label : `T$ { entry . tier } ` }))} value={String(selection.tier)} onchange={(values ) = > changeWeaponTier(index,Number(values[0]))} required/ >
<div class="builder-fixed"><span>{item.rpm} RPM</span><span>{item.magazine} SHOTS</span><span>{item.reload_speed}s RELOAD</span> </div>
</div>
<p class:provisional={selection.level < 40} class="builder-scaling-note">{selection.level < 40 ? 'Estimated range below level 40.' : 'Damage scales with the selected item level.'}</p>
<div class="builder-range-grid weapon-damage"><RangeValueControl label="DAMAGE" min={damageRange.min} max={damageRange.max} provisional={damageRange.provisional} bind:value={selection.damage}/></div>
{#if item.baseBonuses.length}<div class="builder-inherited">{#each item.baseBonuses as bonus}<span><small>{attribute(bonus.stat_attribute_id)?.name}</small><b>{unitValue(bonus.value,bonus.stat_attribute_id)}</b></span>{/each}</div>{/if}
</section>
<section class="builder-editor-lane" aria-label="Weapon attributes and talents">
<div class="builder-section-title"><div><span>CORE ALLOCATION</span><small>SHARED T{selection.tier} POOL</small></div><b class:restricted={coreTotal(selection) < weaponCore.poolMin || coreTotal(selection) > weaponCore.poolMax}>{coreTotal(selection)} / {weaponCore.poolMin}– {weaponCore.poolMax}</b></div>
<div class="builder-core-grid">{#each coreKeys as key}<RangeValueControl label={key.toUpperCase()} min={0} max={weaponCore.poolMax} bind:value={selection[key]}/>{/each}</div>
<div class="builder-section-title"><div><span>ROLLED ATTRIBUTES</span><small>TWO REQUIRED SLOTS</small></div></div>
<div class="builder-rolls required">{#each selection.rolls as roll, rollIndex}{@const rollRange = attributeRange(attribute(roll.statId), 'weapon', selection.level)}<div><BuilderPicker items={weaponAttributes} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'weapon',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== rollIndex).map((entry) => entry.statId).filter(Boolean)} label={` ATTRIBUTE $ { rollIndex + 1 } `}/><RangeValueControl label="VALUE" min={rollRange.min} max={rollRange.max} provisional={rollRange.provisional} bind:value={roll.value}/></div>{/each}</div>
<div class="builder-talent-grids">
{#if item.talent1Ids.length}<section class="builder-talent-slot"><BuilderRadioGrid label="TALENT 1" name={` weapon - $ { index } - talent - 1 ` } items = { item . talent1Ids . map (( id : string ) => talent ( id )). filter ( Boolean )} value = { selection . talent1Id } onchange = {( id ) => { selection . talent1Id = id ; selection . talent1Tier = supportedTalentTier ( id , selection . talent1Tier ); }} columns = { Math . min ( item . talent1Ids . length , 4 )} searchable = { item . talent1Ids . length > 6 } required /> { #if selection . talent1Id && talent ( selection . talent1Id )}< TalentTierChoice name = { `weapon- ${ index } -talent-1-tier` } talent = { talent ( selection . talent1Id )} bind :value = { selection . talent1Tier } /> { /if }</ section >{ /if }
{ #if item . talent2Ids . length }< section class = "builder-talent-slot" >< BuilderRadioGrid label = "TALENT 2" name = { `weapon- ${ index } -talent-2` } items = { item . talent2Ids . map (( id : string ) => talent ( id )). filter ( Boolean )} value = { selection . talent2Id } onchange = {( id ) => { selection . talent2Id = id ; selection . talent2Tier = supportedTalentTier ( id , selection . talent2Tier ); }} columns = { Math . min ( item . talent2Ids . length , 4 )} searchable = { item . talent2Ids . length > 6 } required /> { #if selection . talent2Id && talent ( selection . talent2Id )}< TalentTierChoice name = { `weapon- ${ index } -talent-2-tier` } talent = { talent ( selection . talent2Id )} bind :value = { selection . talent2Tier } /> { /if }</ section >{ /if }
{ #if item . exotic_talent_id && talent ( item . exotic_talent_id )}< section class = "builder-talent-slot builder-exotic-talent" >< div >< ItemIcon src = { talent ( item . exotic_talent_id ). iconUrl } alt = "" /> < span >< small > EXOTIC TALENT </ small >< b >{ talent ( item . exotic_talent_id ). name }</ b ></ span ></ div >< TalentTierChoice name = { `weapon- ${ index } -exotic-talent-tier` } talent = { talent ( item . exotic_talent_id )} bind :value = { selection . exoticTalentTier } /></section> { /if }
</ div >
</ section >
</ div >
{ /if }
<div class="builder-section-title"><div><span>ROLLED ATTRIBUTES</span><small>TWO REQUIRED SLOTS · VALUES ARE LIMITED TO WEAPON RANGES</small></div></div>
<div class="builder-rolls required">{#each selection.rolls as roll, rollIndex}{@const rollRange = attributeRange(attribute(roll.statId), 'weapon', selection.level)}<div><BuilderPicker items={weaponAttributes} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'weapon',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== rollIndex).map((entry) => entry.statId).filter(Boolean)} label={` ATTRIBUTE $ { rollIndex + 1 } `}/><RangeValueControl label="VALUE" min={rollRange.min} max={rollRange.max} provisional={rollRange.provisional} bind:value={roll.value}/></div>{/each}</div>
<div class="builder-talent-grids">
{#if item.talent1Ids.length}<BuilderRadioGrid label="TALENT 1" name={` weapon - $ { index } - talent - 1 ` } items = { item . talent1Ids . map (( id : string ) => talent ( id )). filter ( Boolean )} bind :value = { selection . talent1Id } columns = { Math . min ( item . talent1Ids . length , 4 )} searchable = { item . talent1Ids . length > 6 } required /> { /if }
{ #if item . talent2Ids . length }< BuilderRadioGrid label = "TALENT 2" name = { `weapon- ${ index } -talent-2` } items = { item . talent2Ids . map (( id : string ) => talent ( id )). filter ( Boolean )} bind :value = { selection . talent2Id } columns = { Math . min ( item . talent2Ids . length , 4 )} searchable = { item . talent2Ids . length > 6 } required /> { /if }
</ div >
{ #if item . baseBonuses . length }< div class = "builder-inherited" >{ #each item . baseBonuses as bonus }< span >< small >{ attribute ( bonus . stat_attribute_id ) ? . name }</ small >< b >{ unitValue ( bonus . value , bonus . stat_attribute_id )}</ b ></ span >{ /each }</ div >{ /if }
{ /if }
{ :else if activeParts [ 0 ] === 'chip' }
{ @const index = Number ( activeParts [ 1 ])}{ @const chip = draft . chips [ index ]}{ @const item = chipItem ( index )}{ @const abilityIndex = Math . floor ( index / 3 )}{ @const ability = selectedFocus ? . abilities [ abilityIndex ]}
@@ -615,24 +640,19 @@
{#if ability}<BuilderPicker items={data.chipSets.filter((entry: any) => entry.specialization_ability_id === ability.id)} bind:value={chip.setId} onchange={(id) => configureChip(index,id)} label="ABILITY-COMPATIBLE SKILL CHIP SET"/>{:else}<p class="error">Select a focus with three configured class abilities before equipping Skill Chips.</p>{/if}
{#if !chipProfileConfigured}<p class="error">SHARED SKILL CHIP PROFILE · CONFIG REQUIRED. Complete every global quality range in ISAC before this loadout can be complete.</p>{/if}
{#if item}
<div class="builder-field-grid ">
<SegmentedChoice legend="QUALITY" name={` chip - $ { index } - quality ` } options = { skillChipQualities . map (( quality ) => ({ value : quality , label : quality.toUpperCase () }))} value = { chip . quality } onchange = {( values ) => changeChipQuality ( index , values [ 0 ] as SkillChipQuality )} required />
<div class="builder-editor-columns builder-chip-editor ">
<section class="builder-editor-lane" aria-label="Skill Chip quality and primary Attribute">
<div class="builder-field-grid"><SegmentedChoice legend="QUALITY" name={` chip - $ { index } - quality ` } options = { skillChipQualities . map (( quality ) => ({ value : quality , label : quality.toUpperCase () }))} value = { chip . quality } onchange = {( values ) => changeChipQuality ( index , values [ 0 ] as SkillChipQuality )} required /> </ div >
< div class = "builder-section-title" >< div >< span > ABILITY - SCOPED PRIMARY </ span >< small >{ ability ? . name ? . toUpperCase () ?? 'ASSIGNED ABILITY' } ONLY </ small ></ div >< b > NOT YET DEFINED </ b ></ div >
< p class = "builder-scaling-note" > Excluded from current Agent totals .</ p >
< div class = "builder-chip-rolls" >< div >< BuilderPicker items = { chipPrimaryAttributes } bind :value = { chip . primary . statId } onchange = {( id ) => setChipRoll ( chip . primary , id , 'primary' , chip . quality )} label = "PRIMARY ATTRIBUTE" /> { #if validChipRange ( chipRange ( 'primary' , chip . primary . statId , chip . quality ))}< RangeValueControl label = "VALUE" min = { chipRangeMin ( 'primary' , chip . primary . statId , chip . quality )} max = { chipRangeMax ( 'primary' , chip . primary . statId , chip . quality )} step = { 0.01 } bind :value = { chip . primary . value } /> { : else }< p class = "error" > PRIMARY RANGE · CONFIG REQUIRED </ p >{ /if }</ div ></ div >
</ section >
< section class = "builder-editor-lane" aria-label = "Skill Chip secondary Attributes" >
< div class = "builder-section-title" >< div >< span > SECONDARY ATTRIBUTES </ span >< small >{ skillChipSecondaryCapacity [ chip . quality ]} SLOT { skillChipSecondaryCapacity [ chip . quality ] === 1 ? '' : 'S' }</ small ></ div ></ div >
< div class = "builder-chip-rolls" >{ #each chip . secondary as roll , secondaryIndex }< div >< BuilderPicker items = { chipSecondaryAttributes } bind :value = { roll . statId } onchange = {( id ) => setChipRoll ( roll , id , 'secondary' , chip . quality )} disabledIds = { chip . secondary . filter (( _ , i ) => i !== secondaryIndex ). map (( entry ) => entry . statId ). filter ( Boolean )} label = { `SECONDARY ATTRIBUTE ${ secondaryIndex + 1 } ` } /> { #if validChipRange ( chipRange ( 'secondary' , roll . statId , chip . quality ))}< RangeValueControl label = "VALUE" min = { chipRangeMin ( 'secondary' , roll . statId , chip . quality )} max = { chipRangeMax ( 'secondary' , roll . statId , chip . quality )} step = { 0.01 } bind :value = { roll . value } /> { : else }< p class = "error" > SECONDARY RANGE · CONFIG REQUIRED </ p >{ /if }</ div >{ /each }</ div >
{ #if skillChipSecondaryCapacity [ chip . quality ] === 0 }< p class = "builder-scaling-note" > STANDARD QUALITY · NO SECONDARY ATTRIBUTES </ p >{ /if }
</ section >
</ div >
< div class = "builder-section-title" >< div >< span > ABILITY - SCOPED PRIMARY </ span >< small >{ ability ? . name ? . toUpperCase () ?? 'ASSIGNED ABILITY' } ONLY · EXCLUDED FROM KNOWN AGENT STATS </ small ></ div >< b > NOT YET DEFINED </ b ></ div >
< p class = "builder-scaling-note" > The primary roll belongs to this ability . Its calculation application is NOT YET DEFINED , so it does not contribute to current Agent totals .</ p >
< div class = "builder-chip-rolls" >
< div >
< BuilderPicker items = { chipPrimaryAttributes } bind :value = { chip . primary . statId } onchange = {( id ) => setChipRoll ( chip . primary , id , 'primary' , chip . quality )} label = "PRIMARY ATTRIBUTE" />
{ #if validChipRange ( chipRange ( 'primary' , chip . primary . statId , chip . quality ))}< RangeValueControl label = "VALUE" min = { chipRangeMin ( 'primary' , chip . primary . statId , chip . quality )} max = { chipRangeMax ( 'primary' , chip . primary . statId , chip . quality )} step = { 0.01 } bind :value = { chip . primary . value } /> { : else }< p class = "error" > PRIMARY RANGE · CONFIG REQUIRED </ p >{ /if }
</ div >
{ #each chip . secondary as roll , secondaryIndex }
< div >
< BuilderPicker items = { chipSecondaryAttributes } bind :value = { roll . statId } onchange = {( id ) => setChipRoll ( roll , id , 'secondary' , chip . quality )} disabledIds = { chip . secondary . filter (( _ , i ) => i !== secondaryIndex ). map (( entry ) => entry . statId ). filter ( Boolean )} label = { `SECONDARY ATTRIBUTE ${ secondaryIndex + 1 } ` } />
{ #if validChipRange ( chipRange ( 'secondary' , roll . statId , chip . quality ))}< RangeValueControl label = "VALUE" min = { chipRangeMin ( 'secondary' , roll . statId , chip . quality )} max = { chipRangeMax ( 'secondary' , roll . statId , chip . quality )} step = { 0.01 } bind :value = { roll . value } /> { : else }< p class = "error" > SECONDARY RANGE · CONFIG REQUIRED </ p >{ /if }
</ div >
{ /each }
</ div >
{ #if skillChipSecondaryCapacity [ chip . quality ] === 0 }< p class = "builder-scaling-note" > STANDARD QUALITY · NO SECONDARY ATTRIBUTES </ p >{ /if }
{ /if }
{ : else }
{ @const protocol = protocolItem ()}