Improve ISAC libraries, roles, and public profiles
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user