forked from xziino/ff14-mitigator
i forgot what i have added so... JUST TRUST BRO
This commit is contained in:
parent
1dfc727940
commit
646a7252c8
@ -707,6 +707,39 @@
|
||||
.timeline-hint {
|
||||
font-size: 12px;
|
||||
color: var(--t3);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.timeline-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.timeline-toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--r);
|
||||
background: rgba(255,255,255,.025);
|
||||
color: var(--t2);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.timeline-toggle:hover {
|
||||
border-color: var(--borderem);
|
||||
color: var(--t1);
|
||||
}
|
||||
|
||||
.timeline-toggle input {
|
||||
width: auto !important;
|
||||
min-width: 0 !important;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.timeline-empty,
|
||||
|
||||
@ -41,6 +41,7 @@ function createPlan(name) {
|
||||
updatedAt: Date.now(),
|
||||
source: null,
|
||||
mitigationNames: {},
|
||||
timelineOptions: { includeShields: false, includePersonal: false },
|
||||
folderId: null,
|
||||
jobComposition: Array(8).fill(''),
|
||||
mechanics: []
|
||||
@ -392,8 +393,18 @@ function renderPlanDetail(plan) {
|
||||
<div class="card section-gap">
|
||||
<div class="card-title-row">
|
||||
<div class="card-title">Zeitstrahl</div>
|
||||
<div class="timeline-hint">Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten</div>
|
||||
<div class="timeline-controls">
|
||||
<label class="timeline-toggle">
|
||||
<input type="checkbox" id="timeline-include-shields"${timelineOptions(plan).includeShields ? ' checked' : ''}>
|
||||
<span>Include Shields</span>
|
||||
</label>
|
||||
<label class="timeline-toggle">
|
||||
<input type="checkbox" id="timeline-include-personal"${timelineOptions(plan).includePersonal ? ' checked' : ''}>
|
||||
<span>Include Personal</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="timeline-hint">Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten</div>
|
||||
<div id="planner-timeline">
|
||||
${renderTimelineHtml(plan)}
|
||||
</div>
|
||||
@ -419,6 +430,7 @@ function renderPlanDetail(plan) {
|
||||
document.getElementById('name-import-open-btn')?.addEventListener('click', () => {
|
||||
showNameImportModal(plan.id);
|
||||
});
|
||||
initTimelineOptions(plan.id);
|
||||
initTimeline(plan.id);
|
||||
initMechanicClicks(plan.id);
|
||||
renderInfoPanel(plan);
|
||||
@ -586,6 +598,25 @@ function initJobSlots(planId) {
|
||||
});
|
||||
}
|
||||
|
||||
function initTimelineOptions(planId) {
|
||||
const shields = document.getElementById('timeline-include-shields');
|
||||
const personal = document.getElementById('timeline-include-personal');
|
||||
const update = () => {
|
||||
const plan = getPlan(planId);
|
||||
if (!plan) return;
|
||||
updatePlan(planId, {
|
||||
timelineOptions: {
|
||||
...(plan.timelineOptions ?? {}),
|
||||
includeShields: !!shields?.checked,
|
||||
includePersonal: !!personal?.checked,
|
||||
},
|
||||
});
|
||||
refreshMechanicList(planId);
|
||||
};
|
||||
shields?.addEventListener('change', update);
|
||||
personal?.addEventListener('change', update);
|
||||
}
|
||||
|
||||
// ── Timeline ─────────────────────────────────────────────────────────────────
|
||||
|
||||
function planDurationMs(plan) {
|
||||
@ -609,9 +640,39 @@ const JOB_GANTT_ORDER = {
|
||||
'PLD': 40, 'WAR': 41, 'DRK': 42, 'GNB': 43,
|
||||
};
|
||||
|
||||
const TIMELINE_PERSONAL_ABILITIES = new Set([
|
||||
'Guardian',
|
||||
'Bloodwhetting',
|
||||
'Divine Benison',
|
||||
'Intersection',
|
||||
'the Spire',
|
||||
'Haima',
|
||||
'Eukrasian Diagnosis',
|
||||
'Differential Diagnosis',
|
||||
'Seraphic Veil',
|
||||
'Radiant Aegis',
|
||||
'Tempera Coat',
|
||||
]);
|
||||
|
||||
function timelineOptions(plan) {
|
||||
return {
|
||||
includeShields: !!plan?.timelineOptions?.includeShields,
|
||||
includePersonal: !!plan?.timelineOptions?.includePersonal,
|
||||
};
|
||||
}
|
||||
|
||||
function timelineAbilityVisible(ability, options) {
|
||||
const isPersonal = TIMELINE_PERSONAL_ABILITIES.has(ability.name);
|
||||
const isShield = ability.buffType === 'shield';
|
||||
if (isPersonal && !options.includePersonal) return false;
|
||||
if (isShield && !options.includeShields && ability.name !== 'Panhaima') return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function timelinePlayerRows(plan) {
|
||||
const roster = plan.playerRoster ?? [];
|
||||
const rows = [];
|
||||
const options = timelineOptions(plan);
|
||||
const jobEntries = (plan.jobComposition ?? [])
|
||||
.map((job, idx) => ({ job, idx }))
|
||||
.filter(e => !!e.job)
|
||||
@ -620,7 +681,7 @@ function timelinePlayerRows(plan) {
|
||||
const name = roster[idx]?.name ?? '';
|
||||
const role = JOB_ROLE[job] ?? '';
|
||||
const abilities = (JOB_ABILITIES[job] ?? [])
|
||||
.filter(ab => ab.buffType !== 'shield' || ab.name === 'Panhaima');
|
||||
.filter(ab => timelineAbilityVisible(ab, options));
|
||||
abilities.forEach((ab, abilityIdx) => {
|
||||
rows.push({ idx, job, ability: ab.name, buffType: ab.buffType, name, role, firstForJob: abilityIdx === 0 });
|
||||
});
|
||||
@ -1305,7 +1366,16 @@ async function showMissingBossActionsMenu(planId, x, y) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isGenericAttack = event => String(event?.abilityName ?? '').trim().toLowerCase() === 'attack';
|
||||
const genericAttackNames = new Set(['attack', 'attacke', 'auto attack', 'auto-attack', 'angriff', 'attaque', '攻撃']);
|
||||
const isGenericAttack = event => {
|
||||
const name = String(event?.abilityName ?? '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.normalize('NFD')
|
||||
.replace(/[\u0300-\u036f]/g, '');
|
||||
const id = parseInt(event?.abilityId ?? 0, 10) || 0;
|
||||
return id > 0 && id <= 7 || genericAttackNames.has(name);
|
||||
};
|
||||
const missing = (json.boss_events ?? json.aoe_events ?? [])
|
||||
.filter(event => !isGenericAttack(event))
|
||||
.filter(event => !mechanicExistsInPlan(plan, event))
|
||||
@ -1313,8 +1383,15 @@ async function showMissingBossActionsMenu(planId, x, y) {
|
||||
|
||||
const sourceStart = plan.source?.fightStart ?? json.fight_start ?? 0;
|
||||
const byAttack = new Map();
|
||||
const idsByNameType = new Map();
|
||||
for (const event of missing) {
|
||||
const key = `${event.abilityId || event.abilityName}::${event.abilityName}`;
|
||||
const nameKey = String(event.abilityName ?? '').trim().toLowerCase();
|
||||
const typeKey = event.isHeavyTankbuster ? 'tankbuster' : 'aoe';
|
||||
const idKey = String(event.abilityId ?? '');
|
||||
const nameTypeKey = `${nameKey}::${typeKey}`;
|
||||
if (!idsByNameType.has(nameTypeKey)) idsByNameType.set(nameTypeKey, new Set());
|
||||
if (idKey) idsByNameType.get(nameTypeKey).add(idKey);
|
||||
const key = `${idKey || nameKey}::${nameTypeKey}`;
|
||||
if (!byAttack.has(key)) byAttack.set(key, []);
|
||||
byAttack.get(key).push(event);
|
||||
}
|
||||
@ -1325,8 +1402,11 @@ async function showMissingBossActionsMenu(planId, x, y) {
|
||||
for (const events of [...byAttack.values()].sort((a, b) => a[0].abilityName.localeCompare(b[0].abilityName))) {
|
||||
const first = events[0];
|
||||
const count = events.length > 1 ? ` (${events.length}x)` : '';
|
||||
const nameTypeKey = `${String(first.abilityName ?? '').trim().toLowerCase()}::${first.isHeavyTankbuster ? 'tankbuster' : 'aoe'}`;
|
||||
const duplicateName = (idsByNameType.get(nameTypeKey)?.size ?? 0) > 1;
|
||||
const idLabel = duplicateName && first.abilityId ? ` [${first.abilityId}]` : '';
|
||||
attackItems.push({
|
||||
label: `${first.abilityName}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`,
|
||||
label: `${first.abilityName}${idLabel}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`,
|
||||
onClick: () => addMechanicsFromEvents(planId, events, json),
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user