forked from xziino/ff14-mitigator
more timeline fixes and addle fix
This commit is contained in:
parent
fb6d50961a
commit
8f00c22682
@ -756,6 +756,10 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--t2);
|
color: var(--t2);
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
position: sticky;
|
||||||
|
left: 0;
|
||||||
|
z-index: 8;
|
||||||
|
box-shadow: 8px 0 12px rgba(0,0,0,.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-boss-row {
|
.timeline-boss-row {
|
||||||
@ -765,6 +769,7 @@
|
|||||||
.timeline-boss-row .timeline-row-label {
|
.timeline-boss-row .timeline-row-label {
|
||||||
color: var(--gold);
|
color: var(--gold);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-track,
|
.timeline-track,
|
||||||
|
|||||||
@ -91,7 +91,7 @@
|
|||||||
'Shield Samba': ['DNC'],
|
'Shield Samba': ['DNC'],
|
||||||
'Improvised Finish': ['DNC'],
|
'Improvised Finish': ['DNC'],
|
||||||
'Feint': ['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR'],
|
'Feint': ['MNK', 'DRG', 'NIN', 'SAM', 'RPR', 'VPR'],
|
||||||
'Addle': ['SCH', 'SGE', 'BLM', 'SMN', 'RDM', 'PCT'],
|
'Addle': ['BLM', 'SMN', 'RDM', 'PCT'],
|
||||||
'Radiant Aegis': ['SMN'],
|
'Radiant Aegis': ['SMN'],
|
||||||
'Magick Barrier': ['RDM'],
|
'Magick Barrier': ['RDM'],
|
||||||
'Tempera Coat': ['PCT'],
|
'Tempera Coat': ['PCT'],
|
||||||
|
|||||||
105
js/planner.js
105
js/planner.js
@ -448,10 +448,10 @@ function effectiveAssignmentsForMechanic(plan, targetMechanic) {
|
|||||||
const result = [];
|
const result = [];
|
||||||
const seen = new Set();
|
const seen = new Set();
|
||||||
|
|
||||||
for (const entry of canonicalAssignmentActivations(plan)) {
|
for (const entry of canonicalAssignmentActivations(plan, { dedupeKey: canonicalMechanicKey })) {
|
||||||
if (targetMechanic.timestamp < entry.start || targetMechanic.timestamp > entry.end) continue;
|
if (targetMechanic.timestamp < entry.start || targetMechanic.timestamp > entry.end) continue;
|
||||||
const assignment = entry.assignment;
|
const assignment = entry.assignment;
|
||||||
const key = `${assignment.ability}::${assignment.job ?? ''}`;
|
const key = canonicalMechanicKey(entry);
|
||||||
if (seen.has(key)) continue;
|
if (seen.has(key)) continue;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
result.push({
|
result.push({
|
||||||
@ -641,7 +641,15 @@ function assignmentStartMs(mechanic, assignment) {
|
|||||||
return Number.isFinite(Number(assignment?.timestamp)) ? Number(assignment.timestamp) : mechanic.timestamp;
|
return Number.isFinite(Number(assignment?.timestamp)) ? Number(assignment.timestamp) : mechanic.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
function canonicalAssignmentActivations(plan) {
|
function canonicalMechanicKey(entry) {
|
||||||
|
return `${entry.assignment.ability}::${entry.assignment.job || '*'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalTimelineKey(entry, row) {
|
||||||
|
return `${row.job}::${row.ability}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function canonicalAssignmentActivations(plan, { dedupeKey = canonicalMechanicKey, includeEntry = null } = {}) {
|
||||||
const entries = [];
|
const entries = [];
|
||||||
for (const mechanic of visiblePlanMechanics(plan)) {
|
for (const mechanic of visiblePlanMechanics(plan)) {
|
||||||
for (const assignment of mechanic.assignments ?? []) {
|
for (const assignment of mechanic.assignments ?? []) {
|
||||||
@ -660,9 +668,10 @@ function canonicalAssignmentActivations(plan) {
|
|||||||
entries.sort((a, b) => a.start - b.start);
|
entries.sort((a, b) => a.start - b.start);
|
||||||
const activeUntilBySkill = new Map();
|
const activeUntilBySkill = new Map();
|
||||||
return entries.filter(entry => {
|
return entries.filter(entry => {
|
||||||
const key = `${entry.assignment.ability}::${entry.assignment.job ?? ''}`;
|
if (includeEntry && !includeEntry(entry)) return false;
|
||||||
|
const key = dedupeKey(entry);
|
||||||
const activeUntil = activeUntilBySkill.get(key) ?? -Infinity;
|
const activeUntil = activeUntilBySkill.get(key) ?? -Infinity;
|
||||||
if (entry.start < activeUntil) return false;
|
if (entry.start <= activeUntil) return false;
|
||||||
activeUntilBySkill.set(key, entry.end);
|
activeUntilBySkill.set(key, entry.end);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@ -698,17 +707,35 @@ function layoutBossActions(mechanics, duration) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function assignmentOverlapsJob(plan, job, ability, timestamp, ignore = null) {
|
function assignmentWindowMs(assignment) {
|
||||||
|
return Math.max(assignmentCooldownSeconds(assignment), assignmentDurationSeconds(assignment)) * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sameAssignmentRef(mechanic, assignment, ref) {
|
||||||
|
return !!ref
|
||||||
|
&& ref.mechanicId === mechanic.id
|
||||||
|
&& ref.ability === assignment.ability
|
||||||
|
&& ref.job === (assignment.job ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignmentOverlapsJob(plan, job, ability, timestamp, ignore = null, candidate = null) {
|
||||||
|
const candidateWindow = Math.max(
|
||||||
|
candidate ? assignmentCooldownSeconds(candidate) : 0,
|
||||||
|
candidate ? assignmentDurationSeconds(candidate) : 0,
|
||||||
|
1
|
||||||
|
) * 1000;
|
||||||
|
const candidateStart = Math.max(0, timestamp);
|
||||||
|
const candidateEnd = candidateStart + candidateWindow;
|
||||||
|
|
||||||
for (const mechanic of visiblePlanMechanics(plan)) {
|
for (const mechanic of visiblePlanMechanics(plan)) {
|
||||||
for (const assignment of mechanic.assignments ?? []) {
|
for (const assignment of mechanic.assignments ?? []) {
|
||||||
if (assignment.ability !== ability) continue;
|
if (assignment.ability !== ability) continue;
|
||||||
if ((assignment.job ?? '') !== job && !(!(assignment.job ?? '') && jobCanUseAbility(job, ability))) continue;
|
if ((assignment.job ?? '') !== job && !(!(assignment.job ?? '') && jobCanUseAbility(job, ability))) continue;
|
||||||
if (ignore && ignore.mechanicId === mechanic.id && ignore.ability === ability && ignore.job === (assignment.job ?? '')) continue;
|
if (sameAssignmentRef(mechanic, assignment, ignore)) continue;
|
||||||
|
|
||||||
const start = assignmentStartMs(mechanic, assignment);
|
const start = assignmentStartMs(mechanic, assignment);
|
||||||
const cooldownMs = Math.max(assignmentCooldownSeconds(assignment), assignmentDurationSeconds(assignment)) * 1000;
|
const end = start + assignmentWindowMs(assignment);
|
||||||
const end = start + cooldownMs;
|
if (candidateStart < end && candidateEnd > start) return true;
|
||||||
if (timestamp >= start && timestamp < end) return true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -762,13 +789,16 @@ function renderTimelineHtml(plan) {
|
|||||||
|
|
||||||
const playerRows = rows.map(row => {
|
const playerRows = rows.map(row => {
|
||||||
const blocks = [];
|
const blocks = [];
|
||||||
const rowAssignments = canonicalAssignmentActivations(plan).filter(entry => {
|
for (const entry of canonicalAssignmentActivations(plan, {
|
||||||
const assignment = entry.assignment;
|
dedupeKey: item => canonicalTimelineKey(item, row),
|
||||||
|
includeEntry: item => {
|
||||||
|
const assignment = item.assignment;
|
||||||
if (assignment.ability !== row.ability) return false;
|
if (assignment.ability !== row.ability) return false;
|
||||||
const assignedJob = assignment.job ?? '';
|
const assignedJob = assignment.job ?? '';
|
||||||
return !assignedJob || assignedJob === row.job;
|
return !assignedJob || assignedJob === row.job;
|
||||||
});
|
},
|
||||||
for (const item of rowAssignments) {
|
})) {
|
||||||
|
const item = entry;
|
||||||
const m = item.mechanic;
|
const m = item.mechanic;
|
||||||
const a = item.assignment;
|
const a = item.assignment;
|
||||||
const start = item.start;
|
const start = item.start;
|
||||||
@ -899,7 +929,7 @@ function setTimelineAssignmentJob(planId, mechanicId, ability, job, nextJob) {
|
|||||||
const assignment = mechanic?.assignments?.find(a => a.ability === ability && (a.job ?? '') === job);
|
const assignment = mechanic?.assignments?.find(a => a.ability === ability && (a.job ?? '') === job);
|
||||||
if (!assignment) return;
|
if (!assignment) return;
|
||||||
const timestamp = assignmentStartMs(mechanic, assignment);
|
const timestamp = assignmentStartMs(mechanic, assignment);
|
||||||
if (assignmentOverlapsJob(plan, nextJob, ability, timestamp, { mechanicId, ability, job })) return;
|
if (assignmentOverlapsJob(plan, nextJob, ability, timestamp, { mechanicId, ability, job }, assignment)) return;
|
||||||
assignment.job = nextJob;
|
assignment.job = nextJob;
|
||||||
selectedTimelineAssignment = { mechanicId, ability, job: nextJob };
|
selectedTimelineAssignment = { mechanicId, ability, job: nextJob };
|
||||||
updatePlan(planId, { mechanics: plan.mechanics });
|
updatePlan(planId, { mechanics: plan.mechanics });
|
||||||
@ -925,14 +955,16 @@ function addTimelineAssignment(planId, mechanicId, ability, job, buffType, times
|
|||||||
const mechanic = plan.mechanics.find(m => m.id === mechanicId);
|
const mechanic = plan.mechanics.find(m => m.id === mechanicId);
|
||||||
if (!mechanic) return;
|
if (!mechanic) return;
|
||||||
mechanic.assignments = mechanic.assignments ?? [];
|
mechanic.assignments = mechanic.assignments ?? [];
|
||||||
mechanic.assignments.push({
|
const assignment = {
|
||||||
ability,
|
ability,
|
||||||
abilityName: plan.mitigationNames?.[ability],
|
abilityName: plan.mitigationNames?.[ability],
|
||||||
actionId: actionMetaByName[ability]?.id ?? null,
|
actionId: actionMetaByName[ability]?.id ?? null,
|
||||||
job,
|
job,
|
||||||
buffType,
|
buffType,
|
||||||
timestamp: Math.max(0, Math.round(timestamp)),
|
timestamp: Math.max(0, Math.round(timestamp)),
|
||||||
});
|
};
|
||||||
|
if (assignmentOverlapsJob(plan, job, ability, assignment.timestamp, null, assignment)) return;
|
||||||
|
mechanic.assignments.push(assignment);
|
||||||
selectedTimelineAssignment = { mechanicId, ability, job };
|
selectedTimelineAssignment = { mechanicId, ability, job };
|
||||||
updatePlan(planId, { mechanics: plan.mechanics });
|
updatePlan(planId, { mechanics: plan.mechanics });
|
||||||
refreshMechanicList(planId);
|
refreshMechanicList(planId);
|
||||||
@ -989,7 +1021,9 @@ function updateTimelineAssignmentPosition(planId, mechanicId, ability, job, rowJ
|
|||||||
const assignment = mechanic?.assignments?.find(a => a.ability === ability && (a.job ?? '') === job);
|
const assignment = mechanic?.assignments?.find(a => a.ability === ability && (a.job ?? '') === job);
|
||||||
if (!assignment) return;
|
if (!assignment) return;
|
||||||
if (!jobCanUseAbility(rowJob, ability)) return;
|
if (!jobCanUseAbility(rowJob, ability)) return;
|
||||||
assignment.timestamp = Math.max(0, Math.round(timestamp));
|
const nextTimestamp = Math.max(0, Math.round(timestamp));
|
||||||
|
if (assignmentOverlapsJob(plan, rowJob, ability, nextTimestamp, { mechanicId, ability, job }, assignment)) return;
|
||||||
|
assignment.timestamp = nextTimestamp;
|
||||||
assignment.job = rowJob;
|
assignment.job = rowJob;
|
||||||
selectedTimelineAssignment = { mechanicId, ability, job: rowJob };
|
selectedTimelineAssignment = { mechanicId, ability, job: rowJob };
|
||||||
updatePlan(planId, { mechanics: plan.mechanics });
|
updatePlan(planId, { mechanics: plan.mechanics });
|
||||||
@ -1031,7 +1065,17 @@ function initTimeline(planId) {
|
|||||||
const cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100));
|
const cooldownPct = Math.max(durationPct, Math.min(100 - left, (timelineDrag.cooldownSec * 1000 / duration) * 100));
|
||||||
const activePct = Math.min(100, (durationPct / cooldownPct) * 100);
|
const activePct = Math.min(100, (durationPct / cooldownPct) * 100);
|
||||||
const valid = row.dataset.ability === timelineDrag.ability
|
const valid = row.dataset.ability === timelineDrag.ability
|
||||||
&& jobCanUseAbility(row.dataset.job, timelineDrag.ability);
|
&& jobCanUseAbility(row.dataset.job, timelineDrag.ability)
|
||||||
|
&& !assignmentOverlapsJob(plan, row.dataset.job, timelineDrag.ability, timestamp, {
|
||||||
|
mechanicId: timelineDrag.mechanicId,
|
||||||
|
ability: timelineDrag.ability,
|
||||||
|
job: timelineDrag.job,
|
||||||
|
}, {
|
||||||
|
ability: timelineDrag.ability,
|
||||||
|
job: row.dataset.job,
|
||||||
|
durationSeconds: timelineDrag.durationSec,
|
||||||
|
cooldownSeconds: timelineDrag.cooldownSec,
|
||||||
|
});
|
||||||
|
|
||||||
row.classList.add(valid ? 'timeline-player-row--drop-ok' : 'timeline-player-row--drop-bad');
|
row.classList.add(valid ? 'timeline-player-row--drop-ok' : 'timeline-player-row--drop-bad');
|
||||||
|
|
||||||
@ -1126,7 +1170,7 @@ function initTimeline(planId) {
|
|||||||
mechanicId: block.dataset.mechanicId,
|
mechanicId: block.dataset.mechanicId,
|
||||||
ability: block.dataset.ability,
|
ability: block.dataset.ability,
|
||||||
job: block.dataset.job,
|
job: block.dataset.job,
|
||||||
}),
|
}, found?.assignment ?? null),
|
||||||
onClick: () => setTimelineAssignmentJob(planId, block.dataset.mechanicId, block.dataset.ability, block.dataset.job, row.job),
|
onClick: () => setTimelineAssignmentJob(planId, block.dataset.mechanicId, block.dataset.ability, block.dataset.job, row.job),
|
||||||
}));
|
}));
|
||||||
showTimelineMenu(e.clientX, e.clientY, items);
|
showTimelineMenu(e.clientX, e.clientY, items);
|
||||||
@ -1144,7 +1188,13 @@ function initTimeline(planId) {
|
|||||||
const rowAbility = row.dataset.ability;
|
const rowAbility = row.dataset.ability;
|
||||||
const rowJob = row.dataset.job;
|
const rowJob = row.dataset.job;
|
||||||
const ab = (JOB_ABILITIES[rowJob] ?? []).find(a => a.name === rowAbility);
|
const ab = (JOB_ABILITIES[rowJob] ?? []).find(a => a.name === rowAbility);
|
||||||
if (!ab || assignmentOverlapsJob(plan, rowJob, rowAbility, timestamp)) return;
|
const candidate = ab ? {
|
||||||
|
ability: rowAbility,
|
||||||
|
actionId: actionMetaByName[rowAbility]?.id ?? null,
|
||||||
|
job: rowJob,
|
||||||
|
buffType: ab.buffType,
|
||||||
|
} : null;
|
||||||
|
if (!ab || assignmentOverlapsJob(plan, rowJob, rowAbility, timestamp, null, candidate)) return;
|
||||||
addTimelineAssignment(planId, mechanic.id, rowAbility, rowJob, ab.buffType, timestamp);
|
addTimelineAssignment(planId, mechanic.id, rowAbility, rowJob, ab.buffType, timestamp);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1171,6 +1221,7 @@ function initTimeline(planId) {
|
|||||||
transparent.height = 1;
|
transparent.height = 1;
|
||||||
e.dataTransfer.setDragImage(transparent, 0, 0);
|
e.dataTransfer.setDragImage(transparent, 0, 0);
|
||||||
timelineDrag = {
|
timelineDrag = {
|
||||||
|
mechanicId: block.dataset.mechanicId,
|
||||||
ability: block.dataset.ability,
|
ability: block.dataset.ability,
|
||||||
job: block.dataset.job,
|
job: block.dataset.job,
|
||||||
label: block.querySelector('span:last-child')?.textContent ?? block.dataset.ability,
|
label: block.querySelector('span:last-child')?.textContent ?? block.dataset.ability,
|
||||||
@ -1623,7 +1674,6 @@ const JOB_ABILITIES = {
|
|||||||
{ name: 'Galvanize', buffType: 'shield' },
|
{ name: 'Galvanize', buffType: 'shield' },
|
||||||
{ name: 'Seraphic Veil', buffType: 'shield' },
|
{ name: 'Seraphic Veil', buffType: 'shield' },
|
||||||
{ name: 'Catalyze', buffType: 'shield' },
|
{ name: 'Catalyze', buffType: 'shield' },
|
||||||
{ name: 'Addle', buffType: 'debuff' },
|
|
||||||
],
|
],
|
||||||
'AST': [
|
'AST': [
|
||||||
{ name: 'Collective Unconscious', buffType: 'buff' },
|
{ name: 'Collective Unconscious', buffType: 'buff' },
|
||||||
@ -2073,16 +2123,23 @@ function toggleAbilityAssignment(abilityName, job, buffType) {
|
|||||||
if (mechanic.assignments[idx].job === job) {
|
if (mechanic.assignments[idx].job === job) {
|
||||||
mechanic.assignments.splice(idx, 1);
|
mechanic.assignments.splice(idx, 1);
|
||||||
} else {
|
} else {
|
||||||
|
if (assignmentOverlapsJob(plan, job, abilityName, assignmentStartMs(mechanic, mechanic.assignments[idx]), {
|
||||||
|
mechanicId: mechanic.id,
|
||||||
|
ability: abilityName,
|
||||||
|
job: mechanic.assignments[idx].job ?? '',
|
||||||
|
}, mechanic.assignments[idx])) return;
|
||||||
mechanic.assignments[idx].job = job;
|
mechanic.assignments[idx].job = job;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mechanic.assignments.push({
|
const assignment = {
|
||||||
ability: abilityName,
|
ability: abilityName,
|
||||||
abilityName: plan.mitigationNames?.[abilityName],
|
abilityName: plan.mitigationNames?.[abilityName],
|
||||||
actionId: actionMetaByName[abilityName]?.id ?? null,
|
actionId: actionMetaByName[abilityName]?.id ?? null,
|
||||||
job,
|
job,
|
||||||
buffType,
|
buffType,
|
||||||
});
|
};
|
||||||
|
if (assignmentOverlapsJob(plan, job, abilityName, mechanic.timestamp, null, assignment)) return;
|
||||||
|
mechanic.assignments.push(assignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePlan(abilityModalPlanId, { mechanics: plan.mechanics });
|
updatePlan(abilityModalPlanId, { mechanics: plan.mechanics });
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user