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