feat: add source-owned combat mechanic programs

This commit is contained in:
JB
2026-07-27 13:35:00 -04:00
parent 5a67b252aa
commit 4eb754e7c8
62 changed files with 14815 additions and 905 deletions
+77 -1
View File
@@ -6,6 +6,7 @@ import { getDatabase } from '$lib/server/database';
import { getPagination, getRegistrySearch } from '$lib/server/pagination';
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { MECHANIC_EVENT_REGISTRY, type MechanicEventKind } from '$lib/calculations/mechanic';
const number = (value: unknown) => {
const numeric = Number(value);
@@ -18,6 +19,19 @@ const gearIcon = (slot: unknown) => String(slot).toLowerCase().replaceAll(' ', '
const weaponIcon = (category: unknown) => String(category).toLowerCase() as GameIconName;
const iconUrl = (type: GameDataIconType, id: unknown, updatedAt: unknown) =>
updatedAt ? gameDataIconUrl(type, String(id), updatedAt) : '';
const parsedObject = (value: unknown): Record<string, unknown> => {
try {
const parsed = JSON.parse(String(value ?? '{}'));
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
? parsed as Record<string, unknown>
: {};
} catch {
return {};
}
};
const humanized = (value: unknown) => String(value ?? '')
.replaceAll('_', ' ')
.replace(/\b\w/g, (character) => character.toUpperCase());
export const load: PageServerLoad = async ({ params, url }) => {
const category = databaseCategory(params.category);
@@ -330,6 +344,21 @@ 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
? 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,
outcomes.variant_key,outcomes.outcome_key,outcomes.application_kind,outcomes.effect_scope,
outcomes.value,outcomes.damage_basis,attributes.name AS attribute_name,attributes.unit AS attribute_unit,
outcomes.sort_order
FROM conditional_effect_rules rules
JOIN conditional_effect_clauses clauses ON clauses.rule_id=rules.id AND clauses.active=1
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))
: [];
const items = rows.map((row) => {
switch (category.slug) {
@@ -491,6 +520,49 @@ export const load: PageServerLoad = async ({ params, url }) => {
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),
@@ -507,7 +579,11 @@ export const load: PageServerLoad = async ({ params, url }) => {
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: row.cooldown_seconds ? [{ label: 'TALENT COOLDOWN', value: `${number(row.cooldown_seconds)} SECONDS` }] : []
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 'brands': {