Adding skills & isac talent updates
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
id: string;
|
||||
name: string;
|
||||
unit?: string;
|
||||
detail?: string;
|
||||
disabled?: boolean;
|
||||
disabledReason?: string;
|
||||
pending?: boolean;
|
||||
proposerName?: string;
|
||||
iconUrl?: string | null;
|
||||
@@ -52,8 +55,14 @@
|
||||
const visible = $derived(presentation === 'list' ? available.slice(0, 30) : available);
|
||||
const selectedItems = $derived(selectedIds.map((id) => items.find((item) => item.id === id)).filter((item): item is Item => Boolean(item)));
|
||||
const unitLabel = (item: Item) => item.unit === undefined ? '' : item.unit || 'FLAT';
|
||||
const accessibleName = (item: Item) => unitLabel(item) ? `${item.name}, unit ${unitLabel(item)}` : item.name;
|
||||
const accessibleName = (item: Item) => [
|
||||
item.name,
|
||||
unitLabel(item) ? `unit ${unitLabel(item)}` : '',
|
||||
item.detail ?? '',
|
||||
item.disabledReason ?? ''
|
||||
].filter(Boolean).join(', ');
|
||||
const toggle = (id: string) => {
|
||||
if (items.find((item) => item.id === id)?.disabled && !selectedIds.includes(id)) return;
|
||||
if (mode === 'single') {
|
||||
value = value === id ? '' : id;
|
||||
onvaluechange?.(value);
|
||||
@@ -77,7 +86,7 @@
|
||||
{#each selectedItems as item}
|
||||
<button type="button" class:pending-reference={item.pending} onclick={() => toggle(item.id)} aria-label={`Remove ${accessibleName(item)}`}>
|
||||
{#if item.showIcon || item.iconUrl}<ItemIcon src={item.iconUrl} alt="" className="assignment-item-icon assignment-chip-icon"/>{/if}
|
||||
<span>{item.name}{#if unitLabel(item)}<small class="assignment-item-unit">UNIT · {unitLabel(item)}</small>{/if}{#if item.pending}<small>PENDING</small>{/if}</span><b>×</b>
|
||||
<span>{item.name}{#if unitLabel(item)}<small class="assignment-item-unit">UNIT · {unitLabel(item)}</small>{/if}{#if item.detail}<small>{item.detail}</small>{/if}{#if item.pending}<small>PENDING</small>{/if}</span><b>×</b>
|
||||
</button>
|
||||
{:else}
|
||||
<small>{emptySelectionLabel}</small>
|
||||
@@ -89,12 +98,12 @@
|
||||
{/if}
|
||||
<div class="assignment-results" role="list">
|
||||
{#each visible as item}
|
||||
<button type="button" class:selected={selectedIds.includes(item.id)} class:pending-reference={item.pending} class:with-icon={Boolean(item.showIcon || item.iconUrl)} class:visual-grid-option={presentation === 'visual-grid'} class:toggle-grid-option={presentation === 'toggle-grid'} aria-pressed={selectedIds.includes(item.id)} aria-label={presentation !== 'list' ? `${accessibleName(item)}, ${selectedIds.includes(item.id) ? 'assigned' : 'not assigned'}` : undefined} data-tooltip={presentation === 'visual-grid' ? item.name : undefined} onclick={() => toggle(item.id)}>
|
||||
<button type="button" class:selected={selectedIds.includes(item.id)} class:pending-reference={item.pending} class:with-icon={Boolean(item.showIcon || item.iconUrl)} class:visual-grid-option={presentation === 'visual-grid'} class:toggle-grid-option={presentation === 'toggle-grid'} aria-pressed={selectedIds.includes(item.id)} aria-label={presentation !== 'list' ? `${accessibleName(item)}, ${selectedIds.includes(item.id) ? 'assigned' : 'not assigned'}` : undefined} data-tooltip={presentation === 'visual-grid' ? item.name : undefined} disabled={item.disabled && !selectedIds.includes(item.id)} title={item.disabledReason} onclick={() => toggle(item.id)}>
|
||||
{#if presentation === 'visual-grid' || item.showIcon || item.iconUrl}<ItemIcon src={item.iconUrl} alt="" className={`assignment-item-icon ${presentation === 'visual-grid' ? 'assignment-grid-icon' : 'assignment-result-icon'}`}/>{/if}
|
||||
{#if presentation === 'toggle-grid'}
|
||||
<span>{item.name}</span>
|
||||
{:else if presentation === 'list'}
|
||||
<span>{item.name}{#if unitLabel(item)}<small class="assignment-item-unit">UNIT · {unitLabel(item)}</small>{/if}{#if item.pending}<small>PENDING · {item.proposerName ?? 'CONTRIBUTOR'}</small>{/if}</span><b>{selectedIds.includes(item.id) ? (mode === 'single' ? 'SELECTED' : 'ASSIGNED') : (mode === 'single' ? 'SELECT →' : 'ADD +')}</b>
|
||||
<span>{item.name}{#if unitLabel(item)}<small class="assignment-item-unit">UNIT · {unitLabel(item)}</small>{/if}{#if item.detail}<small>{item.detail}</small>{/if}{#if item.pending}<small>PENDING · {item.proposerName ?? 'CONTRIBUTOR'}</small>{/if}</span><b>{item.disabled && !selectedIds.includes(item.id) ? 'UNAVAILABLE' : selectedIds.includes(item.id) ? (mode === 'single' ? 'SELECTED' : 'ASSIGNED') : (mode === 'single' ? 'SELECT →' : 'ADD +')}</b>
|
||||
{/if}
|
||||
</button>
|
||||
{:else}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
meta: string;
|
||||
summary: string;
|
||||
iconUrl?: string;
|
||||
issue?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
@@ -34,6 +35,12 @@
|
||||
active === cell.id
|
||||
|| (cell.id.startsWith('ability:') && active.startsWith('chip:')
|
||||
&& Math.floor(Number(active.split(':')[1]) / 3) === Number(cell.id.split(':')[1]));
|
||||
|
||||
const issueLabel = (issue: string) => {
|
||||
if (/not configured|required/i.test(issue)) return 'ACTION REQUIRED';
|
||||
if (/incomplete/i.test(issue)) return 'DATA INCOMPLETE';
|
||||
return 'CHECK CONFIGURATION';
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="loadout-matrix" aria-label="Loadout matrix">
|
||||
@@ -48,19 +55,20 @@
|
||||
type="button"
|
||||
class="doctrine-cell"
|
||||
class:active={selected(doctrine)}
|
||||
class:needs-attention={Boolean(doctrine.issue)}
|
||||
aria-pressed={selected(doctrine)}
|
||||
onclick={() => onselect(doctrine.id)}
|
||||
>
|
||||
<ItemIcon src={doctrine.iconUrl} alt="" className="matrix-icon" />
|
||||
<span><small>{doctrine.label}</small><strong>{doctrine.name}</strong><em>{doctrine.summary}</em></span>
|
||||
<span><small>{doctrine.label}</small><strong>{doctrine.name}</strong><em>{doctrine.summary}</em>{#if doctrine.issue}<mark title={doctrine.issue} aria-hidden="true">{issueLabel(doctrine.issue)}</mark><span class="sr-only">{doctrine.issue}</span>{/if}</span>
|
||||
<i aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="supply-line"><span>SUPPLIES 3 ABILITIES</span></div>
|
||||
<div class="ability-cells">
|
||||
{#each abilities as cell}
|
||||
<button type="button" class:active={selected(cell)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<button type="button" class:active={selected(cell)} class:needs-attention={Boolean(cell.issue)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<ItemIcon src={cell.iconUrl} alt="" className="matrix-icon" />
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em></span>
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em>{#if cell.issue}<mark title={cell.issue} aria-hidden="true">{issueLabel(cell.issue)}</mark><span class="sr-only">{cell.issue}</span>{/if}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -70,9 +78,9 @@
|
||||
<small class="section-label">WEAPONS · PRIMARY + SECONDARY</small>
|
||||
<div class="equipment-cells">
|
||||
{#each weapons as cell}
|
||||
<button type="button" class:active={selected(cell)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<button type="button" class:active={selected(cell)} class:needs-attention={Boolean(cell.issue)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<ItemIcon src={cell.iconUrl} alt="" className="matrix-icon" />
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em></span>
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em>{#if cell.issue}<mark title={cell.issue} aria-hidden="true">{issueLabel(cell.issue)}</mark><span class="sr-only">{cell.issue}</span>{/if}</span>
|
||||
<i aria-hidden="true"></i>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -83,9 +91,9 @@
|
||||
<small class="section-label">GEAR · 6 POSITIONS</small>
|
||||
<div class="equipment-cells gear-cells">
|
||||
{#each gear as cell}
|
||||
<button type="button" class:active={selected(cell)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<button type="button" class:active={selected(cell)} class:needs-attention={Boolean(cell.issue)} aria-pressed={selected(cell)} onclick={() => onselect(cell.id)}>
|
||||
<ItemIcon src={cell.iconUrl} alt="" className="matrix-icon" />
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em></span>
|
||||
<span><small>{cell.label}</small><strong>{cell.name}</strong><em>{cell.summary}</em>{#if cell.issue}<mark title={cell.issue} aria-hidden="true">{issueLabel(cell.issue)}</mark><span class="sr-only">{cell.issue}</span>{/if}</span>
|
||||
<i aria-hidden="true"></i>
|
||||
</button>
|
||||
{/each}
|
||||
@@ -94,9 +102,9 @@
|
||||
|
||||
<div class="matrix-section protocol-section">
|
||||
<small class="section-label">AGENT SYSTEM · ONE OS PROTOCOL</small>
|
||||
<button type="button" class:active={selected(protocol)} aria-pressed={selected(protocol)} onclick={() => onselect(protocol.id)}>
|
||||
<button type="button" class:active={selected(protocol)} class:needs-attention={Boolean(protocol.issue)} aria-pressed={selected(protocol)} onclick={() => onselect(protocol.id)}>
|
||||
<ItemIcon src={protocol.iconUrl} alt="" className="matrix-icon" />
|
||||
<span><small>{protocol.label}</small><strong>{protocol.name}</strong><em>{protocol.summary}</em></span>
|
||||
<span><small>{protocol.label}</small><strong>{protocol.name}</strong><em>{protocol.summary}</em>{#if protocol.issue}<mark title={protocol.issue} aria-hidden="true">{issueLabel(protocol.issue)}</mark><span class="sr-only">{protocol.issue}</span>{/if}</span>
|
||||
<i aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -113,9 +121,10 @@
|
||||
:global(.matrix-icon){width:30px;height:30px;object-fit:contain}
|
||||
button>span{min-width:0;display:grid;gap:2px}button small,button em{overflow:hidden;color:var(--shd-text-dim);font-size:9px;font-style:normal;letter-spacing:.04em;text-overflow:ellipsis;white-space:nowrap}button strong{overflow:hidden;font-size:11px;text-overflow:ellipsis;white-space:nowrap}button>i{align-self:stretch;background:var(--quality-high-end)}
|
||||
button:hover,button:focus-visible{border-color:var(--shd-border-strong);background:var(--shd-surface-active);outline:none}button.active{border-color:var(--shd-builder-selection-border);background:var(--shd-builder-selection);box-shadow:inset 2px 0 var(--shd-orange)}
|
||||
button.needs-attention{border-bottom-color:color-mix(in srgb,var(--shd-danger) 68%,var(--shd-border-soft))}button.needs-attention>i{background:var(--shd-danger)}button mark{width:fit-content;max-width:100%;overflow:hidden;margin-top:2px;padding:2px 4px;border:1px solid color-mix(in srgb,var(--shd-danger) 58%,transparent);background:#1d1212;color:var(--shd-danger);font-size:8px;font-weight:900;letter-spacing:.07em;line-height:1.2;text-overflow:ellipsis;text-transform:uppercase;white-space:nowrap}
|
||||
.supply-line{height:22px;display:grid;place-items:center;background:linear-gradient(90deg,transparent 49.7%,var(--shd-orange) 49.7% 50.3%,transparent 50.3%)}.supply-line span{padding:0 6px;background:var(--shd-surface);color:var(--shd-orange);font-size:8px;font-weight:900;letter-spacing:.08em}
|
||||
.ability-cells{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:3px}.ability-cells button{min-height:68px;grid-template-columns:25px minmax(0,1fr);padding:6px}.ability-cells :global(.matrix-icon){width:24px;height:24px}
|
||||
.equipment-cells{display:grid;grid-template-columns:1fr 1fr;gap:3px}.equipment-cells button{min-height:69px}.protocol-section>button{width:100%;min-height:68px}
|
||||
.equipment-cells{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:3px}.equipment-cells button{min-height:69px}.protocol-section>button{width:100%;min-height:68px}
|
||||
button:focus-visible{outline:2px solid var(--shd-focus-ring);outline-offset:2px}
|
||||
@media(max-width:1240px){.ability-cells{grid-template-columns:1fr}.ability-cells button{min-height:48px}}
|
||||
@media(max-width:800px){.ability-cells{grid-template-columns:repeat(3,minmax(0,1fr))}}
|
||||
|
||||
@@ -3,10 +3,12 @@
|
||||
|
||||
let {
|
||||
result,
|
||||
abilityNames = {}
|
||||
abilityNames = {},
|
||||
resolverHint = 'Complete the required Specialization ability data.'
|
||||
}: {
|
||||
result: SkillCalculation;
|
||||
abilityNames?: Record<string, string>;
|
||||
resolverHint?: string;
|
||||
} = $props();
|
||||
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
|
||||
</script>
|
||||
@@ -17,10 +19,10 @@
|
||||
<b>{result.status === 'OK' ? 'CALCULATED' : 'INSUFFICIENT DATA'}</b>
|
||||
</header>
|
||||
{#if result.status === 'INSUFFICIENT_DATA'}
|
||||
<div class="missing">
|
||||
<strong>Skill calculation unavailable</strong>
|
||||
<span>ISAC requires the following data:</span>
|
||||
<ul>{#each result.missing as item}<li>{item.replaceAll(':', ' · ')}</li>{/each}</ul>
|
||||
<div class="missing" role="status">
|
||||
<strong>SKILL OUTPUT BLOCKED</strong>
|
||||
<span>{result.missing[0]?.replaceAll(':', ' · ') ?? 'Missing ability data'}</span>
|
||||
<small>{result.missing.length > 1 ? `+${result.missing.length - 1} additional requirement${result.missing.length === 2 ? '' : 's'}. ` : ''}{resolverHint}</small>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="combined">
|
||||
@@ -39,19 +41,19 @@
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
<footer>SKILLS CAN CRIT AND MULTI-SHOT · SKILLS DO NOT HEADSHOT · ENCOUNTER TRIGGERS FOLLOW THE PRIMARY WEAPON TIMELINE</footer>
|
||||
{#if result.status === 'OK'}<footer>SKILLS CAN CRIT AND MULTI-SHOT · SKILLS DO NOT HEADSHOT · ENCOUNTER TRIGGERS FOLLOW THE PRIMARY WEAPON TIMELINE</footer>{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.skill-calculation{display:grid;gap:var(--shd-gap-md);padding:var(--shd-gap-md);border:1px solid var(--shd-border);border-top:2px solid var(--shd-info);background:var(--shd-surface)}
|
||||
.skill-calculation.incomplete{border-top-color:var(--shd-danger)}
|
||||
.skill-calculation.incomplete{gap:6px;padding:8px;border-top-color:var(--shd-danger)}
|
||||
header{display:flex;justify-content:space-between;align-items:start;gap:var(--shd-gap-md)}
|
||||
header p,h3{margin:0}header p{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1.1px}h3{text-transform:uppercase;font-size:var(--shd-type-body);letter-spacing:.7px}
|
||||
header>b{padding:5px 7px;border:1px solid var(--shd-info);color:var(--shd-info);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.incomplete header>b{border-color:var(--shd-danger);color:var(--shd-danger)}
|
||||
header>b{padding:5px 7px;border:1px solid var(--shd-info);color:var(--shd-info);font-size:var(--shd-type-telemetry);letter-spacing:.8px}.incomplete header h3{font-size:10px}.incomplete header>b{padding:3px 5px;border-color:var(--shd-danger);color:var(--shd-danger);font-size:8px}
|
||||
.combined{display:grid;grid-template-columns:repeat(3,1fr);border:1px solid var(--shd-border)}.combined span{display:grid;gap:4px;padding:9px;border-left:1px solid var(--shd-border)}.combined span:first-child{border-left:0}.combined b{color:var(--shd-info);font-size:20px}
|
||||
small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
|
||||
.ability-results{display:grid;border:1px solid var(--shd-border)}.ability-results>div{display:grid;grid-template-columns:minmax(140px,1fr) repeat(3,auto);gap:var(--shd-gap-sm);padding:7px 8px;border-top:1px solid var(--shd-border);align-items:center}.ability-results>div:first-child{border-top:0}.ability-results>div.disabled{opacity:.55}.ability-results span{color:var(--shd-orange);font-size:var(--shd-type-telemetry);font-weight:900}
|
||||
.missing{display:grid;gap:4px;padding:10px;border:1px solid var(--shd-danger);border-left-width:2px;background:#1d1212;color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.missing strong{color:var(--shd-danger);text-transform:uppercase}.missing ul{margin:2px 0 0;padding-left:18px}
|
||||
.missing{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:3px 8px;padding:7px 8px;border:1px solid var(--shd-danger);border-left-width:2px;background:#171111}.missing strong{color:var(--shd-danger);font-size:9px;letter-spacing:.8px;white-space:nowrap}.missing>span{overflow:hidden;color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:800;text-overflow:ellipsis;white-space:nowrap}.missing>small{grid-column:1/-1;color:var(--shd-text-dim);font-size:9px;letter-spacing:.35px}
|
||||
footer{color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
|
||||
@media(max-width:700px){header,.combined{display:grid;grid-template-columns:1fr}.combined span{border-left:0;border-top:1px solid var(--shd-border)}.combined span:first-child{border-top:0}.ability-results>div{grid-template-columns:1fr 1fr}.ability-results strong{grid-column:1/-1}}
|
||||
@media(max-width:700px){header,.combined{display:grid;grid-template-columns:1fr}.skill-calculation.incomplete>header{display:flex}.combined span{border-left:0;border-top:1px solid var(--shd-border)}.combined span:first-child{border-top:0}.ability-results>div{grid-template-columns:1fr 1fr}.ability-results strong{grid-column:1/-1}}
|
||||
</style>
|
||||
|
||||
@@ -28,9 +28,19 @@
|
||||
revisionLabel?: string;
|
||||
};
|
||||
|
||||
let { result }: { result: WeaponCalculationResult } = $props();
|
||||
let {
|
||||
result,
|
||||
resolverHint = 'Complete the required build configuration.'
|
||||
}: {
|
||||
result: WeaponCalculationResult;
|
||||
resolverHint?: string;
|
||||
} = $props();
|
||||
const number = new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 });
|
||||
function format(value: number | undefined) { return value === undefined ? '—' : number.format(value); }
|
||||
const loadoutRequirement = (item: string) =>
|
||||
/weapon$| gear$|OS Protocol|Specialization focus|focus abilities|Skill Chips?$/i.test(item);
|
||||
const systemRequirements = $derived((result.missing ?? []).filter((item) => !loadoutRequirement(item)));
|
||||
const loadoutRequirementCount = $derived((result.missing ?? []).length - systemRequirements.length);
|
||||
</script>
|
||||
|
||||
<article class:incomplete={result.status === 'insufficient'} class="weapon-calculation" aria-labelledby={`${result.weaponLabel}-calculation`} aria-live="polite">
|
||||
@@ -39,7 +49,11 @@
|
||||
<b>{result.status === 'complete' ? 'CALCULATED' : 'INSUFFICIENT DATA'}</b>
|
||||
</header>
|
||||
{#if result.status === 'insufficient'}
|
||||
<div class="missing"><strong>Calculation unavailable</strong><span>Complete the following required data:</span><ul>{#each result.missing ?? [] as item}<li>{item}</li>{/each}</ul></div>
|
||||
<div class="missing" role="status">
|
||||
<strong>OUTPUT BLOCKED</strong>
|
||||
<span>{systemRequirements[0] ?? `${loadoutRequirementCount} loadout requirement${loadoutRequirementCount === 1 ? '' : 's'}`}</span>
|
||||
<small>{loadoutRequirementCount ? `${loadoutRequirementCount} loadout requirement${loadoutRequirementCount === 1 ? '' : 's'} remain. ${resolverHint}` : resolverHint}</small>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="damage-grid" aria-label={`${result.weaponName} damage estimates`}>
|
||||
<span><small>BODY HIT</small><b>{format(result.bodyHit)}</b></span><span><small>CRITICAL BODY</small><b>{format(result.criticalBodyHit)}</b></span>
|
||||
@@ -63,14 +77,14 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
<footer><span>MODELED, BEFORE ENEMY MITIGATION</span>{#if result.revisionLabel}<span>{result.revisionLabel}</span>{/if}</footer>
|
||||
{#if result.status === 'complete'}<footer><span>MODELED, BEFORE ENEMY MITIGATION</span>{#if result.revisionLabel}<span>{result.revisionLabel}</span>{/if}</footer>{/if}
|
||||
</article>
|
||||
|
||||
<style>
|
||||
.weapon-calculation{display:grid;gap:var(--shd-gap-md);padding:var(--shd-gap-md);border:1px solid var(--shd-border);border-top:2px solid var(--shd-orange);background:var(--shd-surface)}
|
||||
.weapon-calculation.incomplete{border-top-color:var(--shd-danger)}header{display:flex;justify-content:space-between;gap:var(--shd-gap-md);align-items:start}header p{margin:0 0 3px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1.1px}h3{margin:0;font-size:var(--shd-type-body);letter-spacing:.7px;text-transform:uppercase}header>b{padding:5px 7px;border:1px solid #74552a;background:#1d180e;color:var(--shd-orange);font-size:var(--shd-type-telemetry);letter-spacing:.8px;white-space:nowrap}.incomplete header>b{border-color:#563734;background:#1d1212;color:var(--shd-danger)}
|
||||
.damage-grid,.dps-grid{display:grid;border:1px solid var(--shd-border)}.damage-grid{grid-template-columns:repeat(2,1fr)}.dps-grid{grid-template-columns:repeat(3,1fr)}.damage-grid span,.dps-grid span{display:grid;gap:4px;padding:9px;border-left:1px solid var(--shd-border);border-top:1px solid var(--shd-border)}.damage-grid span:nth-child(odd),.dps-grid span:first-child{border-left:0}.damage-grid span:nth-child(-n+2){border-top:0}.dps-grid b{color:var(--shd-orange);font-size:20px}.damage-grid b{font-size:var(--shd-type-body)}small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.missing{display:grid;gap:4px;padding:10px;border:1px solid #563734;border-left:2px solid var(--shd-danger);background:#1d1212;color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.missing strong{color:var(--shd-danger);text-transform:uppercase;font-size:var(--shd-type-telemetry);letter-spacing:.8px}.missing ul{margin:2px 0 0;padding-left:18px}.missing li+li{margin-top:2px}footer{display:flex;justify-content:space-between;gap:var(--shd-gap-sm);color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
|
||||
.weapon-calculation.incomplete{gap:6px;padding:8px;border-top-color:var(--shd-danger)}header{display:flex;justify-content:space-between;gap:var(--shd-gap-md);align-items:start}header p{margin:0 0 3px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:1.1px}h3{margin:0;font-size:var(--shd-type-body);letter-spacing:.7px;text-transform:uppercase}header>b{padding:5px 7px;border:1px solid #74552a;background:#1d180e;color:var(--shd-orange);font-size:var(--shd-type-telemetry);letter-spacing:.8px;white-space:nowrap}.incomplete header h3{font-size:10px}.incomplete header>b{padding:3px 5px;border-color:#563734;background:#1d1212;color:var(--shd-danger);font-size:8px}
|
||||
.damage-grid,.dps-grid{display:grid;border:1px solid var(--shd-border)}.damage-grid{grid-template-columns:repeat(2,1fr)}.dps-grid{grid-template-columns:repeat(3,1fr)}.damage-grid span,.dps-grid span{display:grid;gap:4px;padding:9px;border-left:1px solid var(--shd-border);border-top:1px solid var(--shd-border)}.damage-grid span:nth-child(odd),.dps-grid span:first-child{border-left:0}.damage-grid span:nth-child(-n+2){border-top:0}.dps-grid b{color:var(--shd-orange);font-size:20px}.damage-grid b{font-size:var(--shd-type-body)}small{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.missing{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:3px 8px;padding:7px 8px;border:1px solid #563734;border-left:2px solid var(--shd-danger);background:#171111;color:var(--shd-text-muted)}.missing strong{color:var(--shd-danger);font-size:9px;letter-spacing:.8px;white-space:nowrap}.missing>span{overflow:hidden;color:var(--shd-text);font-size:var(--shd-type-telemetry);font-weight:800;text-overflow:ellipsis;white-space:nowrap}.missing>small{grid-column:1/-1;color:var(--shd-text-dim);font-size:9px;font-weight:700;letter-spacing:.35px}footer{display:flex;justify-content:space-between;gap:var(--shd-gap-sm);color:var(--shd-text-dim);font-size:var(--shd-type-telemetry);font-weight:800;letter-spacing:.6px}
|
||||
.stack-telemetry{display:grid;border:1px solid var(--shd-model-border);background:var(--shd-model-surface)}.stack-telemetry>header{display:flex;align-items:center;justify-content:space-between;padding:6px 8px;border-bottom:1px solid var(--shd-model-border);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}.stack-telemetry>header span{color:var(--shd-info)}.stack-telemetry>header b{color:var(--shd-text-muted)}.stack-telemetry>div{display:grid;grid-template-columns:minmax(120px,1.2fr) .45fr .7fr .8fr .9fr .85fr .8fr;gap:7px;align-items:center;padding:7px 8px;border-top:1px solid var(--shd-border-soft);font-size:var(--shd-type-telemetry)}.stack-telemetry>div:first-of-type{border-top:0}.stack-telemetry>div>span,.stack-telemetry>div>small,.stack-telemetry>div>em,.stack-telemetry>div>b{font-size:inherit;font-style:normal;letter-spacing:.5px}.stack-telemetry>div>span{color:var(--shd-orange);font-weight:900}.stack-telemetry>div>em{color:var(--shd-text-muted)}.stack-telemetry>div>b{color:var(--shd-danger);text-align:right}.stack-telemetry>div>b.maintained{color:var(--shd-success)}
|
||||
@media(max-width:820px){.stack-telemetry>div{grid-template-columns:1fr 1fr}.stack-telemetry>div>strong{grid-column:1/-1}.stack-telemetry>div>b{text-align:left}}
|
||||
@media(max-width:620px){header{display:grid}.dps-grid{grid-template-columns:1fr}.dps-grid span{border-left:0;border-top:1px solid var(--shd-border)}.dps-grid span:first-child{border-top:0}footer{display:grid}.damage-grid span{min-height:44px}}
|
||||
@media(max-width:620px){header{display:grid}.weapon-calculation.incomplete>header{display:flex}.dps-grid{grid-template-columns:1fr}.dps-grid span{border-left:0;border-top:1px solid var(--shd-border)}.dps-grid span:first-child{border-top:0}footer{display:grid}.damage-grid span{min-height:44px}}
|
||||
</style>
|
||||
|
||||
@@ -7,11 +7,21 @@
|
||||
import type { AiMechanicContext } from '$lib/ai/mechanic-authoring';
|
||||
import {
|
||||
blankConditionalMechanicDraft,
|
||||
currentHealthPercentBandPredicate,
|
||||
eventFieldComparisonPredicate,
|
||||
parseConditionalMechanicDraft,
|
||||
readCurrentHealthPercentBand,
|
||||
readEventFieldComparison,
|
||||
readStatePoolThreshold,
|
||||
statePoolThresholdPredicate,
|
||||
type ConditionalMechanicClauseDraft,
|
||||
type ConditionalMechanicDraftV1,
|
||||
type ConditionalMechanicOutcomeDraft,
|
||||
type ConditionalMechanicPoolDraft
|
||||
type ConditionalMechanicPoolDraft,
|
||||
type EditablePredicateScalar,
|
||||
type EventFieldComparison,
|
||||
type PredicateComparisonOperator,
|
||||
type StatePoolThresholdComparison
|
||||
} from '$lib/calculations/mechanic-editor';
|
||||
import {
|
||||
formatMechanicProgram,
|
||||
@@ -59,6 +69,11 @@
|
||||
let recipeCount = $state(9);
|
||||
let recipeWindow = $state(3);
|
||||
let recipeDistance = $state(10);
|
||||
let recipeHealthBands = $state([
|
||||
{ thresholdPercent: 25, outcomeKey: '15-percent-reduction' },
|
||||
{ thresholdPercent: 50, outcomeKey: '12-percent-reduction' },
|
||||
{ thresholdPercent: 75, outcomeKey: '9-percent-reduction' }
|
||||
]);
|
||||
let replaceConfirmation = $state(false);
|
||||
let valueModelOverrides = $state<Record<string, OutcomeValueModel>>({});
|
||||
|
||||
@@ -90,17 +105,32 @@
|
||||
{ value: 'direct_damage', label: 'DAMAGE' },
|
||||
{ value: 'restoration', label: 'RESTORE' },
|
||||
{ value: 'resource_cost', label: 'COST' },
|
||||
{ value: 'weapon_reload', label: 'WEAPON RELOAD' },
|
||||
{ value: 'marker', label: 'MARKER' }
|
||||
];
|
||||
const scopeOptions = $derived([
|
||||
{ value: 'self', label: 'SELF' },
|
||||
{ value: 'equipped_weapon', label: 'WEAPON' },
|
||||
{ value: 'inactive_weapon', label: 'INACTIVE WEAPON' },
|
||||
{ value: 'target', label: 'TARGET' },
|
||||
{ value: 'allies', label: 'ALLIES' },
|
||||
{ value: 'self_and_allies', label: 'SELF + ALLIES' },
|
||||
...(allowAssignedAbility ? [{ value: 'assigned_ability', label: 'ABILITY' }] : []),
|
||||
{ value: 'all_skills', label: 'ALL SKILLS' }
|
||||
]);
|
||||
const predicateOperatorOptions = [
|
||||
{ value: 'eq', label: '=' },
|
||||
{ value: 'ne', label: '≠' },
|
||||
{ value: 'gt', label: '>' },
|
||||
{ value: 'gte', label: '≥' },
|
||||
{ value: 'lt', label: '<' },
|
||||
{ value: 'lte', label: '≤' }
|
||||
];
|
||||
const predicateScalarTypeOptions = [
|
||||
{ value: 'string', label: 'TEXT' },
|
||||
{ value: 'number', label: 'NUMBER' },
|
||||
{ value: 'boolean', label: 'BOOLEAN' }
|
||||
];
|
||||
|
||||
const slug = (value: string) => value.toLocaleLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
@@ -114,10 +144,119 @@
|
||||
if (recipeKey === 'consecutive_same_target_hits') return { hitsRequired: recipeCount };
|
||||
if (recipeKey === 'continuous_firing_duration') return { seconds: recipeWindow };
|
||||
if (recipeKey === 'target_hit_within_distance' || recipeKey === 'within_distance') return { meters: recipeDistance };
|
||||
if (recipeKey === 'health_threshold_bands') return { bands: [{ thresholdPercent: 50, outcomeKey: 'active' }] };
|
||||
if (recipeKey === 'health_threshold_bands') return { bands: recipeHealthBands };
|
||||
return {};
|
||||
}
|
||||
|
||||
function conditionModel(clause: ConditionalMechanicClauseDraft) {
|
||||
if (!clause.if || clause.if.kind === 'true') return 'always';
|
||||
if (readCurrentHealthPercentBand(clause.if)) return 'current_health_band';
|
||||
if (readEventFieldComparison(clause.if)) return 'event_field';
|
||||
if (readStatePoolThreshold(clause.if)) return 'state_pool_threshold';
|
||||
return 'advanced';
|
||||
}
|
||||
|
||||
function conditionOptions(clause: ConditionalMechanicClauseDraft) {
|
||||
const options = [
|
||||
{ value: 'always', label: 'ALWAYS' },
|
||||
{ value: 'current_health_band', label: 'HEALTH BAND' },
|
||||
{ value: 'event_field', label: 'EVENT FIELD' },
|
||||
{ value: 'state_pool_threshold', label: 'STATE THRESHOLD', disabled: !rule.pools.length }
|
||||
];
|
||||
return conditionModel(clause) === 'advanced'
|
||||
? [
|
||||
...options.map((option) => ({ ...option, disabled: true })),
|
||||
{ value: 'advanced', label: 'ADVANCED', disabled: true }
|
||||
]
|
||||
: options;
|
||||
}
|
||||
|
||||
function setConditionModel(clauseIndex: number, model: string) {
|
||||
if (model === 'always') updateClause(clauseIndex, { if: { kind: 'true' } });
|
||||
if (model === 'current_health_band') {
|
||||
updateClause(clauseIndex, {
|
||||
if: currentHealthPercentBandPredicate({ maximum: 75 })
|
||||
});
|
||||
}
|
||||
if (model === 'event_field') {
|
||||
updateClause(clauseIndex, {
|
||||
if: eventFieldComparisonPredicate({
|
||||
path: 'values.state',
|
||||
operator: 'eq',
|
||||
value: 'moving'
|
||||
})
|
||||
});
|
||||
}
|
||||
if (model === 'state_pool_threshold' && rule.pools[0]) {
|
||||
updateClause(clauseIndex, {
|
||||
if: statePoolThresholdPredicate({
|
||||
pool: rule.pools[0].key,
|
||||
operator: 'gt',
|
||||
value: 0
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateHealthBand(
|
||||
clauseIndex: number,
|
||||
boundary: 'minimum' | 'maximum',
|
||||
rawValue: string
|
||||
) {
|
||||
const current = readCurrentHealthPercentBand(rule.clauses[clauseIndex].if) ?? { maximum: 75 };
|
||||
const numeric = rawValue === '' ? undefined : Number(rawValue);
|
||||
const next = {
|
||||
...(boundary === 'minimum'
|
||||
? numeric === undefined ? {} : { minimum: numeric }
|
||||
: current.minimum === undefined ? {} : { minimum: current.minimum }),
|
||||
maximum: boundary === 'maximum' ? numeric ?? current.maximum : current.maximum
|
||||
};
|
||||
updateClause(clauseIndex, { if: currentHealthPercentBandPredicate(next) });
|
||||
}
|
||||
|
||||
function updateEventFieldComparison(
|
||||
clauseIndex: number,
|
||||
patch: Partial<EventFieldComparison>
|
||||
) {
|
||||
const current = readEventFieldComparison(rule.clauses[clauseIndex].if) ?? {
|
||||
path: 'values.state',
|
||||
operator: 'eq',
|
||||
value: 'moving'
|
||||
} as const;
|
||||
updateClause(clauseIndex, {
|
||||
if: eventFieldComparisonPredicate({ ...current, ...patch })
|
||||
});
|
||||
}
|
||||
|
||||
function predicateScalarType(value: EditablePredicateScalar) {
|
||||
return typeof value;
|
||||
}
|
||||
|
||||
function setEventFieldScalarType(clauseIndex: number, type: string) {
|
||||
const current = readEventFieldComparison(rule.clauses[clauseIndex].if);
|
||||
if (!current) return;
|
||||
const value: EditablePredicateScalar = type === 'number'
|
||||
? (typeof current.value === 'number' ? current.value : 0)
|
||||
: type === 'boolean'
|
||||
? (typeof current.value === 'boolean' ? current.value : true)
|
||||
: typeof current.value === 'string' ? current.value : String(current.value);
|
||||
updateEventFieldComparison(clauseIndex, { value });
|
||||
}
|
||||
|
||||
function updateStatePoolThreshold(
|
||||
clauseIndex: number,
|
||||
patch: Partial<StatePoolThresholdComparison>
|
||||
) {
|
||||
const current = readStatePoolThreshold(rule.clauses[clauseIndex].if) ?? {
|
||||
pool: rule.pools[0]?.key ?? '',
|
||||
operator: 'gt',
|
||||
value: 0
|
||||
} as const;
|
||||
updateClause(clauseIndex, {
|
||||
if: statePoolThresholdPredicate({ ...current, ...patch })
|
||||
});
|
||||
}
|
||||
|
||||
function applyRecipe(replace = false) {
|
||||
if (!recipeKey) return;
|
||||
const selectedRecipeKey = recipeKey;
|
||||
@@ -647,6 +786,19 @@
|
||||
{#if ['target_hit_within_distance', 'within_distance'].includes(recipeKey)}
|
||||
<label class="field">METERS<input type="number" min="0" step="any" bind:value={recipeDistance}/></label>
|
||||
{/if}
|
||||
{#if recipeKey === 'health_threshold_bands'}
|
||||
<div class="health-band-config">
|
||||
<p>Define strict “below Health” bands from strongest to weakest. Exact thresholds enter the next weaker band.</p>
|
||||
{#each recipeHealthBands as band, index}
|
||||
<div>
|
||||
<label class="field">BELOW · HEALTH %<input type="number" min="0.01" max="100" step="any" bind:value={band.thresholdPercent}/></label>
|
||||
<label class="field">BAND LABEL<input maxlength="80" bind:value={band.outcomeKey}/></label>
|
||||
<button type="button" class="remove" onclick={() => recipeHealthBands = recipeHealthBands.filter((_, current) => current !== index)} disabled={recipeHealthBands.length === 1}>REMOVE</button>
|
||||
</div>
|
||||
{/each}
|
||||
<button type="button" class="secondary compact-action" onclick={() => recipeHealthBands = [...recipeHealthBands,{thresholdPercent:Math.min(100,(recipeHealthBands.at(-1)?.thresholdPercent ?? 0) + 25),outcomeKey:`health-band-${recipeHealthBands.length + 1}`}]} disabled={recipeHealthBands.length >= 5}>ADD BAND +</button>
|
||||
</div>
|
||||
{/if}
|
||||
<button type="button" class="secondary" disabled={!recipeKey} onclick={() => applyRecipe(false)}>ADD RECIPE BRANCH +</button>
|
||||
{#if rule.clauses.length && recipeKey}
|
||||
<button type="button" class="danger-quiet" onclick={() => replaceConfirmation = true}>REPLACE PROGRAM</button>
|
||||
@@ -669,7 +821,13 @@
|
||||
<label class="field">INITIAL<input type="number" value={pool.initial} oninput={(event) => updatePool(index,{initial:Number(event.currentTarget.value)})}/></label>
|
||||
<label class="field">MAX<input type="number" min="1" value={pool.max ?? 1} oninput={(event) => updatePool(index,{max:Number(event.currentTarget.value)})}/></label>
|
||||
<label class="field">BEST CASE<input type="number" value={pool.bestCaseValue ?? pool.max ?? 1} oninput={(event) => updatePool(index,{bestCaseValue:Number(event.currentTarget.value)})}/></label>
|
||||
<label class="field">EXPIRY · SECONDS<input type="number" min="0.01" step="any" value={pool.expiresAfterSeconds ?? ''} oninput={(event) => updatePool(index,{expiresAfterSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
|
||||
{#if pool.expiresAfterSecondsVariants}
|
||||
{#each valueColumns as column}
|
||||
<label class="field">EXPIRY · {column.label}<input type="number" min="0.01" step="any" value={pool.expiresAfterSecondsVariants?.[column.variantKey] ?? ''} oninput={(event) => updatePool(index,{expiresAfterSecondsVariants:{...pool.expiresAfterSecondsVariants,[column.variantKey]:Number(event.currentTarget.value)}})}/></label>
|
||||
{/each}
|
||||
{:else}
|
||||
<label class="field">EXPIRY · SECONDS<input type="number" min="0.01" step="any" value={pool.expiresAfterSeconds ?? ''} oninput={(event) => updatePool(index,{expiresAfterSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
|
||||
{/if}
|
||||
<button type="button" class="remove" onclick={() => setRule({...rule,pools:rule.pools.filter((_,current) => current !== index)})}>REMOVE</button>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -679,6 +837,9 @@
|
||||
|
||||
<div class="branch-stack">
|
||||
{#each rule.clauses as clause, clauseIndex}
|
||||
{@const conditionBand = readCurrentHealthPercentBand(clause.if)}
|
||||
{@const eventFieldCondition = readEventFieldComparison(clause.if)}
|
||||
{@const statePoolCondition = readStatePoolThreshold(clause.if)}
|
||||
<section class="branch">
|
||||
<header>
|
||||
<div><span>{String(clauseIndex + 1).padStart(2, '0')}</span><label class="field">BRANCH LABEL<input value={clause.label ?? ''} oninput={(event) => updateClause(clauseIndex,{label:event.currentTarget.value,key:slug(event.currentTarget.value)})}/></label></div>
|
||||
@@ -689,6 +850,75 @@
|
||||
<div class="branch-readout"><span>MATCH POLICY</span><strong>{reducerItems.find((item) => item.id === clause.reducer.kind)?.name ?? clause.reducer.kind}</strong></div>
|
||||
<label class="field">COOLDOWN · SECONDS<input type="number" min="0.01" step="any" value={clause.cooldownSeconds ?? ''} oninput={(event) => updateClause(clauseIndex,{cooldownSeconds:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
|
||||
</div>
|
||||
<div class="condition-controls">
|
||||
<SegmentedChoice
|
||||
legend="BRANCH CONDITION"
|
||||
name={`${idPrefix}-condition-${clauseIndex}`}
|
||||
options={conditionOptions(clause)}
|
||||
value={conditionModel(clause)}
|
||||
onchange={(values) => setConditionModel(clauseIndex,values[0])}
|
||||
/>
|
||||
{#if conditionBand}
|
||||
<div class="health-boundaries">
|
||||
<label class="field">FROM · HEALTH %<input type="number" min="0" max="100" step="any" value={conditionBand.minimum ?? ''} placeholder="0" oninput={(event) => updateHealthBand(clauseIndex,'minimum',event.currentTarget.value)}/><small>Inclusive; leave blank for zero.</small></label>
|
||||
<label class="field">BELOW · HEALTH %<input type="number" min="0.01" max="100" step="any" value={conditionBand.maximum} oninput={(event) => updateHealthBand(clauseIndex,'maximum',event.currentTarget.value)}/><small>Exclusive; exact values do not match.</small></label>
|
||||
</div>
|
||||
{:else if eventFieldCondition}
|
||||
<div class="predicate-fields event-predicate-fields">
|
||||
<label class="field">EVENT FIELD PATH<input value={eventFieldCondition.path} placeholder="values.state" oninput={(event) => updateEventFieldComparison(clauseIndex,{path:event.currentTarget.value})}/></label>
|
||||
<SegmentedChoice
|
||||
legend="COMPARISON"
|
||||
name={`${idPrefix}-event-operator-${clauseIndex}`}
|
||||
options={predicateOperatorOptions}
|
||||
value={eventFieldCondition.operator}
|
||||
columns={6}
|
||||
onchange={(values) => updateEventFieldComparison(clauseIndex,{operator:values[0] as PredicateComparisonOperator})}
|
||||
/>
|
||||
<SegmentedChoice
|
||||
legend="VALUE TYPE"
|
||||
name={`${idPrefix}-event-value-type-${clauseIndex}`}
|
||||
options={predicateScalarTypeOptions}
|
||||
value={predicateScalarType(eventFieldCondition.value)}
|
||||
onchange={(values) => setEventFieldScalarType(clauseIndex,values[0])}
|
||||
/>
|
||||
{#if typeof eventFieldCondition.value === 'number'}
|
||||
<label class="field">COMPARE VALUE<input type="number" step="any" value={eventFieldCondition.value} oninput={(event) => updateEventFieldComparison(clauseIndex,{value:Number(event.currentTarget.value)})}/></label>
|
||||
{:else if typeof eventFieldCondition.value === 'boolean'}
|
||||
<SegmentedChoice
|
||||
legend="COMPARE VALUE"
|
||||
name={`${idPrefix}-event-boolean-${clauseIndex}`}
|
||||
options={[{value:'true',label:'TRUE'},{value:'false',label:'FALSE'}]}
|
||||
value={String(eventFieldCondition.value)}
|
||||
onchange={(values) => updateEventFieldComparison(clauseIndex,{value:values[0] === 'true'})}
|
||||
/>
|
||||
{:else}
|
||||
<label class="field">COMPARE VALUE<input value={eventFieldCondition.value} placeholder="moving" oninput={(event) => updateEventFieldComparison(clauseIndex,{value:event.currentTarget.value})}/></label>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if statePoolCondition}
|
||||
<div class="predicate-fields state-predicate-fields">
|
||||
<div class="field"><span>STATE POOL</span><SearchableAssignment
|
||||
items={rule.pools.map((pool) => ({id:pool.key,name:pool.label}))}
|
||||
mode="single"
|
||||
compact
|
||||
subjectLabel="STATE POOL"
|
||||
value={statePoolCondition.pool}
|
||||
onvaluechange={(value) => updateStatePoolThreshold(clauseIndex,{pool:value})}
|
||||
/></div>
|
||||
<SegmentedChoice
|
||||
legend="COMPARISON"
|
||||
name={`${idPrefix}-state-operator-${clauseIndex}`}
|
||||
options={predicateOperatorOptions}
|
||||
value={statePoolCondition.operator}
|
||||
columns={6}
|
||||
onchange={(values) => updateStatePoolThreshold(clauseIndex,{operator:values[0] as PredicateComparisonOperator})}
|
||||
/>
|
||||
<label class="field">THRESHOLD<input type="number" step="any" value={statePoolCondition.value} oninput={(event) => updateStatePoolThreshold(clauseIndex,{value:Number(event.currentTarget.value)})}/></label>
|
||||
</div>
|
||||
{:else if conditionModel(clause) === 'advanced'}
|
||||
<p class="advanced-condition-note">This saved condition remains active and is preserved unchanged. Use the generated mechanic preview to inspect it.</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="reducer-controls">
|
||||
<SegmentedChoice
|
||||
legend="MATCH POLICY"
|
||||
@@ -760,10 +990,28 @@
|
||||
<label class="field">DESCRIPTION SLOT<input type="number" min="1" step="1" value={outcome.descriptionSlot ?? ''} oninput={(event) => updateOutcome(clauseIndex,outcomeIndex,{descriptionSlot:event.currentTarget.value ? Number(event.currentTarget.value) : undefined})}/></label>
|
||||
</div>
|
||||
<SegmentedChoice legend="TARGET SCOPE" name={`${idPrefix}-scope-${clauseIndex}-${outcomeIndex}`} options={scopeOptions} value={outcome.scope} columns={scopeOptions.length > 4 ? 4 : scopeOptions.length} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{scope:values[0] as MechanicOutcomeScope})}/>
|
||||
{#if outcome.scope === 'allies' || outcome.scope === 'self_and_allies' || outcome.scope === 'target'}
|
||||
<div class="delivery-controls">
|
||||
<SegmentedChoice
|
||||
legend="AREA TARGETING"
|
||||
name={`${idPrefix}-targeting-${clauseIndex}-${outcomeIndex}`}
|
||||
options={[{value:'none',label:'NONE'},{value:'allies',label:'ALLIES IN RADIUS'},{value:'enemies',label:'ENEMIES IN RADIUS'}]}
|
||||
value={outcome.targeting?.population ?? 'none'}
|
||||
onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{
|
||||
targeting: values[0] === 'none'
|
||||
? undefined
|
||||
: {kind:'within_radius',population:values[0] as 'allies'|'enemies',radiusMeters:outcome.targeting?.radiusMeters ?? 20}
|
||||
})}
|
||||
/>
|
||||
{#if outcome.targeting}
|
||||
<label class="field">RADIUS (METERS)<input type="number" min="0.01" step="any" value={outcome.targeting.radiusMeters} oninput={(event) => updateOutcome(clauseIndex,outcomeIndex,{targeting:{...outcome.targeting!,radiusMeters:Number(event.currentTarget.value)}})}/></label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if outcome.kind === 'attribute_modifier'}
|
||||
<SearchableAssignment items={attributes} mode="single" compact subjectLabel="ATTRIBUTE" bind:value={outcome.attributeId}/>
|
||||
{:else if outcome.kind === 'restoration' || outcome.kind === 'resource_cost'}
|
||||
<SegmentedChoice legend="RESOURCE" name={`${idPrefix}-resource-${clauseIndex}-${outcomeIndex}`} options={[{value:'health',label:'HEALTH'},{value:'armor',label:'ARMOR'},{value:'ammo',label:'AMMO'}]} value={outcome.resource ?? 'health'} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{resource:values[0] as 'health'|'armor'|'ammo'})}/>
|
||||
<SegmentedChoice legend="RESOURCE" name={`${idPrefix}-resource-${clauseIndex}-${outcomeIndex}`} options={[{value:'health',label:'HEALTH'},{value:'armor',label:'ARMOR'},{value:'ammo',label:'AMMO'},{value:'signature_ability_charge',label:'SIGNATURE CHARGE'}]} value={outcome.resource ?? 'health'} onchange={(values) => updateOutcome(clauseIndex,outcomeIndex,{resource:values[0] as 'health'|'armor'|'ammo'|'signature_ability_charge'})}/>
|
||||
{/if}
|
||||
{#if outcome.kind === 'restoration'}
|
||||
<div class="delivery-controls">
|
||||
@@ -876,6 +1124,9 @@
|
||||
.scenario-row{display:grid;grid-template-columns:minmax(170px,1fr) minmax(260px,1.25fr) minmax(220px,1.3fr) repeat(4,minmax(85px,.45fr)) auto;align-items:end;gap:8px;padding:8px;border-bottom:1px solid var(--shd-border-soft)}
|
||||
.scenario-row>.field{margin:0}.scenario-row>.description-field{min-width:0}
|
||||
.recipe-grid{display:grid;grid-template-columns:minmax(260px,1fr) repeat(3,minmax(110px,.35fr)) auto auto;align-items:end;gap:var(--shd-gap-sm);padding:var(--shd-gap-sm)}
|
||||
.health-band-config{grid-column:1/-1;display:grid;gap:var(--shd-gap-xs);padding:var(--shd-gap-sm);border:1px solid var(--shd-model-border);background:var(--shd-model-surface)}
|
||||
.health-band-config>p,.advanced-condition-note{margin:0;color:var(--shd-text-muted);font-size:var(--shd-type-supporting);line-height:1.4}
|
||||
.health-band-config>div{display:grid;grid-template-columns:minmax(150px,.5fr) minmax(220px,1fr) auto;align-items:end;gap:var(--shd-gap-sm)}
|
||||
.recipe-grid>.field,.pool-row>.field,.branch-grid>.field,.outcome-grid>.field,.variant-grid>.field{margin:0}
|
||||
.replace-confirm{display:flex;align-items:center;justify-content:space-between;gap:var(--shd-gap-sm);padding:9px;border-top:1px solid var(--shd-danger);background:color-mix(in srgb,var(--shd-danger) 10%,var(--shd-surface))}
|
||||
.replace-confirm p{display:grid;gap:2px;margin:0}.replace-confirm span{color:var(--shd-text-muted);font-size:var(--shd-type-supporting)}.replace-confirm>div{display:flex;gap:7px}
|
||||
@@ -883,6 +1134,12 @@
|
||||
.branch-stack{display:grid;gap:var(--shd-gap-sm)}
|
||||
.branch>header>div{display:flex;align-items:end;gap:9px}.branch>header>div>span{align-self:center;color:var(--shd-orange);font:900 var(--shd-type-telemetry)/1 var(--shd-font-mono)}.branch>header .field{min-width:min(360px,55vw);margin:0}
|
||||
.branch-grid{display:grid;grid-template-columns:repeat(2,minmax(240px,1fr)) minmax(160px,.45fr);align-items:end;gap:var(--shd-gap-sm);padding:var(--shd-gap-sm)}
|
||||
.condition-controls{display:grid;grid-template-columns:minmax(260px,.7fr) minmax(320px,1.3fr);align-items:end;gap:var(--shd-gap-sm);padding:0 var(--shd-gap-sm) var(--shd-gap-sm)}
|
||||
.health-boundaries{display:grid;grid-template-columns:repeat(2,minmax(150px,1fr));gap:var(--shd-gap-sm)}
|
||||
.health-boundaries .field small{display:block;margin-top:4px;color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);line-height:1.3}
|
||||
.predicate-fields{display:grid;grid-template-columns:minmax(170px,1fr) minmax(190px,.8fr);align-items:end;gap:var(--shd-gap-sm)}
|
||||
.event-predicate-fields{grid-template-columns:minmax(180px,1fr) minmax(190px,.85fr) minmax(180px,.75fr) minmax(150px,.75fr)}
|
||||
.predicate-fields>.field{margin:0}
|
||||
.field>span{display:block;margin-bottom:5px;font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}
|
||||
.branch-readout{display:grid;align-content:center;gap:3px;min-height:44px;padding:7px 9px;border:1px solid var(--shd-border);background:var(--shd-surface-raised)}.branch-readout span{color:var(--shd-text-muted);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.7px}.branch-readout strong{font-size:var(--shd-type-supporting)}
|
||||
.reducer-controls{display:grid;grid-template-columns:minmax(0,1fr) repeat(2,minmax(130px,.25fr));align-items:end;gap:var(--shd-gap-sm);padding:0 var(--shd-gap-sm) var(--shd-gap-sm)}
|
||||
@@ -899,7 +1156,7 @@
|
||||
.compact-action{min-height:36px}.add-branch{min-height:44px}.empty-state{padding:10px}.script-preview{display:grid;grid-template-columns:minmax(190px,.28fr) minmax(0,1fr);gap:var(--shd-gap-sm);padding:var(--shd-gap-sm);border:1px solid var(--shd-border);border-top:2px solid var(--shd-success);background:var(--shd-surface)}
|
||||
.script-preview>div{display:grid;align-content:start;gap:3px}.script-preview>div span{color:var(--shd-success);font-size:var(--shd-type-telemetry);font-weight:900;letter-spacing:.8px}
|
||||
@media(max-width:1300px){.scenario-row{grid-template-columns:repeat(2,minmax(0,1fr))}.scenario-row>.description-field{grid-column:1/-1}}
|
||||
@media(max-width:1000px){.recipe-grid,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr 1fr}.recipe-grid>:first-child,.script-preview>:last-child{grid-column:1/-1}.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:1/-1}}
|
||||
@media(max-width:620px){.mechanic-heading,.recipe-panel>header,.scenario-panel>header,.state-panel>header,.branch>header,.outcome-heading,.replace-confirm,.outcome-footer{align-items:stretch;flex-direction:column}.recipe-grid,.scenario-row,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr}.recipe-grid>:first-child,.script-preview>:last-child,.scenario-row>.description-field,.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:auto}.branch>header>div{align-items:stretch;flex-direction:column}.branch>header .field{min-width:0}.remove,.danger-quiet,.compact-action,.add-branch{width:100%;min-height:44px}}
|
||||
@media(max-width:1000px){.recipe-grid,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview{grid-template-columns:1fr 1fr}.recipe-grid>:first-child,.script-preview>:last-child{grid-column:1/-1}.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:1/-1}.condition-controls{grid-template-columns:1fr}.event-predicate-fields{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:620px){.mechanic-heading,.recipe-panel>header,.scenario-panel>header,.state-panel>header,.branch>header,.outcome-heading,.replace-confirm,.outcome-footer{align-items:stretch;flex-direction:column}.recipe-grid,.scenario-row,.pool-row,.branch-grid,.outcome-grid,.reducer-controls,.mutation-row,.state-scaling,.delivery-controls,.script-preview,.health-band-config>div,.health-boundaries,.predicate-fields,.event-predicate-fields{grid-template-columns:1fr}.recipe-grid>:first-child,.script-preview>:last-child,.scenario-row>.description-field,.pool-row>:first-child,.branch-grid>:first-child,.outcome-grid>:first-child{grid-column:auto}.branch>header>div{align-items:stretch;flex-direction:column}.branch>header .field{min-width:0}.remove,.danger-quiet,.compact-action,.add-branch{width:100%;min-height:44px}}
|
||||
@media(prefers-reduced-motion:reduce){.remove,.danger-quiet{transition:none}}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user