feat: expand ISAC content modeling
Validate and deploy production / validate (push) Canceled after 0s
Validate and deploy production / deploy (push) Canceled after 0s
Validate and deploy staging / validate (push) Successful in 28s
Validate and deploy staging / deploy (push) Successful in 1m3s

This commit is contained in:
jb
2026-08-03 01:23:20 -04:00
parent d9924a95b9
commit 57068b61cd
90 changed files with 9635 additions and 1270 deletions
+90 -126
View File
@@ -21,8 +21,16 @@ import {
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
import {
MOD_QUALITIES,
normalizeModSecondaryPool,
} from '$lib/mods/secondary-pool';
import {
getModSecondaryPool,
replaceModSecondaryPool,
} from '$lib/server/mod-secondary-pools';
const qualities = ['Standard', 'Specialized', 'Superior', 'High-end'] as const;
const qualities = MOD_QUALITIES;
type QualityValue = {
quality: string;
primaryLevel1Min: number;
@@ -30,11 +38,6 @@ type QualityValue = {
primaryLevel20Min: number;
primaryLevel20Max: number;
};
type SecondaryRange = {
statId: string;
values: Record<string, { min: number; max: number }>;
};
function parseJson<T>(value: FormDataEntryValue | null, fallback: T): T {
try {
return JSON.parse(String(value ?? '')) as T;
@@ -52,16 +55,6 @@ async function snapshot(id: string) {
'SELECT * FROM gear_mod_quality_values WHERE gear_mod_id=? ORDER BY quality',
)
).all(id),
secondaryOptions: await (
await database.prepare(
'SELECT stat_attribute_id FROM gear_mod_secondary_options WHERE gear_mod_id=? ORDER BY stat_attribute_id',
)
).all(id),
secondaryQualityValues: await (
await database.prepare(
'SELECT * FROM gear_mod_secondary_quality_values WHERE gear_mod_id=? ORDER BY stat_attribute_id,quality',
)
).all(id),
render:
(await (
await database.prepare(
@@ -87,10 +80,6 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
asc: 'name COLLATE NOCASE ASC,id ASC',
desc: 'name COLLATE NOCASE DESC,id DESC',
},
secondaries: {
asc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) ASC,name COLLATE NOCASE ASC',
desc: '(SELECT COUNT(*) FROM gear_mod_secondary_options option WHERE option.gear_mod_id=gear_mods.id) DESC,name COLLATE NOCASE ASC',
},
state: {
asc: 'active ASC,name COLLATE NOCASE ASC',
desc: 'active DESC,name COLLATE NOCASE ASC',
@@ -140,7 +129,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
module: 'gearmods',
action: 'save',
kind: 'gearmod',
fields: ['qualityValues', 'secondaryRanges'],
fields: ['qualityValues'],
},
]),
]);
@@ -161,38 +150,29 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
pagination.page === 1 ? pendingRows.slice(0, pagination.perPage) : [];
const databaseOffset = Math.max(0, pagination.offset - pendingRows.length);
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
const [mods, qualityRows, secondaryOptions, secondaryValues, stats, renders] =
await Promise.all([
databaseLimit
? (
await database.prepare(`SELECT * FROM gear_mods ${where}
const [mods, qualityRows, stats, renders, secondaryPool] = await Promise.all([
databaseLimit
? (
await database.prepare(`SELECT * FROM gear_mods ${where}
ORDER BY ${sort.orderBy} LIMIT ? OFFSET ?`)
).all(...args, databaseLimit, databaseOffset)
: [],
(await database.prepare('SELECT * FROM gear_mod_quality_values')).all(),
(
await database.prepare('SELECT * FROM gear_mod_secondary_options')
).all(),
(
await database.prepare(
'SELECT * FROM gear_mod_secondary_quality_values',
)
).all(),
(
await database.prepare(
'SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE',
)
).all(),
(
await database.prepare(
"SELECT source_entity_id,id,updated_at FROM managed_images WHERE source_entity_type='gear_mod_render'",
)
).all(),
]);
).all(...args, databaseLimit, databaseOffset)
: [],
(await database.prepare('SELECT * FROM gear_mod_quality_values')).all(),
(
await database.prepare(
'SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE',
)
).all(),
(
await database.prepare(
"SELECT source_entity_id,id,updated_at FROM managed_images WHERE source_entity_type='gear_mod_render'",
)
).all(),
getModSecondaryPool(database, 'gear'),
]);
const parsedPending = pendingOnPage.map((item) => ({
...item,
qualityValues: parseJson(String(item.qualityValues), []),
secondaryRanges: parseJson(String(item.secondaryRanges), []),
}));
return {
mods: [
@@ -208,23 +188,6 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
primaryLevel20Min: Number(row.primary_level_20_min),
primaryLevel20Max: Number(row.primary_level_20_max),
})),
secondaryRanges: secondaryOptions
.filter((option) => option.gear_mod_id === mod.id)
.map((option) => ({
statId: String(option.stat_attribute_id),
values: Object.fromEntries(
secondaryValues
.filter(
(row) =>
row.gear_mod_id === mod.id &&
row.stat_attribute_id === option.stat_attribute_id,
)
.map((row) => [
String(row.quality),
{ min: Number(row.min_value), max: Number(row.max_value) },
]),
),
})),
renderUrl: (() => {
const render = renders.find(
(item) => item.source_entity_id === mod.id,
@@ -235,6 +198,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
})(),
})),
],
secondaryPool,
stats: [...pendingStats, ...stats],
qualities,
summary: {
@@ -251,6 +215,60 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
};
export const actions: Actions = {
saveSecondaryPool: async ({ request, cookies }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
'data.manage',
);
const form = await request.formData();
if (String(form.get('id') ?? '') !== 'gear-mod-secondary-pool') {
return fail(400, {
gearModSecondaryPoolError:
'The shared Gear Mod pool target is invalid.',
});
}
const normalized = normalizeModSecondaryPool(
parseJson<unknown>(form.get('secondaryRanges'), null),
);
if (normalized.error) {
return fail(400, { gearModSecondaryPoolError: normalized.error });
}
const database = await getDatabase();
const statIds = normalized.pool.map((range) => range.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, {
gearModSecondaryPoolError:
'One or more selected Attributes are unavailable.',
});
}
await database.exec('BEGIN IMMEDIATE');
try {
const before = await getModSecondaryPool(database, 'gear');
await replaceModSecondaryPool(database, 'gear', normalized.pool);
await recordIsacAudit({
actorId: operator.id,
category: 'game_data',
action: 'gear_mod_secondary_pool.updated',
entityId: 'gear-mod-secondary-pool',
before,
after: await getModSecondaryPool(database, 'gear'),
database,
});
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');
throw error;
}
return {
gearModSecondaryPoolSaved: true,
gearModSecondaryPoolMode: 'edit',
resultToken: randomUUID(),
};
},
save: async ({ request, cookies }) => {
const operator = await requirePermission(
await getSessionUser(cookies),
@@ -275,14 +293,16 @@ export const actions: Actions = {
const name = String(form.get('name') ?? '')
.normalize('NFKC')
.trim();
if (form.has('secondaryRanges')) {
return fail(400, {
gearModError:
'Secondary Attributes must be submitted through the shared pool editor.',
});
}
const qualityValues = parseJson<QualityValue[]>(
form.get('qualityValues'),
[],
);
const secondaryRanges = parseJson<SecondaryRange[]>(
form.get('secondaryRanges'),
[],
);
if (name.length < 2 || name.length > 100)
return fail(400, {
gearModError: 'Gear Mod names must be 2–100 characters.',
@@ -311,30 +331,6 @@ export const actions: Actions = {
'Configure valid primary Level 1 and Level 20 ranges for all four qualities.',
});
}
if (
secondaryRanges.length < 2 ||
secondaryRanges.length > 50 ||
new Set(secondaryRanges.map((range) => range.statId)).size !==
secondaryRanges.length ||
secondaryRanges.some(
(range) =>
!range.statId ||
qualities.some((quality) => {
const values = range.values?.[quality];
return (
!values ||
!Number.isFinite(Number(values.min)) ||
!Number.isFinite(Number(values.max)) ||
Number(values.max) < Number(values.min)
);
}),
)
) {
return fail(400, {
gearModError:
'Configure at least two unique possible secondary attributes with valid ranges for every quality.',
});
}
const database = await getDatabase();
if (
await (
@@ -347,15 +343,6 @@ export const actions: Actions = {
gearModError: 'That Gear Mod name is already registered.',
});
}
const statIds = secondaryRanges.map((range) => range.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, {
gearModError: 'One or more selected attributes are unavailable.',
});
const modId =
id || String(form.get('_proposedEntityId') ?? '') || randomUUID();
const before = id ? await snapshot(id) : null;
@@ -394,29 +381,6 @@ export const actions: Actions = {
row.primaryLevel20Min,
row.primaryLevel20Max,
);
await (
await database.prepare(
'DELETE FROM gear_mod_secondary_options WHERE gear_mod_id=?',
)
).run(modId);
for (const range of secondaryRanges) {
await (
await database.prepare(
'INSERT INTO gear_mod_secondary_options(gear_mod_id,stat_attribute_id) VALUES(?,?)',
)
).run(modId, range.statId);
for (const quality of qualities)
await (
await database.prepare(`INSERT INTO gear_mod_secondary_quality_values
(gear_mod_id,stat_attribute_id,quality,min_value,max_value) VALUES(?,?,?,?,?)`)
).run(
modId,
range.statId,
quality,
range.values[quality].min,
range.values[quality].max,
);
}
await database.exec('COMMIT');
} catch (error) {
await database.exec('ROLLBACK');