Refine weapon progression and icon controls

This commit is contained in:
JB
2026-07-25 18:37:02 -04:00
parent cef3e6e78b
commit 92cda8a576
16 changed files with 69 additions and 25 deletions
+29
View File
@@ -772,6 +772,13 @@ same change. Both shared choice patterns expose explicit selection, hover,
pressed, focus-visible, disabled, empty/all, responsive, and 44-pixel
mobile-target states so modules do not reimplement them.
Numeric inputs inside ISAC use overwrite-first focus behavior: focusing an
enabled `type="number"` control selects its complete current value so a
contributor can immediately type a replacement instead of appending to a
default zero. This behavior belongs to the shared ISAC layout boundary and must
not be reimplemented in individual modules. Disabled numeric controls remain
unfocusable and unchanged.
`SegmentedChoice` supports two restrained density variants. Icon-only segments
may omit visible text when a canonical, unambiguous game icon exists; they must
retain an accessible label and hover title. Text-only segments may omit the
@@ -842,6 +849,28 @@ dense segmented controls. On desktop, icon-only Gear slot segments are square
record-name input receives the dominant width. Increase those cells to square
44-pixel targets at mobile sizes without reintroducing visible slot text.
Weapon-category editing follows the same compact icon-rail treatment: Shotgun,
Rifle, MMR, AR, LMG, and SMG use their canonical weapon silhouettes in square
32-pixel desktop cells and 44-pixel mobile cells, with accessible names and
hover titles retained. Exotic remains an independent icon-backed status because
an Exotic weapon must also retain its underlying weapon category. Weapon
Damage Type follows the same icon-only treatment when canonical Blast, Pierce,
and Shredding marks are available. Exotic status is a single square checkbox
whose icon, orange rail, and background show its checked state; no visible
“Exotic weapon” label is repeated inside the control. Weapon registry rows show
both the category silhouette and, when applicable, the Exotic mark alongside
the textual class for rapid scanning.
Weapon Tier Progression begins at the selected Native Tier. Lower tier cards
remain visible for orientation but are disabled, desaturated, and explicitly
labeled unavailable; their values are neither required nor persisted. Each
available tier has one Damage range labeled with the selected damage-type icon
and one shared Core range for Firepower, Toughness, and Engineering, represented
by all three primary-attribute icons. Registry completeness compares configured
tiers with the number possible from the weapon’s Native Tier. Archive Weapon
uses the standard separated ISAC destructive-action control on the left side of
the editor action band.
Root-level static PNGs are interface icons and use content-fitted transparent
bounds. Run `npm run assets:trim-icons` after adding or replacing them. The
normalizer trims fully transparent padding, restores a two-pixel transparent
+2 -1
View File
@@ -117,6 +117,7 @@
.registry-pagination+.dashboard-table{margin-top:0}
@media(max-width:900px){.registry-pagination{grid-template-columns:1fr}.pagination-range,.pagination-pages,.pagination-sizes{justify-content:center}}@media(max-width:560px){.pagination-pages a:not(:first-child):not(:last-child):not(.active),.pagination-gap{display:none}.pagination-sizes>span{display:none}}
@media(max-width:1200px){.weapon-identity-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-fixed-grid{grid-template-columns:repeat(3,minmax(0,1fr))}.weapon-tier-grid{grid-template-columns:1fr}.weapon-talent-pools{grid-template-columns:1fr}}@media(max-width:700px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr 1fr}.compact-selection{grid-template-columns:1fr}.base-bonus-row,.augment-effect,.augment-effect:not(:has(>button)){grid-template-columns:30px 1fr}.base-bonus-row .field:nth-of-type(2),.augment-effect .field:nth-of-type(2){grid-column:2}.base-bonus-row>button,.augment-effect>button{grid-column:2}.augment-sequence{grid-template-columns:1fr 1fr}}@media(max-width:480px){.weapon-identity-grid,.weapon-fixed-grid{grid-template-columns:1fr}.weapon-range-row{grid-template-columns:45px 1fr 1fr}}
.weapon-tier-card.unavailable{position:relative;opacity:.42;border-color:#292d2e}.weapon-tier-card.unavailable .weapon-range-row{filter:grayscale(1)}.tier-unavailable{margin:0;padding:7px;border-bottom:1px solid #282e2f;background:#0d1011;color:#7b8284;font-size:6px;font-weight:900;letter-spacing:.8px;text-align:center}.core-range-row>strong{gap:7px}.core-icon-stack{display:flex;align-items:center}.core-icon-stack .game-icon{width:17px;height:17px;margin-left:-7px}.core-icon-stack .game-icon:first-child{margin-left:0}.weapon-archive-action{min-height:40px;padding:0 13px}.weapon-archive-action>b{font-size:9px;transform:rotate(90deg)}
.legal-table-wrap{overflow-x:auto;margin:18px 0}.legal-table{width:100%;min-width:720px;border-collapse:collapse;border:1px solid var(--shd-border);background:#0b0e0f}.legal-table th,.legal-table td{padding:12px 14px;border:1px solid var(--shd-border);text-align:left;vertical-align:top}.legal-table th{color:var(--shd-orange);background:#111516;font-size:7px;letter-spacing:1px}.legal-table td{color:#a1a7a8;font:12px/1.55 Georgia,serif}.legal-table code,.legal-document code{color:#d9dedf;font:11px Consolas,monospace}.cookie-notice{position:fixed;z-index:1000;right:24px;bottom:24px;display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:end;gap:22px;width:min(620px,calc(100vw - 48px));padding:18px 20px;border:1px solid var(--shd-orange);background:rgba(10,13,14,.98);box-shadow:0 14px 48px #000c,4px 0 0 var(--shd-orange)}.cookie-notice .eyebrow{margin:0 0 8px}.cookie-notice div>p:not(.eyebrow){margin:0;color:var(--shd-text-muted);font:12px/1.5 Georgia,serif}.cookie-notice a{display:inline-block;margin-top:10px;color:var(--shd-orange);font-size:7px;font-weight:900;letter-spacing:1px}.cookie-notice button{min-width:84px}@media(max-width:600px){.cookie-notice{right:12px;bottom:12px;grid-template-columns:1fr;width:calc(100vw - 24px)}.cookie-notice button{width:100%}}
.kpi-filters{height:auto}.kpi-filters>a{position:relative;min-height:70px;padding:11px 13px;background:#111415;border:1px solid #2d3233;border-top:2px solid #3a4041;display:grid;color:inherit;cursor:pointer}.kpi-filters>a:hover,.kpi-filters>a:focus-visible{border-color:#8a672b;border-top-color:var(--shd-orange);background:#171a1b}.kpi-filters>a.active{border-color:#8a672b;border-top-color:var(--shd-orange);background:#211a0e;box-shadow:inset 0 -2px #f6a821}.kpi-filters>a:active{transform:translateY(1px);filter:brightness(.88)}.kpi-filters>a span{font-size:7px;letter-spacing:1.3px;color:#737a7c}.kpi-filters>a strong{font-size:23px;line-height:1.1;margin:3px 0}.kpi-filters>a small{font-size:7px;color:#555d5f}.kpi-filters>a.active span,.kpi-filters>a.active strong{color:var(--shd-orange)}
.role-kpis{grid-template-columns:repeat(5,minmax(105px,1fr))}.role-identity{grid-template-columns:minmax(220px,.7fr) minmax(320px,1.3fr)!important}.permission-catalogue{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px;margin-top:8px}.permission-catalogue label{min-height:54px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;align-items:center;gap:9px;padding:8px 10px;border:1px solid #303536;background:#0d1011;cursor:pointer}.permission-catalogue label:hover,.permission-catalogue label:focus-within{border-color:#8a672b;background:#151819}.permission-catalogue label:active{transform:translateY(1px);filter:brightness(.88)}.permission-catalogue input{width:auto;margin:0;accent-color:var(--shd-orange)}.permission-catalogue span{display:grid;gap:4px}.permission-catalogue b{color:#d7d9d5;font-size:9px}.permission-catalogue small{color:#626a6c;font-size:7px;line-height:1.35}.permission-catalogue em{color:#596164;font-size:6px;font-style:normal;font-weight:900;letter-spacing:.7px}.permission-catalogue label:has(input:checked){border-color:#6e5226;background:#211a0e}.permission-catalogue label:has(input:checked) em{color:var(--shd-orange)}.role-row{min-width:960px;grid-template-columns:1.1fr .55fr 2fr .65fr .3fr}.role-permission-summary{display:flex!important;flex-wrap:wrap;gap:3px}.role-permission-summary b{padding:3px 5px;border:1px solid #3a4840;color:#75c08c;font-size:6px;letter-spacing:.5px}@media(max-width:1000px){.role-kpis{grid-template-columns:repeat(3,minmax(105px,1fr))}}@media(max-width:700px){.permission-catalogue,.role-identity{grid-template-columns:1fr!important}.role-kpis{grid-template-columns:1fr 1fr}}
@@ -165,7 +166,7 @@
.notification-filter-console{flex:1;display:grid!important;grid-template-columns:minmax(520px,1fr) minmax(190px,.34fr);align-items:end!important;gap:8px!important}.notification-filter-console>input{display:none}.notification-filter-group{min-width:0;margin:0;padding:0;border:0}.notification-filter-group legend{margin-bottom:5px;color:var(--shd-text-muted);font-size:6px;font-weight:900;letter-spacing:1px}.notification-filter-group>div{height:40px;display:grid;grid-template-columns:repeat(6,minmax(0,1fr));border:1px solid #363b3c;background:#0d0f10}.notification-filter-group label{min-width:0;border-left:1px solid var(--shd-border);cursor:pointer}.notification-filter-group label:first-child{border-left:0}.notification-filter-group input{position:absolute;opacity:0;pointer-events:none}.notification-filter-group span{height:100%;display:flex;align-items:center;justify-content:center;gap:5px;color:#777f81;transition:var(--shd-transition)}.notification-filter-group span i{width:5px;height:5px;border:1px solid currentColor;background:transparent}.notification-filter-group b{overflow:hidden;text-overflow:ellipsis;color:inherit;font-size:6px;letter-spacing:.6px;white-space:nowrap}.notification-filter-group label:hover span{background:#171b1c;color:#c4c8c9}.notification-filter-group label:active span{transform:translateY(1px);filter:brightness(.88)}.notification-filter-group input:checked+span{background:#171b1c;color:var(--shd-orange);box-shadow:inset 0 -2px currentColor}.notification-filter-group input:checked+span i{background:currentColor}.notification-filter-group input:focus-visible+span{outline:2px solid var(--shd-orange);outline-offset:2px}.state-filter-group>div{grid-template-columns:repeat(2,minmax(0,1fr))}@media(max-width:900px){.notification-filter-console{grid-template-columns:1fr}.notification-filter-group>div{height:auto;grid-template-columns:repeat(3,1fr)}.notification-filter-group span{min-height:44px}.state-filter-group>div{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.notification-filter-group>div{grid-template-columns:repeat(2,1fr)}}
.segmented-choice{min-width:0;margin:0;padding:0;border:0}.segmented-choice legend{margin-bottom:5px;color:var(--shd-text-muted);font-size:6px;font-weight:900;letter-spacing:1px}.segmented-choice>div{height:32px;display:grid;grid-template-columns:repeat(var(--segmented-columns),minmax(0,1fr));border:1px solid #363b3c;background:#0d0f10}.segmented-choice label{min-width:0;border-left:1px solid var(--shd-border);cursor:pointer}.segmented-choice label:first-child{border-left:0}.segmented-choice input{position:absolute;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}.segmented-choice span{height:100%;display:flex;align-items:center;justify-content:center;gap:5px;color:#777f81;transition:var(--shd-transition)}.segmented-choice span i{width:5px;height:5px;border:1px solid currentColor;background:transparent}.segmented-choice .game-icon{width:18px;height:18px;opacity:.55}.segmented-choice b{overflow:hidden;color:inherit;font-size:6px;letter-spacing:.6px;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}.segmented-choice label:hover span{background:#171b1c;color:#c4c8c9}.segmented-choice label:active span{transform:translateY(1px);filter:brightness(.88)}.segmented-choice input:checked+span{background:#171b1c;color:var(--shd-orange);box-shadow:inset 0 -2px currentColor}.segmented-choice input:checked+span i{background:currentColor}.segmented-choice input:checked+span .game-icon{opacity:1}.segmented-choice input:focus-visible+span{outline:2px solid var(--shd-orange);outline-offset:2px}.segmented-choice input:disabled+span{opacity:.45;cursor:not-allowed}.protocol-classification-stack .segmented-choice legend{font-size:8px}.protocol-classification-stack .segmented-choice>div{margin-top:5px}.protocol-classification-stack .quality-standard i,.protocol-classification-stack .quality-specialized i,.protocol-classification-stack .quality-superior i,.protocol-classification-stack .quality-highend i{background:currentColor}
.segmented-choice span.icon-only{gap:0}.segmented-choice span.icon-only .game-icon{width:24px;height:24px}
.dashboard-editor .form-grid>.segmented-choice{align-self:end;margin:8px 0}.dashboard-editor .form-grid>.segmented-choice legend,.talent-identity .segmented-choice legend,.weapon-identity-grid .segmented-choice legend{font-size:8px}.talent-identity{display:grid;grid-template-columns:minmax(260px,1fr) minmax(430px,1.5fr)!important;align-items:end;gap:9px}.talent-identity>.field{margin:0}.weapon-identity-grid{grid-template-columns:minmax(230px,1.1fr) minmax(390px,1.75fr) minmax(180px,.7fr) minmax(220px,.9fr) minmax(145px,.6fr);align-items:end}.weapon-identity-grid>.field,.weapon-identity-grid>.segmented-choice{margin:0}.history-filter{align-items:end;grid-template-columns:minmax(180px,1fr) minmax(250px,.9fr) 80px}.history-filter .segmented-choice>div{height:34px}.audit-filter>.assignment-picker.compact{height:40px}.moderation-filter{grid-template-columns:minmax(220px,1fr) minmax(280px,.9fr) auto}@media(max-width:1200px){.weapon-identity-grid{grid-template-columns:1fr 1fr}.weapon-identity-grid>.weapon-name,.weapon-identity-grid>.segmented-choice:first-of-type{grid-column:1/3}}@media(max-width:700px){.talent-identity,.weapon-identity-grid{grid-template-columns:1fr!important}.weapon-identity-grid>.weapon-name,.weapon-identity-grid>.segmented-choice:first-of-type{grid-column:auto}.talent-identity .segmented-choice>div,.weapon-identity-grid .segmented-choice>div,.dashboard-editor .form-grid>.segmented-choice>div{height:auto}.talent-identity .segmented-choice span,.weapon-identity-grid .segmented-choice span,.dashboard-editor .form-grid>.segmented-choice span{min-height:44px}.talent-identity .segmented-choice>div{grid-template-columns:repeat(2,1fr)}.weapon-identity-grid>.segmented-choice:first-of-type>div{grid-template-columns:repeat(3,1fr)}}@media(max-width:650px){.history-filter{grid-template-columns:1fr}.history-filter .segmented-choice>div{height:auto}.history-filter .segmented-choice span{min-height:44px}.moderation-filter{grid-template-columns:1fr}}
.dashboard-editor .form-grid>.segmented-choice{align-self:end;margin:8px 0}.dashboard-editor .form-grid>.segmented-choice legend,.talent-identity .segmented-choice legend,.weapon-identity-grid .segmented-choice legend{font-size:8px}.talent-identity{display:grid;grid-template-columns:minmax(260px,1fr) minmax(430px,1.5fr)!important;align-items:end;gap:9px}.talent-identity>.field{margin:0}.weapon-identity-grid{grid-template-columns:minmax(360px,1fr) 200px minmax(145px,170px) 104px 40px;align-items:end}.weapon-identity-grid>.field,.weapon-identity-grid>.segmented-choice{margin:0}.weapon-identity-grid>.segmented-choice:first-of-type>div{grid-template-columns:repeat(6,32px);width:max-content}.weapon-identity-grid>.segmented-choice:first-of-type label,.weapon-identity-grid>.segmented-choice:first-of-type span{width:32px}.weapon-identity-grid>.segmented-choice:nth-of-type(3)>div{grid-template-columns:repeat(3,32px);width:max-content}.weapon-identity-grid>.segmented-choice:nth-of-type(3) label,.weapon-identity-grid>.segmented-choice:nth-of-type(3) span{width:32px}.exotic-toggle>span{justify-content:center;padding:0;cursor:pointer}.weapon-identity-grid .exotic-toggle input{position:absolute;width:1px;height:1px;margin:0;opacity:0;pointer-events:none}.exotic-toggle .game-icon{width:24px;height:24px;opacity:.55}.exotic-toggle span:has(input:checked){border-color:#875d25;background:#211a0e;color:var(--shd-orange);box-shadow:inset 0 -2px var(--shd-orange)}.exotic-toggle span:has(input:checked) .game-icon{opacity:1}.exotic-toggle span:has(input:focus-visible){outline:2px solid var(--shd-orange);outline-offset:2px}.weapon-category-label{display:flex!important;align-items:center;gap:4px}.weapon-category-label .game-icon{width:20px;height:20px}.weapon-category-label .game-icon:first-child:not(:last-child){width:16px;height:16px}.history-filter{align-items:end;grid-template-columns:minmax(180px,1fr) minmax(250px,.9fr) 80px}.history-filter .segmented-choice>div{height:34px}.audit-filter>.assignment-picker.compact{height:40px}.moderation-filter{grid-template-columns:minmax(220px,1fr) minmax(280px,.9fr) auto}@media(max-width:1000px){.weapon-identity-grid{grid-template-columns:1fr 1fr}.weapon-identity-grid>.weapon-name{grid-column:1/3}}@media(max-width:700px){.talent-identity,.weapon-identity-grid{grid-template-columns:1fr!important}.weapon-identity-grid>.weapon-name{grid-column:auto}.talent-identity .segmented-choice>div,.weapon-identity-grid .segmented-choice>div,.dashboard-editor .form-grid>.segmented-choice>div{height:auto}.talent-identity .segmented-choice span,.weapon-identity-grid .segmented-choice span,.dashboard-editor .form-grid>.segmented-choice span{min-height:44px}.talent-identity .segmented-choice>div{grid-template-columns:repeat(2,1fr)}.weapon-identity-grid>.segmented-choice:first-of-type>div{grid-template-columns:repeat(3,44px)}.weapon-identity-grid>.segmented-choice:first-of-type label,.weapon-identity-grid>.segmented-choice:first-of-type span{width:44px}.weapon-identity-grid>.segmented-choice:nth-of-type(3)>div{grid-template-columns:repeat(3,44px)}.weapon-identity-grid>.segmented-choice:nth-of-type(3) label,.weapon-identity-grid>.segmented-choice:nth-of-type(3) span{width:44px}.exotic-toggle>span{width:44px;min-height:44px}}@media(max-width:650px){.history-filter{grid-template-columns:1fr}.history-filter .segmented-choice>div{height:auto}.history-filter .segmented-choice span{min-height:44px}.moderation-filter{grid-template-columns:1fr}}
.gear-slot-label{display:flex!important;align-items:center;gap:5px}.gear-slot-label .game-icon{width:20px;height:20px}
.notification-filter-console{min-width:0;grid-template-columns:minmax(0,3fr) minmax(180px,1fr)}.notification-filter-console .segmented-choice>div{height:40px}@media(max-width:900px){.notification-toolbar .notification-filter-console{grid-template-columns:1fr}.notification-filter-console .segmented-choice>div{height:auto;grid-template-columns:repeat(3,1fr)}.notification-filter-console .segmented-choice span{min-height:44px}.notification-filter-console .segmented-choice:last-child>div{grid-template-columns:repeat(2,1fr)}}@media(max-width:560px){.notification-filter-console .segmented-choice>div{grid-template-columns:repeat(2,1fr)}}
.skill-chip-editor{border-top-color:var(--quality-superior)}.skill-chip-identity{display:grid;grid-template-columns:var(--isac-icon-editor-width) minmax(360px,1fr) minmax(220px,.55fr);align-items:start;gap:10px}.skill-chip-identity>.field{margin:0}.quality-scope{min-height:62px;display:grid;align-content:center;gap:5px;padding:7px 10px;border:1px solid var(--shd-border);background:#0d1011}.quality-scope>span{color:var(--shd-text-muted);font-size:6px;font-weight:900;letter-spacing:1px}.quality-scope>div{display:flex;gap:7px}.quality-scope b{padding:4px 6px;border:1px solid currentColor;font-size:6px;letter-spacing:.7px}.quality-scope small{color:#626a6c;font-size:6px}.skill-chip-bonus,.skill-chip-talent{margin-top:10px;padding:9px;border:1px solid var(--shd-border);background:#0d1011}.skill-chip-bonus{border-left:2px solid var(--quality-superior)}.skill-chip-bonus-grid{display:grid;grid-template-columns:minmax(360px,1fr) minmax(260px,.65fr);gap:8px;margin-top:7px}.skill-chip-range-fields{display:grid;grid-template-columns:1fr 1fr;gap:7px}.skill-chip-range-fields .field{margin:0}.skill-chip-pools{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:10px}.skill-chip-pools>div{min-width:0}.skill-chip-talent{border-left:2px solid var(--quality-high-end)}.skill-chip-talent-identity{display:grid;grid-template-columns:.7fr 1.3fr;gap:8px}.skill-chip-talent-identity textarea{min-height:66px}.skill-chip-preview{display:grid;grid-template-columns:minmax(360px,.9fr) minmax(260px,1.1fr);align-items:end;gap:8px;margin:4px 0 10px}.skill-chip-preview>div{min-height:42px;display:grid;align-content:center;gap:5px;padding:7px 10px;border:1px solid var(--shd-border);background:#111415}.skill-chip-preview>div span{color:var(--quality-high-end);font-size:6px;font-weight:900;letter-spacing:.8px}.skill-chip-preview p{margin:0;color:#c4c9ca;font:10px/1.45 Georgia,serif}.skill-chip-talent-grid{display:grid;grid-template-columns:1fr 1fr;gap:9px}.skill-chip-effects{display:grid;border:1px solid var(--shd-border)}.skill-chip-effects>div{display:grid;grid-template-columns:27px minmax(140px,1fr) 75px 75px 27px;align-items:center;gap:5px;padding:5px;border-top:1px solid #282e2f}.skill-chip-effects>div:first-child{border-top:0}.skill-chip-effects strong{display:grid;place-items:center;height:100%;background:#171b1c;color:var(--shd-orange);font-size:7px}.skill-chip-effects select,.skill-chip-effects input{min-width:0;margin:0;padding:7px;font-size:8px}.skill-chip-effects button{height:28px;border:1px solid #563734;background:#1d1212;color:var(--shd-danger);cursor:pointer}.skill-chip-effects button:disabled{opacity:.3;cursor:not-allowed}.skill-chip-row{min-width:1080px;grid-template-columns:1.15fr .45fr 1.1fr 1.1fr .85fr .5fr .3fr}.skill-chip-quality-pair{display:flex;gap:4px}.skill-chip-quality-pair b{padding:3px 4px;border:1px solid currentColor;font-size:5px}.skill-chip-row>div:nth-child(4){display:grid;gap:3px}.skill-chip-row>div:nth-child(4) small{color:#626a6c;font-size:6px}@media(max-width:1050px){.skill-chip-identity{grid-template-columns:var(--isac-icon-editor-width) 1fr}.quality-scope{grid-column:1/3}.skill-chip-pools,.skill-chip-talent-grid,.skill-chip-preview{grid-template-columns:1fr}}@media(max-width:700px){.skill-chip-identity,.skill-chip-bonus-grid,.skill-chip-range-fields{grid-template-columns:1fr}.quality-scope{grid-column:auto}.skill-chip-preview .segmented-choice>div{height:auto;grid-template-columns:repeat(2,1fr)}.skill-chip-preview .segmented-choice span{min-height:44px}.skill-chip-effects>div{grid-template-columns:27px 1fr}.skill-chip-effects input,.skill-chip-effects button{grid-column:2}.skill-chip-pools{grid-template-columns:1fr}}
+8 -1
View File
@@ -17,7 +17,14 @@ export const gameIcons = {
backpack: { src: '/backpack.png', label: 'Backpack gear slot' },
knee_pads: { src: '/knee_pads.png', label: 'Knee Pads gear slot' },
body_armor: { src: '/body_armor.png', label: 'Body Armor gear slot' },
mask: { src: '/mask.png', label: 'Mask gear slot' }
mask: { src: '/mask.png', label: 'Mask gear slot' },
exotic: { src: '/exotic.png', label: 'Exotic weapon' },
shotgun: { src: '/shotgun.png', label: 'Shotgun weapon category' },
rifle: { src: '/rifle.png', label: 'Rifle weapon category' },
mmr: { src: '/mmr.png', label: 'Marksman rifle weapon category' },
ar: { src: '/ar.png', label: 'Assault rifle weapon category' },
lmg: { src: '/lmg.png', label: 'Light machine gun weapon category' },
smg: { src: '/smg.png', label: 'Submachine gun weapon category' }
} as const;
export type GameIconName = keyof typeof gameIcons;
+5 -1
View File
@@ -30,6 +30,10 @@
{ href: '/admin/metrics', label: 'System metrics', code: 'MTRX', permission: 'metrics.view' }
];
const visibleOversightSections = $derived(oversightSections.filter((section) => can(section.permission)));
const selectNumericValue = (event: FocusEvent) => {
const input = event.target;
if (input instanceof HTMLInputElement && input.type === 'number' && !input.disabled) input.select();
};
</script>
<main class="admin-page">
@@ -55,7 +59,7 @@
{/if}
<div class="sidebar-foot"><span>ISAC NODE</span><strong>NYC-01</strong><small>Access policy synchronized</small></div>
</aside>
<section class="admin-content" class:data-readonly={dataReadOnly} onsubmit={(event) => {
<section class="admin-content" class:data-readonly={dataReadOnly} onfocusin={selectNumericValue} onsubmit={(event) => {
if (dataReadOnly && (event.target as HTMLFormElement).method.toLowerCase() === 'post') {
event.preventDefault();
readOnlyBlocked = true;
+4 -4
View File
@@ -90,11 +90,11 @@
<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} onfocus={(event) => event.currentTarget.select()} required/></label>
<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]} onfocus={(event) => event.currentTarget.select()} required/></label>{/each}</div></fieldset>
<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>
@@ -115,8 +115,8 @@
<SegmentedChoice legend="TIER" name="tier" options={tierOptions} bind:value={editing.tier} showIndicator={false} required/>
</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} onfocus={(event) => event.currentTarget.select()} required /></label><label class="field">MAXIMUM<input name="healthMax" type="number" min="0" step="any" bind:value={editing.base_health_max} onfocus={(event) => event.currentTarget.select()} required /></label></div>
<div class="effect-band range-band" class:unavailable={!armoredSlots.includes(editing.slot)}><strong>AR<small>BASE ARMOR</small></strong><label class="field">MINIMUM<input name="armorMin" type="number" min="0" step="any" bind:value={editing.base_armor_min} onfocus={(event) => event.currentTarget.select()} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label><label class="field">MAXIMUM<input name="armorMax" type="number" min="0" step="any" bind:value={editing.base_armor_max} onfocus={(event) => event.currentTarget.select()} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label></div>
<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>
<div class="effect-band range-band" class:unavailable={!armoredSlots.includes(editing.slot)}><strong>AR<small>BASE ARMOR</small></strong><label class="field">MINIMUM<input name="armorMin" type="number" min="0" step="any" bind:value={editing.base_armor_min} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label><label class="field">MAXIMUM<input name="armorMax" type="number" min="0" step="any" bind:value={editing.base_armor_max} disabled={!armoredSlots.includes(editing.slot)} required={armoredSlots.includes(editing.slot)} /></label></div>
</div>
<div class="selection-head"><div><span>COMPATIBLE BRAND SETS</span><small>SELECT AT LEAST TWO</small></div><b>{editing.brandIds.length} SELECTED</b></div>
<SearchableAssignment items={data.brands} fieldName="brandIds" subjectLabel="BRAND SETS" searchPlaceholder="Search by brand set name…" emptySelectionLabel="NO BRAND SETS ASSIGNED" bind:selected={editing.brandIds}/>
+7 -6
View File
@@ -10,7 +10,7 @@ import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconV
const categories = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG'];
const damageTypes = ['Blast', 'Pierce', 'Shredding'];
const rangeNames = ['damage', 'firepower', 'toughness', 'engineering'] as const;
const rangeNames = ['damage', 'core'] as const;
type ExoticEffect = { statId: string; valueI: number; valueII: number };
const mechanicValue = (value: number, singular: string) => `${value} ${value === 1 ? singular : `${singular}s`}`;
@@ -202,17 +202,18 @@ export const actions: Actions = {
const tiers = [];
try {
for (let tier = 0; tier <= 2; tier++) {
for (let tier = nativeTier; tier <= 2; tier++) {
const core = readRange(form, tier, 'core');
tiers.push({
tier,
damage: readRange(form, tier, 'damage'),
firepower: readRange(form, tier, 'firepower'),
toughness: readRange(form, tier, 'toughness'),
engineering: readRange(form, tier, 'engineering')
firepower: core,
toughness: core,
engineering: core
});
}
} catch {
return fail(400, { weaponError: 'Every tier range requires a non-negative minimum and a maximum greater than or equal to it.' });
return fail(400, { weaponError: 'Every available tier requires valid Damage and shared Core ranges.' });
}
const augmentEffects = [];
+14 -12
View File
@@ -9,13 +9,12 @@
import SegmentedChoice from '$lib/components/SegmentedChoice.svelte';
let { data, form } = $props();
const categories = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG'];
const categoryOptions = categories.map((value) => ({ value, label: value }));
const categoryOptions = categories.map((value) => ({ value, label: value, icon: value.toLowerCase() as GameIconName, iconOnly: true }));
const tierOptions = [0, 1, 2].map((value) => ({ value: String(value), label: `T${value}` }));
const damageTypes = ['Blast', 'Pierce', 'Shredding'];
const ranges = [['damage', 'DMG', 'DAMAGE'], ['firepower', 'FP', 'FIREPOWER'], ['toughness', 'TGH', 'TOUGHNESS'], ['engineering', 'ENG', 'ENGINEERING']] as const;
const iconName = (value: string): GameIconName => value.toLowerCase() as GameIconName;
const damageTypeOptions = damageTypes.map((value) => ({ value, label: value, icon: iconName(value) }));
const blankTier = (tier: number) => ({ tier, damage_min: 0, damage_max: 0, firepower_min: 0, firepower_max: 0, toughness_min: 0, toughness_max: 0, engineering_min: 0, engineering_max: 0 });
const damageTypeOptions = damageTypes.map((value) => ({ value, label: value, icon: iconName(value), iconOnly: true }));
const blankTier = (tier: number) => ({ tier, damage_min: 0, damage_max: 0, core_min: 0, core_max: 0 });
const blankExoticTalent = () => ({ id: '', name: '', iconUrl: '', cooldown_seconds: '', duration_seconds: '', max_stacks: '', descriptionTemplate: '', triggerIds: [], effects: [{ statId: '', valueI: '', valueII: '' }] });
const blank = () => ({ id: '', name: '', category: 'AR', is_exotic: 0, native_tier: '0', damage_type: 'Pierce', rpm: 0, magazine: 0, reload_speed: 0, accuracy: 0, stability: 0, exotic_talent_id: '', exoticTalent: blankExoticTalent(), active: 1, tiers: [blankTier(0), blankTier(1), blankTier(2)], talent1Ids: [], talent2Ids: [], augmentEffects: [{ stat_attribute_id: '', value: 0, sort_order: 1 }], baseBonuses: [{ stat_attribute_id: '', value: 0, sort_order: 1 }] });
let editing = $state<any | null>(null);
@@ -23,7 +22,10 @@
let iconResetToken = $state(0);
let openedRequestedWeapon = $state(false);
const edit = (weapon: any) => {
const tiers = [0, 1, 2].map((tier) => ({ ...blankTier(tier), ...(weapon.tiers.find((item: any) => Number(item.tier) === tier) ?? {}) }));
const tiers = [0, 1, 2].map((tier) => {
const saved = weapon.tiers.find((item: any) => Number(item.tier) === tier);
return { ...blankTier(tier), ...saved, core_min: saved?.firepower_min ?? 0, core_max: saved?.firepower_max ?? 0 };
});
editing = { ...weapon, native_tier: String(weapon.native_tier), tiers, talent1Ids: [...weapon.talent1Ids], talent2Ids: [...weapon.talent2Ids], augmentEffects: weapon.augmentEffects.map((effect: any) => ({ ...effect })), baseBonuses: weapon.baseBonuses.map((effect: any) => ({ ...effect })), exoticTalent: weapon.exoticTalent ? { ...weapon.exoticTalent, triggerIds: [...weapon.exoticTalent.triggerIds], effects: weapon.exoticTalent.effects.map((effect: any) => ({ ...effect })) } : blankExoticTalent() };
};
const tierAt = (tier: number) => editing.tiers.find((item: any) => Number(item.tier) === tier);
@@ -95,7 +97,7 @@
<SegmentedChoice legend="CATEGORY" name="category" options={categoryOptions} bind:value={editing.category} onchange={(values) => setCategory(values[0])} required/>
<SegmentedChoice legend="NATIVE TIER" name="nativeTier" options={tierOptions} bind:value={editing.native_tier} showIndicator={false} required/>
<SegmentedChoice legend="DAMAGE TYPE" name="damageType" options={damageTypeOptions} bind:value={editing.damage_type} required/>
<label class="field exotic-toggle">EXOTIC STATUS<span><input name="isExotic" type="checkbox" value="1" checked={Number(editing.is_exotic) === 1} onchange={(event) => setExotic(event.currentTarget.checked)}/> EXOTIC WEAPON</span></label>
<label class="field exotic-toggle">EXOTIC STATUS<span title="Exotic weapon"><input name="isExotic" type="checkbox" value="1" checked={Number(editing.is_exotic) === 1} onchange={(event) => setExotic(event.currentTarget.checked)}/><GameIcon name="exotic" decorative/><b class="sr-only">EXOTIC WEAPON</b></span></label>
</div>
{#if Number(editing.is_exotic)}
<input type="hidden" name="exoticTalentId" value={editing.exoticTalent.id}/>
@@ -130,10 +132,10 @@
<div class="selection-head"><div><span>TIER PROGRESSION</span><small>DAMAGE AND CORE ATTRIBUTE RANGES</small></div><b>3 TIERS REQUIRED</b></div>
<div class="weapon-tier-grid">
{#each [0, 1, 2] as tier}
<fieldset class="weapon-tier-card"><legend>TIER {tier}</legend>
{#each ranges as range}
<div class="weapon-range-row"><strong>{#if range[0] !== 'damage'}<GameIcon name={iconName(range[0])} decorative/>{/if}<span>{range[1]}<small>{range[2]}</small></span></strong><label>MIN<input name={`tier${tier}${range[0]}Min`} type="number" min="0" step="any" value={tierAt(tier)[`${range[0]}_min`]} oninput={(event) => tierAt(tier)[`${range[0]}_min`] = event.currentTarget.valueAsNumber} required/></label><label>MAX<input name={`tier${tier}${range[0]}Max`} type="number" min="0" step="any" value={tierAt(tier)[`${range[0]}_max`]} oninput={(event) => tierAt(tier)[`${range[0]}_max`] = event.currentTarget.valueAsNumber} required/></label></div>
{/each}
<fieldset class="weapon-tier-card" class:unavailable={tier < Number(editing.native_tier)} disabled={tier < Number(editing.native_tier)}><legend>TIER {tier}</legend>
{#if tier < Number(editing.native_tier)}<p class="tier-unavailable">BELOW NATIVE TIER · NOT AVAILABLE</p>{/if}
<div class="weapon-range-row"><strong><GameIcon name={iconName(editing.damage_type)} decorative/><span>DMG<small>{editing.damage_type.toUpperCase()} DAMAGE</small></span></strong><label>MIN<input name={`tier${tier}damageMin`} type="number" min="0" step="any" value={tierAt(tier).damage_min} oninput={(event) => tierAt(tier).damage_min = event.currentTarget.valueAsNumber} required/></label><label>MAX<input name={`tier${tier}damageMax`} type="number" min="0" step="any" value={tierAt(tier).damage_max} oninput={(event) => tierAt(tier).damage_max = event.currentTarget.valueAsNumber} required/></label></div>
<div class="weapon-range-row core-range-row"><strong><span class="core-icon-stack"><GameIcon name="firepower" decorative/><GameIcon name="toughness" decorative/><GameIcon name="engineering" decorative/></span><span>CORE<small>FP · TGH · ENG</small></span></strong><label>MIN<input name={`tier${tier}coreMin`} type="number" min="0" step="any" value={tierAt(tier).core_min} oninput={(event) => tierAt(tier).core_min = event.currentTarget.valueAsNumber} required/></label><label>MAX<input name={`tier${tier}coreMax`} type="number" min="0" step="any" value={tierAt(tier).core_max} oninput={(event) => tierAt(tier).core_max = event.currentTarget.valueAsNumber} required/></label></div>
</fieldset>
{/each}
</div>
@@ -151,7 +153,7 @@
{#if data.talent1Options.length === 0 || data.talent2Options.length === 0}<p class="error">Create and classify at least one active Talent 1 and Talent 2 weapon talent before adding weapons.</p>{/if}
{#if data.stats.length === 0}<p class="error">Create an active attribute before configuring tactical augmentation 01.</p>{/if}
{#if form?.weaponError}<p class="error">{form.weaponError}</p>{/if}
<div class="form-actions split-actions"><div>{#if editing.id}<button class="danger" formaction="?/archive" name="id" value={editing.id}>ARCHIVE WEAPON</button>{/if}</div><div><button type="button" class="secondary" onclick={() => editing = null}>CANCEL</button><button class="primary" disabled={data.talent1Options.length === 0 || data.talent2Options.length === 0 || data.stats.length === 0}>SAVE WEAPON →</button></div></div>
<div class="form-actions split-actions"><div>{#if editing.id}<button class="moderation-action danger weapon-archive-action" formaction="?/archive" name="id" value={editing.id}><b aria-hidden="true">⌁</b> ARCHIVE WEAPON</button>{/if}</div><div><button type="button" class="secondary" onclick={() => editing = null}>CANCEL</button><button class="primary" disabled={data.talent1Options.length === 0 || data.talent2Options.length === 0 || data.stats.length === 0}>SAVE WEAPON →</button></div></div>
</PendingForm>
</div>
{/if}
@@ -160,6 +162,6 @@
<div class="dashboard-table weapon-dashboard-table">
<div class="dashboard-row weapon-row table-heading"><span>WEAPON</span><span>CLASS</span><span>FIXED ATTRIBUTES</span><span>TIER RANGE</span><span>TALENT POOLS</span><span>AUGMENTS</span><span>STATE</span><span></span></div>
{#each data.weapons as weapon}
<div class="dashboard-row weapon-row" class:archived={Number(weapon.active) !== 1}><div><strong>{weapon.name}</strong><small class="weapon-damage-label"><GameIcon name={iconName(weapon.damage_type)} decorative/>{Number(weapon.is_exotic) ? 'EXOTIC · ' : ''}{weapon.damage_type.toUpperCase()} DAMAGE · {weapon.baseBonuses.length} BASE BONUSES</small></div><span>{weapon.category} · T{weapon.native_tier}</span><span>{weapon.rpm} RPM · {weapon.magazine} MAG<small> · {weapon.reload_speed} RELOAD</small></span><span>{weapon.tiers.length}/3 CONFIGURED</span><span title={`T1: ${weapon.talent1Ids.map(talentLabel).join(', ')} · T2: ${weapon.talent2Ids.map(talentLabel).join(', ')}`}>{weapon.talent1Ids.length} / {weapon.talent2Ids.length}</span><span title={weapon.augmentEffects.map((effect: any) => `${statLabel(String(effect.stat_attribute_id))} +${effect.value}`).join(', ')}>{Number(weapon.is_exotic) ? '6' : '5'} SLOTS</span><b class="state-dot">{Number(weapon.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(weapon)}>EDIT</button></div>
<div class="dashboard-row weapon-row" class:archived={Number(weapon.active) !== 1}><div><strong>{weapon.name}</strong><small class="weapon-damage-label"><GameIcon name={iconName(weapon.damage_type)} decorative/>{Number(weapon.is_exotic) ? 'EXOTIC · ' : ''}{weapon.damage_type.toUpperCase()} DAMAGE · {weapon.baseBonuses.length} BASE BONUSES</small></div><span class="weapon-category-label">{#if Number(weapon.is_exotic)}<GameIcon name="exotic" decorative/>{/if}<GameIcon name={iconName(weapon.category)} decorative/><span>{weapon.category} · T{weapon.native_tier}</span></span><span>{weapon.rpm} RPM · {weapon.magazine} MAG<small> · {weapon.reload_speed} RELOAD</small></span><span>{weapon.tiers.length}/{3 - Number(weapon.native_tier)} CONFIGURED</span><span title={`T1: ${weapon.talent1Ids.map(talentLabel).join(', ')} · T2: ${weapon.talent2Ids.map(talentLabel).join(', ')}`}>{weapon.talent1Ids.length} / {weapon.talent2Ids.length}</span><span title={weapon.augmentEffects.map((effect: any) => `${statLabel(String(effect.stat_attribute_id))} +${effect.value}`).join(', ')}>{Number(weapon.is_exotic) ? '6' : '5'} SLOTS</span><b class="state-dot">{Number(weapon.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(weapon)}>EDIT</button></div>
{:else}<div class="empty-dashboard"><strong>NO WEAPONS REGISTERED</strong><span>Add active weapon talents and attributes, then create the first armament definition.</span></div>{/each}
</div>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB