import { requireDataView, 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'; import { getPagination } from '$lib/server/pagination'; import { auditRecord, recordIsacAudit } from '$lib/server/audit'; import { applyIconMutation, gameDataIconMetadata, gameDataIconUrl, GameDataIconValidationError, readIconMutation } from '$lib/server/game-data-icons'; const categories = ['Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG']; const damageTypes = ['Blast', 'Pierce', 'Shredding']; const rangeNames = ['damage', 'firepower', 'toughness', 'engineering'] as const; type ExoticEffect = { statId: string; valueI: number; valueII: number }; const renderTalentDescription = (template: string, effects: ExoticEffect[], units: Map, tier: 'I' | 'II') => template.replace(/\{(\d+)\}/g, (placeholder, rawIndex) => { const effect = effects[Number(rawIndex) - 1]; if (!effect) return placeholder; const value = tier === 'I' ? effect.valueI : effect.valueII; const unit = units.get(effect.statId) === 'flat' ? '' : String(units.get(effect.statId) ?? ''); return unit === 'Shots' ? `${value} ${unit}` : `${value}${unit}`; }); export const load: PageServerLoad = async ({ cookies, url }) => { await requireDataView(await getSessionUser(cookies)); const database = await getDatabase(); const summary = await (await database.prepare(`SELECT COUNT(*) AS total, SUM(CASE WHEN active=1 THEN 1 ELSE 0 END) AS active, SUM(CASE WHEN is_exotic=0 THEN 1 ELSE 0 END) AS standard, SUM(CASE WHEN is_exotic=1 THEN 1 ELSE 0 END) AS exotic FROM weapons`)).get(); const filter = ['active', 'standard', 'exotic'].includes(url.searchParams.get('filter') ?? '') ? String(url.searchParams.get('filter')) : ''; const requestedWeaponId = url.searchParams.get('weapon')?.trim() ?? ''; const where = requestedWeaponId ? 'WHERE id=?' : filter === 'active' ? 'WHERE active=1' : filter === 'standard' ? 'WHERE is_exotic=0' : filter === 'exotic' ? 'WHERE is_exotic=1' : ''; const whereArgs = requestedWeaponId ? [requestedWeaponId] : []; const filtered = await (await database.prepare(`SELECT COUNT(*) AS total FROM weapons ${where}`)).get(...whereArgs); const pagination = getPagination(url, Number(filtered?.total ?? 0)); const [weapons, tierRanges, talentOptions, augmentEffects, baseBonuses, talent1Options, talent2Options, stats, triggers, exoticTemplates, exoticTiers, exoticTierTriggers, exoticTierEffects, talentIcons] = await Promise.all([ (await database.prepare(`SELECT * FROM weapons ${where} ORDER BY active DESC, category, name COLLATE NOCASE LIMIT ? OFFSET ?`)).all(...whereArgs, pagination.perPage, pagination.offset), (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 id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE")).all(), (await database.prepare("SELECT id,name,description FROM talent_triggers WHERE active=1 ORDER BY name COLLATE NOCASE")).all(), (await database.prepare(`SELECT talent_description_templates.talent_id,talent_description_templates.template,talents.name FROM talent_description_templates JOIN talents ON talents.id=talent_description_templates.talent_id`)).all(), (await database.prepare(`SELECT talent_tiers.id,talent_tiers.talent_id,talent_tiers.tier,talent_tiers.description FROM talent_tiers JOIN talents ON talents.id=talent_tiers.talent_id WHERE talents.category='Weapon'`)).all(), (await database.prepare('SELECT talent_tier_id,trigger_id FROM talent_tier_triggers')).all(), (await database.prepare('SELECT talent_tier_id,stat_attribute_id,value FROM talent_tier_effects')).all(), (await database.prepare("SELECT entity_id,updated_at FROM game_data_icons WHERE entity_type='talent'")).all() ]); return { weapons: weapons.map((weapon) => { const talentId = typeof weapon.exotic_talent_id === 'string' ? weapon.exotic_talent_id : ''; const talentRows = exoticTiers.filter((tier) => tier.talent_id === talentId); const tier = (name: string) => talentRows.find((item) => item.tier === name); const tierI = tier('I'), tierII = tier('II'); const effectIds = [...new Set([ ...exoticTierEffects.filter((effect) => effect.talent_tier_id === tierI?.id), ...exoticTierEffects.filter((effect) => effect.talent_tier_id === tierII?.id) ].map((effect) => String(effect.stat_attribute_id)))]; return { ...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), exoticTalent: talentId ? { id: talentId, iconUrl: (() => { const icon = talentIcons.find((item) => item.entity_id === talentId); return icon ? gameDataIconUrl('talent', talentId, icon.updated_at) : ''; })(), name: String(exoticTemplates.find((item) => item.talent_id === talentId)?.name ?? ''), descriptionTemplate: String(exoticTemplates.find((item) => item.talent_id === talentId)?.template ?? ''), triggerIds: [...new Set([ ...exoticTierTriggers.filter((link) => link.talent_tier_id === tierI?.id), ...exoticTierTriggers.filter((link) => link.talent_tier_id === tierII?.id) ].map((link) => String(link.trigger_id)))], effects: effectIds.map((statId) => ({ statId, valueI: exoticTierEffects.find((effect) => effect.talent_tier_id === tierI?.id && String(effect.stat_attribute_id) === statId)?.value ?? '', valueII: exoticTierEffects.find((effect) => effect.talent_tier_id === tierII?.id && String(effect.stat_attribute_id) === statId)?.value ?? '' })) } : null }}), talent1Options, talent2Options, stats, triggers, summary, pagination, filter, requestedWeaponId }; }; 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 }) => { const operator = await requirePermission(await getSessionUser(cookies), 'data.manage'); const form = await request.formData(); let exoticIconMutation; try { exoticIconMutation = await readIconMutation(form); } catch (error) { if (error instanceof GameDataIconValidationError) return fail(400, { weaponError: error.message }); throw error; } 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 existingExoticTalentId = isExotic ? String(form.get('exoticTalentId') ?? '') : ''; const exoticTalentName = isExotic ? String(form.get('exoticTalentName') ?? '').normalize('NFKC').trim() : ''; const exoticDescriptionTemplate = isExotic ? String(form.get('exoticDescriptionTemplate') ?? '').trim() : ''; const exoticTriggerIds = isExotic ? [...new Set(form.getAll('exoticTriggerIds').map(String))] : []; let exoticEffects: ExoticEffect[] = []; try { const parsed = JSON.parse(String(form.get('exoticEffects') ?? '[]')) as Array>; if (!Array.isArray(parsed)) throw new Error(); exoticEffects = parsed.map((effect) => ({ statId: String(effect.statId ?? ''), valueI: Number(effect.valueI), valueII: Number(effect.valueII) })); } catch { return fail(400, { weaponError: 'The exotic talent effects could not be read.' }); } 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 && (exoticTalentName.length < 2 || !exoticDescriptionTemplate || !exoticTriggerIds.length || !exoticEffects.length)) { return fail(400, { weaponError: 'Exotic weapons require a named talent, description template, trigger, and at least one effect.' }); } if (isExotic && (exoticEffects.some((effect) => !effect.statId || !Number.isFinite(effect.valueI) || !Number.isFinite(effect.valueII)) || new Set(exoticEffects.map((effect) => effect.statId)).size !== exoticEffects.length)) { return fail(400, { weaponError: 'Every exotic talent effect requires one unique attribute and valid Tier I and Tier II values.' }); } if (isExotic) { const placeholders = [...exoticDescriptionTemplate.matchAll(/\{(\d+)\}/g)].map((match) => Number(match[1])); if (placeholders.some((index) => index < 1 || index > exoticEffects.length)) return fail(400, { weaponError: 'The exotic talent description references a missing effect.' }); } 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.` }); } const statIds = [...new Set([...augmentEffects.map((effect) => effect.statId), ...baseBonuses.map((effect) => effect.statId), ...exoticEffects.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.' }); if (isExotic) { const validTriggers = await (await database.prepare(`SELECT COUNT(*) AS count FROM talent_triggers WHERE active=1 AND id IN (${exoticTriggerIds.map(() => '?').join(',')})`)).get(...exoticTriggerIds); if (Number(validTriggers?.count) !== exoticTriggerIds.length) return fail(400, { weaponError: 'One or more exotic talent triggers are unavailable.' }); if (existingExoticTalentId) { const owned = id ? await (await database.prepare('SELECT id FROM weapons WHERE id=? AND exotic_talent_id=?')).get(id, existingExoticTalentId) : null; if (!owned) return fail(400, { weaponError: 'The submitted exotic talent is not owned by this weapon.' }); const shared = await (await database.prepare('SELECT id FROM weapons WHERE exotic_talent_id=? AND id<>?')).get(existingExoticTalentId, id || 'new'); if (shared) return fail(409, { weaponError: 'That exotic talent is already owned by another weapon and cannot be shared.' }); } if (await (await database.prepare("SELECT id FROM talents WHERE name=? COLLATE NOCASE AND category='Weapon' AND id<>?")).get(exoticTalentName, existingExoticTalentId || 'new')) { return fail(409, { weaponError: 'An exotic talent with that name already exists.' }); } } const weaponId = id || randomUUID(); const now = new Date().toISOString(); const exoticTalentId = isExotic ? existingExoticTalentId || randomUUID() : null; const before = id ? { record: await auditRecord('weapons', id), tiers: await (await database.prepare('SELECT * FROM weapon_tier_ranges WHERE weapon_id=? ORDER BY tier')).all(id), talents: await (await database.prepare('SELECT * FROM weapon_talent_options WHERE weapon_id=? ORDER BY talent_slot')).all(id), augmentEffects: await (await database.prepare('SELECT * FROM weapon_augmentation_effects WHERE weapon_id=? ORDER BY sort_order')).all(id), baseBonuses: await (await database.prepare('SELECT * FROM weapon_base_attribute_bonuses WHERE weapon_id=? ORDER BY sort_order')).all(id), exoticTalent: existingExoticTalentId ? { record: await (await database.prepare('SELECT * FROM talents WHERE id=?')).get(existingExoticTalentId), icon: await gameDataIconMetadata(database, 'talent', existingExoticTalentId), template: await (await database.prepare('SELECT template FROM talent_description_templates WHERE talent_id=?')).get(existingExoticTalentId), tiers: await (await database.prepare('SELECT * FROM talent_tiers WHERE talent_id=? ORDER BY tier')).all(existingExoticTalentId), triggers: await (await database.prepare(`SELECT talent_tier_triggers.* FROM talent_tier_triggers JOIN talent_tiers ON talent_tiers.id=talent_tier_triggers.talent_tier_id WHERE talent_tiers.talent_id=?`)).all(existingExoticTalentId), effects: await (await database.prepare(`SELECT talent_tier_effects.* FROM talent_tier_effects JOIN talent_tiers ON talent_tiers.id=talent_tier_effects.talent_tier_id WHERE talent_tiers.talent_id=?`)).all(existingExoticTalentId) } : null } : null; if (isExotic && exoticTalentId) { if (existingExoticTalentId) { await (await database.prepare("UPDATE talents SET name=?,category='Weapon',active=1,updated_at=? WHERE id=?")).run(exoticTalentName, now, exoticTalentId); } else { await (await database.prepare("INSERT INTO talents(id,name,category,active,created_at,updated_at) VALUES(?,?,'Weapon',1,?,?)")).run(exoticTalentId, exoticTalentName, now, now); } await (await database.prepare(`INSERT INTO talent_weapon_scopes(talent_id,scope) VALUES(?,'Exotic') ON CONFLICT(talent_id) DO UPDATE SET scope='Exotic'`)).run(exoticTalentId); await (await database.prepare('DELETE FROM weapon_talent_slots WHERE talent_id=?')).run(exoticTalentId); await (await database.prepare(`INSERT INTO talent_description_templates(talent_id,template,updated_at) VALUES(?,?,?) ON CONFLICT(talent_id) DO UPDATE SET template=excluded.template,updated_at=excluded.updated_at`)).run(exoticTalentId, exoticDescriptionTemplate, now); const unitRows = await (await database.prepare(`SELECT id,unit FROM stat_attributes WHERE id IN (${exoticEffects.map(() => '?').join(',')})`)).all(...exoticEffects.map((effect) => effect.statId)); const units = new Map(unitRows.map((row) => [String(row.id), row.unit])); for (const talentTier of ['I', 'II'] as const) { const description = renderTalentDescription(exoticDescriptionTemplate, exoticEffects, units, talentTier); await (await database.prepare(`INSERT INTO talent_tiers(id,talent_id,tier,description,trigger_text,effect_text,created_at,updated_at) VALUES(?,?,?,?,?,?,?,?) ON CONFLICT(talent_id,tier) DO UPDATE SET description=excluded.description,updated_at=excluded.updated_at`)) .run(randomUUID(), exoticTalentId, talentTier, description, '', '', now, now); const tierRow = await (await database.prepare('SELECT id FROM talent_tiers WHERE talent_id=? AND tier=?')).get(exoticTalentId, talentTier); const tierId = String(tierRow?.id); await (await database.prepare('DELETE FROM talent_tier_triggers WHERE talent_tier_id=?')).run(tierId); await (await database.prepare('DELETE FROM talent_tier_effects WHERE talent_tier_id=?')).run(tierId); for (const triggerId of exoticTriggerIds) await (await database.prepare('INSERT INTO talent_tier_triggers(talent_tier_id,trigger_id) VALUES(?,?)')).run(tierId, triggerId); for (const effect of exoticEffects) await (await database.prepare('INSERT INTO talent_tier_effects(id,talent_tier_id,stat_attribute_id,value,created_at) VALUES(?,?,?,?,?)')) .run(randomUUID(), tierId, effect.statId, talentTier === 'I' ? effect.valueI : effect.valueII, now); } await applyIconMutation(database, 'talent', exoticTalentId, exoticIconMutation); } 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); } const previousExoticTalentId = typeof before?.record?.exotic_talent_id === 'string' ? before.record.exotic_talent_id : ''; if (!isExotic && previousExoticTalentId) { const anotherOwner = await (await database.prepare('SELECT id FROM weapons WHERE exotic_talent_id=? AND id<>?')).get(previousExoticTalentId, weaponId); if (!anotherOwner) await (await database.prepare('UPDATE talents SET active=0,updated_at=? WHERE id=?')).run(now, previousExoticTalentId); } 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); } await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: id ? 'weapon.updated' : 'weapon.created', entityId: weaponId, before, after: { record: await auditRecord('weapons', weaponId), tiers, talent1Ids, talent2Ids, augmentEffects, baseBonuses, exoticTalent: isExotic ? { id: exoticTalentId, name: exoticTalentName, icon: await gameDataIconMetadata(database, 'talent', String(exoticTalentId)), descriptionTemplate: exoticDescriptionTemplate, triggerIds: exoticTriggerIds, effects: exoticEffects } : null } }); return { weaponSaved: true, weaponMode: id ? 'edit' : 'add', resultToken: randomUUID() }; }, archive: async ({ request, cookies }) => { const operator = await requirePermission(await getSessionUser(cookies), 'data.manage'); const id = String((await request.formData()).get('id') ?? ''); const before = await auditRecord('weapons', id); await (await (await getDatabase()).prepare('UPDATE weapons SET active=0,updated_at=? WHERE id=?')).run(new Date().toISOString(), id); await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'weapon.archived', entityId: id, before, after: await auditRecord('weapons', id) }); return { weaponArchived: true }; } };