Improve ISAC libraries, roles, and public profiles

This commit is contained in:
JB
2026-07-27 18:54:46 -04:00
parent fe4acec191
commit 6e12b2f3b7
40 changed files with 1196 additions and 271 deletions
+69 -104
View File
@@ -1,4 +1,5 @@
import { databaseCategory } from '$lib/database/categories';
import type { DatabaseFact, DatabaseItem } from '$lib/database/items';
import type { GameIconName } from '$lib/game-icons';
import { weaponBaseRange } from '$lib/builder/loadout';
import { gameDataIconUrl, type GameDataIconType } from '$lib/server/game-data-icons';
@@ -33,6 +34,54 @@ const humanized = (value: unknown) => String(value ?? '')
.replaceAll('_', ' ')
.replace(/\b\w/g, (character) => character.toUpperCase());
const talentCardItem = (
row: Record<string, any>,
mechanics: Array<Record<string, any>>,
weaponSlotOverride?: number
): DatabaseItem => {
const talentCategory = String(row.category);
const isWeaponTalent = talentCategory === 'Weapon';
const normalizedCategory = talentCategory === 'Vest' ? 'Body Armor' : talentCategory;
const weaponSlot = weaponSlotOverride ?? (row.weapon_slot ? Number(row.weapon_slot) : undefined);
const slotLabel = isWeaponTalent && weaponSlot ? `WEAPON ${weaponSlot}` : normalizedCategory.toUpperCase();
const activation = [...new Map(mechanics.map((mechanic) => [
String(mechanic.clause_id),
`${mechanic.clause_label || humanized(mechanic.event_kind)} · ${MECHANIC_EVENT_REGISTRY[mechanic.event_kind as MechanicEventKind]?.label ?? humanized(mechanic.event_kind)}`
])).values()].join(' / ');
const afterLines: DatabaseFact[] = [
...(activation ? [{ label: 'ACTIVATES WHEN', value: activation }] : []),
...(row.duration_seconds ? [{ label: 'EFFECT DURATION', value: `${number(row.duration_seconds)} SECONDS` }] : []),
...(Number(row.max_stacks) > 1 ? [{ label: 'MAXIMUM STACKS', value: String(row.max_stacks) }] : []),
...(row.cooldown_seconds ? [{ label: 'TALENT COOLDOWN', value: `${number(row.cooldown_seconds)} SECONDS` }] : [])
];
const filterKey = isWeaponTalent ? 'weaponTalent' : 'talent';
const destination = isWeaponTalent ? 'weapons' : 'gear';
return {
id: String(row.id),
name: String(row.name),
kicker: `${slotLabel} TALENT`,
variant: 'talent',
iconUrl: iconUrl('talent', row.id, row.icon_updated_at ?? row.updated_at),
headerMetaIcon: isWeaponTalent ? 'smg' : gearIcon(normalizedCategory),
headerBadge: isWeaponTalent && weaponSlot
? `${weaponSlot} · ${weaponSlot === 1 ? 'TOP' : 'BOTTOM'}`
: undefined,
badges: [
...(row.weapon_scope === 'Exotic' ? [{ label: 'EXOTIC', tone: 'exotic' }] : [])
],
facts: [],
description: row.tier_i ? String(row.tier_i) : 'Description is not available.',
secondaryDescription: row.tier_ii ? String(row.tier_ii) : 'Description is not available.',
descriptionStyle: 'data',
afterLines,
actions: [{
label: `FIND COMPATIBLE ${isWeaponTalent ? 'WEAPONS' : 'GEAR'}`,
href: `/database/${destination}?${filterKey}=${encodeURIComponent(String(row.id))}`
}]
};
};
export const load: PageServerLoad = async ({ params, url }) => {
const category = databaseCategory(params.category);
if (!category) error(404, 'Database category not found');
@@ -344,7 +393,16 @@ export const load: PageServerLoad = async ({ params, url }) => {
JOIN stat_attributes attributes ON attributes.id=effects.stat_attribute_id
WHERE effects.brand_set_id IN (${rows.map(() => '?').join(',')}) ORDER BY effects.pieces_required`)).all(...rows.map((row) => row.id))
: [];
const talentMechanics = category.slug === 'talents' && rows.length
const talentIds = [...new Set(
category.slug === 'talents'
? rows.map((row) => String(row.id))
: category.slug === 'weapons'
? weaponRelations.map((relation) => String(relation.id))
: category.slug === 'gear'
? gearRelations.filter((relation) => relation.kind === 'talent').map((relation) => String(relation.id))
: []
)];
const talentMechanics = talentIds.length
? await (await database.prepare(`SELECT rules.source_id AS talent_id,rules.id AS rule_id,
clauses.id AS clause_id,clauses.label AS clause_label,clauses.event_kind,
clauses.conditions_json,clauses.reducer_json,clauses.cooldown_seconds,
@@ -356,9 +414,10 @@ export const load: PageServerLoad = async ({ params, url }) => {
LEFT JOIN conditional_effect_outcomes outcomes ON outcomes.clause_id=clauses.id
LEFT JOIN stat_attributes attributes ON attributes.id=outcomes.stat_attribute_id
WHERE rules.source_type='talent' AND rules.configured=1
AND rules.source_id IN (${rows.map(() => '?').join(',')})
ORDER BY rules.source_id,clauses.sort_order,outcomes.outcome_key,outcomes.sort_order`)).all(...rows.map((row) => row.id))
AND rules.source_id IN (${talentIds.map(() => '?').join(',')})
ORDER BY rules.source_id,clauses.sort_order,outcomes.outcome_key,outcomes.sort_order`)).all(...talentIds)
: [];
const mechanicsFor = (talentId: unknown) => talentMechanics.filter((mechanic) => String(mechanic.talent_id) === String(talentId));
const items = rows.map((row) => {
switch (category.slug) {
@@ -383,21 +442,10 @@ export const load: PageServerLoad = async ({ params, url }) => {
label: `${effect.pieces_required} PIECES`,
value: `${effect.name} · +${valueWithUnit(effect.value,effect.unit)}`
}))
} : {
id: String(relation.id), name: String(relation.name), kicker: `${relation.category} // GEAR TALENT`,
iconUrl: relationIcon,
badges: [
...(relation.cooldown_seconds ? [{ label: `${number(relation.cooldown_seconds)}s COOLDOWN` }] : []),
...(relation.duration_seconds ? [{ label: `${number(relation.duration_seconds)}s DURATION` }] : []),
...(relation.max_stacks ? [{ label: `${relation.max_stacks} STACKS` }] : [])
],
facts: [
{ label: 'TIER I', value: relation.tier_i ? 'CONFIGURED' : 'MISSING' },
{ label: 'TIER II', value: relation.tier_ii ? 'CONFIGURED' : 'MISSING' }
],
description: relation.tier_i ? `TIER I · ${relation.tier_i}` : 'Tier I description is not configured.',
secondaryDescription: relation.tier_ii ? `TIER II · ${relation.tier_ii}` : 'Tier II description is not configured.'
}
} : talentCardItem({
...relation,
icon_updated_at: relation.updated_at
}, mechanicsFor(relation.id))
};
});
const iconGroups = [
@@ -428,21 +476,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
id: String(relation.id),
name: String(relation.name),
iconUrl: talentIcon,
preview: {
id: String(relation.id), name: String(relation.name), kicker: `WEAPON TALENT // SLOT ${slot}`,
iconUrl: talentIcon,
badges: [
...(relation.cooldown_seconds ? [{ label: `${number(relation.cooldown_seconds)}s COOLDOWN` }] : []),
...(relation.duration_seconds ? [{ label: `${number(relation.duration_seconds)}s DURATION` }] : []),
...(relation.max_stacks ? [{ label: `${relation.max_stacks} STACKS` }] : [])
],
facts: [
{ label: 'TIER I', value: relation.tier_i ? 'CONFIGURED' : 'MISSING' },
{ label: 'TIER II', value: relation.tier_ii ? 'CONFIGURED' : 'MISSING' }
],
description: relation.tier_i ? `TIER I · ${relation.tier_i}` : 'Tier I description is not configured.',
secondaryDescription: relation.tier_ii ? `TIER II · ${relation.tier_ii}` : 'Tier II description is not configured.'
}
preview: talentCardItem(relation, mechanicsFor(relation.id), slot)
};
});
const level50Damage = row.damage_min === null ? null : weaponBaseRange(row.damage_min, row.damage_max, 50);
@@ -515,77 +549,8 @@ export const load: PageServerLoad = async ({ params, url }) => {
],
lines: [{ label: 'UNIQUE TALENT', value: list(row.talent_name,'NOT CONFIGURED') }]
};
case 'talents': {
const talentCategory = String(row.category);
const isWeaponTalent = talentCategory === 'Weapon';
const normalizedCategory = talentCategory === 'Vest' ? 'Body Armor' : talentCategory;
const slotLabel = isWeaponTalent && row.weapon_slot ? `WEAPON ${row.weapon_slot}` : normalizedCategory.toUpperCase();
const mechanics = talentMechanics.filter((mechanic) => mechanic.talent_id === row.id);
const rule = mechanics[0];
const groupedOutcomes = new Map<string, any>();
for (const outcome of mechanics.filter((mechanic) => mechanic.variant_key)) {
const key = [
outcome.outcome_key,
outcome.application_kind,
outcome.effect_scope,
outcome.attribute_name ?? '',
outcome.damage_basis ?? ''
].join('|');
const current = groupedOutcomes.get(key) ?? {
outcomeKey: String(outcome.outcome_key),
application: String(outcome.application_kind),
scope: String(outcome.effect_scope),
attribute: String(outcome.attribute_name ?? ''),
unit: outcome.attribute_unit,
damageBasis: outcome.damage_basis,
values: {}
};
current.values[String(outcome.variant_key)] = Number(outcome.value);
groupedOutcomes.set(key, current);
}
const outcomeSummary = [...groupedOutcomes.values()].map((outcome) => {
const subject = outcome.application === 'active_modifier'
? outcome.attribute
: outcome.application === 'direct_damage'
? `Direct Damage · ${outcome.damageBasis === 'weapon_base_percent' ? 'Weapon Base' : 'Flat'}`
: humanized(outcome.application);
const values = ['I', 'II']
.filter((variant) => Number.isFinite(outcome.values[variant]))
.map((variant) => {
const raw = Number(outcome.values[variant]);
const prefix = outcome.application === 'active_modifier' && raw > 0 ? '+' : '';
return `T${variant} ${prefix}${valueWithUnit(raw, outcome.unit)}`;
})
.join(' · ');
return `${subject} · ${values}`;
}).join(' / ');
const activation = [...new Map(mechanics.map((mechanic) => [
String(mechanic.clause_id),
`${mechanic.clause_label || humanized(mechanic.event_kind)} · ${MECHANIC_EVENT_REGISTRY[mechanic.event_kind as MechanicEventKind]?.label ?? humanized(mechanic.event_kind)}`
])).values()].join(' / ');
return {
id: String(row.id), name: String(row.name), kicker: `${slotLabel} TALENT`,
iconUrl: iconUrl('talent',row.id,row.icon_updated_at),
headerMetaIcon: isWeaponTalent ? 'smg' : gearIcon(normalizedCategory),
headerBadge: isWeaponTalent && row.weapon_slot
? `${row.weapon_slot} · ${Number(row.weapon_slot) === 1 ? 'TOP' : 'BOTTOM'}`
: undefined,
badges: [
...(row.weapon_scope === 'Exotic' ? [{ label: 'EXOTIC', tone: 'exotic' }] : []),
...(row.duration_seconds ? [{ label: `${number(row.duration_seconds)}s DURATION` }] : []),
...(row.max_stacks ? [{ label: `${row.max_stacks} STACKS` }] : [])
],
facts: [],
description: row.tier_i ? String(row.tier_i) : 'Description is not available.',
secondaryDescription: row.tier_ii ? String(row.tier_ii) : 'Description is not available.',
descriptionStyle: 'data' as const,
afterLines: [
...(activation ? [{ label: 'ACTIVATES WHEN', value: activation }] : []),
...(outcomeSummary ? [{ label: 'CALCULATION EFFECTS', value: outcomeSummary }] : []),
...(row.cooldown_seconds ? [{ label: 'TALENT COOLDOWN', value: `${number(row.cooldown_seconds)} SECONDS` }] : [])
]
};
}
case 'talents':
return talentCardItem(row, mechanicsFor(row.id));
case 'brands': {
const effects = brandEffects.filter((effect) => effect.brand_set_id === row.id);
return {