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 {
|
.timeline-hint {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--t3);
|
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,
|
.timeline-empty,
|
||||||
|
|||||||
@ -41,6 +41,7 @@ function createPlan(name) {
|
|||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
source: null,
|
source: null,
|
||||||
mitigationNames: {},
|
mitigationNames: {},
|
||||||
|
timelineOptions: { includeShields: false, includePersonal: false },
|
||||||
folderId: null,
|
folderId: null,
|
||||||
jobComposition: Array(8).fill(''),
|
jobComposition: Array(8).fill(''),
|
||||||
mechanics: []
|
mechanics: []
|
||||||
@ -392,8 +393,18 @@ function renderPlanDetail(plan) {
|
|||||||
<div class="card section-gap">
|
<div class="card section-gap">
|
||||||
<div class="card-title-row">
|
<div class="card-title-row">
|
||||||
<div class="card-title">Zeitstrahl</div>
|
<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>
|
||||||
|
<div class="timeline-hint">Boss-Aktion klicken zum Zuweisen · Mitigation ziehen · Klick für Zeiten</div>
|
||||||
<div id="planner-timeline">
|
<div id="planner-timeline">
|
||||||
${renderTimelineHtml(plan)}
|
${renderTimelineHtml(plan)}
|
||||||
</div>
|
</div>
|
||||||
@ -419,6 +430,7 @@ function renderPlanDetail(plan) {
|
|||||||
document.getElementById('name-import-open-btn')?.addEventListener('click', () => {
|
document.getElementById('name-import-open-btn')?.addEventListener('click', () => {
|
||||||
showNameImportModal(plan.id);
|
showNameImportModal(plan.id);
|
||||||
});
|
});
|
||||||
|
initTimelineOptions(plan.id);
|
||||||
initTimeline(plan.id);
|
initTimeline(plan.id);
|
||||||
initMechanicClicks(plan.id);
|
initMechanicClicks(plan.id);
|
||||||
renderInfoPanel(plan);
|
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 ─────────────────────────────────────────────────────────────────
|
// ── Timeline ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function planDurationMs(plan) {
|
function planDurationMs(plan) {
|
||||||
@ -609,9 +640,39 @@ const JOB_GANTT_ORDER = {
|
|||||||
'PLD': 40, 'WAR': 41, 'DRK': 42, 'GNB': 43,
|
'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) {
|
function timelinePlayerRows(plan) {
|
||||||
const roster = plan.playerRoster ?? [];
|
const roster = plan.playerRoster ?? [];
|
||||||
const rows = [];
|
const rows = [];
|
||||||
|
const options = timelineOptions(plan);
|
||||||
const jobEntries = (plan.jobComposition ?? [])
|
const jobEntries = (plan.jobComposition ?? [])
|
||||||
.map((job, idx) => ({ job, idx }))
|
.map((job, idx) => ({ job, idx }))
|
||||||
.filter(e => !!e.job)
|
.filter(e => !!e.job)
|
||||||
@ -620,7 +681,7 @@ function timelinePlayerRows(plan) {
|
|||||||
const name = roster[idx]?.name ?? '';
|
const name = roster[idx]?.name ?? '';
|
||||||
const role = JOB_ROLE[job] ?? '';
|
const role = JOB_ROLE[job] ?? '';
|
||||||
const abilities = (JOB_ABILITIES[job] ?? [])
|
const abilities = (JOB_ABILITIES[job] ?? [])
|
||||||
.filter(ab => ab.buffType !== 'shield' || ab.name === 'Panhaima');
|
.filter(ab => timelineAbilityVisible(ab, options));
|
||||||
abilities.forEach((ab, abilityIdx) => {
|
abilities.forEach((ab, abilityIdx) => {
|
||||||
rows.push({ idx, job, ability: ab.name, buffType: ab.buffType, name, role, firstForJob: abilityIdx === 0 });
|
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;
|
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 ?? [])
|
const missing = (json.boss_events ?? json.aoe_events ?? [])
|
||||||
.filter(event => !isGenericAttack(event))
|
.filter(event => !isGenericAttack(event))
|
||||||
.filter(event => !mechanicExistsInPlan(plan, 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 sourceStart = plan.source?.fightStart ?? json.fight_start ?? 0;
|
||||||
const byAttack = new Map();
|
const byAttack = new Map();
|
||||||
|
const idsByNameType = new Map();
|
||||||
for (const event of missing) {
|
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, []);
|
if (!byAttack.has(key)) byAttack.set(key, []);
|
||||||
byAttack.get(key).push(event);
|
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))) {
|
for (const events of [...byAttack.values()].sort((a, b) => a[0].abilityName.localeCompare(b[0].abilityName))) {
|
||||||
const first = events[0];
|
const first = events[0];
|
||||||
const count = events.length > 1 ? ` (${events.length}x)` : '';
|
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({
|
attackItems.push({
|
||||||
label: `${first.abilityName}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`,
|
label: `${first.abilityName}${idLabel}${count} · ${first.isHeavyTankbuster ? 'Tankbuster' : 'AoE'}`,
|
||||||
onClick: () => addMechanicsFromEvents(planId, events, json),
|
onClick: () => addMechanicsFromEvents(planId, events, json),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user