Build a loadout — SHD.build

COMMAND GRID / LOADOUT 01

SAVED LOCALLY {#if data.viewer} {#snippet children(pending)} {/snippet} {:else} SIGN IN TO SHARE {/if}
{#if data.viewer}
persistedVisibility = values[0]} required/>

{persistedVisibility === 'private' ? 'Only you can open this build.' : persistedVisibility === 'unlisted' ? 'Anyone with its direct link can open this build.' : 'Listed publicly and accessible by direct link.'}

{#if persistedSlug && persistedVisibility !== 'private'}VIEW SHARE PAGE →{/if}
{/if}
LIVE CALCULATION{draft.calculationMode === 'maximum' ? 'MAXIMUM POTENTIAL' : 'CUSTOM SCENARIO'}12 LOADOUT CELLS + 9 CHIPS{data.gear.length + data.weapons.length + data.chipSets.length + data.protocols.length} CATALOG ITEMS
{#if message}

{message}

{/if} {#if form?.buildError}{/if} {#if !data.gear.length && !data.weapons.length && !data.chipSets.length && !data.protocols.length}
ISAC DATA LINK EMPTYAdd active equipment definitions in ISAC to populate this workbench.
{/if}
active = id} />
{#if activeParts[0] === 'specialization'}
AGENT CLASS

SPECIALIZATION FOCUS

{selectedFocus ? `${selectedSpecializationPoints} / ${data.specializationProgression.availablePoints} POINTS` : 'UNASSIGNED'}
{#if selectedFocus}
{selectedFocus.specialization.name}

{selectedFocus.name}

{selectedFocus.description || 'No focus description has been entered.'}

{Number(selectedFocus.builder_ready) === 1 ? 'BUILDER READY' : 'ISAC DATA INCOMPLETE'}
{#each [0,1,2] as abilityIndex} {@const ability = selectedFocus.abilities[abilityIndex]}
ABILITY {abilityIndex + 1}{ability?.name ?? 'NOT CONFIGURED'}
{draft.chips.slice(abilityIndex * 3, abilityIndex * 3 + 3).filter((chip) => chip.setId).length} / 3 CHIPS

{ability?.description ?? 'ISAC requires this class ability before compatible Skill Chips can be equipped.'}

{#each [0,1,2] as chipSlot}{@const chipIndex = abilityIndex * 3 + chipSlot}{@const item = chipItem(chipIndex)}{/each}
{/each}
{#if draft.calculationMode === 'custom'}
SKILL ACTIVATION PROFILEALL ENABLED ABILITIES CONTRIBUTE TO COMBINED SKILL DPS
{selectedFocus.abilities.filter((ability: any) => (draft.skillActivations[ability.id]?.mode ?? 'off') !== 'off').length} ENABLED
{#each selectedFocus.abilities as ability} {@const activation = draft.skillActivations[ability.id] ?? { mode: 'off', targets: 1 }}
{ability.name}{ability.attack ? `${ability.attack.delivery.toUpperCase()} · ${ability.attack.damage_type}` : 'ATTACK PROFILE NOT CONFIGURED'}
setSkillActivation(ability.id,{mode:values[0]})}/>
{#if activation.mode === 'custom_per_minute'} setSkillActivation(ability.id,{usesPerMinute:value})}/>{/if} setSkillActivation(ability.id,{targets:value})}/>
{/each}
{/if}
POINTS {selectedSpecializationPoints} / {data.specializationProgression.availablePoints}CREDITS {selectedSpecializationCosts.credits.toLocaleString()}{#each selectedSpecializationCosts.resources as resource}{String(resource[0]).replace('resource-electronics-','').replaceAll('-',' ').toUpperCase()} × {resource[1]}{/each}
FOCUS UPGRADE TREEPREVIOUS ROWS REQUIRE EVERY AVAILABLE RANK 1 + 2 BEFORE ADVANCING
{selectedFocus.nodes.length} NODES
{#each [...new Set(selectedFocus.nodes.map((node: any) => Number(node.row_number)))] as row}
ROW {String(row).padStart(2, '0')}{row === 1 ? 'ENTRY ROW' : 'LOCKED BY PREVIOUS ROWS'}
{#each selectedFocus.nodes.filter((node: any) => Number(node.row_number) === row) as node}
{node.name}{node.description_template}
({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]))}/>
{/each}
{/each}
{#if selectedCombatEffects.length}
CONDITIONAL EFFECT PROFILESCHOOSE HOW EACH UNLOCKED EFFECT CONTRIBUTES TO DPS
{selectedCombatEffects.length} EFFECTS
{#each selectedCombatEffects as effect} {/each}
{/if} {#if modeledTalents.length}
EQUIPPED TALENT MODELINGMODEL EACH MECHANIC PROGRAM OR EXCLUDE IT FROM CALCULATIONS
{modeledTalents.length} TALENTS
{#each modeledTalents as equipped} {/each}
{/if} {/if} {:else if activeParts[0] === 'ability'} {@const abilityIndex = Number(activeParts[1])} {@const ability = selectedFocus?.abilities[abilityIndex]}
DOCTRINE DEPENDENCY · ABILITY 0{abilityIndex + 1}

{ability?.name?.toUpperCase() ?? 'ABILITY NOT ASSIGNED'}

{draft.chips.slice(abilityIndex * 3, abilityIndex * 3 + 3).filter((chip) => chip.setId).length} / 3 CHIPS
{#if ability}
{selectedFocus.specialization.name} · {selectedFocus.name}

{ability.name}

{ability.description || 'No ability description has been entered.'}

{ability.attack ? `${ability.attack.delivery.toUpperCase()} · ${ability.attack.damage_type}` : 'ATTACK DATA INCOMPLETE'}
ATTACHED SKILL CHIPSCHIPS ARE COMPATIBLE WITH THIS ABILITY AND MOVE WITH ITS FOCUS
03 POSITIONS
{#each [0,1,2] as chipSlot}{@const chipIndex = abilityIndex * 3 + chipSlot}{@const item = chipItem(chipIndex)}{/each}
{#if draft.calculationMode === 'custom'} {@const activation = draft.skillActivations[ability.id] ?? { mode: 'off', targets: 1 }}
ACTIVATION PROFILECONTROLS THIS ABILITY'S CONTRIBUTION TO COMBINED SKILL DPS
setSkillActivation(ability.id,{mode:values[0]})}/>
{#if activation.mode === 'custom_per_minute'} setSkillActivation(ability.id,{usesPerMinute:value})}/>{/if} setSkillActivation(ability.id,{targets:value})}/>
{/if} {:else}

Select a Specialization and Focus with configured abilities before attaching Skill Chips.

{/if} {:else if activeParts[0] === 'gear'} {@const slot = activeParts[1]}{@const selection = draft.gear[slot]}{@const item = gearItem(slot)}
GEAR SLOT

{slot.toUpperCase()}

{item ? `TIER ${item.tier} · BUILD LEVEL ${selection.level}` : 'UNASSIGNED'}
entry.slot === slot)} bind:value={selection.itemId} onchange={(id) => configureGear(slot,id)} label="ITEM DEFINITION"/> {#if item} {@const healthRange = gearBaseRange(item, 'health', selection.level)} {@const armorRange = item.base_armor_min !== null ? gearBaseRange(item, 'armor', selection.level) : null}
changeGearLevel(slot,value)}/> brand(id)).filter(Boolean)} bind:value={selection.brandId} columns={Math.min(item.brandIds.length,4)} iconOnly required/> {#if item.talentIds.length} talent(id)).filter(Boolean)} bind:value={selection.talentId} columns={Math.min(item.talentIds.length,4)} searchable={item.talentIds.length > 6} required/>{/if}

{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.'}

{#if armorRange}{/if}
{@const gearCore = coreAllocationRange(Number(item.tier), selection.level, 'gear')}
CORE ATTRIBUTE ALLOCATIONSHARED T{item.tier} POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES
gearCore.poolMax}>{coreTotal(selection)} / {gearCore.poolMin}–{gearCore.poolMax}
{#each coreKeys as key}{/each}
ROLLED ATTRIBUTESTWO REQUIRED SLOTS · VALUES ARE LIMITED TO GEAR RANGES
{#each selection.rolls as roll, index}{@const rollRange = attributeRange(attribute(roll.statId), 'gear', selection.level)}
setRollStat(roll,id,'gear',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== index).map((entry) => entry.statId).filter(Boolean)} label={`ATTRIBUTE ${index + 1}`}/>
{/each}
{@const augments = data.gearAugments.filter((entry) => entry.gear_slot === slot)} {#if augments.length}
TACTICAL AUGMENTSENABLE COLLECTED UPGRADES · BASE BONUSES APPLY TO THIS ITEM ONLY
{selection.augmentSlots.length} / {augments.length} ACTIVE
{#each augments as augment}{/each}
{/if} {/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)}
WEAPON SLOT

{index ? 'SECONDARY' : 'PRIMARY'} WEAPON

{item ? item.damage_type : 'UNASSIGNED'}
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)}

Only one exotic weapon may be equipped. Choose a standard weapon in either slot.

{/if}
changeWeaponLevel(index,value)}/> ({ value: String(range.tier), label: `T${range.tier}` }))} value={String(selection.tier)} onchange={(values) => changeWeaponTier(index,Number(values[0]))} required/>
{item.rpm} RPM{item.magazine} SHOTS{item.reload_speed}s RELOAD
{@const range = item.ranges.find((entry: any) => Number(entry.tier) === selection.tier)} {#if range}

{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.'}

{@const damageRange = weaponStatRange(range,'damage',selection.level)}
{@const weaponCore = coreAllocationRange(selection.tier, selection.level, 'weapon')}
CORE ATTRIBUTE ALLOCATIONSHARED T{selection.tier} POOL · VALUES MAY BE REDISTRIBUTED BETWEEN CORES
weaponCore.poolMax}>{coreTotal(selection)} / {weaponCore.poolMin}–{weaponCore.poolMax}
{#each coreKeys as key}{/each}
{/if}
ROLLED ATTRIBUTESTWO REQUIRED SLOTS · VALUES ARE LIMITED TO WEAPON RANGES
{#each selection.rolls as roll, rollIndex}{@const rollRange = attributeRange(attribute(roll.statId), 'weapon', selection.level)}
setRollStat(roll,id,'weapon',selection.level)} disabledIds={selection.rolls.filter((_, itemIndex) => itemIndex !== rollIndex).map((entry) => entry.statId).filter(Boolean)} label={`ATTRIBUTE ${rollIndex + 1}`}/>
{/each}
{#if item.talent1Ids.length} 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} talent(id)).filter(Boolean)} bind:value={selection.talent2Id} columns={Math.min(item.talent2Ids.length,4)} searchable={item.talent2Ids.length > 6} required/>{/if}
{#if item.baseBonuses.length}
{#each item.baseBonuses as bonus}{attribute(bonus.stat_attribute_id)?.name}{unitValue(bonus.value,bonus.stat_attribute_id)}{/each}
{/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]}
ABILITY {abilityIndex + 1} · CHIP {(index % 3) + 1}

{ability?.name?.toUpperCase() ?? 'SKILL CHIP'}

{chip.quality.toUpperCase()}
{#if ability} entry.specialization_ability_id === ability.id)} bind:value={chip.setId} onchange={(id) => configureChip(index,id)} label="ABILITY-COMPATIBLE SKILL CHIP SET"/>{:else}

Select a focus with three configured class abilities before equipping Skill Chips.

{/if} {#if item}
item.primaryStatIds.includes(entry.id))} bind:value={chip.primary.statId} onchange={(id) => setRollStat(chip.primary,id,'chip')} label="PRIMARY ATTRIBUTE"/>
{#each chip.secondary as roll,index}
item.secondaryStatIds.includes(entry.id))} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'chip')} disabledIds={chip.secondary.filter((_,i) => i !== index).map((entry) => entry.statId)} label={`SECONDARY ATTRIBUTE ${index + 1}`}/>
{/each}
{/if} {:else} {@const protocol = protocolItem()}
AGENT SYSTEM

OS PROTOCOL

{protocol?.quality?.toUpperCase() ?? 'UNASSIGNED'}
{#if protocol}
{#if protocol.primaryEffect}{attribute(protocol.primaryEffect.stat_attribute_id)?.name} · LEVEL {draft.protocol.level}{unitValue(Number(protocol.primaryEffect.base_value)*(1+(draft.protocol.level-1)/49),protocol.primaryEffect.stat_attribute_id)}{/if}{#each protocol.secondaryEffects as effect}{attribute(effect.stat_attribute_id)?.name}{unitValue(effect.value,effect.stat_attribute_id)}{/each}
{/if} {/if}