feat(isac): link skill chips to abilities
This commit is contained in:
@@ -15,6 +15,10 @@ import {
|
||||
GameDataIconValidationError,
|
||||
readIconMutation
|
||||
} from '$lib/server/game-data-icons';
|
||||
import {
|
||||
refreshSpecializationFocusReady,
|
||||
refreshSpecializationFocuses
|
||||
} from '$lib/server/specialization-readiness';
|
||||
|
||||
const PRIMARY_ATTRIBUTES = ['Firepower', 'Toughness', 'Engineering'] as const;
|
||||
const RANK_TWO_RESOURCES = new Set([
|
||||
@@ -114,48 +118,6 @@ async function nodeSnapshot(database: Database, id: string) {
|
||||
};
|
||||
}
|
||||
|
||||
async function refreshFocusReady(database: Database, focusId: string) {
|
||||
const focus = await (await database.prepare(
|
||||
'SELECT id,active FROM specialization_focuses WHERE id=?'
|
||||
)).get(focusId);
|
||||
if (!focus) return { ready: false, pointTotal: 0, availablePoints: 0, abilityCount: 0 };
|
||||
const [config, abilityCountRow, nodes, rankProfiles] = await Promise.all([
|
||||
(await database.prepare("SELECT xp_points,shd_cache_points FROM specialization_progression_config WHERE id='default'")).get(),
|
||||
(await database.prepare(`SELECT COUNT(*) AS count FROM specialization_focus_abilities link
|
||||
JOIN specialization_abilities ability ON ability.id=link.ability_id
|
||||
WHERE link.focus_id=? AND ability.active=1`)).get(focusId),
|
||||
(await database.prepare(
|
||||
'SELECT row_number,max_rank FROM specialization_tree_nodes WHERE focus_id=? AND active=1 ORDER BY row_number,column_number'
|
||||
)).all(focusId),
|
||||
(await database.prepare('SELECT rank,point_cost FROM specialization_rank_cost_profiles ORDER BY rank')).all()
|
||||
]);
|
||||
const pointForRank = new Map(rankProfiles.map((profile) => [Number(profile.rank), Number(profile.point_cost)]));
|
||||
const pointTotal = nodes.reduce((sum, node) => {
|
||||
for (let rank = 1; rank <= Number(node.max_rank); rank += 1) sum += pointForRank.get(rank) ?? rank;
|
||||
return sum;
|
||||
}, 0);
|
||||
const rows = [...new Set(nodes.map((node) => Number(node.row_number)))];
|
||||
const contiguousRows = rows.every((row, index) => row === index + 1);
|
||||
const abilityCount = Number(abilityCountRow?.count ?? 0);
|
||||
const availablePoints = Number(config?.xp_points ?? 30) + Number(config?.shd_cache_points ?? 19);
|
||||
const ready = Number(focus.active) === 1
|
||||
&& abilityCount === 3
|
||||
&& nodes.length > 0
|
||||
&& contiguousRows
|
||||
&& pointTotal === availablePoints;
|
||||
await (await database.prepare(
|
||||
'UPDATE specialization_focuses SET builder_ready=?,updated_at=? WHERE id=?'
|
||||
)).run(ready ? 1 : 0, new Date().toISOString(), focusId);
|
||||
return { ready, pointTotal, availablePoints, abilityCount };
|
||||
}
|
||||
|
||||
async function refreshSpecializationFocuses(database: Database, specializationId: string) {
|
||||
const focuses = await (await database.prepare(
|
||||
'SELECT id FROM specialization_focuses WHERE specialization_id=?'
|
||||
)).all(specializationId);
|
||||
for (const focus of focuses) await refreshFocusReady(database, String(focus.id));
|
||||
}
|
||||
|
||||
async function validateAttributeIds(database: Database, ids: string[]) {
|
||||
const unique = [...new Set(ids.filter(Boolean))];
|
||||
if (!unique.length) return true;
|
||||
@@ -218,7 +180,7 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
const pendingOnPage = pagination.page === 1 ? pendingSpecializations.slice(0, pagination.perPage) : [];
|
||||
const databaseOffset = Math.max(0, pagination.offset - pendingSpecializations.length);
|
||||
const databaseLimit = Math.max(0, pagination.perPage - pendingOnPage.length);
|
||||
const [specializations, focuses, abilities, abilityEffects, focusAbilities, nodes, nodeRanks, nodeEffects, nodeCosts, resources, rankProfiles, progression, attributes, icons, summary] = await Promise.all([
|
||||
const [specializations, focuses, abilities, abilityEffects, focusAbilities, nodes, nodeRanks, nodeEffects, nodeCosts, resources, rankProfiles, progression, attributes, chipCounts, icons, summary] = await Promise.all([
|
||||
databaseLimit ? (await database.prepare(
|
||||
`SELECT * FROM specializations ${where} ORDER BY active DESC,name COLLATE NOCASE LIMIT ? OFFSET ?`
|
||||
)).all(...args, databaseLimit, databaseOffset) : [],
|
||||
@@ -234,6 +196,9 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
(await database.prepare('SELECT * FROM specialization_rank_cost_profiles ORDER BY rank')).all(),
|
||||
(await database.prepare("SELECT * FROM specialization_progression_config WHERE id='default'")).get(),
|
||||
(await database.prepare('SELECT id,name,unit FROM stat_attributes WHERE active=1 ORDER BY name COLLATE NOCASE')).all(),
|
||||
(await database.prepare(`SELECT specialization_ability_id,COUNT(*) AS count
|
||||
FROM skill_chip_sets WHERE active=1 AND specialization_ability_id IS NOT NULL
|
||||
GROUP BY specialization_ability_id`)).all(),
|
||||
(await database.prepare(`SELECT entity_type,entity_id,updated_at FROM game_data_icons
|
||||
WHERE entity_type IN ('specialization','specialization_ability','specialization_node')`)).all(),
|
||||
(await database.prepare(`SELECT
|
||||
@@ -282,11 +247,13 @@ export const load: PageServerLoad = async ({ cookies, url }) => {
|
||||
effects: parseJsonArray<AbilityEffectInput>(item.effectsJson),
|
||||
active: 1,
|
||||
pending: true,
|
||||
skillChipCount: 0,
|
||||
iconUrl: ''
|
||||
})),
|
||||
...abilities.map((ability) => ({
|
||||
...ability,
|
||||
effects: abilityEffects.filter((effect) => effect.ability_id === ability.id),
|
||||
skillChipCount: Number(chipCounts.find((item) => item.specialization_ability_id === ability.id)?.count ?? 0),
|
||||
iconUrl: iconUrl('specialization_ability', String(ability.id))
|
||||
}))
|
||||
];
|
||||
@@ -494,7 +461,7 @@ export const actions: Actions = {
|
||||
VALUES(?,?,?,?,?,?,?,?)`))
|
||||
.run(focusId, specializationId, name, description || null, primaryAttribute, displayOrder, now, now);
|
||||
}
|
||||
await refreshFocusReady(database, focusId);
|
||||
await refreshSpecializationFocusReady(database, focusId);
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'game_data',
|
||||
@@ -647,7 +614,7 @@ export const actions: Actions = {
|
||||
await database.exec('ROLLBACK');
|
||||
throw error;
|
||||
}
|
||||
const readiness = await refreshFocusReady(database, focusId);
|
||||
const readiness = await refreshSpecializationFocusReady(database, focusId);
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'game_data',
|
||||
@@ -802,7 +769,7 @@ export const actions: Actions = {
|
||||
throw error;
|
||||
}
|
||||
await applyIconMutation(database, 'specialization_node', nodeId, iconMutation);
|
||||
const readiness = await refreshFocusReady(database, focusId);
|
||||
const readiness = await refreshSpecializationFocusReady(database, focusId);
|
||||
await recordIsacAudit({
|
||||
actorId: operator.id,
|
||||
category: 'game_data',
|
||||
@@ -852,7 +819,7 @@ export const actions: Actions = {
|
||||
const database = await getDatabase();
|
||||
const before = await focusSnapshot(database, id);
|
||||
await (await database.prepare('UPDATE specialization_focuses SET active=1,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
|
||||
const readiness = await refreshFocusReady(database, id);
|
||||
const readiness = await refreshSpecializationFocusReady(database, id);
|
||||
await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'specialization_focus.unarchived', entityId: id, before, after: { ...(await focusSnapshot(database, id)), readiness } });
|
||||
return { focusUnarchived: true, resultToken: randomUUID() };
|
||||
},
|
||||
@@ -888,7 +855,7 @@ export const actions: Actions = {
|
||||
const node = await (await database.prepare('SELECT focus_id FROM specialization_tree_nodes WHERE id=?')).get(id);
|
||||
const before = await nodeSnapshot(database, id);
|
||||
await (await database.prepare('UPDATE specialization_tree_nodes SET active=0,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
|
||||
const readiness = node ? await refreshFocusReady(database, String(node.focus_id)) : null;
|
||||
const readiness = node ? await refreshSpecializationFocusReady(database, String(node.focus_id)) : null;
|
||||
await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'specialization_node.archived', entityId: id, before, after: { ...(await nodeSnapshot(database, id)), readiness } });
|
||||
return { nodeArchived: true, resultToken: randomUUID() };
|
||||
},
|
||||
@@ -900,7 +867,7 @@ export const actions: Actions = {
|
||||
const node = await (await database.prepare('SELECT focus_id FROM specialization_tree_nodes WHERE id=?')).get(id);
|
||||
const before = await nodeSnapshot(database, id);
|
||||
await (await database.prepare('UPDATE specialization_tree_nodes SET active=1,updated_at=? WHERE id=?')).run(new Date().toISOString(), id);
|
||||
const readiness = node ? await refreshFocusReady(database, String(node.focus_id)) : null;
|
||||
const readiness = node ? await refreshSpecializationFocusReady(database, String(node.focus_id)) : null;
|
||||
await recordIsacAudit({ actorId: operator.id, category: 'game_data', action: 'specialization_node.unarchived', entityId: id, before, after: { ...(await nodeSnapshot(database, id)), readiness } });
|
||||
return { nodeUnarchived: true, resultToken: randomUUID() };
|
||||
}
|
||||
|
||||
@@ -101,6 +101,7 @@
|
||||
const specializationById = (id: string) => data.specializations.find((item: any) => item.id === id);
|
||||
const focusById = (id: string) => data.focuses.find((item: any) => item.id === id);
|
||||
const abilityById = (id: string) => data.abilities.find((item: any) => item.id === id);
|
||||
const abilityChipCount = (ability: any) => Number(ability?.skillChipCount ?? 0);
|
||||
const focusList = (specializationId: string) => data.focuses.filter((focus: any) => focus.specialization_id === specializationId);
|
||||
const abilityList = (specializationId: string) => data.abilities.filter((ability: any) =>
|
||||
ability.specialization_id === specializationId && Number(ability.active) === 1
|
||||
@@ -543,7 +544,14 @@
|
||||
{@const ability = assignedAbility(slot)}
|
||||
<article class:tree-linked={slot !== 2}>
|
||||
<ItemIcon src={ability?.iconUrl} alt={ability?.name ?? `Ability slot ${slot}`}/>
|
||||
<div><span>ABILITY {slot} · {slot === 2 ? 'COMBAT SKILLS' : 'TREE + COMBAT SKILLS'}</span><strong>{ability?.name ?? 'NOT ASSIGNED'}</strong><p>{ability?.description ?? 'Assign an active class ability to complete this focus.'}</p></div>
|
||||
<div>
|
||||
<span>ABILITY {slot} · {slot === 2 ? 'COMBAT SKILLS' : 'TREE + COMBAT SKILLS'}</span>
|
||||
<strong>{ability?.name ?? 'NOT ASSIGNED'}</strong>
|
||||
{#if ability}
|
||||
<small class:ready={abilityChipCount(ability) === 3} class="ability-chip-readiness">{abilityChipCount(ability)} / 3 SKILL CHIPS · {abilityChipCount(ability) === 3 ? 'READY' : 'INCOMPLETE'}</small>
|
||||
{/if}
|
||||
<p>{ability?.description ?? 'Assign an active class ability to complete this focus.'}</p>
|
||||
</div>
|
||||
{#if ability}<button class="specialization-mutation" onclick={() => editAbility(ability)}>EDIT</button>{/if}
|
||||
</article>
|
||||
{/each}
|
||||
@@ -558,7 +566,7 @@
|
||||
{#each data.abilities.filter((ability: any) => ability.specialization_id === selectedSpecialization.id) as ability}
|
||||
<article class:archived={Number(ability.active) === 0}>
|
||||
<ItemIcon src={ability.iconUrl} alt={ability.name}/>
|
||||
<div><strong>{ability.name}</strong><small>{Number(ability.active) === 1 ? 'ACTIVE CLASS ABILITY' : 'ARCHIVED CLASS ABILITY'}</small></div>
|
||||
<div><strong>{ability.name}</strong><small>{Number(ability.active) === 1 ? `${abilityChipCount(ability)} / 3 SKILL CHIPS` : 'ARCHIVED CLASS ABILITY'}</small></div>
|
||||
<button class="secondary specialization-mutation" onclick={() => editAbility(ability)}>{Number(ability.active) === 1 ? 'EDIT' : 'REVIEW + RESTORE'} →</button>
|
||||
</article>
|
||||
{:else}
|
||||
|
||||
Reference in New Issue
Block a user