feat(isac): author exotic talents in weapon editor

This commit is contained in:
JB
2026-07-25 04:19:57 -04:00
parent 14ba14dfab
commit fa21913112
7 changed files with 203 additions and 46 deletions
+38 -6
View File
@@ -8,12 +8,14 @@
const damageTypes = ['Blast', 'Pierce', 'Shredding'];
const ranges = [['damage', 'DMG', 'DAMAGE'], ['firepower', 'FP', 'FIREPOWER'], ['toughness', 'TGH', 'TOUGHNESS'], ['engineering', 'ENG', 'ENGINEERING']] as const;
const blankTier = (tier: number) => ({ tier, damage_min: 0, damage_max: 0, firepower_min: 0, firepower_max: 0, toughness_min: 0, toughness_max: 0, engineering_min: 0, engineering_max: 0 });
const blank = () => ({ id: '', name: '', category: 'AR', is_exotic: 0, native_tier: 0, damage_type: 'Pierce', rpm: 0, magazine: 0, reload_speed: 0, accuracy: 0, stability: 0, exotic_talent_id: '', active: 1, tiers: [blankTier(0), blankTier(1), blankTier(2)], talent1Ids: [], talent2Ids: [], augmentEffects: [{ stat_attribute_id: '', value: 0, sort_order: 1 }], baseBonuses: [{ stat_attribute_id: '', value: 0, sort_order: 1 }] });
const blankExoticTalent = () => ({ id: '', name: '', descriptionTemplate: '', triggerIds: [], effects: [{ statId: '', valueI: '', valueII: '' }] });
const blank = () => ({ id: '', name: '', category: 'AR', is_exotic: 0, native_tier: 0, damage_type: 'Pierce', rpm: 0, magazine: 0, reload_speed: 0, accuracy: 0, stability: 0, exotic_talent_id: '', exoticTalent: blankExoticTalent(), active: 1, tiers: [blankTier(0), blankTier(1), blankTier(2)], talent1Ids: [], talent2Ids: [], augmentEffects: [{ stat_attribute_id: '', value: 0, sort_order: 1 }], baseBonuses: [{ stat_attribute_id: '', value: 0, sort_order: 1 }] });
let editing = $state<any | null>(null);
let handledResultToken = $state('');
let openedRequestedWeapon = $state(false);
const edit = (weapon: any) => {
const tiers = [0, 1, 2].map((tier) => ({ ...blankTier(tier), ...(weapon.tiers.find((item: any) => Number(item.tier) === tier) ?? {}) }));
editing = { ...weapon, tiers, talent1Ids: [...weapon.talent1Ids], talent2Ids: [...weapon.talent2Ids], augmentEffects: weapon.augmentEffects.map((effect: any) => ({ ...effect })), baseBonuses: weapon.baseBonuses.map((effect: any) => ({ ...effect })) };
editing = { ...weapon, tiers, talent1Ids: [...weapon.talent1Ids], talent2Ids: [...weapon.talent2Ids], augmentEffects: weapon.augmentEffects.map((effect: any) => ({ ...effect })), baseBonuses: weapon.baseBonuses.map((effect: any) => ({ ...effect })), exoticTalent: weapon.exoticTalent ? { ...weapon.exoticTalent, triggerIds: [...weapon.exoticTalent.triggerIds], effects: weapon.exoticTalent.effects.map((effect: any) => ({ ...effect })) } : blankExoticTalent() };
};
const tierAt = (tier: number) => editing.tiers.find((item: any) => Number(item.tier) === tier);
const statLabel = (id: string) => data.stats.find((stat) => stat.id === id)?.name ?? 'Unknown attribute';
@@ -25,8 +27,22 @@
const setExotic = (isExotic: boolean) => {
if (!editing) return;
editing.is_exotic = isExotic ? 1 : 0;
editing.exotic_talent_id = isExotic ? (editing.exotic_talent_id ?? '') : '';
if (isExotic && !editing.exoticTalent) editing.exoticTalent = blankExoticTalent();
};
const addExoticEffect = () => {
if (editing?.exoticTalent && editing.exoticTalent.effects.length < 12) editing.exoticTalent.effects = [...editing.exoticTalent.effects, { statId: '', valueI: '', valueII: '' }];
};
const removeExoticEffect = (index: number) => {
if (editing?.exoticTalent?.effects.length > 1) editing.exoticTalent.effects = editing.exoticTalent.effects.filter((_: any, effectIndex: number) => effectIndex !== index);
};
const exoticPreview = (tier: 'I' | 'II') => editing.exoticTalent.descriptionTemplate.replace(/\{(\d+)\}/g, (placeholder: string, rawIndex: string) => {
const effect = editing.exoticTalent.effects[Number(rawIndex) - 1];
if (!effect) return placeholder;
const stat = data.stats.find((item) => item.id === effect.statId);
const value = tier === 'I' ? effect.valueI : effect.valueII;
const unit = stat?.unit === 'flat' ? '' : stat?.unit ?? '';
return unit === 'Shots' ? `${value} ${unit}` : `${value}${unit}`;
});
const addAugmentEffect = () => {
if (editing && editing.augmentEffects.length < 2) editing.augmentEffects = [...editing.augmentEffects, { stat_attribute_id: '', value: 0, sort_order: 2 }];
};
@@ -40,6 +56,11 @@
if (editing && editing.baseBonuses.length > 1) editing.baseBonuses = editing.baseBonuses.filter((_: any, bonusIndex: number) => bonusIndex !== index);
};
$effect(() => {
if (!openedRequestedWeapon && data.requestedWeaponId) {
const requested = data.weapons.find((weapon) => weapon.id === data.requestedWeaponId);
if (requested) edit(requested);
openedRequestedWeapon = true;
}
if (form?.weaponSaved && form.resultToken && form.resultToken !== handledResultToken) {
handledResultToken = form.resultToken;
editing = form.weaponMode === 'add' ? blank() : null;
@@ -62,8 +83,20 @@
<label class="field">NATIVE TIER<select name="nativeTier" bind:value={editing.native_tier}><option value={0}>Tier 0</option><option value={1}>Tier 1</option><option value={2}>Tier 2</option></select></label>
<label class="field">DAMAGE TYPE<select name="damageType" bind:value={editing.damage_type}>{#each damageTypes as damageType}<option>{damageType}</option>{/each}</select></label>
<label class="field exotic-toggle">EXOTIC STATUS<span><input name="isExotic" type="checkbox" value="1" checked={Number(editing.is_exotic) === 1} onchange={(event) => setExotic(event.currentTarget.checked)}/> EXOTIC WEAPON</span></label>
<label class="field">EXOTIC TALENT<select name="exoticTalentId" bind:value={editing.exotic_talent_id} disabled={!Number(editing.is_exotic)} required={Boolean(Number(editing.is_exotic))}><option value="" disabled>SELECT EXOTIC TALENT</option>{#each data.exoticTalents as talent}<option value={talent.id}>{talent.name}</option>{/each}</select></label>
</div>
{#if Number(editing.is_exotic)}
<input type="hidden" name="exoticTalentId" value={editing.exoticTalent.id}/>
<input type="hidden" name="exoticEffects" value={JSON.stringify(editing.exoticTalent.effects)}/>
<section class="weapon-exotic-talent">
<div class="selection-head"><div><span>DEDICATED EXOTIC TALENT</span><small>OWNED EXCLUSIVELY BY THIS WEAPON · TIER II VALUES REQUIRED</small></div><b>{editing.exoticTalent.id ? 'LINKED RECORD' : 'NEW RECORD'}</b></div>
<div class="form-grid exotic-talent-identity"><label class="field">TALENT NAME<input name="exoticTalentName" bind:value={editing.exoticTalent.name} maxlength="80" required/></label><label class="field">SHARED DESCRIPTION TEMPLATE<textarea name="exoticDescriptionTemplate" bind:value={editing.exoticTalent.descriptionTemplate} placeholder="Hits grant +{1} damage for {2} seconds…" required></textarea><small>Use {'{1}'}, {'{2}'}, and so on for effect values and units.</small></label></div>
<div class="tier-preview"><div><span>TIER I PREVIEW</span><p>{exoticPreview('I') || 'Description preview'}</p></div><div><span>TIER II PREVIEW</span><p>{exoticPreview('II') || 'Description preview'}</p></div></div>
<div class="weapon-exotic-grid">
<div><div class="selection-head"><div><span>ACTIVATION TRIGGERS</span><small>SHARED BETWEEN BOTH TIERS</small></div><b>{editing.exoticTalent.triggerIds.length}</b></div><SearchableAssignment items={data.triggers} fieldName="exoticTriggerIds" bind:selected={editing.exoticTalent.triggerIds}/></div>
<div><div class="selection-head"><div><span>ATTRIBUTE EFFECTS</span><small>ONE ROW PER TEMPLATE PLACEHOLDER</small></div><button type="button" class="row-action" onclick={addExoticEffect} disabled={editing.exoticTalent.effects.length >= 12}>ADD EFFECT +</button></div><div class="exotic-effect-list">{#each editing.exoticTalent.effects as effect: any, index}<div class="exotic-effect-row"><strong>{index + 1}</strong><select bind:value={effect.statId} required><option value="" disabled>SELECT ATTRIBUTE</option>{#each data.stats as stat}<option value={stat.id}>{stat.name} ({stat.unit || 'no unit'})</option>{/each}</select><input type="number" step="any" bind:value={effect.valueI} placeholder="TIER I" required/><input type="number" step="any" bind:value={effect.valueII} placeholder="TIER II" required/><button type="button" aria-label={`Remove exotic effect ${index + 1}`} onclick={() => removeExoticEffect(index)} disabled={editing.exoticTalent.effects.length === 1}>×</button></div>{/each}</div></div>
</div>
</section>
{/if}
<div class="selection-head"><div><span>FIXED WEAPON ATTRIBUTES</span><small>UNCHANGED ACROSS TIERS</small></div><b>BASE HANDLING</b></div>
<div class="weapon-fixed-grid">
<label class="field">RPM<input name="rpm" type="number" min="0" step="1" bind:value={editing.rpm} required/></label>
@@ -102,9 +135,8 @@
<div class="augment-sequence"><div><b>02</b><span>+20% DAMAGE</span></div><div><b>03</b><span>+40% DAMAGE</span></div><div><b>04</b><span>+60% DAMAGE</span></div><div><b>05</b><span>+80% DAMAGE</span></div>{#if Number(editing.is_exotic)}<div class="exotic-step"><b>06</b><span>EXOTIC TALENT → TIER II</span></div>{/if}</div>
{#if data.talent1Options.length === 0 || data.talent2Options.length === 0}<p class="error">Create and classify at least one active Talent 1 and Talent 2 weapon talent before adding weapons.</p>{/if}
{#if data.stats.length === 0}<p class="error">Create an active attribute before configuring tactical augmentation 01.</p>{/if}
{#if Number(editing.is_exotic) && data.exoticTalents.length === 0}<p class="error">Create an active Exotic Weapon talent before adding an exotic weapon.</p>{/if}
{#if form?.weaponError}<p class="error">{form.weaponError}</p>{/if}
<div class="form-actions split-actions"><div>{#if editing.id}<button class="danger" formaction="?/archive" name="id" value={editing.id}>ARCHIVE WEAPON</button>{/if}</div><div><button type="button" class="secondary" onclick={() => editing = null}>CANCEL</button><button class="primary" disabled={data.talent1Options.length === 0 || data.talent2Options.length === 0 || data.stats.length === 0 || (Number(editing.is_exotic) === 1 && data.exoticTalents.length === 0)}>SAVE WEAPON →</button></div></div>
<div class="form-actions split-actions"><div>{#if editing.id}<button class="danger" formaction="?/archive" name="id" value={editing.id}>ARCHIVE WEAPON</button>{/if}</div><div><button type="button" class="secondary" onclick={() => editing = null}>CANCEL</button><button class="primary" disabled={data.talent1Options.length === 0 || data.talent2Options.length === 0 || data.stats.length === 0}>SAVE WEAPON →</button></div></div>
</PendingForm>
</div>
{/if}