i forgot what i have added so... JUST TRUST BRO

This commit is contained in:
Akurosia Kamo 2026-05-24 10:42:25 +02:00
parent 1dfc727940
commit 646a7252c8
2 changed files with 118 additions and 5 deletions

View File

@ -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,

View File

@ -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),
}); });
} }