feat(isac): add specialization data management

This commit is contained in:
JB
2026-07-26 16:20:04 -04:00
parent ed31748102
commit 6722665f5c
25 changed files with 3369 additions and 25 deletions
@@ -35,6 +35,70 @@
};
return proposed[names[key] ?? key];
};
const specializationKind = $derived(
action === 'saveProgression' ? 'PROGRESSION PROFILE'
: action === 'saveSpecialization' ? 'SPECIALIZATION'
: action === 'saveFocus' ? 'FOCUS'
: action === 'saveAbility' ? 'CLASS ABILITY'
: action === 'saveFocusAbilities' ? 'FOCUS ABILITY SET'
: 'TREE NODE'
);
const specializationRecord = $derived((currentSnapshot?.record ?? null) as Record<string, unknown> | null);
const specializationFields = $derived(
action === 'saveProgression'
? [
{ label: 'XP POINTS', current: specializationRecord?.xp_points, proposed: proposed.xpPoints },
{ label: 'SHD TECH CACHE POINTS', current: specializationRecord?.shd_cache_points, proposed: proposed.cachePoints }
]
: action === 'saveSpecialization'
? [
{ label: 'NAME', current: specializationRecord?.name, proposed: proposed.name },
{ label: 'DESCRIPTION', current: specializationRecord?.description, proposed: proposed.description }
]
: action === 'saveFocus'
? [
{ label: 'NAME', current: specializationRecord?.name, proposed: proposed.name },
{ label: 'SPECIALIZATION', current: specializationRecord?.specialization_id, proposed: proposed.specializationId },
{ label: 'PRIMARY ATTRIBUTE', current: specializationRecord?.primary_attribute, proposed: proposed.primaryAttribute },
{ label: 'DISPLAY ORDER', current: specializationRecord?.display_order, proposed: proposed.displayOrder },
{ label: 'DESCRIPTION', current: specializationRecord?.description, proposed: proposed.description }
]
: action === 'saveAbility'
? [
{ label: 'NAME', current: specializationRecord?.name, proposed: proposed.name },
{ label: 'SPECIALIZATION', current: specializationRecord?.specialization_id, proposed: proposed.specializationId },
{ label: 'DURATION · SECONDS', current: specializationRecord?.duration_seconds, proposed: proposed.durationSeconds },
{ label: 'COOLDOWN · SECONDS', current: specializationRecord?.cooldown_seconds, proposed: proposed.cooldownSeconds },
{ label: 'DESCRIPTION', current: specializationRecord?.description, proposed: proposed.description }
]
: action === 'saveNode'
? [
{ label: 'NAME', current: specializationRecord?.name, proposed: proposed.name },
{ label: 'FOCUS', current: specializationRecord?.focus_id, proposed: proposed.focusId },
{ label: 'ROW', current: specializationRecord?.row_number, proposed: proposed.rowNumber },
{ label: 'COLUMN', current: specializationRecord?.column_number, proposed: proposed.columnNumber },
{ label: 'MAXIMUM RANK', current: specializationRecord?.max_rank, proposed: proposed.maxRank },
{ label: 'DESCRIPTION TEMPLATE', current: specializationRecord?.description_template, proposed: proposed.descriptionTemplate }
]
: [
{ label: 'FOCUS', current: specializationRecord?.name ?? specializationRecord?.id, proposed: proposed.id },
{ label: 'ABILITY SLOT 1 · TREE', current: (currentSnapshot?.abilities as any[])?.find((item) => Number(item.ability_slot) === 1)?.ability_id, proposed: proposed.abilityId1 },
{ label: 'ABILITY SLOT 2 · COMBAT SKILLS', current: (currentSnapshot?.abilities as any[])?.find((item) => Number(item.ability_slot) === 2)?.ability_id, proposed: proposed.abilityId2 },
{ label: 'ABILITY SLOT 3 · TREE', current: (currentSnapshot?.abilities as any[])?.find((item) => Number(item.ability_slot) === 3)?.ability_id, proposed: proposed.abilityId3 }
]
);
const proposedEffects = $derived((jsonValue(proposed.effectsJson) ?? []) as any[]);
const proposedRanks = $derived((jsonValue(proposed.ranksJson) ?? []) as any[]);
const currentAbilityEffects = $derived((currentSnapshot?.effects ?? []) as any[]);
const currentNodeRanks = $derived((currentSnapshot?.ranks ?? []) as any[]);
const currentNodeEffects = $derived((currentSnapshot?.effects ?? []) as any[]);
const currentNodeCosts = $derived((currentSnapshot?.costs ?? []) as any[]);
const currentRankProfiles = $derived((currentSnapshot?.rankProfiles ?? []) as any[]);
const effectSummary = (effect: any) => {
const target = effect.statId || effect.stat_attribute_id || effect.abilityId || effect.ability_id || effect.label || 'CUSTOM EFFECT';
const value = effect.value === null || effect.value === undefined || effect.value === '' ? '' : `${effect.value}${effect.unit ? ` ${effect.unit}` : ''}`;
return `${target}${value ? ` · ${value}` : ''}`;
};
</script>
<section class="proposal-change-preview" aria-label="Proposed data comparison">
@@ -50,6 +114,7 @@
{#if currentSnapshot}
<div class="proposal-attribute-grid">
<div class="preview-field identity"><span>NAME</span><strong>{shown(attributeValue('current', 'name'))}</strong></div>
<div class="preview-field description"><span>DESCRIPTION · OPTIONAL</span><strong>{shown(attributeValue('current', 'description'))}</strong></div>
{#each [['type','TYPE'],['category','CATEGORY'],['calculation_type','CALCULATION'],['unit','UNIT']] as field}
<div class="preview-field"><span>{field[1]}</span><strong>{shown(attributeValue('current', field[0]))}</strong></div>
{/each}
@@ -66,6 +131,7 @@
<header><span>PROPOSED DEFINITION</span><b>AFTER</b></header>
<div class="proposal-attribute-grid">
<div class="preview-field identity"><span>NAME</span><strong>{shown(attributeValue('proposed', 'name'))}</strong></div>
<div class="preview-field description"><span>DESCRIPTION · OPTIONAL</span><strong>{shown(attributeValue('proposed', 'description'))}</strong></div>
{#each [['type','TYPE'],['category','CATEGORY'],['calculation_type','CALCULATION'],['unit','UNIT']] as field}
<div class="preview-field"><span>{field[1]}</span><strong>{shown(attributeValue('proposed', field[0]))}</strong></div>
{/each}
@@ -76,6 +142,70 @@
</div>
</article>
</div>
{:else if module === 'specializations' && ['saveProgression','saveSpecialization','saveFocus','saveAbility','saveFocusAbilities','saveNode'].includes(action)}
<div class="proposal-compare-grid specialization-proposal-preview">
<article class="proposal-editor-preview before">
<header><span>CURRENT LIVE · {specializationKind}</span><b>{specializationRecord ? 'BEFORE' : 'NO LIVE RECORD'}</b></header>
{#if specializationRecord}
<div class="proposal-field-summary specialization-fields">
{#each specializationFields as field}
<div><span>{field.label}</span><strong>{shown(field.current)}</strong></div>
{/each}
</div>
{#if action === 'saveAbility'}
<div class="proposal-structured-list">
<header><span>STRUCTURED EFFECTS</span><b>{currentAbilityEffects.length}</b></header>
{#each currentAbilityEffects as effect}<p>{effectSummary(effect)}</p>{:else}<p>NO STRUCTURED EFFECTS</p>{/each}
</div>
{:else if action === 'saveNode'}
<div class="proposal-rank-preview">
{#each currentNodeRanks as rank}
<section><header><span>RANK {rank.rank}</span><b>{currentNodeEffects.filter((effect) => Number(effect.rank) === Number(rank.rank)).length} EFFECTS</b></header><p>{rank.description}</p><small>{currentNodeCosts.filter((cost) => Number(cost.rank) === Number(rank.rank)).map((cost) => `${cost.resource_id} × ${cost.quantity}`).join(' · ') || 'CREDITS ONLY'}</small></section>
{/each}
</div>
{:else if action === 'saveProgression'}
<div class="proposal-rank-preview">
{#each currentRankProfiles as profile}
<section><header><span>RANK {profile.rank}</span><b>{profile.point_cost} POINTS</b></header><p>{Number(profile.credit_cost).toLocaleString()} CREDITS</p></section>
{/each}
</div>
{/if}
{:else}
<div class="proposal-empty-state"><b>NEW {specializationKind}</b><span>No live registry record will be replaced.</span></div>
{/if}
</article>
<article class="proposal-editor-preview after">
<header><span>PROPOSED · {specializationKind}</span><b>AFTER</b></header>
<div class="proposal-field-summary specialization-fields">
{#each specializationFields as field}
<div><span>{field.label}</span><strong>{shown(field.proposed)}</strong></div>
{/each}
</div>
{#if action === 'saveAbility'}
<div class="proposal-structured-list">
<header><span>STRUCTURED EFFECTS</span><b>{proposedEffects.length}</b></header>
{#each proposedEffects as effect}<p>{effectSummary(effect)}</p>{:else}<p>NO STRUCTURED EFFECTS</p>{/each}
</div>
{:else if action === 'saveNode'}
<div class="proposal-rank-preview">
{#each proposedRanks as rank}
<section>
<header><span>RANK {rank.rank}</span><b>{rank.effects?.length ?? 0} EFFECTS</b></header>
<p>{rank.description || 'NO RANK DESCRIPTION'}</p>
{#each rank.effects ?? [] as effect}<small>{effectSummary(effect)}</small>{/each}
<small>{Object.entries(rank.costs ?? {}).filter(([, quantity]) => Number(quantity) > 0).map(([resource, quantity]) => `${resource} × ${quantity}`).join(' · ') || 'CREDITS ONLY'}</small>
</section>
{/each}
</div>
{:else if action === 'saveProgression'}
<div class="proposal-rank-preview">
{#each [1,2,3] as rank}
<section><header><span>RANK {rank}</span><b>{shown(proposed[`rank${rank}PointCost`])} POINTS</b></header><p>{Number(proposed[`rank${rank}CreditCost`] || 0).toLocaleString()} CREDITS</p></section>
{/each}
</div>
{/if}
</article>
</div>
{:else}
<div class="proposal-editor-preview generic after">
<header><span>PROPOSED {label(module)} RECORD</span><b>AFTER</b></header>