From f1b473ffa8865ce916a837b64fbe14fa02b022ba Mon Sep 17 00:00:00 2001 From: JB Date: Wed, 29 Jul 2026 20:18:58 -0400 Subject: [PATCH] feat: promote command grid builder --- docs/DESIGN_SYSTEM.md | 44 ++++- src/app.css | 10 +- .../components/builder/LoadoutMatrix.svelte | 123 +++++++++++++ src/routes/builder/+page.server.ts | 25 ++- src/routes/builder/+page.svelte | 163 ++++++++++++++++-- src/routes/builds/[slug]/+page.server.ts | 5 +- src/routes/builds/[slug]/+page.svelte | 10 ++ 7 files changed, 355 insertions(+), 25 deletions(-) create mode 100644 src/lib/components/builder/LoadoutMatrix.svelte diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 36a705d..0289935 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -332,6 +332,33 @@ squeezed beside long module headings on mobile. - Community build cards retain the colored top rule, abstract loadout artwork, concise tags, agent attribution, and a full-width terminal action. +### Command Grid builder + +The production builder uses Command Grid as its loadout workbench. Keep the +entire build visible in a dense matrix while one connected editor configures +the selected cell. The full-width Specialization and Focus cell is the root +choice. Its three supplied abilities appear directly below it, followed by two +weapons, six gear positions, and one full-width OS Protocol. + +Matrix cells summarize selected Talent names, Attribute names and values, +Brand Sets, or Skill Chips. Do not replace those choices with configuration +counts. Selecting an ability opens its three attached Skill Chip positions. +Selecting equipment opens the searchable real-data catalog and its compatible +configuration controls. Do not display a prototype option that the current +loadout schema cannot save. + +Use `--shd-builder-canvas` for recessed equipment fields. Use +`--shd-builder-selection` with `--shd-builder-selection-border` for the +selected cell. Preserve button semantics, `aria-pressed`, focus-visible +outlines, pressed feedback, and textual readiness states. At narrower desktop +widths, keep the matrix and editor together and move live results below them. +On mobile, stack the matrix, editor, and live results in that order. + +The command header contains the required build name, local and account save +actions, account visibility, and optional sharing notes. Sharing notes include +a short strengths summary and a Markdown usage guide. Persist them separately +from the validated loadout payload and render them on the shared build page. + ### Prelaunch network shell Use the standalone prelaunch shell for `/coming-soon`. It replaces the global @@ -1615,6 +1642,12 @@ Scenario uses the source-defined advanced assumptions saved with the loadout. Exclude a mechanic that lacks a required assumption and label it `REQUIRES ASSUMPTIONS`. This distinction must remain clear without relying on color. +Loadout summaries name selected Talents, Attributes and their exact values, +nodes, and Skill Chips. Do not replace these selections with configured +item counts. Build identity includes a required name plus optional strengths +summary and usage guide. Shared build views include those author-written fields +so another player can understand the build's role and operating sequence. + ### Pending ISAC registry records Pending additions appear directly in the same paginated ISAC registry as live @@ -2355,11 +2388,12 @@ required inputs. ### Specialization loadout composer -The builder places Specialization focus selection after equipment -configuration. The focus identity combines Specialization artwork and the -primary Attribute mark, followed by three ability panels. Each ability panel -contains three fixed Skill Chip positions. A chip picker shows definitions -assigned to that ability and excludes definitions assigned elsewhere. +The builder presents Specialization and Focus as the first loadout decision. +The Focus sets the primary Attribute, progression tree, and three ordered +abilities. Its identity combines Specialization artwork and the primary +Attribute mark. Each ability panel contains three fixed Skill Chip positions. +A chip picker shows definitions assigned to that ability and excludes +definitions assigned elsewhere. The focus tree groups nodes by row and uses segmented rank controls. Disabled ranks remain visible with their lock state. The builder permits a row after the diff --git a/src/app.css b/src/app.css index 3095942..890a6c6 100644 --- a/src/app.css +++ b/src/app.css @@ -39,6 +39,9 @@ --shd-transition:transform .12s ease,filter .18s ease,opacity .18s ease,box-shadow .18s ease; --shd-model-surface:#0d1213; --shd-model-border:#3b4648; + --shd-builder-canvas:#0e1112; + --shd-builder-selection:#1b170f; + --shd-builder-selection-border:#8d6728; } *{box-sizing:border-box} html{scroll-behavior:smooth} body{min-height:calc(100vh / var(--shd-display-scale));display:flex;flex-direction:column;margin:0;background:var(--shd-bg);color:var(--shd-text);font-family:var(--shd-font-sans);font-optical-sizing:auto;font-synthesis:none} button,input{font:inherit} a,a:hover,a:focus,a:focus-visible,a:active,a:visited{text-decoration:none} ::selection{background:var(--shd-selection-bg);color:var(--shd-selection-text)}input,textarea{caret-color:var(--shd-orange)}input[type=checkbox],input[type=radio],input[type=range]{accent-color:var(--shd-orange)}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]),select,textarea):focus{border-color:var(--shd-focus-ring);outline:none}:where(input:not([type=checkbox]):not([type=radio]):not([type=range]):not([type=file]):not([type=hidden]),select,textarea):focus-visible{outline:1px solid var(--shd-focus-ring);outline-offset:1px}:where(input,textarea):-webkit-autofill,:where(input,textarea):-webkit-autofill:hover,:where(input,textarea):-webkit-autofill:focus{-webkit-text-fill-color:var(--shd-text);caret-color:var(--shd-orange);box-shadow:inset 0 0 0 1000px #15130e;transition:background-color 9999s ease-out} @@ -100,7 +103,7 @@ .auth-mode-note{margin:-3px 0 14px;color:#7b8385;font-size:var(--shd-type-telemetry);line-height:1.5}.auth-mode-switch{display:grid;grid-template-columns:1fr 1fr;margin:12px 0 4px;border:1px solid #383d3e}.auth-mode-switch button{height:34px;border:0;border-left:1px solid #383d3e;background:#0d1011;color:#747c7e;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1px;cursor:pointer}.auth-mode-switch button:first-child{border-left:0}.auth-mode-switch button.active{background:#2b2111;color:var(--shd-orange);box-shadow:inset 0 -2px var(--shd-orange)}.auth-mode-switch button:focus-visible{outline:2px solid var(--shd-orange);outline-offset:2px} .auth-page .auth-console form .full + .identity-note{margin-top:-14px} .passkey-signin{position:relative;min-height:48px;margin-top:18px}.passkey-signin[aria-busy=true]{padding-right:42px}.passkey-signin[aria-busy=true]:after{content:"";position:absolute;right:16px;width:12px;height:12px;border:2px solid #111;border-right-color:transparent;border-radius:50%;animation:interaction-spin .65s linear infinite}.auth-note{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.password-fallback{margin:18px 0;border:1px solid var(--shd-border);background:#101314}.password-fallback summary{padding:12px;color:#8e9698;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1px;cursor:pointer}.password-fallback summary:hover{color:var(--shd-orange)}.password-fallback form{padding:0 12px 12px}.password-fallback .full{margin-bottom:0} -.builder-page{padding-top:28px}.builder-command{margin-bottom:14px;border-bottom:1px solid #303536}.builder-command .page-title{margin-bottom:12px}.builder-status{display:flex;gap:24px;padding:9px 2px;color:#61696b;font-size:var(--shd-type-telemetry);letter-spacing:1.2px}.builder-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.builder-page .slot-card{position:relative;overflow:hidden}.builder-page .slot-card:after{content:"";position:absolute;right:-24px;bottom:-24px;width:52px;height:52px;border:1px solid #f6a82122;transform:rotate(45deg)}.builder-page .summary{border-top:2px solid #f6a821} +.builder-page{width:100%;min-width:0;padding-top:28px}.builder-command{margin-bottom:14px;border-bottom:1px solid #303536}.builder-command .page-title{margin-bottom:12px}.builder-status{display:flex;gap:24px;padding:9px 2px;color:#61696b;font-size:var(--shd-type-telemetry);letter-spacing:1.2px}.builder-status i{display:inline-block;width:5px;height:5px;border-radius:50%;background:#75c08c;box-shadow:0 0 6px #75c08c;margin-right:5px}.builder-page .slot-card{position:relative;overflow:hidden}.builder-page .slot-card:after{content:"";position:absolute;right:-24px;bottom:-24px;width:52px;height:52px;border:1px solid #f6a82122;transform:rotate(45deg)}.builder-page .summary{border-top:2px solid #f6a821} @media(max-width:900px){.filter-bar{grid-template-columns:1fr 1fr}.catalogue-hero{align-items:flex-start}.guide-layout,.profile-dashboard,.auth-frame{grid-template-columns:1fr}.profile-command{align-items:flex-start}.profile-metrics{display:grid;grid-template-columns:1fr 1fr}.account-console{order:-1}.auth-intel{border-right:0!important;border-bottom:1px solid #303536!important;padding:42px!important}.auth-intel h1{font-size:52px!important}}@media(max-width:560px){.catalogue-hero,.profile-command{flex-direction:column;align-items:flex-start}.catalogue-count{text-align:left;margin-top:20px}.catalogue-count .primary{justify-self:start}.filter-bar{grid-template-columns:1fr}.guide-meta{position:static;margin-top:25px;flex-wrap:wrap}.profile-metrics{width:100%;margin-top:20px}.profile-columns{grid-template-columns:1fr}.auth-page{padding:25px 14px}.auth-intel,.auth-console{padding:28px!important}} @media(max-width:420px){.signin-provider-rail{grid-template-columns:repeat(2,minmax(0,1fr))}} @@ -201,6 +204,11 @@ .notification-command{align-items:center}.notification-command>div:first-child{display:grid;align-content:center}.notification-command>div:last-child{align-self:center}.notification-preference-head>span:not(:first-child){text-align:center}.notification-preference-row>label{justify-content:center}.notification-list>article{grid-template-columns:190px minmax(0,1fr);transition:border-color .14s ease,background .14s ease}.notification-list>article.isac-message{border-left:2px solid #68a9c4;background:linear-gradient(90deg,#101a1e 0,#101314 34%)}.notification-list>article.positive{border-left-color:var(--shd-success)}.notification-list>article.negative{border-left-color:var(--shd-danger)}.notification-list>article.positive:not(.isac-message){background:linear-gradient(90deg,#101914 0,#101314 34%)}.notification-list>article.negative:not(.isac-message){background:linear-gradient(90deg,#1b1111 0,#101314 34%)}.notification-source{grid-template-columns:38px minmax(0,1fr);align-items:center;align-content:center;gap:9px}.notification-source>img{width:38px;height:38px;object-fit:cover;border:1px solid #3a4548;border-radius:50%;background:#111}.notification-source>span{min-width:0;display:grid;gap:4px}.notification-source>span b,.notification-source>span small{overflow:hidden;text-overflow:ellipsis}.notification-source>span b{white-space:normal}.notification-source>span small{color:#667073;font-size:var(--shd-type-telemetry);white-space:nowrap}.notification-list article header>div{display:flex;align-items:center;gap:8px;min-width:0}.notification-tone{flex:0 0 auto;padding:3px 5px;border:1px solid #50585a;color:#899193;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.6px}.positive .notification-tone{border-color:#41684b;color:var(--shd-success)}.negative .notification-tone{border-color:#6d3f39;color:var(--shd-danger)}.isac-message .notification-source b{color:#8ecce5}.notification-list article footer>button:first-child{color:var(--shd-orange)}.notification-detail-modal{width:min(720px,calc(100vw - 24px));border-top:3px solid #687174}.notification-detail-modal.isac-message{border-top-color:#68a9c4}.notification-detail-modal.positive{border-top-color:var(--shd-success)}.notification-detail-modal.negative{border-top-color:var(--shd-danger)}.notification-detail-sender{display:grid;grid-template-columns:48px 1fr 34px;align-items:center;gap:10px;padding-bottom:10px;border-bottom:1px solid var(--shd-border)}.notification-detail-sender>img{width:48px;height:48px;object-fit:cover;border:1px solid #485052;border-radius:50%;background:#111}.notification-detail-sender>div{display:grid;gap:4px}.notification-detail-sender span{color:#697174;font-size:var(--shd-type-telemetry);letter-spacing:1px}.notification-detail-sender b{font-size:var(--shd-type-telemetry)}.notification-detail-sender>button{height:34px;border:1px solid var(--shd-border);background:#111415;color:#aeb4b5;cursor:pointer}.notification-detail-state{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:12px;margin:10px 0;padding:7px 9px;border:1px solid var(--shd-border);background:#0d1011}.notification-detail-state span,.notification-detail-state time{color:#6f7779;font-size:var(--shd-type-telemetry);letter-spacing:.7px}.notification-detail-state b{color:#899193;font-size:var(--shd-type-telemetry)}.notification-detail-modal.positive .notification-detail-state b{color:var(--shd-success)}.notification-detail-modal.negative .notification-detail-state b{color:var(--shd-danger)}.notification-detail-modal h2{margin:15px 0 8px;font-size:20px}.notification-detail-modal>p{min-height:90px;margin:0;padding:13px;border-left:2px solid var(--shd-border);background:#0e1112;color:#c0c5c6;font:13px/1.7 var(--shd-font-editorial)}.notification-detail-modal.isac-message>p{border-left-color:#68a9c4}.notification-detail-modal.positive>p{border-left-color:var(--shd-success)}.notification-detail-modal.negative>p{border-left-color:var(--shd-danger)}.notification-detail-modal .form-actions{align-items:center}.notification-detail-modal .form-actions form{margin-right:auto}.notification-detail-modal .form-actions .button{min-height:40px}@media(max-width:700px){.notification-list>article{grid-template-columns:1fr}.notification-source{display:grid;grid-template-columns:38px 1fr;justify-content:initial}.notification-list article header>div{align-items:flex-start;flex-direction:column}.notification-detail-state{grid-template-columns:1fr auto}.notification-detail-state time{grid-column:1/3}.notification-detail-modal .form-actions{display:grid;grid-template-columns:1fr}.notification-detail-modal .form-actions form,.notification-detail-modal .form-actions button,.notification-detail-modal .form-actions a{width:100%;margin:0}} .notification-command{padding:0 0 18px}.notification-command-actions{display:flex!important;align-items:stretch!important;gap:8px!important}.notification-unread-summary{min-width:86px;display:grid;grid-template-columns:auto 1fr;align-items:center;gap:7px;padding:7px 11px;border:1px solid var(--shd-border);background:#0d1011}.notification-unread-summary strong{grid-row:auto!important;font-size:25px!important;line-height:1}.notification-unread-summary span{align-self:center;line-height:1.2}.notification-command-actions>button,.notification-toolbar button{min-height:40px;display:inline-flex;align-items:center;justify-content:center;margin:0!important;padding:0 18px;line-height:1;white-space:nowrap}.notification-command-actions>button>span{font-size:var(--shd-type-telemetry)}.notification-toolbar{align-items:center;padding:10px 0}.notification-toolbar form{align-items:center;margin:0}.notification-toolbar select{min-width:125px;height:40px}.notification-toolbar form:first-child button{min-width:150px}.notification-toolbar>form:last-child{margin-left:auto}@media(max-width:700px){.notification-command-actions{width:100%;display:grid!important;grid-template-columns:auto 1fr}.notification-command-actions>button{width:100%}.notification-toolbar{align-items:stretch}.notification-toolbar>form:last-child{margin-left:0}} .profile-row>a{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:700}.builder-save-form{display:inline-block}.builder-save-form fieldset{display:flex}.builder-persistence{display:grid;grid-template-columns:minmax(300px,520px) 1fr auto;gap:14px;align-items:end;padding:11px 2px;border-top:1px solid var(--shd-border)}.builder-persistence p{margin:0 0 9px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.builder-share-link{align-self:center;color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:700;letter-spacing:1px}.catalogue-empty{grid-column:1/-1;display:grid;justify-items:center;gap:12px;padding:60px;border:1px solid var(--shd-border);background:var(--shd-panel);text-align:center}.catalogue-empty>span{color:var(--shd-orange);font-size:34px}.catalogue-empty p{margin:0;color:var(--shd-text-muted)}.build-open{display:block;text-align:center}.shared-build-page{padding-top:28px}.shared-build-command{display:flex;justify-content:space-between;gap:24px;align-items:end;padding:22px;border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:var(--shd-panel)}.shared-build-command h1{margin:8px 0;font-size:clamp(30px,4vw,52px)}.shared-build-command p:last-child{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.shared-build-command>div:last-child{display:flex;gap:8px}.shared-build-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;margin-top:10px}.shared-build-grid>section{border:1px solid var(--shd-border);background:var(--shd-panel)}.shared-build-grid>section>header{display:flex;justify-content:space-between;padding:10px 12px;border-bottom:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:1.2px}.shared-build-grid article{display:grid;gap:4px;min-height:70px;padding:12px;border-bottom:1px solid var(--shd-border)}.shared-build-grid article:last-child{border-bottom:0}.shared-build-grid article small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:1px}.shared-build-grid article strong{font-size:12px}.shared-build-grid article span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}@media(max-width:760px){.builder-command .page-title,.shared-build-command{align-items:stretch;flex-direction:column}.builder-command .actions,.shared-build-command>div:last-child{flex-wrap:wrap}.builder-persistence{grid-template-columns:1fr}.shared-build-grid{grid-template-columns:1fr}} + +/* Command Grid production builder: matrix navigation, contextual editor, and live telemetry rail. */ +.builder-command{border:1px solid var(--shd-border);background:var(--shd-surface)}.builder-command .page-title{padding:14px 16px;margin:0}.builder-command .page-title>div:first-child{min-width:min(620px,55vw)}.builder-command .page-title input{width:100%;max-width:none;font-size:clamp(27px,3.1vw,46px);letter-spacing:-.03em;text-transform:uppercase}.builder-command .actions{align-items:center;flex-wrap:wrap}.builder-command .actions>span{color:var(--shd-success);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.08em}.builder-command .builder-persistence{padding:10px 14px}.builder-status{padding:9px 14px;border-top:1px solid var(--shd-border);background:#0c0f10}.builder-notes{display:grid;grid-template-columns:minmax(260px,.65fr) minmax(420px,1.35fr);gap:8px;padding:10px 14px;border-top:1px solid var(--shd-border);background:var(--shd-builder-canvas)}.builder-notes>label{display:grid;align-content:start;gap:6px}.builder-notes>label>span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.08em}.builder-notes>label small{color:var(--shd-text-muted)}.builder-notes textarea{width:100%;min-height:150px;padding:11px;border:1px solid var(--shd-border);border-radius:0;background:#0d0f10;color:var(--shd-text);font:var(--shd-type-body)/1.55 var(--shd-font-editorial);resize:vertical}.loadout-workspace{grid-template-columns:minmax(300px,350px) minmax(480px,1fr) minmax(290px,340px);gap:6px}.loadout-editor{border-top-color:var(--shd-orange);background:var(--shd-surface)}.loadout-editor-head{margin:-12px -12px 10px;padding:11px 12px;border-bottom-color:var(--shd-border);background:#0c0f10}.loadout-summary{background:var(--shd-surface)}.builder-ability-chip-panels.single{grid-template-columns:1fr}.builder-ability-chip-panels.single section>div{grid-template-columns:repeat(3,minmax(0,1fr))}.builder-specialization-identity.ability-identity{grid-template-columns:54px minmax(0,1fr) auto}.builder-specialization-identity.ability-identity>.item-icon{width:50px;height:50px}.shared-build-guide{margin-top:10px;border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:var(--shd-panel)}.shared-build-guide>header{display:flex;justify-content:space-between;padding:9px 12px;border-bottom:1px solid var(--shd-border);color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.09em}.shared-build-guide>header b{color:var(--shd-orange)}.shared-build-summary{max-width:90ch;margin:0;padding:15px 18px;border-bottom:1px solid var(--shd-border);color:var(--shd-text);font:16px/1.6 var(--shd-font-editorial)}.shared-build-guide>div{padding:18px}.shared-build-guide h2{margin:0 0 10px;font-size:18px;letter-spacing:.04em} +@media(max-width:1320px){.loadout-workspace{grid-template-columns:minmax(270px,320px) minmax(440px,1fr)}.loadout-summary{position:static;grid-column:1/3}.summary-scroll{max-height:none;display:grid;grid-template-columns:repeat(3,1fr)}} +@media(max-width:800px){.builder-command .page-title>div:first-child{min-width:0}.builder-notes{grid-template-columns:1fr}.loadout-workspace{grid-template-columns:1fr}.loadout-workspace>.loadout-matrix,.loadout-editor,.loadout-summary{grid-column:auto}.builder-ability-chip-panels.single section>div{grid-template-columns:1fr}.builder-specialization-identity.ability-identity{grid-template-columns:48px minmax(0,1fr)}.builder-specialization-identity.ability-identity>b{grid-column:1/-1}.summary-scroll{display:block}} .weapon-mod-slot-config{margin-top:7px;padding-top:6px;border-top:1px solid var(--shd-border)}.weapon-mod-slot-config .assignment-picker{margin-top:5px}.weapon-mod-editor{border-top-color:var(--shd-orange)}.weapon-mod-identity{display:grid;grid-template-columns:minmax(300px,1.4fr) 52px minmax(280px,1fr);align-items:end;gap:8px}.weapon-mod-identity>.field,.weapon-mod-identity>.segmented-choice{margin:0}.weapon-mod-identity>.segmented-choice{grid-column:1/-1}.weapon-mod-identity>.segmented-choice>div{height:38px}.weapon-mod-assignment-grid{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:10px}.weapon-mod-secondary{display:grid;grid-template-columns:1fr 1fr;gap:7px}.weapon-mod-quality-section,.weapon-mod-effect-section{margin-top:10px;padding:9px;border:1px solid var(--shd-border);background:#0d1011}.weapon-mod-quality-section{border-left:2px solid var(--quality-high-end)}.weapon-mod-quality-table{display:grid;margin-top:7px;border:1px solid var(--shd-border)}.weapon-mod-quality-row{display:grid;grid-template-columns:110px repeat(4,minmax(180px,1fr));align-items:center;border-top:1px solid var(--shd-border)}.weapon-mod-quality-row:first-child{border-top:0}.weapon-mod-quality-row.heading{min-height:29px;background:#111415;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.weapon-mod-quality-row>span,.weapon-mod-quality-row>strong{padding:8px}.weapon-mod-quality-row>strong{font-size:var(--shd-type-telemetry);letter-spacing:.7px}.weapon-mod-quality-row>div{display:grid;grid-template-columns:1fr 14px 1fr;align-items:center;gap:4px;padding:5px;border-left:1px solid var(--shd-border)}.weapon-mod-quality-row input{height:30px;margin:0;padding:0 6px;font-size:var(--shd-type-telemetry)}.weapon-mod-quality-row i{color:#596164;font-size:var(--shd-type-telemetry);font-style:normal;text-align:center}.weapon-mod-preview{display:grid;grid-template-columns:minmax(360px,.8fr) minmax(420px,1.2fr);align-items:end;gap:8px;margin-top:8px}.weapon-mod-preview>div:first-child{display:grid;grid-template-columns:1fr 1.2fr;align-items:end;gap:8px}.weapon-mod-preview>div:first-child>label{display:grid;gap:5px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.weapon-mod-preview input[type=range]{height:32px;margin:0;padding:0;accent-color:var(--shd-orange)}.weapon-mod-preview-values{display:grid;grid-template-columns:1.2fr 1fr 1fr;border:1px solid var(--shd-border)}.weapon-mod-preview-values>span{min-height:48px;display:grid;grid-template-columns:auto 1fr;grid-template-rows:1fr 1fr;align-items:center;gap:2px 7px;padding:7px;border-left:1px solid var(--shd-border)}.weapon-mod-preview-values>span:first-child{border-left:0}.weapon-mod-preview-values .game-icon{grid-row:1/3;width:28px;height:28px}.weapon-mod-preview-values small{align-self:end;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry)}.weapon-mod-preview-values b{align-self:start;color:var(--shd-orange);font-size:var(--shd-type-telemetry)}.weapon-mod-effects{display:grid;gap:6px;margin-top:7px}.weapon-mod-effects>div{display:grid;grid-template-columns:30px minmax(230px,1fr) repeat(4,minmax(90px,.45fr)) 30px;align-items:end;gap:6px;padding:6px;border:1px solid var(--shd-border);background:#101314}.weapon-mod-effects>div>strong{height:100%;display:grid;place-items:center;background:#171b1c;color:var(--shd-orange);font-size:var(--shd-type-telemetry)}.weapon-mod-effects label{display:grid;gap:4px}.weapon-mod-effects label span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.5px}.weapon-mod-effects input{height:32px;margin:0;padding:0 6px}.weapon-mod-effects>div>button{height:32px;border:1px solid #563734;background:#1d1212;color:var(--shd-danger);cursor:pointer}.weapon-mod-effects>div>button:disabled{opacity:.3;cursor:not-allowed}.weapon-mod-rule{padding:10px;border:1px solid #74552a;border-left:2px solid var(--quality-high-end);background:#18150d;color:#a89568;font-size:var(--shd-type-telemetry)}.weapon-mod-rule b{color:var(--quality-high-end)}.weapon-mod-row{min-width:1240px;grid-template-columns:1.15fr .55fr .65fr 1fr 1fr .55fr .5fr .3fr}.weapon-mod-primary{display:flex;align-items:center;gap:5px}.weapon-mod-primary .game-icon{width:22px;height:22px}.weapon-mod-row>span small{color:var(--shd-text-muted)}@media(max-width:1100px){.weapon-mod-identity{grid-template-columns:1fr 52px 1fr}.weapon-mod-assignment-grid,.weapon-mod-preview{grid-template-columns:1fr}.weapon-mod-quality-table{overflow-x:auto}.weapon-mod-quality-row{min-width:900px}.weapon-mod-effects>div{grid-template-columns:30px 1fr 1fr 1fr}.weapon-mod-effects label:nth-of-type(3),.weapon-mod-effects label:nth-of-type(4),.weapon-mod-effects>div>button{grid-column:auto}}@media(max-width:700px){.weapon-mod-identity,.weapon-mod-assignment-grid,.weapon-mod-secondary,.weapon-mod-preview>div:first-child{grid-template-columns:1fr}.weapon-mod-identity>.segmented-choice{grid-column:auto}.weapon-mod-identity>.segmented-choice>div{height:auto;grid-template-columns:repeat(2,1fr)}.weapon-mod-identity>.segmented-choice span{min-height:44px}.weapon-mod-effects>div{grid-template-columns:30px 1fr}.weapon-mod-effects label,.weapon-mod-effects>div>button{grid-column:2}.weapon-mod-preview-values{grid-template-columns:1fr}.weapon-mod-preview-values>span{border-left:0;border-top:1px solid var(--shd-border)}.weapon-mod-preview-values>span:first-child{border-top:0}} .weapon-mod-primary-types{min-height:58px;display:grid;align-content:center;gap:5px;padding:7px;border:1px solid var(--shd-border);background:#0d1011}.weapon-mod-primary-types>span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.weapon-mod-primary-types>div{display:flex;gap:8px}.weapon-mod-primary-types b{display:flex;align-items:center;gap:3px;color:#aab0b1;font-size:var(--shd-type-telemetry)}.weapon-mod-primary-types .game-icon{width:18px;height:18px}.weapon-mod-primary-types small{color:#596164;font-size:var(--shd-type-telemetry)}.weapon-mod-compatibility{margin-top:10px}.weapon-mod-quality-row{grid-template-columns:110px repeat(2,minmax(220px,1fr))}.weapon-mod-quality-row.heading{grid-template-columns:110px repeat(2,minmax(220px,1fr))}.weapon-mod-preview-values{grid-template-columns:1.3fr .7fr}.weapon-mod-preview-values .core-icon-stack{grid-row:1/3}.weapon-mod-preview-values .core-icon-stack .game-icon{grid-row:auto;width:22px;height:22px}.weapon-mod-secondary-section{margin-top:10px;padding:9px;border:1px solid var(--shd-border);border-left:2px solid var(--quality-specialized);background:#0d1011}.weapon-mod-secondary-options{display:grid;gap:6px;margin-top:7px}.weapon-mod-secondary-options>div{display:grid;grid-template-columns:30px minmax(230px,1fr) repeat(4,minmax(150px,.65fr)) 30px;align-items:end;gap:6px;padding:6px;border:1px solid var(--shd-border);background:#101314}.weapon-mod-secondary-options>div>strong{height:100%;display:grid;place-items:center;background:#171b1c;color:var(--shd-orange);font-size:var(--shd-type-telemetry)}.weapon-mod-secondary-options label{display:grid;gap:4px}.weapon-mod-secondary-options label>span{font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.5px}.weapon-mod-secondary-options label>div{display:grid;grid-template-columns:1fr 12px 1fr;align-items:center}.weapon-mod-secondary-options input{height:32px;margin:0;padding:0 5px}.weapon-mod-secondary-options i{color:#596164;font-size:var(--shd-type-telemetry);font-style:normal;text-align:center}.weapon-mod-secondary-options>div>button{height:32px;border:1px solid #563734;background:#1d1212;color:var(--shd-danger);cursor:pointer}.weapon-mod-secondary-options>div>button:disabled{opacity:.3;cursor:not-allowed}@media(max-width:1100px){.weapon-mod-quality-row,.weapon-mod-quality-row.heading{min-width:580px}.weapon-mod-secondary-options>div{grid-template-columns:30px 1fr 1fr 1fr}.weapon-mod-secondary-options label:nth-of-type(3),.weapon-mod-secondary-options label:nth-of-type(4){grid-column:auto}}@media(max-width:700px){.weapon-mod-secondary-options>div{grid-template-columns:30px 1fr}.weapon-mod-secondary-options label,.weapon-mod-secondary-options>div>button{grid-column:2}} .weapon-mod-exotic-quality{min-height:58px;display:flex;align-items:center;gap:8px;padding:7px;border:1px solid #74552a;border-left:2px solid var(--quality-high-end);background:#18150d}.weapon-mod-exotic-quality .game-icon{width:34px;height:34px}.weapon-mod-exotic-quality span{display:grid;gap:3px}.weapon-mod-exotic-quality b{color:var(--quality-high-end);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.weapon-mod-exotic-quality small{color:#a89568;font-size:var(--shd-type-telemetry);letter-spacing:.35px} diff --git a/src/lib/components/builder/LoadoutMatrix.svelte b/src/lib/components/builder/LoadoutMatrix.svelte new file mode 100644 index 0000000..5389e3d --- /dev/null +++ b/src/lib/components/builder/LoadoutMatrix.svelte @@ -0,0 +1,123 @@ + + +
+
+ LOADOUT MATRIX + {configured ? 'COMBAT READY' : 'CONFIGURATION INCOMPLETE'} +
+ +
+ + +
SUPPLIES 3 ABILITIES
+
+ {#each abilities as cell} + + {/each} +
+
+ +
+ +
+ {#each weapons as cell} + + {/each} +
+
+ +
+ +
+ {#each gear as cell} + + {/each} +
+
+ +
+ + +
+
+ + diff --git a/src/routes/builder/+page.server.ts b/src/routes/builder/+page.server.ts index 583df94..85f901a 100644 --- a/src/routes/builder/+page.server.ts +++ b/src/routes/builder/+page.server.ts @@ -4,6 +4,7 @@ import { getSessionUser } from '$lib/server/auth/session'; import { fail } from '@sveltejs/kit'; import { randomUUID } from 'node:crypto'; import { parseLoadoutDraft } from '$lib/builder/loadout'; +import { normalizeUserMarkdown } from '$lib/markdown'; import type { Actions, PageServerLoad } from './$types'; import { attachWeaponCategoryBonuses } from '$lib/weapons/category-bonuses'; @@ -27,10 +28,17 @@ export const load: PageServerLoad = async ({ cookies, url }) => { let initialBuild = null; if (requestedSlug && viewer) { const row = await (await database.prepare( - 'SELECT id,title,slug,visibility,loadout_json FROM builds WHERE slug=? AND owner_id=?' + 'SELECT id,title,slug,visibility,summary,guide_body,loadout_json FROM builds WHERE slug=? AND owner_id=?' )).get(requestedSlug, viewer.id); const loadout = typeof row?.loadout_json === 'string' ? parseLoadout(row.loadout_json) : null; - if (row && loadout) initialBuild = { id: String(row.id), slug: String(row.slug), visibility: String(row.visibility), loadout }; + if (row && loadout) initialBuild = { + id: String(row.id), + slug: String(row.slug), + visibility: String(row.visibility), + summary: String(row.summary ?? ''), + guideBody: String(row.guide_body ?? ''), + loadout + }; } const [ attributes, gear, gearBrands, gearTalents, gearAugments, gearAugmentCosts, brands, brandEffects, @@ -239,7 +247,10 @@ export const actions: Actions = { const loadout = parseLoadout(formData.get('loadout')); const visibility = String(formData.get('visibility') ?? 'private'); const id = String(formData.get('id') ?? '').trim(); + const summary = normalizeUserMarkdown(formData.get('summary'), 500); + const guideBody = normalizeUserMarkdown(formData.get('guideBody'), 12_000); if (!loadout) return fail(400, { buildError: 'The loadout data is invalid. Refresh the builder and try again.' }); + if (summary === null || guideBody === null) return fail(400, { buildError: 'Build notes contain unsupported characters or exceed their length limit.' }); const title = loadout.buildName.trim(); if (title.length < 2 || title.length > 100) return fail(400, { buildError: 'Build names must be between 2 and 100 characters.' }); if (!validVisibility.has(visibility)) return fail(400, { buildError: 'Choose a valid build visibility.' }); @@ -253,15 +264,15 @@ export const actions: Actions = { if (!existing) return fail(404, { buildError: 'This saved build could not be found.' }); slug = String(existing.slug); await (await database.prepare( - 'UPDATE builds SET title=?,visibility=?,loadout_json=?,updated_at=? WHERE id=? AND owner_id=?' - )).run(title, visibility, JSON.stringify(loadout), now, buildId, user.id); + 'UPDATE builds SET title=?,visibility=?,summary=?,guide_body=?,loadout_json=?,updated_at=? WHERE id=? AND owner_id=?' + )).run(title, visibility, summary, guideBody, JSON.stringify(loadout), now, buildId, user.id); } else { buildId = randomUUID(); slug = `${slugify(title)}-${buildId.slice(0, 8)}`; await (await database.prepare( - `INSERT INTO builds(id,owner_id,title,slug,visibility,loadout_json,created_at,updated_at) - VALUES(?,?,?,?,?,?,?,?)` - )).run(buildId, user.id, title, slug, visibility, JSON.stringify(loadout), now, now); + `INSERT INTO builds(id,owner_id,title,slug,visibility,summary,guide_body,loadout_json,created_at,updated_at) + VALUES(?,?,?,?,?,?,?,?,?,?)` + )).run(buildId, user.id, title, slug, visibility, summary, guideBody, JSON.stringify(loadout), now, now); } return { buildSaved: true, diff --git a/src/routes/builder/+page.svelte b/src/routes/builder/+page.svelte index 6e7bbc1..3ca391b 100644 --- a/src/routes/builder/+page.svelte +++ b/src/routes/builder/+page.svelte @@ -8,8 +8,10 @@ import SegmentedChoice from '$lib/components/SegmentedChoice.svelte'; import PendingForm from '$lib/components/PendingForm.svelte'; import ItemIcon from '$lib/components/ItemIcon.svelte'; + import MarkdownEditor from '$lib/components/MarkdownEditor.svelte'; import CombatAssumptions from '$lib/components/builder/CombatAssumptions.svelte'; import ConditionalEffectCard from '$lib/components/builder/ConditionalEffectCard.svelte'; + import LoadoutMatrix, { type MatrixCell } from '$lib/components/builder/LoadoutMatrix.svelte'; import WeaponCalculationResult from '$lib/components/builder/WeaponCalculationResult.svelte'; import SkillCalculationResult from '$lib/components/builder/SkillCalculationResult.svelte'; import { evaluateLoadoutCombat, evaluateLoadoutSkillCombat, summarizeLoadout } from '$lib/builder/loadout-combat'; @@ -22,9 +24,12 @@ let { data, form } = $props(); let draft = $state(untrack(() => parseLoadoutDraft(data.initialBuild?.loadout) ?? createDraft())); - let active = $state('gear:Holster'); + let active = $state('specialization:focus'); let saved = $state(false); let message = $state(''); + let buildSummary = $state(untrack(() => data.initialBuild?.summary ?? '')); + let buildGuide = $state(untrack(() => data.initialBuild?.guideBody ?? '')); + let buildNotesOpen = $state(untrack(() => Boolean(data.initialBuild?.summary || data.initialBuild?.guideBody))); let persistedBuildId = $state(untrack(() => data.initialBuild?.id ?? '')); let persistedSlug = $state(untrack(() => data.initialBuild?.slug ?? '')); let persistedVisibility = $state(untrack(() => data.initialBuild?.visibility ?? 'private')); @@ -382,6 +387,95 @@ const skillAbilityNames = $derived(Object.fromEntries( (selectedFocus?.abilities ?? []).map((ability: any) => [String(ability.id), String(ability.name)]) )); + const rollSummary = (rolls: Roll[]) => rolls + .filter((roll) => roll.statId) + .map((roll) => `${attribute(roll.statId)?.name ?? 'Attribute'} ${unitValue(roll.value, roll.statId)}`) + .join(' · '); + const gearIcon = (slot: string) => `/${({ + Holster: 'holster.png', + Gloves: 'gloves.png', + Backpack: 'backpack.png', + 'Knee Pads': 'knee_pads.png', + 'Body Armor': 'body_armor.png', + Mask: 'mask.png' + } as Record)[slot] ?? 'backpack.png'}`; + const weaponIcon = (category: unknown) => { + const value = String(category ?? '').toLowerCase(); + if (value.includes('shotgun')) return '/shotgun.png'; + if (value.includes('smg') || value.includes('submachine')) return '/smg.png'; + return '/rifle.png'; + }; + const doctrineCell = $derived({ + id: 'specialization:focus', + label: 'SPECIALIZATION + FOCUS', + name: selectedFocus ? `${selectedFocus.specialization.name} · ${selectedFocus.name}` : 'SELECT DOCTRINE', + meta: selectedFocus?.primary_attribute ?? '', + summary: selectedFocus + ? `${selectedFocus.primary_attribute} · ${selectedSpecializationPoints} / ${data.specializationProgression.availablePoints} POINTS` + : 'ROOT CHOICE · DEFINES ALL THREE ABILITIES', + iconUrl: selectedFocus?.specialization.iconUrl + }); + const abilityCells = $derived.by((): MatrixCell[] => [0, 1, 2].map((index) => { + const ability = selectedFocus?.abilities[index]; + const configuredChips = draft.chips.slice(index * 3, index * 3 + 3).filter((chip) => chip.setId); + const chipNames = configuredChips.map((chip) => data.chipSets.find((item) => item.id === chip.setId)?.name).filter(Boolean); + return { + id: `ability:${index}`, + label: `ABILITY 0${index + 1}`, + name: ability?.name ?? 'NOT ASSIGNED', + meta: ability?.attack?.damage_type ?? '', + summary: chipNames.length ? chipNames.join(' · ') : `${configuredChips.length} / 3 SKILL CHIPS`, + iconUrl: ability?.iconUrl + }; + })); + 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 details = [...talents, rollSummary(selection.rolls)].filter(Boolean).join(' · '); + return { + id: `weapon:${index}`, + label: `${index ? 'SECONDARY' : 'PRIMARY'} WEAPON`, + name: item?.name ?? 'EMPTY SLOT', + meta: item?.category ?? '', + summary: item ? details || `T${selection.tier} · ${unitValue(selection.damage, '')} DAMAGE` : 'SELECT WEAPON', + iconUrl: weaponIcon(item?.category) + }; + })); + const gearCells = $derived.by((): MatrixCell[] => gearSlots.map((slot) => { + const selection = draft.gear[slot]; + const item = gearItem(slot); + const details = [ + brand(selection.brandId)?.name, + talent(selection.talentId)?.name, + rollSummary(selection.rolls) + ].filter(Boolean).join(' · '); + return { + id: `gear:${slot}`, + label: slot.toUpperCase(), + name: item?.name ?? 'EMPTY SLOT', + meta: item ? `T${item.tier}` : '', + summary: item ? details || `T${item.tier} · LEVEL ${selection.level}` : 'SELECT GEAR', + iconUrl: gearIcon(slot) + }; + })); + const protocolCell = $derived({ + id: 'protocol:0', + label: 'OS PROTOCOL', + name: protocolItem()?.name ?? 'EMPTY SLOT', + meta: protocolItem()?.quality ?? '', + summary: protocolItem() + ? `LEVEL ${draft.protocol.level} · ${draft.protocol.talentUpgrades} / 5 TALENT UPGRADES` + : 'SELECT ONE PROTOCOL', + iconUrl: protocolItem()?.iconUrl + }); + const loadoutConfigured = $derived(Boolean( + selectedFocus + && selectedFocus.abilities.length === 3 + && draft.weapons.every((item) => item.itemId) + && gearSlots.every((slot) => draft.gear[slot].itemId) + && draft.chips.every((item) => item.setId) + && draft.protocol.itemId + )); $effect(() => { const sources = [ @@ -402,6 +496,9 @@ onMount(() => { if (data.initialBuild) return; localStorage.removeItem('shd-build-loadout-draft-v3'); + buildSummary = localStorage.getItem('shd-build-summary-v1') ?? ''; + buildGuide = localStorage.getItem('shd-build-guide-v1') ?? ''; + buildNotesOpen = Boolean(buildSummary || buildGuide); const stored = localStorage.getItem('shd-build-loadout-draft-v4'); if (!stored) return; try { @@ -429,6 +526,8 @@ }); function saveDraft() { localStorage.setItem('shd-build-loadout-draft-v4', JSON.stringify(draft)); + localStorage.setItem('shd-build-summary-v1', buildSummary); + localStorage.setItem('shd-build-guide-v1', buildGuide); saved = true; toast.success('Draft saved locally', 'This loadout is available on this device.'); } @@ -438,15 +537,18 @@
-

LOADOUT WORKBENCH

+

COMMAND GRID / LOADOUT 01

SAVED LOCALLY + {#if data.viewer} {#snippet children(pending)} + + {/snippet} @@ -466,18 +568,28 @@ {#if persistedSlug && persistedVisibility !== 'private'}VIEW SHARE PAGE →{/if}
{/if} -
LIVE CALCULATION{draft.calculationMode === 'maximum' ? 'MAXIMUM POTENTIAL' : 'CUSTOM SCENARIO'}18 EQUIPMENT POSITIONS{data.gear.length + data.weapons.length + data.chipSets.length + data.protocols.length} CATALOG ITEMS
+ {#if buildNotesOpen} +
+ + +
+ {/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}
-
-
EQUIPMENT{gearSlots.filter((slot) => draft.gear[slot].itemId).length + draft.weapons.filter((item) => item.itemId).length + draft.chips.filter((item) => item.setId).length + (draft.protocol.itemId ? 1 : 0)} / 18 EQUIPPED
-
{#each ['PRIMARY', 'SECONDARY'] as label, index}{@const item = weaponItem(index)}{/each}
-
{#each gearSlots as slot}{@const item = gearItem(slot)}{/each}
-
-
+ active = id} + />
{#if activeParts[0] === 'specialization'} @@ -556,7 +668,36 @@
{/if} {/if} - {:else if activeParts[0] === 'gear'} + {: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"/> @@ -609,7 +750,7 @@ {: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} diff --git a/src/routes/builds/[slug]/+page.server.ts b/src/routes/builds/[slug]/+page.server.ts index bf9f4bc..1dd63bb 100644 --- a/src/routes/builds/[slug]/+page.server.ts +++ b/src/routes/builds/[slug]/+page.server.ts @@ -54,7 +54,10 @@ export const load: PageServerLoad = async ({ params, cookies }) => { String(build.owner_id), build.avatar_updated_at ), - owner: build.owner_id === viewer?.id, updatedAt: String(build.updated_at) + owner: build.owner_id === viewer?.id, + updatedAt: String(build.updated_at), + summary: String(build.summary ?? ''), + guideBody: String(build.guide_body ?? '') }, loadout, gear, weapons, chips, protocols, brands, combatResults }; diff --git a/src/routes/builds/[slug]/+page.svelte b/src/routes/builds/[slug]/+page.svelte index 4a7f737..0d27ff3 100644 --- a/src/routes/builds/[slug]/+page.svelte +++ b/src/routes/builds/[slug]/+page.svelte @@ -1,13 +1,16 @@ {data.build.title} — SHD.build @@ -16,6 +19,13 @@

SHD LOADOUT ARCHIVE / {data.build.visibility.toUpperCase()}

{data.build.title}

{#if data.build.owner}EDIT LOADOUT{/if}NEW BUILD +
+ {#if data.build.summary || data.build.guideBody} +
+
FIELD GUIDEAUTHOR NOTES
+ {#if data.build.summary}

{data.build.summary}

{/if} + {#if data.build.guideBody}

HOW TO RUN THIS BUILD

{/if} +
+ {/if}