Adding skills & isac talent updates
This commit is contained in:
@@ -11,7 +11,11 @@
|
||||
import type { ConditionalMechanicDraftV1 } from '$lib/calculations/mechanic-editor';
|
||||
import { tick } from 'svelte';
|
||||
let { data, form } = $props();
|
||||
const categoryOptions = ['Weapon Talent 1', 'Weapon Talent 2', 'Backpack', 'Body Armor'].map((value) => ({ value, label: value }));
|
||||
const categoryOptions = ['Weapon Talent 1', 'Weapon Talent 2', 'Exotic Weapon', 'Backpack', 'Body Armor'].map((value) => ({ value, label: value }));
|
||||
const tierIIDocumentationOptions = [
|
||||
{ value: 'documented', label: 'DOCUMENTED' },
|
||||
{ value: 'not_documented', label: 'NOT YET DOCUMENTED' }
|
||||
];
|
||||
let talent = $state<any>(null);
|
||||
let handledResultToken = $state('');
|
||||
let iconResetToken = $state(0);
|
||||
@@ -44,7 +48,7 @@
|
||||
pools: [],
|
||||
clauses: []
|
||||
});
|
||||
const blankTalent = () => ({ id: '', name: '', iconUrl: '', category: 'Weapon Talent 1', descriptionTemplate: '', tierIDescription: '', tierIIDescription: '', conditionalRule: blankRule(), modelingOpen: false });
|
||||
const blankTalent = () => ({ id: '', name: '', iconUrl: '', category: 'Weapon Talent 1', descriptionTemplate: '', tierIDescription: '', tierIIDescription: '', tierIIDocumented: true, tierIIDocumentation: 'documented', conditionalRule: blankRule(), modelingOpen: false });
|
||||
const editTalent = (row: any) => {
|
||||
const tier = (name: string) => row.tiers.find((item: any) => item.tier === name) ?? { description: '', effects: [] };
|
||||
const one = tier('I'), two = tier('II');
|
||||
@@ -53,10 +57,51 @@
|
||||
descriptionTemplate: row.descriptionTemplate || one.description,
|
||||
tierIDescription: one.description,
|
||||
tierIIDescription: two.description,
|
||||
tierIIDocumented: row.category !== 'Exotic Weapon' || Boolean(row.tierIIDocumented),
|
||||
tierIIDocumentation: row.category === 'Exotic Weapon' && !row.tierIIDocumented ? 'not_documented' : 'documented',
|
||||
conditionalRule: row.conditionalRule ?? blankRule(),
|
||||
modelingOpen: Boolean(row.conditionalRule?.clauses?.length)
|
||||
};
|
||||
};
|
||||
const setTierIIDocumented = (documented: boolean) => {
|
||||
if (!talent) return;
|
||||
talent.tierIIDocumented = documented;
|
||||
talent.tierIIDocumentation = documented ? 'documented' : 'not_documented';
|
||||
talent.conditionalRule = {
|
||||
...talent.conditionalRule,
|
||||
pools: talent.conditionalRule.pools.map((pool: any) => {
|
||||
if (!pool.expiresAfterSecondsVariants) return pool;
|
||||
const tierI = pool.expiresAfterSecondsVariants.I;
|
||||
return {
|
||||
...pool,
|
||||
expiresAfterSecondsVariants: documented
|
||||
? { ...pool.expiresAfterSecondsVariants, I: tierI, II: pool.expiresAfterSecondsVariants.II ?? tierI }
|
||||
: { I: tierI }
|
||||
};
|
||||
}),
|
||||
clauses: talent.conditionalRule.clauses.map((clause: any) => ({
|
||||
...clause,
|
||||
outcomes: clause.outcomes.map((outcome: any) => ({
|
||||
...outcome,
|
||||
variants: documented
|
||||
? {
|
||||
...outcome.variants,
|
||||
I: outcome.variants.I,
|
||||
II: structuredClone(outcome.variants.II ?? outcome.variants.I ?? { value: 0 })
|
||||
}
|
||||
: { I: outcome.variants.I }
|
||||
}))
|
||||
}))
|
||||
};
|
||||
};
|
||||
const setCategory = (category: string) => {
|
||||
if (!talent) return;
|
||||
talent.category = category;
|
||||
if (category !== 'Exotic Weapon' && !talent.tierIIDocumented) setTierIIDocumented(true);
|
||||
};
|
||||
const valueColumns = () => talent?.category === 'Exotic Weapon' && !talent.tierIIDocumented
|
||||
? [{ key: 'tierI', variantKey: 'I', label: 'TIER I' }]
|
||||
: [{ key: 'tierI', variantKey: 'I', label: 'TIER I' }, { key: 'tierII', variantKey: 'II', label: 'TIER II' }];
|
||||
const configured = (tiers: Array<{ tier: unknown }>, name: string) => tiers.some((item) => item.tier === name);
|
||||
const tierDescription = (tiers: Array<{ tier: unknown; description?: unknown }>, name: string) => {
|
||||
const description = tiers.find((item) => item.tier === name)?.description;
|
||||
@@ -70,16 +115,22 @@
|
||||
};
|
||||
const mechanicValue = (value: string | number, singular: string) => `${value} ${Number(value) === 1 ? singular : `${singular}s`}`;
|
||||
const cooldownLine = (value: unknown) => value === null || value === undefined || value === '' ? '' : `Talent Cooldown: ${mechanicValue(value as string | number, 'second')}`;
|
||||
const preview = (tier: 'I' | 'II') => talent.descriptionTemplate.replace(/\{(\d+|duration|max_stacks)\}/g, (placeholder: string, expression: string) => {
|
||||
const duration = talent.conditionalRule.pools.find((pool: any) => pool.expiresAfterSeconds)?.expiresAfterSeconds;
|
||||
const maxStacks = talent.conditionalRule.pools.length ? Math.max(...talent.conditionalRule.pools.map((pool: any) => pool.max ?? 1)) : null;
|
||||
const preview = (tier: 'I' | 'II') => {
|
||||
if (tier === 'II' && talent.category === 'Exotic Weapon' && !talent.tierIIDocumented) return 'NOT YET DOCUMENTED';
|
||||
return talent.descriptionTemplate.replace(/\{(\d+|duration|max_stacks)\}/g, (placeholder: string, expression: string) => {
|
||||
const durationPool = talent.conditionalRule.pools.find((pool: any) =>
|
||||
pool.expiresAfterSeconds !== undefined || pool.expiresAfterSecondsVariants?.[tier] !== undefined);
|
||||
const duration = durationPool?.expiresAfterSecondsVariants?.[tier] ?? durationPool?.expiresAfterSeconds;
|
||||
const stackPools = talent.conditionalRule.pools.filter((pool: any) => pool.stateKind === 'stack');
|
||||
const maxStacks = stackPools.length ? Math.max(...stackPools.map((pool: any) => pool.max ?? 1)) : null;
|
||||
if (expression === 'duration') return duration == null ? placeholder : mechanicValue(duration, 'second');
|
||||
if (expression === 'max_stacks') return maxStacks == null ? placeholder : mechanicValue(maxStacks, 'stack');
|
||||
const effect = talent.conditionalRule.clauses
|
||||
.flatMap((clause: any) => clause.outcomes)
|
||||
.find((outcome: any) => outcome.descriptionSlot === Number(expression));
|
||||
return effect ? displayValue(effect, tier) : placeholder;
|
||||
});
|
||||
});
|
||||
};
|
||||
$effect(() => {
|
||||
if (!form?.resultToken || form.resultToken === handledResultToken) return;
|
||||
handledResultToken = form.resultToken;
|
||||
@@ -103,6 +154,7 @@
|
||||
<div class="editor-title"><div><p class="eyebrow">{talent.id ? 'MODIFY TALENT' : 'NEW TALENT'}</p><h3>{talent.id ? talent.name : 'TALENT DEFINITION'}</h3></div><button aria-label="Close talent editor" onclick={() => talent = null}>×</button></div>
|
||||
<PendingForm action="?/saveTalent" enctype="multipart/form-data">
|
||||
<input type="hidden" name="id" value={talent.id}/><input type="hidden" name="conditionalRule" value={JSON.stringify(talent.conditionalRule)}/>
|
||||
<input type="hidden" name="tierIIDocumented" value={talent.tierIIDocumented ? '1' : '0'}/>
|
||||
{#if talentErrors.length}
|
||||
<div class="validation-summary" role="alert" tabindex="-1" bind:this={validationSummary}>
|
||||
<strong>CHECK {new Set(talentErrors.map((error) => error.area)).size} HIGHLIGHTED {new Set(talentErrors.map((error) => error.area)).size === 1 ? 'AREA' : 'AREAS'}</strong>
|
||||
@@ -120,10 +172,16 @@
|
||||
</div>
|
||||
<div id="talent-identity" class="talent-identity" class:validation-area-error={hasErrors(['identity'])}>
|
||||
<label class="field">NAME<input name="name" bind:value={talent.name} required aria-invalid={hasErrors(['identity']) ? 'true' : undefined}/></label>
|
||||
<SegmentedChoice legend="CATEGORY / SLOT" name="category" options={categoryOptions} bind:value={talent.category} required/>
|
||||
<SegmentedChoice legend="CATEGORY / SLOT" name="category" options={categoryOptions} bind:value={talent.category} onchange={(values) => setCategory(values[0])} required/>
|
||||
{#each errorsFor('identity') as error}<small class="field-error">{error.message}</small>{/each}
|
||||
</div>
|
||||
</div>
|
||||
{#if talent.category === 'Exotic Weapon'}
|
||||
<div class="form-grid">
|
||||
<SegmentedChoice legend="TIER II DOCUMENTATION" name="tierIIDocumentation" options={tierIIDocumentationOptions} bind:value={talent.tierIIDocumentation} onchange={(values) => setTierIIDocumented(values[0] === 'documented')} required/>
|
||||
<p class="console-note">{talent.tierIIDocumented ? 'Tier I and Tier II values will be modeled and saved.' : 'Tier I is required. Tier II will remain absent until its game values are documented.'}</p>
|
||||
</div>
|
||||
{/if}
|
||||
<label id="talent-description" class="field template-field" class:validation-area-error={hasErrors(['description'])}>SHARED DESCRIPTION TEMPLATE<textarea name="descriptionTemplate" bind:value={talent.descriptionTemplate} placeholder={'Hitting an enemy {1} times grants +{2} for {duration}...'} required aria-invalid={hasErrors(['description']) ? 'true' : undefined}></textarea><small>Use {'{1}'}, {'{2}'}, and so on for effect values. Use {'{duration}'} and {'{max_stacks}'} for defined mechanics. Cooldown is displayed separately.</small>{#each errorsFor('description') as error}<small class="field-error">{error.message}</small>{/each}</label>
|
||||
<div class="tier-preview"><div><span>TIER I PREVIEW</span><p>{preview('I') || 'Description preview'}</p></div><div><span>TIER II PREVIEW</span><p>{preview('II') || 'Description preview'}</p></div></div>
|
||||
<details id="talent-mechanic" class="modeling-disclosure" class:validation-area-error={hasErrors(['mechanic', 'outcomes'])} bind:open={talent.modelingOpen}>
|
||||
@@ -134,7 +192,7 @@
|
||||
<div class="modeling-content">
|
||||
<MechanicProgramEditor
|
||||
attributes={data.stats}
|
||||
valueColumns={[{key:'tierI',variantKey:'I',label:'TIER I'},{key:'tierII',variantKey:'II',label:'TIER II'}]}
|
||||
valueColumns={valueColumns()}
|
||||
idPrefix={`talent-${talent.id || 'new'}`}
|
||||
aiContext={{
|
||||
sourceKind: 'talent',
|
||||
@@ -157,4 +215,4 @@
|
||||
{/if}
|
||||
|
||||
<RegistryPagination pagination={data.pagination} search={data.search}/>
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row talent-row table-heading"><SortableColumnHeader label="TALENT" column="name" sort={data.sort}/><SortableColumnHeader label="CATEGORY" column="category" sort={data.sort}/><SortableColumnHeader label="TIER I EFFECT" column="tier1" sort={data.sort}/><SortableColumnHeader label="TIER II EFFECT" column="tier2" sort={data.sort}/><SortableColumnHeader label="STATE" column="state" sort={data.sort} defaultDirection="desc"/><span></span></div>{#each data.talents as item}<div class="dashboard-row talent-row" class:pending-record={item.pending} class:registry-row-editing={String(talent?.id ?? '') === String(item.id)}><div class="registry-icon-identity"><ItemIcon src={item.iconUrl} alt=""/><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.category === 'Exotic Weapon' ? `OWNED BY ${item.ownerWeaponName ?? 'UNASSIGNED WEAPON'}` : 'Tiered talent'}</small>{#if cooldownLine(item.cooldown_seconds)}<small class="talent-cooldown">{cooldownLine(item.cooldown_seconds)}</small>{/if}</div></div><span>{item.category}</span><p class="talent-description" class:unconfigured={!configured(item.tiers, 'I')}>{tierDescription(item.tiers, 'I')}</p><p class="talent-description" class:unconfigured={!configured(item.tiers, 'II')}>{tierDescription(item.tiers, 'II')}</p><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : item.conditionalRule?.clauses?.length ? 'MODELED' : 'CONFIG REQUIRED'}</b>{#if item.pending}<span></span>{:else if item.category === 'Exotic Weapon'}{#if item.ownerWeaponId}<a class="row-action" href={`/isac/weapons?weapon=${item.ownerWeaponId}`}>EDIT WEAPON</a>{:else}<span></span>{/if}{:else}<button aria-expanded={String(talent?.id ?? '') === String(item.id)} aria-controls={`talent-editor-${item.id}`} onclick={() => editTalent(item)}>EDIT</button>{/if}</div>{#if String(talent?.id ?? '') === String(item.id)}<RegistryInlineEditor id={`talent-editor-${item.id}`} label={`Edit ${item.name}`}>{@render talentEditor()}</RegistryInlineEditor>{/if}{/each}</div>
|
||||
<div class="dashboard-table talent-dashboard-table"><div class="dashboard-row talent-row table-heading"><SortableColumnHeader label="TALENT" column="name" sort={data.sort}/><SortableColumnHeader label="CATEGORY" column="category" sort={data.sort}/><SortableColumnHeader label="TIER I EFFECT" column="tier1" sort={data.sort}/><SortableColumnHeader label="TIER II EFFECT" column="tier2" sort={data.sort}/><SortableColumnHeader label="STATE" column="state" sort={data.sort} defaultDirection="desc"/><span></span></div>{#each data.talents as item}<div class="dashboard-row talent-row" class:pending-record={item.pending} class:registry-row-editing={String(talent?.id ?? '') === String(item.id)}><div class="registry-icon-identity"><ItemIcon src={item.iconUrl} alt=""/><div><strong>{item.name}</strong><small>{item.pending ? `PROPOSED BY ${item.proposerName}` : item.category === 'Exotic Weapon' ? `OWNED BY ${item.ownerWeaponName ?? 'UNASSIGNED WEAPON'}` : 'Tiered talent'}</small>{#if cooldownLine(item.cooldown_seconds)}<small class="talent-cooldown">{cooldownLine(item.cooldown_seconds)}</small>{/if}</div></div><span>{item.category}</span><p class="talent-description" class:unconfigured={!configured(item.tiers, 'I')}>{tierDescription(item.tiers, 'I')}</p><p class="talent-description" class:unconfigured={!configured(item.tiers, 'II')}>{item.category === 'Exotic Weapon' && !item.tierIIDocumented ? 'NOT YET DOCUMENTED' : tierDescription(item.tiers, 'II')}</p><b class:pending={item.pending} class="state-dot">{item.pending ? 'PENDING' : item.conditionalRule?.clauses?.length ? 'MODELED' : 'CONFIG REQUIRED'}</b>{#if item.pending}<span></span>{:else}<button aria-expanded={String(talent?.id ?? '') === String(item.id)} aria-controls={`talent-editor-${item.id}`} onclick={() => editTalent(item)}>EDIT</button>{/if}</div>{#if String(talent?.id ?? '') === String(item.id)}<RegistryInlineEditor id={`talent-editor-${item.id}`} label={`Edit ${item.name}`}>{@render talentEditor()}</RegistryInlineEditor>{/if}{/each}</div>
|
||||
|
||||
Reference in New Issue
Block a user