Files
shd.build/src/routes/database/+page.server.ts
T

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 ?? {}),
};
}),
};
};