feat: model specialization combat effects

This commit is contained in:
JB
2026-07-26 19:04:26 -04:00
parent b1656c430f
commit 338ea8761d
34 changed files with 3396 additions and 146 deletions
+72 -9
View File
@@ -3,6 +3,7 @@ import { gameDataIconUrl } from '$lib/server/game-data-icons';
import { getSessionUser } from '$lib/server/auth/session';
import { fail } from '@sveltejs/kit';
import { randomUUID } from 'node:crypto';
import { migrateLoadoutDraft } from '$lib/builder/loadout';
import type { Actions, PageServerLoad } from './$types';
const validVisibility = new Set(['private', 'unlisted', 'public']);
@@ -12,11 +13,7 @@ const slugify = (value: string) => value.toLowerCase().normalize('NFKD')
function parseLoadout(raw: FormDataEntryValue | null) {
if (typeof raw !== 'string' || raw.length > 250_000) return null;
try {
const value = JSON.parse(raw);
if (value?.version !== 1 || typeof value.buildName !== 'string' || !value.gear ||
!Array.isArray(value.weapons) || value.weapons.length !== 2 ||
!Array.isArray(value.chips) || value.chips.length !== 3 || !value.protocol) return null;
return value;
return migrateLoadoutDraft(JSON.parse(raw));
} catch {
return null;
}
@@ -40,7 +37,10 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
talents, talentTiers, talentEffects, talentTemplates,
chipSets, chipTwoPiece, chipPrimary, chipSecondary, chipTalents, chipTalentEffects,
protocols, protocolPrimary, protocolProfiles, protocolSecondary, protocolTalents, protocolTalentEffects, coreRanges,
icons
icons, specializations, specializationFocuses, specializationAbilities, focusAbilities,
specializationNodes, specializationRanks, specializationRankEffects, specializationRankCosts,
specializationCombatEffects, specializationRankCombatEffects, specializationCombatModifiers,
progression, rankProfiles, activeCalculationRevision, calculationStages, calculationProfiles
] = await Promise.all([
(await database.prepare('SELECT * FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM gear WHERE active=1 ORDER BY slot,name COLLATE NOCASE')).all(),
@@ -71,9 +71,29 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
(await database.prepare('SELECT * FROM os_protocol_talents')).all(),
(await database.prepare('SELECT * FROM os_protocol_talent_effects ORDER BY sort_order')).all(),
(await database.prepare('SELECT * FROM core_attribute_tier_ranges ORDER BY tier')).all(),
(await database.prepare("SELECT * FROM game_data_icons")).all()
(await database.prepare("SELECT * FROM game_data_icons")).all(),
(await database.prepare('SELECT * FROM specializations WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM specialization_focuses WHERE active=1 ORDER BY specialization_id,display_order,name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM specialization_abilities WHERE active=1 ORDER BY specialization_id,name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM specialization_focus_abilities ORDER BY focus_id,ability_slot')).all(),
(await database.prepare('SELECT * FROM specialization_tree_nodes WHERE active=1 ORDER BY focus_id,row_number,column_number')).all(),
(await database.prepare('SELECT * FROM specialization_tree_node_ranks ORDER BY node_id,rank')).all(),
(await database.prepare('SELECT * FROM specialization_tree_rank_effects ORDER BY node_id,rank,effect_order')).all(),
(await database.prepare('SELECT * FROM specialization_tree_rank_costs ORDER BY node_id,rank,resource_id')).all(),
(await database.prepare('SELECT * FROM specialization_combat_effects WHERE active=1 ORDER BY specialization_id,name COLLATE NOCASE')).all(),
(await database.prepare('SELECT * FROM specialization_tree_rank_combat_effects ORDER BY node_id,rank,effect_order')).all(),
(await database.prepare('SELECT * FROM specialization_tree_rank_combat_modifiers ORDER BY rank_combat_effect_id,modifier_order')).all(),
(await database.prepare("SELECT * FROM specialization_progression_config WHERE id='default'")).get(),
(await database.prepare('SELECT * FROM specialization_rank_cost_profiles ORDER BY rank')).all(),
(await database.prepare("SELECT * FROM calculation_revisions WHERE status='active' ORDER BY version DESC LIMIT 1")).get(),
(await database.prepare(`SELECT stage.* FROM calculation_stages stage
JOIN calculation_revisions revision ON revision.id=stage.revision_id
WHERE revision.status='active' ORDER BY stage.display_order`)).all(),
(await database.prepare(`SELECT profile.* FROM attribute_calculation_profiles profile
JOIN calculation_revisions revision ON revision.id=profile.revision_id
WHERE revision.status='active' ORDER BY profile.stage_key,profile.stat_attribute_id`)).all()
]);
const iconUrl = (type: 'talent' | 'brand_set' | 'os_protocol' | 'skill_chip', id: unknown) => {
const iconUrl = (type: 'talent' | 'brand_set' | 'os_protocol' | 'skill_chip' | 'specialization' | 'specialization_ability' | 'specialization_node', id: unknown) => {
const icon = icons.find((item) => item.entity_type === type && item.entity_id === id);
return icon ? gameDataIconUrl(type, String(id), icon.updated_at) : '';
};
@@ -134,7 +154,50 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
secondaryEffects: profile ? protocolSecondary.filter((effect) => effect.profile_id === profile.id) : [],
talent: talent ? { ...talent, effects: protocolTalentEffects.filter((effect) => effect.talent_id === talent.id) } : null
};
})
}),
specializations: specializations.map((specialization) => ({
...specialization,
iconUrl: iconUrl('specialization', specialization.id),
focuses: specializationFocuses.filter((focus) => focus.specialization_id === specialization.id).map((focus) => {
const assignedAbilities = focusAbilities.filter((link) => link.focus_id === focus.id)
.sort((left, right) => Number(left.ability_slot) - Number(right.ability_slot))
.map((link) => {
const ability = specializationAbilities.find((item) => item.id === link.ability_id);
return ability ? {
...ability,
slot: Number(link.ability_slot),
iconUrl: iconUrl('specialization_ability', ability.id)
} : null;
}).filter(Boolean);
const nodes = specializationNodes.filter((node) => node.focus_id === focus.id).map((node) => ({
...node,
iconUrl: iconUrl('specialization_node', node.id),
ranks: specializationRanks.filter((rank) => rank.node_id === node.id).map((rank) => ({
...rank,
effects: specializationRankEffects.filter((effect) => effect.node_id === node.id && Number(effect.rank) === Number(rank.rank)),
costs: specializationRankCosts.filter((cost) => cost.node_id === node.id && Number(cost.rank) === Number(rank.rank)),
combatEffects: specializationRankCombatEffects
.filter((effect) => effect.node_id === node.id && Number(effect.rank) === Number(rank.rank))
.map((effect) => ({
...effect,
definition: specializationCombatEffects.find((definition) => definition.id === effect.combat_effect_id),
modifiers: specializationCombatModifiers.filter((modifier) => modifier.rank_combat_effect_id === effect.id)
}))
}))
}));
return { ...focus, abilities: assignedAbilities, nodes };
})
})),
specializationProgression: {
...progression,
availablePoints: Number(progression?.xp_points ?? 30) + Number(progression?.shd_cache_points ?? 19),
rankProfiles
},
calculation: {
revision: activeCalculationRevision,
stages: calculationStages,
profiles: calculationProfiles
}
};
};