Refine Gear augments and builder levels
This commit is contained in:
+13
-3
@@ -357,7 +357,6 @@ CREATE TABLE IF NOT EXISTS gear (
|
||||
name TEXT NOT NULL,
|
||||
slot TEXT NOT NULL CHECK(slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')),
|
||||
tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2),
|
||||
level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 50),
|
||||
base_health_min REAL NOT NULL CHECK(base_health_min >= 0),
|
||||
base_health_max REAL NOT NULL CHECK(base_health_max >= base_health_min),
|
||||
base_armor_min REAL,
|
||||
@@ -386,9 +385,9 @@ CREATE TABLE IF NOT EXISTS gear_tactical_augments (
|
||||
id TEXT PRIMARY KEY,
|
||||
gear_slot TEXT NOT NULL CHECK(gear_slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')),
|
||||
augment_slot INTEGER NOT NULL CHECK(augment_slot BETWEEN 1 AND 4),
|
||||
stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id),
|
||||
effect_type TEXT NOT NULL CHECK(effect_type IN ('Custom', 'Base Percentage')),
|
||||
stat_attribute_id TEXT REFERENCES stat_attributes(id),
|
||||
value REAL NOT NULL,
|
||||
unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(gear_slot, augment_slot),
|
||||
@@ -396,8 +395,19 @@ CREATE TABLE IF NOT EXISTS gear_tactical_augments (
|
||||
(gear_slot IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 4)
|
||||
OR
|
||||
(gear_slot NOT IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 3)
|
||||
),
|
||||
CHECK(
|
||||
(augment_slot = 1 AND effect_type = 'Custom' AND stat_attribute_id IS NOT NULL)
|
||||
OR
|
||||
(augment_slot > 1 AND effect_type = 'Base Percentage' AND stat_attribute_id IS NULL AND value > 0)
|
||||
)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS gear_tactical_augment_costs (
|
||||
augment_id TEXT NOT NULL REFERENCES gear_tactical_augments(id) ON DELETE CASCADE,
|
||||
gear_tier INTEGER NOT NULL CHECK(gear_tier BETWEEN 0 AND 2),
|
||||
unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0),
|
||||
PRIMARY KEY(augment_id, gear_tier)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS weapons (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
|
||||
+33
-5
@@ -809,11 +809,20 @@ editor. Successful saves close the profile editor and return to the registry.
|
||||
|
||||
Gear Tactical Augment profiles use this pattern for all six gear types.
|
||||
Backpacks and Body Armor expose four required slots; Holsters, Gloves, Knee
|
||||
Pads, and Masks expose three. Every slot contains exactly one searchable
|
||||
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.
|
||||
Pads, and Masks expose three. Augment 01 contains one searchable Attribute,
|
||||
one numeric effect value, and an unlock cost. Later slots expose only their
|
||||
unlock cost because their effects are fixed base-item upgrades: three-slot
|
||||
profiles use +50% and +100%, while four-slot profiles use +40%, +70%, and
|
||||
+100%. Holsters, Masks, and Backpacks modify the equipped item's base Health;
|
||||
Gloves, Knee Pads, and Body Armor modify that item's base Health and Armor.
|
||||
These percentages never apply to the overall character total. Every augment
|
||||
slot stores separate non-negative whole-number unlock costs for item Tiers 0,
|
||||
1, and 2. Show all three costs together in registry summaries and in a compact
|
||||
three-column cost fieldset within the editor; the builder shows only the cost
|
||||
matching the equipped item's tier. Fixed upgrades
|
||||
use a green inherited-value panel to distinguish governed data from editable
|
||||
orange actions. At narrow widths profile cards and augment editor cards reduce
|
||||
to a single column while retaining 44-pixel interactive targets.
|
||||
|
||||
### Loadout workbench
|
||||
|
||||
@@ -837,3 +846,22 @@ 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.
|
||||
|
||||
Gear level is builder-instance state, never part of an ISAC Gear definition.
|
||||
Each equipped piece stores its own level from 1–50 in the local or persisted
|
||||
build. Until an authoritative level-scaling curve is represented in game data,
|
||||
the workbench must state that level scaling is neutral rather than silently
|
||||
inventing a formula; base Health and Armor remain constrained by their
|
||||
canonical ranges.
|
||||
|
||||
### Registry relationship icon rails
|
||||
|
||||
When a table relationship contains a small bounded set of visually identified
|
||||
records, show a compact icon rail instead of a numeric relationship count.
|
||||
Rails use 30-pixel square cells with four-pixel spacing, uploaded transparent
|
||||
icons, full accessible names, and native hover titles. Missing artwork falls
|
||||
back to a short orange text mark rather than removing the relationship.
|
||||
|
||||
Use this pattern only for small bounded sets that remain scannable, such as the
|
||||
two-to-four compatible Brand Sets on Gear. Long or unbounded relationships
|
||||
continue to use counts, searchable inspection, or expanded detail views.
|
||||
|
||||
@@ -145,6 +145,83 @@ if (typeof iconDefinition?.sql === 'string' && !iconDefinition.sql.includes("'sk
|
||||
await database.exec('ALTER TABLE game_data_icons_expanded RENAME TO game_data_icons');
|
||||
}
|
||||
|
||||
const gearAugmentColumns = await (await database.prepare("PRAGMA table_info('gear_tactical_augments')")).all();
|
||||
if (gearAugmentColumns.length && !gearAugmentColumns.some((column) => column.name === 'effect_type')) {
|
||||
console.log('Separating custom Gear Tactical Augments from fixed base-item percentage upgrades.');
|
||||
await database.exec('ALTER TABLE gear_tactical_augments RENAME TO gear_tactical_augments_legacy');
|
||||
await database.exec(`CREATE TABLE gear_tactical_augments (
|
||||
id TEXT PRIMARY KEY,
|
||||
gear_slot TEXT NOT NULL CHECK(gear_slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')),
|
||||
augment_slot INTEGER NOT NULL CHECK(augment_slot BETWEEN 1 AND 4),
|
||||
effect_type TEXT NOT NULL CHECK(effect_type IN ('Custom', 'Base Percentage')),
|
||||
stat_attribute_id TEXT REFERENCES stat_attributes(id),
|
||||
value REAL NOT NULL,
|
||||
unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(gear_slot, augment_slot),
|
||||
CHECK((gear_slot IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 4) OR (gear_slot NOT IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 3)),
|
||||
CHECK((augment_slot = 1 AND effect_type = 'Custom' AND stat_attribute_id IS NOT NULL) OR (augment_slot > 1 AND effect_type = 'Base Percentage' AND stat_attribute_id IS NULL AND value > 0))
|
||||
)`);
|
||||
await database.exec(`INSERT INTO gear_tactical_augments
|
||||
(id,gear_slot,augment_slot,effect_type,stat_attribute_id,value,unlock_cost,created_at,updated_at)
|
||||
SELECT id,gear_slot,augment_slot,
|
||||
CASE WHEN augment_slot=1 THEN 'Custom' ELSE 'Base Percentage' END,
|
||||
CASE WHEN augment_slot=1 THEN stat_attribute_id ELSE NULL END,
|
||||
CASE
|
||||
WHEN augment_slot=1 THEN value
|
||||
WHEN gear_slot IN ('Backpack','Body Armor') AND augment_slot=2 THEN 40
|
||||
WHEN gear_slot IN ('Backpack','Body Armor') AND augment_slot=3 THEN 70
|
||||
WHEN gear_slot IN ('Backpack','Body Armor') AND augment_slot=4 THEN 100
|
||||
WHEN augment_slot=2 THEN 50
|
||||
ELSE 100
|
||||
END,
|
||||
unlock_cost,created_at,updated_at
|
||||
FROM gear_tactical_augments_legacy`);
|
||||
await database.exec('DROP TABLE gear_tactical_augments_legacy');
|
||||
}
|
||||
|
||||
const tieredGearAugmentColumns = await (await database.prepare("PRAGMA table_info('gear_tactical_augments')")).all();
|
||||
if (tieredGearAugmentColumns.some((column) => column.name === 'unlock_cost')) {
|
||||
console.log('Separating Gear Tactical Augment unlock costs by item tier.');
|
||||
await database.exec('DROP TABLE IF EXISTS gear_tactical_augment_costs');
|
||||
await database.exec('ALTER TABLE gear_tactical_augments RENAME TO gear_tactical_augments_single_cost');
|
||||
await database.exec(`CREATE TABLE gear_tactical_augments (
|
||||
id TEXT PRIMARY KEY,
|
||||
gear_slot TEXT NOT NULL CHECK(gear_slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')),
|
||||
augment_slot INTEGER NOT NULL CHECK(augment_slot BETWEEN 1 AND 4),
|
||||
effect_type TEXT NOT NULL CHECK(effect_type IN ('Custom', 'Base Percentage')),
|
||||
stat_attribute_id TEXT REFERENCES stat_attributes(id),
|
||||
value REAL NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(gear_slot, augment_slot),
|
||||
CHECK((gear_slot IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 4) OR (gear_slot NOT IN ('Backpack', 'Body Armor') AND augment_slot BETWEEN 1 AND 3)),
|
||||
CHECK((augment_slot = 1 AND effect_type = 'Custom' AND stat_attribute_id IS NOT NULL) OR (augment_slot > 1 AND effect_type = 'Base Percentage' AND stat_attribute_id IS NULL AND value > 0))
|
||||
)`);
|
||||
await database.exec(`INSERT INTO gear_tactical_augments
|
||||
(id,gear_slot,augment_slot,effect_type,stat_attribute_id,value,created_at,updated_at)
|
||||
SELECT id,gear_slot,augment_slot,effect_type,stat_attribute_id,value,created_at,updated_at
|
||||
FROM gear_tactical_augments_single_cost`);
|
||||
await database.exec(`CREATE TABLE gear_tactical_augment_costs (
|
||||
augment_id TEXT NOT NULL REFERENCES gear_tactical_augments(id) ON DELETE CASCADE,
|
||||
gear_tier INTEGER NOT NULL CHECK(gear_tier BETWEEN 0 AND 2),
|
||||
unlock_cost INTEGER NOT NULL CHECK(unlock_cost >= 0),
|
||||
PRIMARY KEY(augment_id, gear_tier)
|
||||
)`);
|
||||
await database.exec(`INSERT INTO gear_tactical_augment_costs(augment_id,gear_tier,unlock_cost)
|
||||
SELECT id,0,unlock_cost FROM gear_tactical_augments_single_cost
|
||||
UNION ALL SELECT id,1,unlock_cost FROM gear_tactical_augments_single_cost
|
||||
UNION ALL SELECT id,2,unlock_cost FROM gear_tactical_augments_single_cost`);
|
||||
await database.exec('DROP TABLE gear_tactical_augments_single_cost');
|
||||
}
|
||||
|
||||
const gearColumns = await (await database.prepare("PRAGMA table_info('gear')")).all();
|
||||
if (gearColumns.some((column) => column.name === 'level')) {
|
||||
console.log('Moving Gear level from canonical ISAC data to builder-instance state.');
|
||||
await database.exec('ALTER TABLE gear DROP COLUMN level');
|
||||
}
|
||||
|
||||
console.log(`Applied ${statements.length} schema statements to ${databasePath}`);
|
||||
|
||||
if (!databaseAlreadyExisted) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
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 GearSelection = { itemId: string; level: number; 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 };
|
||||
@@ -15,7 +15,7 @@ export type LoadoutDraft = {
|
||||
|
||||
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 blankGear = (): GearSelection => ({ itemId: '', level: 50, 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 => ({
|
||||
|
||||
@@ -7,14 +7,22 @@ import type { Actions, PageServerLoad } from './$types';
|
||||
import { getPagination } from '$lib/server/pagination';
|
||||
import { auditRecord, recordIsacAudit } from '$lib/server/audit';
|
||||
import { getPendingEntities } from '$lib/server/data-moderation';
|
||||
import { gameDataIconUrl } from '$lib/server/game-data-icons';
|
||||
|
||||
const slots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'];
|
||||
const armoredSlots = new Set(['Gloves', 'Knee Pads', 'Body Armor']);
|
||||
const augmentSlotCount = (slot: string) => slot === 'Backpack' || slot === 'Body Armor' ? 4 : 3;
|
||||
const augmentPercentage = (slot: string, augmentSlot: number) =>
|
||||
slot === 'Backpack' || slot === 'Body Armor'
|
||||
? [0, 0, 40, 70, 100][augmentSlot]
|
||||
: [0, 0, 50, 100][augmentSlot];
|
||||
|
||||
async function augmentProfileSnapshot(slot: string) {
|
||||
const database = await getDatabase();
|
||||
return (await database.prepare('SELECT * FROM gear_tactical_augments WHERE gear_slot=? ORDER BY augment_slot')).all(slot);
|
||||
const augments = await (await database.prepare('SELECT * FROM gear_tactical_augments WHERE gear_slot=? ORDER BY augment_slot')).all(slot);
|
||||
const costs = await (await database.prepare(`SELECT cost.* FROM gear_tactical_augment_costs cost
|
||||
JOIN gear_tactical_augments augment ON augment.id=cost.augment_id WHERE augment.gear_slot=? ORDER BY augment.augment_slot,cost.gear_tier`)).all(slot);
|
||||
return augments.map((augment) => ({ ...augment, costs: costs.filter((cost) => cost.augment_id === augment.id) }));
|
||||
}
|
||||
|
||||
export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
@@ -29,14 +37,16 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
: filter === 'talented' ? 'WHERE EXISTS (SELECT 1 FROM gear_talents WHERE gear_id=gear.id)' : '';
|
||||
const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM gear ${where}`)).get();
|
||||
const pagination = getPagination(url, Number(filtered?.total ?? 0));
|
||||
const [gear, brandLinks, talentLinks, brands, talents, augmentRows, stats, pending] = await Promise.all([
|
||||
const [gear, brandLinks, talentLinks, brands, talents, augmentRows, augmentCosts, stats, brandIcons, pending] = await Promise.all([
|
||||
(await database.prepare(`SELECT * FROM gear ${where} ORDER BY active DESC, slot, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(pagination.perPage, pagination.offset),
|
||||
(await database.prepare('SELECT gear_id,brand_set_id FROM gear_brand_sets')).all(),
|
||||
(await database.prepare('SELECT gear_id,talent_id FROM gear_talents')).all(),
|
||||
(await database.prepare('SELECT id,name FROM brand_sets WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
(await database.prepare(`SELECT id,name,category FROM talents WHERE active=1 AND category IN ('Backpack','Vest','Body Armor') ORDER BY name COLLATE NOCASE`)).all(),
|
||||
(await database.prepare('SELECT * FROM gear_tactical_augments ORDER BY gear_slot,augment_slot')).all(),
|
||||
(await database.prepare('SELECT * FROM gear_tactical_augment_costs ORDER BY gear_tier')).all(),
|
||||
(await database.prepare('SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
(await database.prepare("SELECT entity_id,updated_at FROM game_data_icons WHERE entity_type='brand_set'")).all(),
|
||||
getPendingEntities(user.id, [
|
||||
{ module: 'brands', action: 'save', kind: 'brand' },
|
||||
{ module: 'talents', action: 'saveTalent', kind: 'talent', fields: ['category'] }
|
||||
@@ -50,12 +60,18 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
brandIds: brandLinks.filter((link) => link.gear_id === item.id).map((link) => String(link.brand_set_id)),
|
||||
talentIds: talentLinks.filter((link) => link.gear_id === item.id).map((link) => String(link.talent_id))
|
||||
})),
|
||||
brands: [...pendingBrands, ...brands],
|
||||
brands: [...pendingBrands, ...brands.map((brand) => {
|
||||
const icon = brandIcons.find((item) => item.entity_id === brand.id);
|
||||
return { ...brand, iconUrl: icon ? gameDataIconUrl('brand_set', String(brand.id), icon.updated_at) : '' };
|
||||
})],
|
||||
talents: [...pendingTalents, ...talents.map((talent) => ({ ...talent, category: talent.category === 'Vest' ? 'Body Armor' : talent.category }))],
|
||||
augmentProfiles: slots.map((slot) => ({
|
||||
slot,
|
||||
requiredSlots: augmentSlotCount(slot),
|
||||
augments: augmentRows.filter((augment) => augment.gear_slot === slot)
|
||||
augments: augmentRows.filter((augment) => augment.gear_slot === slot).map((augment) => ({
|
||||
...augment,
|
||||
costs: augmentCosts.filter((cost) => cost.augment_id === augment.id)
|
||||
}))
|
||||
})),
|
||||
stats,
|
||||
summary,
|
||||
@@ -80,20 +96,24 @@ export const actions: Actions = {
|
||||
const requiredSlots = augmentSlotCount(slot);
|
||||
const augments = Array.from({ length: requiredSlots }, (_, index) => ({
|
||||
slot: index + 1,
|
||||
statId: String(form.get(`statId${index + 1}`) ?? ''),
|
||||
effectType: index === 0 ? 'Custom' : 'Base Percentage',
|
||||
statId: index === 0 ? String(form.get('statId1') ?? '') : '',
|
||||
rawValue: String(form.get(`value${index + 1}`) ?? '').trim(),
|
||||
rawUnlockCost: String(form.get(`unlockCost${index + 1}`) ?? '').trim(),
|
||||
value: Number(form.get(`value${index + 1}`)),
|
||||
unlockCost: Number(form.get(`unlockCost${index + 1}`))
|
||||
value: index === 0 ? Number(form.get('value1')) : augmentPercentage(slot, index + 1),
|
||||
costs: [0, 1, 2].map((tier) => ({
|
||||
tier,
|
||||
raw: String(form.get(`unlockCost${index + 1}Tier${tier}`) ?? '').trim(),
|
||||
value: Number(form.get(`unlockCost${index + 1}Tier${tier}`))
|
||||
}))
|
||||
}));
|
||||
if (augments.some((augment) => !augment.statId || !augment.rawValue || !Number.isFinite(augment.value))) {
|
||||
return fail(400, { augmentProfileError: 'Every tactical augment slot requires exactly one attribute and value.' });
|
||||
if (!augments[0].statId || !augments[0].rawValue || !Number.isFinite(augments[0].value)) {
|
||||
return fail(400, { augmentProfileError: 'Tactical Augment 01 requires one custom attribute and value.' });
|
||||
}
|
||||
if (augments.some((augment) => !augment.rawUnlockCost || !Number.isInteger(augment.unlockCost) || augment.unlockCost < 0)) {
|
||||
return fail(400, { augmentProfileError: 'Every tactical augment slot requires a non-negative whole-number unlock cost.' });
|
||||
if (augments.some((augment) => augment.costs.some((cost) => !cost.raw || !Number.isInteger(cost.value) || cost.value < 0))) {
|
||||
return fail(400, { augmentProfileError: 'Every tactical augment slot requires non-negative whole-number unlock costs for Tiers 0, 1, and 2.' });
|
||||
}
|
||||
const database = await getDatabase();
|
||||
const statIds = [...new Set(augments.map((augment) => augment.statId))];
|
||||
const statIds = [augments[0].statId];
|
||||
const validStats = await (await database.prepare(`SELECT COUNT(*) AS count FROM stat_attributes WHERE active=1 AND id IN (${statIds.map(() => '?').join(',')})`)).get(...statIds);
|
||||
if (Number(validStats?.count) !== statIds.length) return fail(400, { augmentProfileError: 'One or more selected attributes are unavailable.' });
|
||||
const before = await augmentProfileSnapshot(slot);
|
||||
@@ -102,9 +122,13 @@ export const actions: Actions = {
|
||||
try {
|
||||
await (await database.prepare('DELETE FROM gear_tactical_augments WHERE gear_slot=?')).run(slot);
|
||||
for (const augment of augments) {
|
||||
const augmentId = randomUUID();
|
||||
await (await database.prepare(`INSERT INTO gear_tactical_augments
|
||||
(id,gear_slot,augment_slot,stat_attribute_id,value,unlock_cost,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)`)).run(randomUUID(), slot, augment.slot, augment.statId, augment.value, augment.unlockCost, now, now);
|
||||
(id,gear_slot,augment_slot,effect_type,stat_attribute_id,value,created_at,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?)`)).run(augmentId, slot, augment.slot, augment.effectType, augment.statId || null, augment.value, now, now);
|
||||
for (const cost of augment.costs) {
|
||||
await (await database.prepare('INSERT INTO gear_tactical_augment_costs(augment_id,gear_tier,unlock_cost) VALUES(?,?,?)')).run(augmentId, cost.tier, cost.value);
|
||||
}
|
||||
}
|
||||
await database.exec('COMMIT');
|
||||
} catch (error) {
|
||||
@@ -128,12 +152,11 @@ export const actions: Actions = {
|
||||
const name = String(form.get('name') ?? '').normalize('NFKC').trim();
|
||||
const slot = String(form.get('slot') ?? '');
|
||||
const tier = Number(form.get('tier'));
|
||||
const level = Number(form.get('level'));
|
||||
const brandIds = [...new Set(form.getAll('brandIds').map(String))];
|
||||
const talentIds = [...new Set(form.getAll('talentIds').map(String))];
|
||||
if (name.length < 2 || name.length > 80) return fail(400, { gearError: 'Gear names must be 2–80 characters.' });
|
||||
if (!slots.includes(slot) || !Number.isInteger(tier) || tier < 0 || tier > 2 || !Number.isInteger(level) || level < 1 || level > 50) {
|
||||
return fail(400, { gearError: 'Choose a valid slot, tier, and level.' });
|
||||
if (!slots.includes(slot) || !Number.isInteger(tier) || tier < 0 || tier > 2) {
|
||||
return fail(400, { gearError: 'Choose a valid slot and tier.' });
|
||||
}
|
||||
if (brandIds.length < 2) return fail(400, { gearError: 'Gear must be available from at least two brand sets.' });
|
||||
const talentCategory = slot === 'Backpack' ? 'Backpack' : slot === 'Body Armor' ? 'Vest' : null;
|
||||
@@ -160,11 +183,11 @@ export const actions: Actions = {
|
||||
const gearId = id || String(form.get('_proposedEntityId') ?? '') || randomUUID(), now = new Date().toISOString();
|
||||
const before = id ? { record: await auditRecord('gear', id), brandIds: await (await database.prepare('SELECT brand_set_id FROM gear_brand_sets WHERE gear_id=?')).all(id), talentIds: await (await database.prepare('SELECT talent_id FROM gear_talents WHERE gear_id=?')).all(id) } : null;
|
||||
if (id) {
|
||||
await (await database.prepare(`UPDATE gear SET name=?,slot=?,tier=?,level=?,base_health_min=?,base_health_max=?,base_armor_min=?,base_armor_max=?,active=1,updated_at=? WHERE id=?`))
|
||||
.run(name, slot, tier, level, ...health, ...armor, now, id);
|
||||
await (await database.prepare(`UPDATE gear SET name=?,slot=?,tier=?,base_health_min=?,base_health_max=?,base_armor_min=?,base_armor_max=?,active=1,updated_at=? WHERE id=?`))
|
||||
.run(name, slot, tier, ...health, ...armor, now, id);
|
||||
} else {
|
||||
await (await database.prepare(`INSERT INTO gear(id,name,slot,tier,level,base_health_min,base_health_max,base_armor_min,base_armor_max,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)`))
|
||||
.run(gearId, name, slot, tier, level, ...health, ...armor, now, now);
|
||||
await (await database.prepare(`INSERT INTO gear(id,name,slot,tier,base_health_min,base_health_max,base_armor_min,base_armor_max,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?)`))
|
||||
.run(gearId, name, slot, tier, ...health, ...armor, now, now);
|
||||
}
|
||||
await (await database.prepare('DELETE FROM gear_brand_sets WHERE gear_id=?')).run(gearId);
|
||||
for (const brandId of brandIds) await (await database.prepare('INSERT INTO gear_brand_sets(gear_id,brand_set_id) VALUES(?,?)')).run(gearId, brandId);
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
import KpiFilters from '$lib/components/KpiFilters.svelte';
|
||||
import SearchableAssignment from '$lib/components/SearchableAssignment.svelte';
|
||||
let { data, form } = $props();
|
||||
type Editor = { id: string; name: string; slot: string; tier: number; level: number; base_health_min: number; base_health_max: number; base_armor_min: number | null; base_armor_max: number | null; brandIds: string[]; talentIds: string[] };
|
||||
type Editor = { id: string; name: string; slot: string; tier: number; base_health_min: number; base_health_max: number; base_armor_min: number | null; base_armor_max: number | null; brandIds: string[]; talentIds: string[] };
|
||||
const slots = ['Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask'];
|
||||
const armoredSlots = ['Gloves', 'Knee Pads', 'Body Armor'];
|
||||
const blank = (): Editor => ({ id: '', name: '', slot: 'Holster', tier: 0, level: 1, base_health_min: 0, base_health_max: 0, base_armor_min: null, base_armor_max: null, brandIds: [], talentIds: [] });
|
||||
const blank = (): Editor => ({ id: '', name: '', slot: 'Holster', tier: 0, base_health_min: 0, base_health_max: 0, base_armor_min: null, base_armor_max: null, brandIds: [], talentIds: [] });
|
||||
let editing = $state<Editor | null>(null);
|
||||
let augmentEditing = $state<any | null>(null);
|
||||
let handledResultToken = $state('');
|
||||
const compatibleBrands = (ids: string[]) => ids.map((id) => data.brands.find((brand) => brand.id === id)).filter(Boolean);
|
||||
const stat = (id: string) => data.stats.find((item) => item.id === id);
|
||||
const format = (value: number | string, statId: string) => {
|
||||
const numeric = Number(value);
|
||||
@@ -23,12 +24,24 @@
|
||||
const edit = (item: Editor) => { editing = { ...item, brandIds: [...item.brandIds], talentIds: [...item.talentIds] }; };
|
||||
const gearTalents = (slot: string) => data.talents.filter((talent) => talent.category === slot);
|
||||
const changeSlot = (slot: string) => { if (editing) editing = { ...editing, slot, talentIds: [] }; };
|
||||
const baseTargets = (slot: string) => ['Gloves', 'Knee Pads', 'Body Armor'].includes(slot) ? 'BASE HEALTH + ARMOR' : 'BASE HEALTH';
|
||||
const fixedPercentage = (slot: string, augmentSlot: number) => ['Backpack', 'Body Armor'].includes(slot)
|
||||
? [0, 0, 40, 70, 100][augmentSlot]
|
||||
: [0, 0, 50, 100][augmentSlot];
|
||||
const editAugments = (profile: any) => {
|
||||
augmentEditing = {
|
||||
...profile,
|
||||
augments: Array.from({ length: profile.requiredSlots }, (_, index) => {
|
||||
const augment = profile.augments.find((item: any) => Number(item.augment_slot) === index + 1);
|
||||
return augment ? { statId: augment.stat_attribute_id, value: augment.value, unlockCost: augment.unlock_cost } : { statId: '', value: '', unlockCost: '' };
|
||||
return augment
|
||||
? {
|
||||
effectType: augment.effect_type, statId: augment.stat_attribute_id ?? '', value: augment.value,
|
||||
costs: Object.fromEntries([0,1,2].map((tier) => [tier, augment.costs.find((cost: any) => Number(cost.gear_tier) === tier)?.unlock_cost ?? '']))
|
||||
}
|
||||
: {
|
||||
effectType: index === 0 ? 'Custom' : 'Base Percentage', statId: '',
|
||||
value: index === 0 ? '' : fixedPercentage(profile.slot, index + 1), costs: { 0: '', 1: '', 2: '' }
|
||||
};
|
||||
})
|
||||
};
|
||||
editing = null;
|
||||
@@ -53,7 +66,7 @@
|
||||
{#each data.augmentProfiles as profile}
|
||||
<button type="button" class:configured={profile.augments.length === profile.requiredSlots} onclick={() => editAugments(profile)}>
|
||||
<header><div><strong>{profile.slot}</strong><small>{profile.requiredSlots} AUGMENT SLOTS</small></div><span>{profile.augments.length === profile.requiredSlots ? 'CONFIGURED' : 'REQUIRED'}</span></header>
|
||||
<div>{#each Array(profile.requiredSlots) as _, index}{@const augment = profile.augments.find((item) => Number(item.augment_slot) === index + 1)}<span><small>0{index + 1}</small><b>{augment ? `${stat(augment.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(augment.value, augment.stat_attribute_id)}` : 'NOT SET'}</b><em>{augment ? `${augment.unlock_cost} COST` : '—'}</em></span>{/each}</div>
|
||||
<div>{#each Array(profile.requiredSlots) as _, index}{@const augment = profile.augments.find((item) => Number(item.augment_slot) === index + 1)}<span><small>0{index + 1}</small><b>{index === 0 ? (augment ? `${stat(augment.stat_attribute_id)?.name ?? 'UNKNOWN'} · ${format(augment.value, augment.stat_attribute_id)}` : 'CUSTOM NOT SET') : `+${fixedPercentage(profile.slot,index + 1)}% ${baseTargets(profile.slot)}`}</b><em>{augment ? [0,1,2].map((tier) => `T${tier} ${augment.costs.find((cost: any) => Number(cost.gear_tier) === tier)?.unlock_cost ?? '—'}`).join(' · ') : 'TIER COSTS NOT SET'}</em></span>{/each}</div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -64,14 +77,18 @@
|
||||
<PendingForm action="?/saveAugmentProfile">
|
||||
<input type="hidden" name="slot" value={augmentEditing.slot}/>
|
||||
<input type="hidden" name="name" value={`${augmentEditing.slot} tactical augments`}/>
|
||||
<p class="console-note">These {augmentEditing.requiredSlots} augment slots apply to every {augmentEditing.slot} item. Each slot grants exactly one attribute and unlocks at its configured tactical-augment cost.</p>
|
||||
<p class="console-note">Augment 01 grants a custom attribute. Later slots increase {baseTargets(augmentEditing.slot).toLocaleLowerCase()} on the equipped item itself, before those item values enter agent totals.</p>
|
||||
<div class="gear-augment-slots">
|
||||
{#each augmentEditing.augments as augment: any, index}
|
||||
<section>
|
||||
<header><strong>TACTICAL AUGMENT 0{index + 1}</strong><span>SHARED SLOT</span></header>
|
||||
<div class="field"><span>ATTRIBUTE</span><SearchableAssignment items={data.stats} fieldName={`statId${index + 1}`} mode="single" compact subjectLabel={`AUGMENT ${index + 1} ATTRIBUTE`} searchPlaceholder="Search attributes…" bind:value={augment.statId}/></div>
|
||||
<label class="field">VALUE<input name={`value${index + 1}`} type="number" step="any" bind:value={augment.value} required/></label>
|
||||
<label class="field">UNLOCK COST<input name={`unlockCost${index + 1}`} type="number" min="0" step="1" bind:value={augment.unlockCost} required/></label>
|
||||
<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} 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]} required/></label>{/each}</div></fieldset>
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -90,7 +107,6 @@
|
||||
<label class="field">NAME<input name="name" bind:value={editing.name} required maxlength="80" /></label>
|
||||
<label class="field">SLOT<select name="slot" value={editing.slot} onchange={(event) => changeSlot(event.currentTarget.value)}>{#each slots as slot}<option>{slot}</option>{/each}</select></label>
|
||||
<label class="field">TIER<select name="tier" bind:value={editing.tier}><option value={0}>0</option><option value={1}>1</option><option value={2}>2</option></select></label>
|
||||
<label class="field">LEVEL<input name="level" type="number" min="1" max="50" step="1" bind:value={editing.level} required /></label>
|
||||
</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} required /></label><label class="field">MAXIMUM<input name="healthMax" type="number" min="0" step="any" bind:value={editing.base_health_max} required /></label></div>
|
||||
@@ -111,8 +127,8 @@
|
||||
{/if}
|
||||
<RegistryPagination pagination={data.pagination}/>
|
||||
<div class="dashboard-table">
|
||||
<div class="dashboard-row gear-row table-heading"><span>GEAR</span><span>SLOT</span><span>PROGRESSION</span><span>BASE RANGES</span><span>BRAND SETS</span><span>STATE</span><span></span></div>
|
||||
<div class="dashboard-row gear-row table-heading"><span>GEAR</span><span>SLOT</span><span>TIER</span><span>BASE RANGES</span><span>BRAND SETS</span><span>STATE</span><span></span></div>
|
||||
{#each data.gear as item}
|
||||
<div class="dashboard-row gear-row" class:archived={Number(item.active) !== 1}><div><strong>{item.name}</strong><small>{item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span>{item.slot}</span><span>T{item.tier} · LVL {item.level}</span><span>HP {item.base_health_min}–{item.base_health_max}{#if item.base_armor_min !== null}<small> · AR {item.base_armor_min}–{item.base_armor_max}</small>{/if}</span><span title={brandNames(item.brandIds)}>{item.brandIds.length} COMPATIBLE</span><b class="state-dot">{Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(item)}>EDIT</button></div>
|
||||
<div class="dashboard-row gear-row" class:archived={Number(item.active) !== 1}><div><strong>{item.name}</strong><small>{item.talentIds.length ? `${item.talentIds.length} TALENT${item.talentIds.length === 1 ? '' : 'S'}` : item.id.slice(0, 8).toUpperCase()}</small></div><span>{item.slot}</span><span>T{item.tier}</span><span>HP {item.base_health_min}–{item.base_health_max}{#if item.base_armor_min !== null}<small> · AR {item.base_armor_min}–{item.base_armor_max}</small>{/if}</span><div class="registry-icon-rail" aria-label={`Compatible Brand Sets: ${brandNames(item.brandIds)}`}>{#each compatibleBrands(item.brandIds) as brand}<span title={brand.name}>{#if brand.iconUrl}<img src={brand.iconUrl} alt={brand.name}/>{:else}<i aria-hidden="true">{brand.name.slice(0,2).toUpperCase()}</i><span class="sr-only">{brand.name}</span>{/if}</span>{/each}</div><b class="state-dot">{Number(item.active) === 1 ? 'ACTIVE' : 'ARCHIVED'}</b><button onclick={() => edit(item)}>EDIT</button></div>
|
||||
{:else}<div class="empty-dashboard"><strong>NO GEAR REGISTERED</strong><span>Add the first gear definition after configuring at least two brand sets.</span></div>{/each}
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { PageServerLoad } from './$types';
|
||||
export const load: PageServerLoad = async () => {
|
||||
const database = await getDatabase();
|
||||
const [
|
||||
attributes, gear, gearBrands, gearTalents, gearAugments, brands, brandEffects,
|
||||
attributes, gear, gearBrands, gearTalents, gearAugments, gearAugmentCosts, brands, brandEffects,
|
||||
weapons, weaponRanges, weaponTalentOptions, weaponBonuses,
|
||||
talents, talentTiers, talentEffects, talentTemplates,
|
||||
chipSets, chipTwoPiece, chipPrimary, chipSecondary, chipTalents, chipTalentEffects,
|
||||
@@ -17,6 +17,7 @@ export const load: PageServerLoad = async () => {
|
||||
(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 gear_tactical_augment_costs ORDER BY gear_tier')).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(),
|
||||
@@ -61,7 +62,10 @@ export const load: PageServerLoad = async () => {
|
||||
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,
|
||||
gearAugments: gearAugments.map((augment) => ({
|
||||
...augment,
|
||||
costs: gearAugmentCosts.filter((cost) => cost.augment_id === augment.id)
|
||||
})),
|
||||
brands: brands.map((brand) => ({
|
||||
...brand,
|
||||
iconUrl: iconUrl('brand_set', brand.id),
|
||||
|
||||
@@ -57,6 +57,7 @@
|
||||
const configureGear = (slot: string, id: string) => {
|
||||
const selection = draft.gear[slot], item = data.gear.find((entry) => entry.id === id);
|
||||
selection.itemId = id;
|
||||
selection.level = 50;
|
||||
selection.brandId = item?.brandIds[0] ?? '';
|
||||
selection.talentId = item?.talentIds[0] ?? '';
|
||||
selection.health = Number(item?.base_health_min ?? 0);
|
||||
@@ -110,11 +111,13 @@
|
||||
for (const slot of gearSlots) {
|
||||
const selection = draft.gear[slot], item = gearItem(slot);
|
||||
if (!item) continue;
|
||||
add(byName('Health'), selection.health, `${slot} base`);
|
||||
add(byName('Armor'), selection.armor, `${slot} base`);
|
||||
const enabledAugments = data.gearAugments.filter((entry) => entry.gear_slot === slot && selection.augmentSlots.includes(Number(entry.augment_slot)));
|
||||
const basePercentage = enabledAugments.filter((entry) => entry.effect_type === 'Base Percentage').reduce((total, entry) => total + Number(entry.value), 0);
|
||||
add(byName('Health'), selection.health * (1 + basePercentage / 100), `${slot} base`);
|
||||
if (item.base_armor_min !== null) add(byName('Armor'), selection.armor * (1 + basePercentage / 100), `${slot} base`);
|
||||
for (const roll of selection.rolls) add(roll.statId, roll.value, `${slot} roll`);
|
||||
if (selection.brandId) brandCounts.set(selection.brandId, (brandCounts.get(selection.brandId) ?? 0) + 1);
|
||||
for (const augment of data.gearAugments.filter((entry) => entry.gear_slot === slot && selection.augmentSlots.includes(Number(entry.augment_slot)))) {
|
||||
for (const augment of enabledAugments.filter((entry) => entry.effect_type === 'Custom')) {
|
||||
add(augment.stat_attribute_id, augment.value, `${slot} augment ${augment.augment_slot}`);
|
||||
}
|
||||
if (selection.talentId) equippedTalents.push({ key: `gear:${slot}:${selection.talentId}`, id: selection.talentId, tier: Number(item.tier), source: slot, description: talentDescription(selection.talentId, Number(item.tier)) });
|
||||
@@ -195,6 +198,7 @@
|
||||
const parsed = JSON.parse(stored);
|
||||
if (parsed?.version === 1) {
|
||||
draft = parsed;
|
||||
for (const slot of gearSlots) if (!Number.isInteger(draft.gear[slot].level)) draft.gear[slot].level = 50;
|
||||
if (weaponItem(0) && weaponItem(1) && Number(weaponItem(0).is_exotic) && Number(weaponItem(1).is_exotic)) draft.weapons[1].itemId = '';
|
||||
saved = true;
|
||||
}
|
||||
@@ -223,15 +227,16 @@
|
||||
<section class="loadout-editor">
|
||||
{#if activeParts[0] === 'gear'}
|
||||
{@const slot = activeParts[1]}{@const selection = draft.gear[slot]}{@const item = gearItem(slot)}
|
||||
<div class="loadout-editor-head"><div><span>GEAR SLOT</span><h2>{slot.toUpperCase()}</h2></div><b>{item ? `TIER ${item.tier} · LEVEL ${item.level}` : 'UNASSIGNED'}</b></div>
|
||||
<div class="loadout-editor-head"><div><span>GEAR SLOT</span><h2>{slot.toUpperCase()}</h2></div><b>{item ? `TIER ${item.tier} · BUILD LEVEL ${selection.level}` : 'UNASSIGNED'}</b></div>
|
||||
<BuilderPicker items={data.gear.filter((entry) => entry.slot === slot)} bind:value={selection.itemId} onchange={(id) => configureGear(slot,id)} label="ITEM DEFINITION"/>
|
||||
{#if item}
|
||||
<div class="builder-field-grid"><label>BRAND SET<select value={selection.brandId} onchange={(event) => selection.brandId = event.currentTarget.value}>{#each item.brandIds as id}<option value={id}>{brand(id)?.name}</option>{/each}</select></label>{#if item.talentIds.length}<label>EQUIPPED TALENT<select bind:value={selection.talentId}>{#each item.talentIds as id}<option value={id}>{talent(id)?.name}</option>{/each}</select></label>{/if}</div>
|
||||
<div class="builder-field-grid"><label>GEAR LEVEL <span class="builder-field-range">01–50</span><input type="number" min="1" max="50" bind:value={selection.level} onblur={() => selection.level = clamp(selection.level,1,50)}/></label><label>BRAND SET<select value={selection.brandId} onchange={(event) => selection.brandId = event.currentTarget.value}>{#each item.brandIds as id}<option value={id}>{brand(id)?.name}</option>{/each}</select></label>{#if item.talentIds.length}<label>EQUIPPED TALENT<select bind:value={selection.talentId}>{#each item.talentIds as id}<option value={id}>{talent(id)?.name}</option>{/each}</select></label>{/if}</div>
|
||||
<p class="builder-scaling-note">Gear level belongs to this build instance. Base-range level scaling will remain neutral until the authoritative game scaling curve is configured.</p>
|
||||
<div class="builder-range-grid"><label>BASE HEALTH <span>{item.base_health_min}–{item.base_health_max}</span><input type="number" min={item.base_health_min} max={item.base_health_max} bind:value={selection.health} onblur={() => selection.health = clamp(selection.health,item.base_health_min,item.base_health_max)}/></label>{#if item.base_armor_min !== null}<label>BASE ARMOR <span>{item.base_armor_min}–{item.base_armor_max}</span><input type="number" min={item.base_armor_min} max={item.base_armor_max} bind:value={selection.armor} onblur={() => selection.armor = clamp(selection.armor,item.base_armor_min,item.base_armor_max)}/></label>{/if}</div>
|
||||
<div class="builder-section-title"><div><span>ROLLED ATTRIBUTES</span><small>VALUES ARE LIMITED TO GEAR RANGES</small></div><button onclick={() => selection.rolls = [...selection.rolls, blankRoll()]} disabled={selection.rolls.length >= 6}>ADD ATTRIBUTE +</button></div>
|
||||
<div class="builder-rolls">{#each selection.rolls as roll, index}<div><BuilderPicker items={gearAttributes} bind:value={roll.statId} onchange={(id) => setRollStat(roll,id,'gear')} label={`ATTRIBUTE ${index + 1}`}/><label>VALUE<input type="number" value={roll.value} min={attribute(roll.statId)?.gear_min ?? 0} max={attribute(roll.statId)?.gear_max ?? 0} onchange={(event) => roll.value = clamp(event.currentTarget.value,attribute(roll.statId)?.gear_min,attribute(roll.statId)?.gear_max)}/></label><button aria-label={`Remove attribute ${index + 1}`} onclick={() => selection.rolls = selection.rolls.filter((_, itemIndex) => itemIndex !== index)}>×</button></div>{/each}</div>
|
||||
{@const augments = data.gearAugments.filter((entry) => entry.gear_slot === slot)}
|
||||
{#if augments.length}<div class="builder-section-title"><div><span>TACTICAL AUGMENTS</span><small>ENABLE COLLECTED UPGRADES</small></div><b>{selection.augmentSlots.length} / {augments.length} ACTIVE</b></div><div class="builder-augment-list">{#each augments as augment}<label><input type="checkbox" checked={selection.augmentSlots.includes(Number(augment.augment_slot))} onchange={() => selection.augmentSlots = selection.augmentSlots.includes(Number(augment.augment_slot)) ? selection.augmentSlots.filter((value) => value !== Number(augment.augment_slot)) : [...selection.augmentSlots,Number(augment.augment_slot)]}/><span><b>AUGMENT 0{augment.augment_slot}</b><small>{attribute(augment.stat_attribute_id)?.name} · {unitValue(augment.value,augment.stat_attribute_id)}</small></span><em>{augment.unlock_cost} COST</em></label>{/each}</div>{/if}
|
||||
{#if augments.length}<div class="builder-section-title"><div><span>TACTICAL AUGMENTS</span><small>ENABLE COLLECTED UPGRADES · BASE BONUSES APPLY TO THIS ITEM ONLY</small></div><b>{selection.augmentSlots.length} / {augments.length} ACTIVE</b></div><div class="builder-augment-list">{#each augments as augment}<label><input type="checkbox" checked={selection.augmentSlots.includes(Number(augment.augment_slot))} onchange={() => selection.augmentSlots = selection.augmentSlots.includes(Number(augment.augment_slot)) ? selection.augmentSlots.filter((value) => value !== Number(augment.augment_slot)) : [...selection.augmentSlots,Number(augment.augment_slot)]}/><span><b>AUGMENT 0{augment.augment_slot}</b><small>{augment.effect_type === 'Custom' ? `${attribute(augment.stat_attribute_id)?.name} · ${unitValue(augment.value,augment.stat_attribute_id)}` : `+${augment.value}% BASE HEALTH${item.base_armor_min !== null ? ' + ARMOR' : ''}`}</small></span><em>{augment.costs.find((cost: any) => Number(cost.gear_tier) === Number(item.tier))?.unlock_cost ?? '—'} COST · T{item.tier}</em></label>{/each}</div>{/if}
|
||||
{/if}
|
||||
{:else if activeParts[0] === 'weapon'}
|
||||
{@const index = Number(activeParts[1])}{@const selection = draft.weapons[index]}{@const item = weaponItem(index)}{@const other = weaponItem(index ? 0 : 1)}
|
||||
|
||||
Reference in New Issue
Block a user