diff --git a/docs/DESIGN_SYSTEM.md b/docs/DESIGN_SYSTEM.md index 702df74..72ab6ec 100644 --- a/docs/DESIGN_SYSTEM.md +++ b/docs/DESIGN_SYSTEM.md @@ -814,3 +814,26 @@ Attribute relationship, one numeric effect value, and one non-negative whole-number unlock cost. At narrow widths profile cards and augment editor cards reduce from multi-column grids to a single column while retaining 44-pixel interactive targets. + +### Loadout workbench + +The builder is a three-rail tactical workbench: compact equipment positions on +the left, the currently selected item editor in the center, and a sticky live +calculation summary on the right. At medium widths the summary becomes a +full-width lower rail; on mobile all three rails stack, with equipment first, +editing second, and results third. Equipment positions remain explicit even +when empty so loadout completeness is always visible. + +Use the shared `BuilderPicker` for builder catalogs and Attribute pools. It +combines an always-visible current selection with a searchable overlay, icon or +tactical fallback mark, classification metadata, selected state, and conflict +state. Conflicting catalog entries remain visible but disabled so restrictions +such as the single-exotic-weapon rule are explained rather than hidden. + +Numeric instance values display their authoritative minimum and maximum beside +the field and clamp to that range on completion. Always-on item rolls, fixed +bonuses, inherited profiles, enabled augments, and activated set bonuses feed +the live totals immediately. Trigger-dependent talents remain explicitly +`CONDITIONAL` until the user enables `TRIGGER ACTIVE`; only then may their +effects enter combined totals. This textual distinction must remain clear +without relying on color. diff --git a/src/app.css b/src/app.css index 1b16ea2..e19d05a 100644 --- a/src/app.css +++ b/src/app.css @@ -142,6 +142,9 @@ /* Gear shared Tactical Augment profiles */ .gear-augment-registry{margin:10px 0;border:1px solid var(--shd-border);background:#0d1011}.gear-augment-registry>summary{margin:0;padding:9px 10px;cursor:pointer;list-style:none}.gear-augment-registry>summary::-webkit-details-marker{display:none}.gear-augment-registry>summary:hover,.gear-augment-registry>summary:focus-visible{background:#171b1c}.gear-augment-registry>summary:active{transform:translateY(1px);filter:brightness(.88)}.gear-augment-registry>summary>div:last-child{display:flex;align-items:center;gap:10px}.gear-augment-registry>summary i{color:var(--shd-orange);font-size:13px;font-style:normal;transition:transform .16s ease}.gear-augment-registry[open]>summary{border-bottom:1px solid var(--shd-border);background:#131718}.gear-augment-registry[open]>summary i{transform:rotate(45deg)}.gear-augment-profile-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:6px;padding:8px}.gear-augment-profile-grid>button{min-width:0;padding:8px;border:1px solid var(--shd-border);background:#111415;color:var(--shd-text-muted);text-align:left;cursor:pointer}.gear-augment-profile-grid>button:hover,.gear-augment-profile-grid>button:focus-visible{border-color:#8a672b;background:#171b1c}.gear-augment-profile-grid>button:active{transform:translateY(1px);filter:brightness(.88)}.gear-augment-profile-grid>button.configured{border-left:2px solid var(--shd-success)}.gear-augment-profile-grid header{display:flex;justify-content:space-between;align-items:center;padding-bottom:6px;border-bottom:1px solid #292e2f}.gear-augment-profile-grid header div{display:grid;gap:2px}.gear-augment-profile-grid header strong{color:#d7d9d5;font-size:9px}.gear-augment-profile-grid header small,.gear-augment-profile-grid header>span{color:#626a6c;font-size:6px;letter-spacing:.6px}.gear-augment-profile-grid>button.configured header>span{color:var(--shd-success)}.gear-augment-profile-grid>button>div{display:grid;margin-top:5px}.gear-augment-profile-grid>button>div span{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:6px;padding:5px 2px;border-top:1px solid #232829}.gear-augment-profile-grid>button>div span:first-child{border-top:0}.gear-augment-profile-grid>button>div small{color:var(--shd-orange);font-size:6px}.gear-augment-profile-grid>button>div b{overflow:hidden;color:#aeb4b5;font-size:7px;text-overflow:ellipsis;white-space:nowrap}.gear-augment-profile-grid>button>div em{color:#626a6c;font-size:6px;font-style:normal}.gear-augment-editor{border-top-color:var(--shd-success)}.gear-augment-slots{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:7px}.gear-augment-slots section{min-width:0;padding:8px;border:1px solid var(--shd-border);background:#101314}.gear-augment-slots section>header{display:flex;justify-content:space-between;padding-bottom:6px;border-bottom:1px solid #2a2f30}.gear-augment-slots section>header strong{color:var(--shd-orange);font-size:7px;letter-spacing:.8px}.gear-augment-slots section>header span{color:#596164;font-size:6px}.gear-augment-slots .field{margin:7px 0 0}.gear-augment-slots .field>span,.gear-augment-slots label{font-size:7px}@media(max-width:1100px){.gear-augment-slots{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:900px){.gear-augment-profile-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:560px){.gear-augment-profile-grid,.gear-augment-slots{grid-template-columns:1fr}.gear-augment-profile-grid>button{min-height:44px}} + +/* Builder workbench */ +.loadout-workspace{display:grid;grid-template-columns:minmax(230px,280px) minmax(420px,1fr) minmax(290px,350px);gap:10px;align-items:start}.loadout-slots,.loadout-editor,.loadout-summary{border:1px solid var(--shd-border);background:#111415}.loadout-slots>header,.summary-section>header{display:flex;justify-content:space-between;align-items:center;padding:9px 10px;border-bottom:1px solid var(--shd-border);background:#0d1011;color:#6d7577;font-size:7px;letter-spacing:1px}.loadout-slots>header b,.summary-section>header b{color:var(--shd-orange)}.loadout-slot-group{display:grid;gap:4px;padding:5px;border-bottom:1px solid #292e2f}.loadout-slot-group.weapons{grid-template-columns:1fr 1fr}.loadout-slot-group.gear{grid-template-columns:1fr 1fr}.loadout-slot-group.systems{grid-template-columns:1fr 1fr}.loadout-slot-group button{min-width:0;min-height:65px;display:grid;align-content:center;gap:3px;padding:8px;border:1px solid #2b3031;border-left:2px solid #3b4142;background:#101314;color:#c5c9ca;text-align:left;cursor:pointer}.loadout-slot-group button:hover,.loadout-slot-group button:focus-visible{border-color:#8b682b;background:#171b1c}.loadout-slot-group button:active{transform:translateY(1px);filter:brightness(.88)}.loadout-slot-group button.active{border-left-color:var(--shd-orange);background:#1b170f}.loadout-slot-group small{color:#646c6e;font-size:6px;letter-spacing:.8px}.loadout-slot-group strong{overflow:hidden;font-size:8px;text-overflow:ellipsis;white-space:nowrap}.loadout-slot-group span{overflow:hidden;color:#7c8486;font-size:6px;text-overflow:ellipsis;white-space:nowrap}.loadout-editor{min-height:680px;padding:12px;border-top:2px solid var(--shd-orange)}.loadout-editor-head{display:flex;justify-content:space-between;align-items:end;padding-bottom:9px;border-bottom:1px solid #303536}.loadout-editor-head span{color:#687072;font-size:6px;letter-spacing:1px}.loadout-editor-head h2{margin:3px 0 0;font-size:19px}.loadout-editor-head>b{color:var(--shd-orange);font-size:7px;letter-spacing:.8px}.builder-picker{position:relative;margin-top:9px}.builder-picker>span{display:block;margin-bottom:5px;color:#737b7d;font-size:7px;font-weight:800;letter-spacing:.8px}.builder-picker-current{width:100%;min-height:48px;display:grid;grid-template-columns:34px minmax(0,1fr) auto;grid-template-rows:1fr 1fr;align-items:center;column-gap:8px;padding:6px 9px;border:1px solid #343a3b;background:#0e1112;color:#d7d9d5;text-align:left}.builder-picker-current img,.builder-picker-current>i,.builder-picker-panel button img,.builder-picker-panel button>i{grid-row:1/3;width:30px;height:30px;object-fit:contain}.builder-picker-current>i,.builder-picker-panel button>i{display:grid;place-items:center;color:var(--shd-orange);font-style:normal}.builder-picker-current b{align-self:end;font-size:9px}.builder-picker-current small{align-self:start;color:#666e70;font-size:6px}.builder-picker-current em{grid-column:3;grid-row:1/3;color:var(--shd-orange);font-size:14px;font-style:normal}.builder-picker-panel{position:absolute;z-index:30;top:100%;left:0;right:0;padding:6px;border:1px solid #5a4a2d;background:#0b0e0f;box-shadow:0 12px 25px #000b}.builder-picker-panel>input{height:36px;margin:0 0 5px;padding:0 9px;font-size:8px}.builder-picker-panel>div{max-height:250px;overflow:auto}.builder-picker-panel>div>button{width:100%;min-height:42px;display:grid;grid-template-columns:30px minmax(0,1fr) auto;align-items:center;gap:7px;padding:5px 7px;border:0;border-top:1px solid #272c2d;background:#111415;color:#c4c8c9;text-align:left}.builder-picker-panel>div>button:hover,.builder-picker-panel>div>button.selected{background:#1b170f}.builder-picker-panel>div>button:disabled{opacity:.35}.builder-picker-panel button span{display:grid;gap:2px}.builder-picker-panel button b{font-size:8px}.builder-picker-panel button small,.builder-picker-panel button em{color:#687072;font-size:6px;font-style:normal}.builder-picker-panel button.selected em{color:var(--shd-orange)}.builder-picker-panel p{padding:12px;color:#626a6c;font-size:7px}.builder-field-grid,.builder-range-grid{display:grid;grid-template-columns:1fr 1fr;gap:7px;margin-top:8px}.builder-field-grid>label,.builder-range-grid>label,.builder-chip-rolls label,.builder-rolls label{color:#737b7d;font-size:7px;font-weight:800;letter-spacing:.7px}.builder-field-grid select,.builder-range-grid input,.builder-chip-rolls input,.builder-rolls input{height:38px;margin-top:5px;padding:0 8px;font-size:9px}.builder-range-grid label>span{float:right;color:#5f6769;font-size:6px}.builder-range-grid.weapon{grid-template-columns:repeat(2,1fr)}.builder-fixed{display:flex;align-items:end;gap:5px;padding-bottom:1px}.builder-fixed span{flex:1;padding:9px 5px;border:1px solid #303536;color:#8e9698;font-size:6px;text-align:center}.builder-section-title{display:flex;justify-content:space-between;align-items:end;margin:13px 0 5px}.builder-section-title>div{display:grid;gap:3px}.builder-section-title span{font-size:8px;font-weight:900;letter-spacing:.8px}.builder-section-title small{color:#626a6c;font-size:6px}.builder-section-title button{border:0;background:transparent;color:var(--shd-orange);font-size:7px}.builder-section-title>b{color:var(--shd-orange);font-size:7px}.builder-rolls{display:grid;gap:5px}.builder-rolls>div,.builder-chip-rolls>div{display:grid;grid-template-columns:minmax(0,1fr) 105px 28px;align-items:end;gap:6px;padding:6px;border:1px solid #2d3233;background:#0f1213}.builder-rolls .builder-picker,.builder-chip-rolls .builder-picker{margin:0}.builder-rolls>div>button{height:38px;border:1px solid #563734;background:#1d1212;color:#da746c}.builder-chip-rolls{display:grid;gap:6px;margin-top:8px}.builder-chip-rolls>div{grid-template-columns:minmax(0,1fr) 105px}.builder-augment-list{display:grid;grid-template-columns:1fr 1fr;gap:5px}.builder-augment-list label{min-height:45px;display:grid;grid-template-columns:18px 1fr auto;align-items:center;gap:7px;padding:7px;border:1px solid #303536;background:#0f1213;cursor:pointer}.builder-augment-list input{width:auto;margin:0;accent-color:var(--shd-orange)}.builder-augment-list span{display:grid;gap:3px}.builder-augment-list b{font-size:7px}.builder-augment-list small{color:#687072;font-size:6px}.builder-augment-list em{color:#a77b31;font-size:6px;font-style:normal}.builder-inherited{display:grid;grid-template-columns:repeat(3,1fr);margin-top:8px;border:1px solid #303536}.builder-inherited span{display:grid;gap:4px;padding:8px;border-left:1px solid #303536}.builder-inherited span:first-child{border-left:0}.builder-inherited small{color:#687072;font-size:6px}.builder-inherited b{color:var(--shd-orange);font-size:9px}.loadout-summary{position:sticky;top:88px;border-top:2px solid var(--shd-orange)}.summary-head{display:flex;justify-content:space-between;align-items:end;padding:12px}.summary-head h2{margin:4px 0 0;font-size:20px}.summary-head>b{color:var(--shd-orange);font-size:22px}.summary-core{display:grid;grid-template-columns:repeat(3,1fr);border-top:1px solid #303536;border-bottom:1px solid #303536}.summary-core>div{display:flex;align-items:center;gap:5px;padding:9px 6px;border-left:1px solid #303536}.summary-core>div:first-child{border-left:0}.summary-core .game-icon{width:22px;height:22px}.summary-core span{display:grid;color:#687072;font-size:5px}.summary-core b{color:#d6d9da;font-size:9px}.summary-scroll{max-height:calc(100vh - 340px);overflow:auto}.summary-section>header{position:sticky;top:0;z-index:2}.summary-stat,.summary-bonus{display:flex;justify-content:space-between;gap:8px;padding:8px 10px;border-top:1px solid #252a2b}.summary-stat>span,.summary-bonus>span{display:grid;gap:3px;font-size:8px}.summary-stat small,.summary-bonus small{color:#626a6c;font-size:5px}.summary-stat>b{color:#d9dbdc;font-size:9px}.summary-bonus em{color:var(--shd-success);font-size:7px;font-style:normal;text-align:right}.summary-section>p{margin:0;padding:12px;color:#626a6c;font-size:7px;line-height:1.5}.summary-talent{display:grid;grid-template-columns:18px 1fr;gap:7px;padding:9px 10px;border-top:1px solid #252a2b;cursor:pointer}.summary-talent input{width:auto;margin:2px 0 0;accent-color:var(--shd-orange)}.summary-talent span{display:grid;gap:3px}.summary-talent b{font-size:8px}.summary-talent small{color:#8a682f;font-size:5px}.summary-talent em{color:#727a7c;font:7px/1.45 Georgia,serif}.loadout-summary .notice{margin:0;padding:11px;border-top:1px solid #303536;border-left:0}.builder-empty-data{display:flex;gap:12px;margin-bottom:10px;padding:12px;border:1px solid #65403a;border-left:3px solid var(--shd-danger);background:#1c1111}.builder-empty-data strong{font-size:8px}.builder-empty-data span{color:#a87870;font-size:8px}@media(max-width:1200px){.loadout-workspace{grid-template-columns:240px minmax(390px,1fr)}.loadout-summary{position:static;grid-column:1/3}.summary-scroll{max-height:none;display:grid;grid-template-columns:repeat(3,1fr)}.summary-section{border-left:1px solid #303536}.summary-section:first-child{border-left:0}}@media(max-width:800px){.loadout-workspace{grid-template-columns:1fr}.loadout-slots,.loadout-editor,.loadout-summary{grid-column:auto}.loadout-editor{min-height:0}.summary-scroll{display:block}.summary-section{border-left:0}.loadout-summary{order:3}.loadout-slot-group.gear{grid-template-columns:repeat(3,1fr)}}@media(max-width:560px){.loadout-slot-group.gear,.loadout-slot-group.systems{grid-template-columns:1fr 1fr}.builder-field-grid,.builder-range-grid,.builder-range-grid.weapon,.builder-augment-list,.builder-inherited{grid-template-columns:1fr}.builder-inherited span{border-left:0;border-top:1px solid #303536}.builder-rolls>div,.builder-chip-rolls>div{grid-template-columns:1fr}.builder-rolls>div>button{min-height:44px}.builder-picker-current{min-height:52px}.builder-picker-panel>div>button{min-height:46px}} .mobile-row-open{display:none}@media(max-width:700px){.mobile-detail-row{position:relative;min-height:48px;padding-bottom:25px;cursor:pointer}.dashboard-row>.mobile-row-open{position:absolute;z-index:2;inset:0;width:100%;height:100%;display:block;justify-self:stretch;margin:0;padding:0;border:0;background:transparent;color:transparent;font-size:0;cursor:pointer}.mobile-detail-row>.row-action,.mobile-detail-row>button:not(.mobile-row-open){display:none}.mobile-detail-row::after{content:"TAP TO VIEW ›";position:absolute;right:10px;bottom:7px;color:var(--shd-orange);font-size:6px;font-weight:900;letter-spacing:.8px;pointer-events:none}.moderation-row.mobile-detail-row::after{content:"TAP TO REVIEW ›"}.mobile-detail-row:hover{border-color:#665026;background:#171713}.mobile-detail-row:has(.mobile-row-open:focus-visible){outline:2px solid var(--shd-orange);outline-offset:-2px}.mobile-detail-row:has(.mobile-row-open:active){transform:translateY(1px);filter:brightness(.86)}}@media(prefers-reduced-motion:reduce){.mobile-detail-row:has(.mobile-row-open:active){transform:none}} .avatar-review-filter{grid-template-columns:minmax(220px,1fr) auto}.avatar-review-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:7px}.avatar-review-card{min-width:0;display:grid;grid-template-columns:112px minmax(0,1fr);border:1px solid var(--shd-border);background:#111415}.avatar-review-image{position:relative;min-height:142px;display:grid;place-items:center;overflow:hidden;border-right:1px solid var(--shd-border);background:#0b0e0f}.avatar-review-image img{width:100%;height:100%;object-fit:cover}.avatar-review-image>span{padding:10px;color:#596164;font-size:6px;font-weight:900;line-height:1.5;text-align:center}.avatar-review-image>b{position:absolute;right:0;bottom:0;padding:5px 7px;border-top:1px solid currentColor;border-left:1px solid currentColor;background:#171b1ce8;color:#889092;font-size:6px;letter-spacing:.7px}.avatar-review-image>b.pending{color:var(--shd-orange)}.avatar-review-image>b.approved{color:var(--shd-success)}.avatar-review-image>b.declined{color:var(--shd-danger)}.avatar-review-details{min-width:0;display:grid;align-content:start;gap:4px;padding:9px}.avatar-review-details>strong{font-size:10px}.avatar-review-details>small{overflow:hidden;color:#697174;font-size:6px;text-overflow:ellipsis;white-space:nowrap}.avatar-review-details dl{display:grid;gap:4px;margin:5px 0}.avatar-review-details dl>div{display:flex;justify-content:space-between;gap:8px;border-top:1px solid #292e2f;padding-top:4px}.avatar-review-details dt,.avatar-review-details dd{margin:0;font-size:6px}.avatar-review-details dt{color:#697174}.avatar-review-details dd{color:#b9bebe;text-align:right}.avatar-review-details>p{margin:2px 0;color:#8c9495;font-size:7px;line-height:1.4}.avatar-review-details>button{min-height:31px;margin-top:auto}.avatar-review-modal{width:min(680px,calc(100vw - 24px))}.avatar-review-preview{height:min(46vh,390px);display:grid;place-items:center;margin:9px 0;overflow:hidden;border:1px solid var(--shd-border);background:#090b0c}.avatar-review-preview img{width:100%;height:100%;object-fit:contain}.avatar-review-preview span{color:#697174;font-size:7px;letter-spacing:.8px}.avatar-decision-tabs{grid-template-columns:repeat(2,1fr)}@media(max-width:1150px){.avatar-review-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:700px){.avatar-review-filter,.avatar-review-grid{grid-template-columns:1fr}.avatar-review-card{grid-template-columns:96px minmax(0,1fr)}.avatar-review-image{min-height:132px}.avatar-review-details>button{min-height:44px}.avatar-decision-tabs{grid-template-columns:1fr}} .assignment-chips>button span,.assignment-results>button>span{display:grid;gap:2px}.assignment-chips>button span small,.assignment-results>button>span small{color:var(--shd-orange);font-size:5px;font-weight:900;letter-spacing:.55px} diff --git a/src/lib/builder/loadout.ts b/src/lib/builder/loadout.ts new file mode 100644 index 0000000..4ceff00 --- /dev/null +++ b/src/lib/builder/loadout.ts @@ -0,0 +1,38 @@ +export type Roll = { statId: string; value: number }; +export type GearSelection = { itemId: string; brandId: string; talentId: string; health: number; armor: number; rolls: Roll[]; augmentSlots: number[] }; +export type WeaponSelection = { itemId: string; tier: number; damage: number; firepower: number; toughness: number; engineering: number; talent1Id: string; talent2Id: string }; +export type ChipSelection = { setId: string; quality: 'Superior' | 'High-end'; primary: Roll; secondary: [Roll, Roll] }; +export type ProtocolSelection = { itemId: string; level: number; talentUpgrades: number }; +export type LoadoutDraft = { + version: 1; + buildName: string; + gear: Record; + weapons: [WeaponSelection, WeaponSelection]; + chips: [ChipSelection, ChipSelection, ChipSelection]; + protocol: ProtocolSelection; + activeTalents: string[]; +}; + +export const gearSlots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'] as const; +export const blankRoll = (): Roll => ({ statId: '', value: 0 }); +export const blankGear = (): GearSelection => ({ itemId: '', brandId: '', talentId: '', health: 0, armor: 0, rolls: [blankRoll()], augmentSlots: [] }); +export const blankWeapon = (): WeaponSelection => ({ itemId: '', tier: 0, damage: 0, firepower: 0, toughness: 0, engineering: 0, talent1Id: '', talent2Id: '' }); +export const blankChip = (): ChipSelection => ({ setId: '', quality: 'Superior', primary: blankRoll(), secondary: [blankRoll(), blankRoll()] }); +export const createDraft = (): LoadoutDraft => ({ + version: 1, + buildName: 'Untitled loadout', + gear: Object.fromEntries(gearSlots.map((slot) => [slot, blankGear()])), + weapons: [blankWeapon(), blankWeapon()], + chips: [blankChip(), blankChip(), blankChip()], + protocol: { itemId: '', level: 1, talentUpgrades: 0 }, + activeTalents: [] +}); + +export const clamp = (value: unknown, min: unknown, max: unknown) => { + const low = Number(min), high = Number(max), numeric = Number(value); + if (!Number.isFinite(numeric)) return Number.isFinite(low) ? low : 0; + return Math.min(Number.isFinite(high) ? high : numeric, Math.max(Number.isFinite(low) ? low : numeric, numeric)); +}; + +export const scaled = (min: unknown, max: unknown, progress: number, steps: number) => + Number(min) + (Number(max) - Number(min)) * Math.min(steps, Math.max(0, progress)) / steps; diff --git a/src/lib/components/BuilderPicker.svelte b/src/lib/components/BuilderPicker.svelte new file mode 100644 index 0000000..61a04b0 --- /dev/null +++ b/src/lib/components/BuilderPicker.svelte @@ -0,0 +1,44 @@ + + +
+ {label} + + {#if open} +
+ +
+ {#if selected}{/if} + {#each results as item} + + {:else}

NO MATCHING ITEMS

{/each} +
+
+ {/if} +
diff --git a/src/routes/builder/+page.server.ts b/src/routes/builder/+page.server.ts new file mode 100644 index 0000000..6fbfc1f --- /dev/null +++ b/src/routes/builder/+page.server.ts @@ -0,0 +1,101 @@ +import { getDatabase } from '$lib/server/database'; +import { gameDataIconUrl } from '$lib/server/game-data-icons'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async () => { + const database = await getDatabase(); + const [ + attributes, gear, gearBrands, gearTalents, gearAugments, brands, brandEffects, + weapons, weaponRanges, weaponTalentOptions, weaponBonuses, + talents, talentTiers, talentEffects, talentTemplates, + chipSets, chipTwoPiece, chipPrimary, chipSecondary, chipTalents, chipTalentEffects, + protocols, protocolPrimary, protocolProfiles, protocolSecondary, protocolTalents, protocolTalentEffects, + icons + ] = await Promise.all([ + (await database.prepare('SELECT * FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM gear WHERE active=1 ORDER BY slot,name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM gear_brand_sets')).all(), + (await database.prepare('SELECT * FROM gear_talents')).all(), + (await database.prepare('SELECT * FROM gear_tactical_augments ORDER BY gear_slot,augment_slot')).all(), + (await database.prepare('SELECT * FROM brand_sets WHERE active=1 ORDER BY name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM brand_set_effects ORDER BY pieces_required')).all(), + (await database.prepare('SELECT * FROM weapons WHERE active=1 ORDER BY category,name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM weapon_tier_ranges ORDER BY tier')).all(), + (await database.prepare('SELECT * FROM weapon_talent_options ORDER BY talent_slot')).all(), + (await database.prepare('SELECT * FROM weapon_base_attribute_bonuses ORDER BY sort_order')).all(), + (await database.prepare('SELECT * FROM talents WHERE active=1 ORDER BY name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM talent_tiers ORDER BY tier')).all(), + (await database.prepare('SELECT * FROM talent_tier_effects')).all(), + (await database.prepare('SELECT * FROM talent_description_templates')).all(), + (await database.prepare('SELECT * FROM skill_chip_sets WHERE active=1 ORDER BY name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM skill_chip_two_piece_bonuses')).all(), + (await database.prepare('SELECT * FROM skill_chip_primary_options')).all(), + (await database.prepare('SELECT * FROM skill_chip_secondary_options')).all(), + (await database.prepare('SELECT * FROM skill_chip_talents')).all(), + (await database.prepare('SELECT * FROM skill_chip_talent_effects ORDER BY sort_order')).all(), + (await database.prepare('SELECT * FROM os_protocols WHERE active=1 ORDER BY name COLLATE NOCASE')).all(), + (await database.prepare('SELECT * FROM os_protocol_primary_effects')).all(), + (await database.prepare('SELECT * FROM os_protocol_secondary_profiles')).all(), + (await database.prepare('SELECT * FROM os_protocol_secondary_effects ORDER BY effect_role')).all(), + (await database.prepare('SELECT * FROM os_protocol_talents')).all(), + (await database.prepare('SELECT * FROM os_protocol_talent_effects ORDER BY sort_order')).all(), + (await database.prepare("SELECT * FROM game_data_icons")).all() + ]); + const iconUrl = (type: 'talent' | 'brand_set' | 'os_protocol' | 'skill_chip', id: unknown) => { + const icon = icons.find((item) => item.entity_type === type && item.entity_id === id); + return icon ? gameDataIconUrl(type, String(id), icon.updated_at) : ''; + }; + const configuredTalents = talents.map((talent) => ({ + ...talent, + iconUrl: iconUrl('talent', talent.id), + template: talentTemplates.find((item) => item.talent_id === talent.id)?.template ?? '', + tiers: talentTiers.filter((tier) => tier.talent_id === talent.id).map((tier) => ({ + ...tier, + effects: talentEffects.filter((effect) => effect.talent_tier_id === tier.id) + })) + })); + return { + attributes, + gear: gear.map((item) => ({ + ...item, + brandIds: gearBrands.filter((link) => link.gear_id === item.id).map((link) => String(link.brand_set_id)), + talentIds: gearTalents.filter((link) => link.gear_id === item.id).map((link) => String(link.talent_id)) + })), + gearAugments, + brands: brands.map((brand) => ({ + ...brand, + iconUrl: iconUrl('brand_set', brand.id), + effects: brandEffects.filter((effect) => effect.brand_set_id === brand.id) + })), + weapons: weapons.map((weapon) => ({ + ...weapon, + ranges: weaponRanges.filter((range) => range.weapon_id === weapon.id), + talent1Ids: weaponTalentOptions.filter((link) => link.weapon_id === weapon.id && Number(link.talent_slot) === 1).map((link) => String(link.talent_id)), + talent2Ids: weaponTalentOptions.filter((link) => link.weapon_id === weapon.id && Number(link.talent_slot) === 2).map((link) => String(link.talent_id)), + baseBonuses: weaponBonuses.filter((bonus) => bonus.weapon_id === weapon.id) + })), + talents: configuredTalents, + chipSets: chipSets.map((set) => { + const talent = chipTalents.find((item) => item.skill_chip_set_id === set.id); + return { + ...set, + iconUrl: iconUrl('skill_chip', set.id), + twoPiece: chipTwoPiece.find((item) => item.skill_chip_set_id === set.id) ?? null, + primaryStatIds: chipPrimary.filter((item) => item.skill_chip_set_id === set.id).map((item) => String(item.stat_attribute_id)), + secondaryStatIds: chipSecondary.filter((item) => item.skill_chip_set_id === set.id).map((item) => String(item.stat_attribute_id)), + talent: talent ? { ...talent, effects: chipTalentEffects.filter((effect) => effect.talent_id === talent.id) } : null + }; + }), + protocols: protocols.map((protocol) => { + const profile = protocolProfiles.find((item) => item.quality === protocol.quality && item.primary_attribute_type === protocol.primary_attribute_type); + const talent = protocolTalents.find((item) => item.protocol_id === protocol.id); + return { + ...protocol, + iconUrl: iconUrl('os_protocol', protocol.id), + primaryEffect: protocolPrimary.find((item) => item.protocol_id === protocol.id) ?? null, + secondaryEffects: profile ? protocolSecondary.filter((effect) => effect.profile_id === profile.id) : [], + talent: talent ? { ...talent, effects: protocolTalentEffects.filter((effect) => effect.talent_id === talent.id) } : null + }; + }) + }; +}; diff --git a/src/routes/builder/+page.svelte b/src/routes/builder/+page.svelte index 0448c8d..e71d9eb 100644 --- a/src/routes/builder/+page.svelte +++ b/src/routes/builder/+page.svelte @@ -1,30 +1,207 @@