503 lines
21 KiB
TypeScript
503 lines
21 KiB
TypeScript
import { getDatabase } from '$lib/server/database';
|
|
import { getRegistrySearch } from '$lib/server/pagination';
|
|
import { databaseCategories } from '$lib/database/categories';
|
|
import {
|
|
loadMechanicPublicProjectionsForOwners,
|
|
type MechanicPublicProjectionOwnerRef,
|
|
type MechanicPublicProjectionsByOwner,
|
|
} from '$lib/server/mechanic-public-projection';
|
|
import {
|
|
databaseMechanicGroup,
|
|
databaseMechanicOwnersMatchSearch,
|
|
databaseMechanicSearchSummary,
|
|
} from './public-mechanics';
|
|
import type { PageServerLoad } from './$types';
|
|
|
|
const tables = {
|
|
gear: 'gear',
|
|
weapons: 'weapons',
|
|
'weapon-mods': 'weapon_mods',
|
|
'gear-mods': 'gear_mods',
|
|
'os-protocols': 'os_protocols',
|
|
talents: 'talents',
|
|
brands: 'brand_sets',
|
|
'skill-chips': 'skill_chip_sets',
|
|
specializations: 'specializations',
|
|
} as const;
|
|
|
|
export const load: PageServerLoad = async ({ url }) => {
|
|
const database = await getDatabase();
|
|
const { search, pattern } = getRegistrySearch(url);
|
|
const counts = await Promise.all(
|
|
databaseCategories.map(async (category) => {
|
|
const row = await (
|
|
await database.prepare(
|
|
`SELECT COUNT(*) AS count FROM ${tables[category.slug]} WHERE active=1`,
|
|
)
|
|
).get();
|
|
return { ...category, count: Number(row?.count ?? 0) };
|
|
}),
|
|
);
|
|
const rawSearchResults = search
|
|
? await (
|
|
await database.prepare(`
|
|
SELECT * FROM (
|
|
SELECT 'gear' AS slug,'Gear' AS category,item.id AS parent_id,item.name,
|
|
item.slot || ' · T' || item.tier AS detail
|
|
FROM gear item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.slot LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM gear_talents link
|
|
JOIN talents linked_talent
|
|
ON linked_talent.id=link.talent_id AND linked_talent.active=1
|
|
JOIN mechanic_authored_sources source
|
|
ON source.owner_type='talent' AND source.owner_id=linked_talent.id
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE link.gear_id=item.id
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'weapons','Weapons',item.id,item.name,
|
|
item.category || ' · ' || item.damage_type || ' damage'
|
|
FROM weapons item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.category LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.damage_type LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM talents linked_talent
|
|
JOIN mechanic_authored_sources source
|
|
ON source.owner_type='talent' AND source.owner_id=linked_talent.id
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE linked_talent.active=1
|
|
AND (
|
|
linked_talent.id=item.exotic_talent_id
|
|
OR EXISTS (
|
|
SELECT 1 FROM weapon_talent_options link
|
|
WHERE link.weapon_id=item.id AND link.talent_id=linked_talent.id
|
|
)
|
|
)
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'weapon-mods','Weapon Mods',item.id,item.name,item.slot_type
|
|
FROM weapon_mods item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.slot_type LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
UNION ALL
|
|
SELECT 'gear-mods','Gear Mods',item.id,item.name,'Gear modification'
|
|
FROM gear_mods item
|
|
WHERE item.active=1 AND item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
UNION ALL
|
|
SELECT 'os-protocols','OS Protocols',item.id,item.name,
|
|
item.quality || ' · ' || item.primary_attribute_type
|
|
FROM os_protocols item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.quality LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.primary_attribute_type LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM os_protocol_talents talent
|
|
WHERE talent.protocol_id=item.id AND (
|
|
talent.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR talent.description_template LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM mechanic_authored_sources source
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE source.owner_type='os_protocol_talent' AND source.owner_id=talent.id
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'talents','Talents',item.id,item.name,item.category
|
|
FROM talents item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.category LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM talent_tiers tier
|
|
WHERE tier.talent_id=item.id AND (
|
|
tier.description LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR tier.trigger_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR tier.effect_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM mechanic_authored_sources source
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE source.owner_type='talent' AND source.owner_id=item.id
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'brands','Brands',item.id,item.name,
|
|
COALESCE((
|
|
SELECT GROUP_CONCAT(attribute.name, ', ')
|
|
FROM brand_set_effects effect
|
|
JOIN stat_attributes attribute ON attribute.id=effect.stat_attribute_id
|
|
WHERE effect.brand_set_id=item.id
|
|
),'Brand Set bonuses')
|
|
FROM brand_sets item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM brand_set_effects effect
|
|
JOIN stat_attributes attribute ON attribute.id=effect.stat_attribute_id
|
|
WHERE effect.brand_set_id=item.id
|
|
AND attribute.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'skill-chips','Skill Chips',item.id,item.name,
|
|
COALESCE((SELECT talent.name FROM skill_chip_talents talent
|
|
WHERE talent.skill_chip_set_id=item.id),'Skill Chip set')
|
|
FROM skill_chip_sets item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM skill_chip_talents talent
|
|
WHERE talent.skill_chip_set_id=item.id AND (
|
|
talent.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR talent.description_template LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM mechanic_authored_sources source
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE source.owner_type='skill_chip_talent' AND source.owner_id=talent.id
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'specializations','Specializations',item.id,item.name,'Specialization'
|
|
FROM specializations item
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR item.description LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM specialization_focuses focus
|
|
WHERE focus.specialization_id=item.id AND focus.active=1
|
|
AND focus.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM specialization_abilities ability
|
|
WHERE ability.specialization_id=item.id AND ability.active=1
|
|
AND ability.name LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM specialization_tree_rank_combat_effects attachment
|
|
JOIN specialization_tree_nodes node ON node.id=attachment.node_id AND node.active=1
|
|
JOIN specialization_focuses focus ON focus.id=node.focus_id AND focus.active=1
|
|
JOIN specialization_combat_effects combat_effect
|
|
ON combat_effect.id=attachment.combat_effect_id
|
|
AND combat_effect.specialization_id=focus.specialization_id
|
|
AND combat_effect.active=1
|
|
JOIN mechanic_authored_sources source
|
|
ON source.owner_type='specialization_tree_rank' AND source.owner_id=attachment.id
|
|
JOIN mechanic_public_variants variant ON variant.source_id=source.id
|
|
WHERE focus.specialization_id=item.id
|
|
AND variant.behavior_text LIKE ? ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
) matches
|
|
ORDER BY name COLLATE NOCASE
|
|
`)
|
|
).all(...Array(33).fill(pattern))
|
|
: [];
|
|
// Raw projection text produces a candidate set only. Canonical projections
|
|
// below decide behavior matches; this query preserves independent metadata matches.
|
|
const metadataSearchResultKeys = search
|
|
? new Set(
|
|
(
|
|
await (
|
|
await database.prepare(`WITH search(pattern) AS (VALUES (?))
|
|
SELECT 'gear' AS slug,item.id AS parent_id
|
|
FROM gear item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.slot LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
UNION ALL
|
|
SELECT 'weapons',item.id
|
|
FROM weapons item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.category LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.damage_type LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
UNION ALL
|
|
SELECT 'weapon-mods',item.id
|
|
FROM weapon_mods item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.slot_type LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
UNION ALL
|
|
SELECT 'gear-mods',item.id
|
|
FROM gear_mods item,search
|
|
WHERE item.active=1
|
|
AND item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
UNION ALL
|
|
SELECT 'os-protocols',item.id
|
|
FROM os_protocols item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.quality LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.primary_attribute_type LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM os_protocol_talents talent
|
|
WHERE talent.protocol_id=item.id AND (
|
|
talent.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR talent.description_template LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'talents',item.id
|
|
FROM talents item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.category LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM talent_tiers tier
|
|
WHERE tier.talent_id=item.id AND (
|
|
tier.description LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR tier.trigger_text LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR tier.effect_text LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'brands',item.id
|
|
FROM brand_sets item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM brand_set_effects effect
|
|
JOIN stat_attributes attribute ON attribute.id=effect.stat_attribute_id
|
|
WHERE effect.brand_set_id=item.id
|
|
AND attribute.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'skill-chips',item.id
|
|
FROM skill_chip_sets item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM skill_chip_talents talent
|
|
WHERE talent.skill_chip_set_id=item.id AND (
|
|
talent.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR talent.description_template LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)
|
|
)
|
|
UNION ALL
|
|
SELECT 'specializations',item.id
|
|
FROM specializations item,search
|
|
WHERE item.active=1 AND (
|
|
item.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR item.description LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
OR EXISTS (
|
|
SELECT 1 FROM specialization_focuses focus
|
|
WHERE focus.specialization_id=item.id AND focus.active=1
|
|
AND focus.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
OR EXISTS (
|
|
SELECT 1 FROM specialization_abilities ability
|
|
WHERE ability.specialization_id=item.id AND ability.active=1
|
|
AND ability.name LIKE search.pattern ESCAPE '\\' COLLATE NOCASE
|
|
)
|
|
)`)
|
|
).all(pattern)
|
|
).map((result) => `${result.slug}\u0000${result.parent_id}`),
|
|
)
|
|
: new Set<string>();
|
|
const resultIds = (slug: string) =>
|
|
rawSearchResults
|
|
.filter((result) => String(result.slug) === slug)
|
|
.map((result) => String(result.parent_id));
|
|
const [
|
|
gearTalentRows,
|
|
weaponTalentRows,
|
|
protocolTalentRows,
|
|
skillChipTalentRows,
|
|
specializationAttachmentRows,
|
|
] = await Promise.all([
|
|
resultIds('gear').length
|
|
? (
|
|
await database.prepare(`SELECT link.gear_id,link.talent_id FROM gear_talents link
|
|
JOIN talents ON talents.id=link.talent_id AND talents.active=1
|
|
WHERE link.gear_id IN (${resultIds('gear')
|
|
.map(() => '?')
|
|
.join(',')})
|
|
ORDER BY link.gear_id,link.talent_id`)
|
|
).all(...resultIds('gear'))
|
|
: [],
|
|
resultIds('weapons').length
|
|
? (
|
|
await database.prepare(`SELECT relation.weapon_id,relation.talent_id FROM (
|
|
SELECT weapon_id,talent_slot,talent_id FROM weapon_talent_options
|
|
UNION ALL
|
|
SELECT id AS weapon_id,NULL AS talent_slot,exotic_talent_id AS talent_id
|
|
FROM weapons WHERE exotic_talent_id IS NOT NULL
|
|
) relation
|
|
JOIN talents ON talents.id=relation.talent_id AND talents.active=1
|
|
WHERE relation.weapon_id IN (${resultIds('weapons')
|
|
.map(() => '?')
|
|
.join(',')})
|
|
ORDER BY relation.weapon_id,relation.talent_slot,relation.talent_id`)
|
|
).all(...resultIds('weapons'))
|
|
: [],
|
|
resultIds('os-protocols').length
|
|
? (
|
|
await database.prepare(`SELECT protocol_id,id FROM os_protocol_talents
|
|
WHERE protocol_id IN (${resultIds('os-protocols')
|
|
.map(() => '?')
|
|
.join(',')})
|
|
ORDER BY protocol_id,id`)
|
|
).all(...resultIds('os-protocols'))
|
|
: [],
|
|
resultIds('skill-chips').length
|
|
? (
|
|
await database.prepare(`SELECT skill_chip_set_id,id FROM skill_chip_talents
|
|
WHERE skill_chip_set_id IN (${resultIds('skill-chips')
|
|
.map(() => '?')
|
|
.join(',')})
|
|
ORDER BY skill_chip_set_id,id`)
|
|
).all(...resultIds('skill-chips'))
|
|
: [],
|
|
resultIds('specializations').length
|
|
? (
|
|
await database.prepare(`SELECT attachment.id,focus.specialization_id
|
|
FROM specialization_tree_rank_combat_effects attachment
|
|
JOIN specialization_tree_nodes node ON node.id=attachment.node_id AND node.active=1
|
|
JOIN specialization_focuses focus ON focus.id=node.focus_id AND focus.active=1
|
|
JOIN specialization_combat_effects combat_effect
|
|
ON combat_effect.id=attachment.combat_effect_id
|
|
AND combat_effect.specialization_id=focus.specialization_id
|
|
AND combat_effect.active=1
|
|
WHERE focus.specialization_id IN (${resultIds('specializations')
|
|
.map(() => '?')
|
|
.join(',')})
|
|
ORDER BY focus.specialization_id,focus.display_order,focus.name COLLATE NOCASE,
|
|
node.row_number,node.column_number,attachment.rank,attachment.effect_order`)
|
|
).all(...resultIds('specializations'))
|
|
: [],
|
|
]);
|
|
const resultKey = (slug: string, id: string) => `${slug}\u0000${id}`;
|
|
const ownersByResult = new Map<string, MechanicPublicProjectionOwnerRef[]>();
|
|
const addOwner = (
|
|
slug: string,
|
|
parentId: string,
|
|
owner: MechanicPublicProjectionOwnerRef,
|
|
) => {
|
|
const key = resultKey(slug, parentId);
|
|
ownersByResult.set(key, [...(ownersByResult.get(key) ?? []), owner]);
|
|
};
|
|
for (const result of rawSearchResults) {
|
|
if (String(result.slug) === 'talents') {
|
|
addOwner('talents', String(result.parent_id), {
|
|
type: 'talent',
|
|
id: String(result.parent_id),
|
|
});
|
|
}
|
|
}
|
|
for (const row of gearTalentRows) {
|
|
addOwner('gear', String(row.gear_id), {
|
|
type: 'talent',
|
|
id: String(row.talent_id),
|
|
});
|
|
}
|
|
for (const row of weaponTalentRows) {
|
|
addOwner('weapons', String(row.weapon_id), {
|
|
type: 'talent',
|
|
id: String(row.talent_id),
|
|
});
|
|
}
|
|
for (const row of protocolTalentRows) {
|
|
addOwner('os-protocols', String(row.protocol_id), {
|
|
type: 'os_protocol_talent',
|
|
id: String(row.id),
|
|
});
|
|
}
|
|
for (const row of skillChipTalentRows) {
|
|
addOwner('skill-chips', String(row.skill_chip_set_id), {
|
|
type: 'skill_chip_talent',
|
|
id: String(row.id),
|
|
});
|
|
}
|
|
for (const row of specializationAttachmentRows) {
|
|
addOwner('specializations', String(row.specialization_id), {
|
|
type: 'specialization_tree_rank',
|
|
id: String(row.id),
|
|
});
|
|
}
|
|
const owners = [
|
|
...new Map(
|
|
[...ownersByResult.values()]
|
|
.flat()
|
|
.map((owner) => [`${owner.type}\u0000${owner.id}`, owner]),
|
|
).values(),
|
|
];
|
|
const publicMechanics: MechanicPublicProjectionsByOwner = owners.length
|
|
? await loadMechanicPublicProjectionsForOwners(database, owners)
|
|
: ({} as MechanicPublicProjectionsByOwner);
|
|
const searchResults = rawSearchResults
|
|
.filter(
|
|
(result) =>
|
|
metadataSearchResultKeys.has(
|
|
resultKey(String(result.slug), String(result.parent_id)),
|
|
) ||
|
|
databaseMechanicOwnersMatchSearch(
|
|
ownersByResult.get(
|
|
resultKey(String(result.slug), String(result.parent_id)),
|
|
) ?? [],
|
|
publicMechanics,
|
|
search,
|
|
),
|
|
)
|
|
.slice(0, 40);
|
|
const normalizedSearch = search.toLocaleLowerCase();
|
|
return {
|
|
registries: counts,
|
|
total: counts.reduce((total, registry) => total + registry.count, 0),
|
|
search,
|
|
searchResults: searchResults.map((result) => {
|
|
const groups = (
|
|
ownersByResult.get(
|
|
resultKey(String(result.slug), String(result.parent_id)),
|
|
) ?? []
|
|
)
|
|
.map((owner) =>
|
|
databaseMechanicGroup(owner, publicMechanics, { label: 'BEHAVIOR' }),
|
|
)
|
|
.filter((group) => group !== null);
|
|
const group =
|
|
groups.find((candidate) =>
|
|
candidate.variants.some((variant) =>
|
|
variant.behaviorText.toLocaleLowerCase().includes(normalizedSearch),
|
|
),
|
|
) ??
|
|
groups[0] ??
|
|
null;
|
|
const behavior = databaseMechanicSearchSummary(group);
|
|
return {
|
|
slug: String(result.slug),
|
|
category: String(result.category),
|
|
name: String(result.name),
|
|
detail: String(result.detail ?? ''),
|
|
...(behavior ?? {}),
|
|
};
|
|
}),
|
|
};
|
|
};
|