Add ISAC weapon data management

This commit is contained in:
JB
2026-07-25 00:49:08 -04:00
parent 3f78f0f9d8
commit e528f18d8f
12 changed files with 555 additions and 13 deletions
+208
View File
@@ -0,0 +1,208 @@
import { requirePermission } from '$lib/server/auth/authorization';
import { getSessionUser } from '$lib/server/auth/session';
import { getDatabase } from '$lib/server/database';
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import type { Actions, PageServerLoad } from './$types';
const categories = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG'];
const damageTypes = ['Blast', 'Pierce', 'Shredding'];
const rangeNames = ['damage', 'firepower', 'toughness', 'engineering'] as const;
export const load: PageServerLoad = async ({ cookies }) => {
await requirePermission(await getSessionUser(cookies), 'data.manage');
const database = await getDatabase();
const [weapons, tierRanges, talentOptions, augmentEffects, baseBonuses, talent1Options, talent2Options, exoticTalents, stats] = await Promise.all([
(await database.prepare('SELECT * FROM weapons ORDER BY active DESC, category, name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM weapon_tier_ranges ORDER BY tier')).all(),
(await database.prepare('SELECT weapon_id,talent_slot,talent_id FROM weapon_talent_options')).all(),
(await database.prepare('SELECT weapon_id,stat_attribute_id,value,sort_order FROM weapon_augmentation_effects ORDER BY sort_order')).all(),
(await database.prepare('SELECT weapon_id,stat_attribute_id,value,sort_order FROM weapon_base_attribute_bonuses ORDER BY sort_order')).all(),
(await database.prepare(`SELECT talents.id,talents.name FROM talents
JOIN talent_weapon_scopes ON talent_weapon_scopes.talent_id=talents.id
JOIN weapon_talent_slots ON weapon_talent_slots.talent_id=talents.id
WHERE talents.active=1 AND talents.category='Weapon' AND talent_weapon_scopes.scope='Standard' AND weapon_talent_slots.slot=1
ORDER BY talents.name COLLATE NOCASE`)).all(),
(await database.prepare(`SELECT talents.id,talents.name FROM talents
JOIN talent_weapon_scopes ON talent_weapon_scopes.talent_id=talents.id
JOIN weapon_talent_slots ON weapon_talent_slots.talent_id=talents.id
WHERE talents.active=1 AND talents.category='Weapon' AND talent_weapon_scopes.scope='Standard' AND weapon_talent_slots.slot=2
ORDER BY talents.name COLLATE NOCASE`)).all(),
(await database.prepare(`SELECT talents.id,talents.name FROM talents
JOIN talent_weapon_scopes ON talent_weapon_scopes.talent_id=talents.id
WHERE talents.active=1 AND talents.category='Weapon' AND talent_weapon_scopes.scope='Exotic'
ORDER BY talents.name COLLATE NOCASE`)).all(),
(await database.prepare("SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE")).all()
]);
return {
weapons: weapons.map((weapon) => ({
...weapon,
tiers: tierRanges.filter((range) => range.weapon_id === weapon.id),
talent1Ids: talentOptions.filter((option) => option.weapon_id === weapon.id && Number(option.talent_slot) === 1).map((option) => String(option.talent_id)),
talent2Ids: talentOptions.filter((option) => option.weapon_id === weapon.id && Number(option.talent_slot) === 2).map((option) => String(option.talent_id)),
augmentEffects: augmentEffects.filter((effect) => effect.weapon_id === weapon.id),
baseBonuses: baseBonuses.filter((effect) => effect.weapon_id === weapon.id)
})),
talent1Options,
talent2Options,
exoticTalents,
stats
};
};
function number(form: FormData, name: string): number {
return Number(form.get(name));
}
function readRange(form: FormData, tier: number, range: (typeof rangeNames)[number]) {
const min = number(form, `tier${tier}${range}Min`);
const max = number(form, `tier${tier}${range}Max`);
if (!Number.isFinite(min) || !Number.isFinite(max) || min < 0 || max < min) throw new Error();
return { min, max };
}
export const actions: Actions = {
save: async ({ request, cookies }) => {
await requirePermission(await getSessionUser(cookies), 'data.manage');
const form = await request.formData();
const id = String(form.get('id') ?? '');
const name = String(form.get('name') ?? '').normalize('NFKC').trim();
const category = String(form.get('category') ?? '');
const damageType = String(form.get('damageType') ?? '');
const isExotic = form.get('isExotic') === '1';
const nativeTier = number(form, 'nativeTier');
const fixed = {
rpm: number(form, 'rpm'),
magazine: number(form, 'magazine'),
reloadSpeed: number(form, 'reloadSpeed'),
accuracy: number(form, 'accuracy'),
stability: number(form, 'stability')
};
const talent1Ids = [...new Set(form.getAll('talent1Ids').map(String))];
const talent2Ids = [...new Set(form.getAll('talent2Ids').map(String))];
const exoticTalentId = isExotic ? String(form.get('exoticTalentId') ?? '') : null;
if (name.length < 2 || name.length > 80) return fail(400, { weaponError: 'Weapon names must be 2–80 characters.' });
if (!categories.includes(category) || !damageTypes.includes(damageType) || !Number.isInteger(nativeTier) || nativeTier < 0 || nativeTier > 2) {
return fail(400, { weaponError: 'Choose a valid category, native tier, and damage type.' });
}
if (Object.values(fixed).some((value) => !Number.isFinite(value) || value < 0)) {
return fail(400, { weaponError: 'Fixed weapon attributes require non-negative numeric values.' });
}
if (!Number.isInteger(fixed.magazine) || !Number.isInteger(fixed.rpm)) {
return fail(400, { weaponError: 'RPM and magazine values must be whole numbers.' });
}
if (!talent1Ids.length || !talent2Ids.length) return fail(400, { weaponError: 'Talent 1 and Talent 2 each require at least one available weapon talent.' });
if (talent1Ids.some((talentId) => talent2Ids.includes(talentId))) {
return fail(400, { weaponError: 'Talent 1 and Talent 2 availability lists must be unique.' });
}
if (isExotic && !exoticTalentId) {
return fail(400, { weaponError: 'Exotic weapons require a dedicated exotic talent.' });
}
const tiers = [];
try {
for (let tier = 0; tier <= 2; tier++) {
tiers.push({
tier,
damage: readRange(form, tier, 'damage'),
firepower: readRange(form, tier, 'firepower'),
toughness: readRange(form, tier, 'toughness'),
engineering: readRange(form, tier, 'engineering')
});
}
} catch {
return fail(400, { weaponError: 'Every tier range requires a non-negative minimum and a maximum greater than or equal to it.' });
}
const augmentEffects = [];
for (let order = 1; order <= 2; order++) {
const statId = String(form.get(`augmentStat${order}`) ?? '');
const rawValue = String(form.get(`augmentValue${order}`) ?? '').trim();
if (!statId && !rawValue && order === 2) continue;
const value = Number(rawValue);
if (!statId || !Number.isFinite(value) || value <= 0) return fail(400, { weaponError: `Tactical augmentation effect ${order} requires an attribute and a positive value.` });
augmentEffects.push({ statId, value, order });
}
if (!augmentEffects.length || new Set(augmentEffects.map((effect) => effect.statId)).size !== augmentEffects.length) {
return fail(400, { weaponError: 'The first tactical augmentation requires one or two unique attribute effects.' });
}
const baseBonusCount = number(form, 'baseBonusCount');
if (!Number.isInteger(baseBonusCount) || baseBonusCount < 1 || baseBonusCount > 12) {
return fail(400, { weaponError: 'Weapons require between one and twelve base attribute bonuses.' });
}
const baseBonuses = [];
for (let order = 1; order <= baseBonusCount; order++) {
const statId = String(form.get(`baseBonusStat${order}`) ?? '');
const value = number(form, `baseBonusValue${order}`);
if (!statId || !Number.isFinite(value) || value <= 0) return fail(400, { weaponError: `Base attribute bonus ${order} requires an attribute and a positive value.` });
baseBonuses.push({ statId, value, order });
}
if (new Set(baseBonuses.map((effect) => effect.statId)).size !== baseBonuses.length) {
return fail(400, { weaponError: 'Each base attribute bonus must reference a different attribute.' });
}
const database = await getDatabase();
if (await (await database.prepare('SELECT id FROM weapons WHERE name=? COLLATE NOCASE AND id<>?')).get(name, id || 'new')) {
return fail(409, { weaponError: 'That weapon name already exists.' });
}
for (const [slot, ids] of [[1, talent1Ids], [2, talent2Ids]] as const) {
const validTalents = await (await database.prepare(`SELECT COUNT(*) AS count FROM talents
JOIN talent_weapon_scopes ON talent_weapon_scopes.talent_id=talents.id
JOIN weapon_talent_slots ON weapon_talent_slots.talent_id=talents.id
WHERE talents.active=1 AND talents.category='Weapon' AND talent_weapon_scopes.scope='Standard'
AND weapon_talent_slots.slot=? AND talents.id IN (${ids.map(() => '?').join(',')})`)).get(slot, ...ids);
if (Number(validTalents?.count) !== ids.length) return fail(400, { weaponError: `One or more selected Talent ${slot} options are unavailable or assigned to the wrong slot.` });
}
if (isExotic) {
const validExoticTalent = await (await database.prepare(`SELECT talents.id FROM talents
JOIN talent_weapon_scopes ON talent_weapon_scopes.talent_id=talents.id
WHERE talents.id=? AND talents.active=1 AND talents.category='Weapon' AND talent_weapon_scopes.scope='Exotic'`)).get(exoticTalentId);
if (!validExoticTalent) return fail(400, { weaponError: 'The selected exotic talent is unavailable.' });
}
const statIds = [...new Set([...augmentEffects.map((effect) => effect.statId), ...baseBonuses.map((effect) => effect.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, { weaponError: 'One or more tactical augmentation attributes are unavailable.' });
const weaponId = id || randomUUID();
const now = new Date().toISOString();
if (id) {
const exists = await (await database.prepare('SELECT id FROM weapons WHERE id=?')).get(id);
if (!exists) return fail(404, { weaponError: 'Weapon not found.' });
await (await database.prepare(`UPDATE weapons SET name=?,category=?,is_exotic=?,native_tier=?,damage_type=?,rpm=?,magazine=?,
reload_speed=?,accuracy=?,stability=?,exotic_talent_id=?,active=1,updated_at=? WHERE id=?`))
.run(name, category, isExotic ? 1 : 0, nativeTier, damageType, fixed.rpm, fixed.magazine, fixed.reloadSpeed, fixed.accuracy, fixed.stability, exoticTalentId, now, id);
} else {
await (await database.prepare(`INSERT INTO weapons(id,name,category,is_exotic,native_tier,damage_type,rpm,magazine,reload_speed,
accuracy,stability,exotic_talent_id,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)`))
.run(weaponId, name, category, isExotic ? 1 : 0, nativeTier, damageType, fixed.rpm, fixed.magazine, fixed.reloadSpeed, fixed.accuracy, fixed.stability, exoticTalentId, now, now);
}
await (await database.prepare('DELETE FROM weapon_tier_ranges WHERE weapon_id=?')).run(weaponId);
for (const tier of tiers) {
await (await database.prepare(`INSERT INTO weapon_tier_ranges(weapon_id,tier,damage_min,damage_max,firepower_min,
firepower_max,toughness_min,toughness_max,engineering_min,engineering_max) VALUES(?,?,?,?,?,?,?,?,?,?)`))
.run(weaponId, tier.tier, tier.damage.min, tier.damage.max, tier.firepower.min, tier.firepower.max, tier.toughness.min, tier.toughness.max, tier.engineering.min, tier.engineering.max);
}
await (await database.prepare('DELETE FROM weapon_talent_options WHERE weapon_id=?')).run(weaponId);
for (const talentId of talent1Ids) await (await database.prepare('INSERT INTO weapon_talent_options(weapon_id,talent_slot,talent_id) VALUES(?,?,?)')).run(weaponId, 1, talentId);
for (const talentId of talent2Ids) await (await database.prepare('INSERT INTO weapon_talent_options(weapon_id,talent_slot,talent_id) VALUES(?,?,?)')).run(weaponId, 2, talentId);
await (await database.prepare('DELETE FROM weapon_augmentation_effects WHERE weapon_id=?')).run(weaponId);
for (const effect of augmentEffects) {
await (await database.prepare('INSERT INTO weapon_augmentation_effects(id,weapon_id,stat_attribute_id,value,sort_order) VALUES(?,?,?,?,?)'))
.run(randomUUID(), weaponId, effect.statId, effect.value, effect.order);
}
await (await database.prepare('DELETE FROM weapon_base_attribute_bonuses WHERE weapon_id=?')).run(weaponId);
for (const effect of baseBonuses) {
await (await database.prepare('INSERT INTO weapon_base_attribute_bonuses(id,weapon_id,stat_attribute_id,value,sort_order) VALUES(?,?,?,?,?)'))
.run(randomUUID(), weaponId, effect.statId, effect.value, effect.order);
}
return { weaponSaved: true, weaponMode: id ? 'edit' : 'add', resultToken: randomUUID() };
},
archive: async ({ request, cookies }) => {
await requirePermission(await getSessionUser(cookies), 'data.manage');
const id = String((await request.formData()).get('id') ?? '');
await (await (await getDatabase()).prepare('UPDATE weapons SET active=0,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
return { weaponArchived: true };
}
};