My Progress: Custom lanes option — track any Boost Planner mix (e.g. L4+L4+L2)
Phase dropdown gains 'Custom lanes'; a lanes input derives profit/cycle from LEVELS, maps the ladder rung to the highest lane, estimates cycles-to-next accelerate-basis (labeled est.), persists with the rest of the ROI state. Engine untouched; test_scenarios ALL PASS.
This commit is contained in:
+52
-14
@@ -1440,7 +1440,10 @@
|
||||
<option value="l6l3">L6 + L3 Hybrid</option>
|
||||
<option value="l7solo">L7 Solo</option>
|
||||
<option value="l7x3">3× L7 Endgame</option>
|
||||
<option value="custom">🛠 Custom lanes… (any mix)</option>
|
||||
</select>
|
||||
<input type="text" id="roiCustomLanes" placeholder="e.g. L4+L4+L2" style="display:none;margin-top:6px;">
|
||||
<small id="roiCustomHint" style="display:none;color:var(--text-muted);font-size:0.72rem;margin-top:3px;">The campaigns you're actually running, up to 3 lanes — Boost Planner mixes welcome (e.g. L4+L4+L2)</small>
|
||||
</div>
|
||||
<div class="roi-field">
|
||||
<label>Cycles Completed This Phase</label>
|
||||
@@ -2746,30 +2749,64 @@ function updateROI() {
|
||||
// Update label to reflect profit mode
|
||||
document.getElementById('roiStatRemaining').querySelector('.roi-stat-label').textContent = remaining > 0 ? 'To Breakeven' : 'Pure Profit';
|
||||
|
||||
// Phase progress
|
||||
const pd = PHASE_DATA[phase];
|
||||
// Phase progress. "custom" = the Boost Planner reality: any mix of up to
|
||||
// 3 campaign slots (e.g. L4+L4+L2). Economics derived from LEVELS; the
|
||||
// cycles-to-next estimate is accelerate-basis (bank everything) — pocket
|
||||
// modes stretch it, so it's labeled est.
|
||||
const customEl = document.getElementById('roiCustomLanes');
|
||||
const customHintEl = document.getElementById('roiCustomHint');
|
||||
const isCustom = phase === 'custom';
|
||||
customEl.style.display = isCustom ? '' : 'none';
|
||||
customHintEl.style.display = isCustom ? '' : 'none';
|
||||
let pd, phaseKeyForLadder = phase, customBadge = null, customHi = 0;
|
||||
if (isCustom) {
|
||||
const lanes = (customEl.value.match(/[1-7]/g) || []).slice(0, 3).map(Number);
|
||||
if (lanes.length) {
|
||||
const profitPer = lanes.reduce((s, l) => s + LEVELS[l - 1].netProfit, 0);
|
||||
customHi = Math.max(...lanes);
|
||||
const nextLvl = customHi < 7 ? LEVELS[customHi] : null; // LEVELS[hi] = the (hi+1) record
|
||||
pd = {
|
||||
cycles: nextLvl ? Math.max(1, Math.ceil(nextLvl.total / profitPer)) : 999,
|
||||
profitPer,
|
||||
nextLabel: nextLvl ? `L${customHi + 1}` : null,
|
||||
needsCapital: nextLvl ? nextLvl.total : 0,
|
||||
est: true,
|
||||
};
|
||||
const equiv = { 1: 'l1', 2: 'l2l1', 3: 'l3l1', 4: 'l4', 5: 'l5', 6: 'l6', 7: 'l7solo' };
|
||||
phaseKeyForLadder = equiv[customHi];
|
||||
customBadge = 'Custom: ' + lanes.map(l => 'L' + l).join('+') + ` · $${profitPer.toFixed(2)}/cycle`;
|
||||
} else {
|
||||
pd = { cycles: 0, profitPer: 0, nextLabel: null, needsCapital: 0 };
|
||||
customBadge = 'Type your lanes above (e.g. L4+L4+L2)';
|
||||
}
|
||||
} else {
|
||||
pd = PHASE_DATA[phase] || { cycles: 0, profitPer: 0, nextLabel: null, needsCapital: 0 };
|
||||
}
|
||||
let phasePct = 0;
|
||||
let phaseLabel = '';
|
||||
if (phase === 'l7x3') {
|
||||
phasePct = 100;
|
||||
phaseLabel = '🏆 Endgame Reached';
|
||||
} else {
|
||||
} else if (pd.cycles > 0) {
|
||||
phasePct = Math.min((cyclesDone / pd.cycles) * 100, 100);
|
||||
phaseLabel = `Cycle ${cyclesDone} of ${pd.cycles}`;
|
||||
phaseLabel = pd.est ? `Cycle ${cyclesDone} of ~${pd.cycles} (est.)` : `Cycle ${cyclesDone} of ${pd.cycles}`;
|
||||
} else {
|
||||
phaseLabel = '—';
|
||||
}
|
||||
document.getElementById('roiStatPhase').querySelector('.roi-stat-value').textContent = phaseLabel;
|
||||
document.getElementById('roiBarPhase').style.width = phasePct + '%';
|
||||
|
||||
// Ladder visualization
|
||||
// Ladder visualization (custom lanes map to their highest level's rung)
|
||||
const ladderOrder = ['l1','l2l1','l3l1','l4','l4l3','l5','l5l3','l6','l6l3','l7solo','l7x3'];
|
||||
const phaseIndex = ladderOrder.indexOf(phase);
|
||||
const phaseIndex = ladderOrder.indexOf(phaseKeyForLadder);
|
||||
// Location badge
|
||||
const phaseNames = {
|
||||
l1:'L1 Only', l2l1:'L2 + L1', l3l1:'L3 + L1', l4:'L4 Only',
|
||||
l4l3:'L4 + L3 Hybrid', l5:'L5 Only', l5l3:'L5 + L3 Hybrid',
|
||||
l6:'L6 Only', l6l3:'L6 + L3 Hybrid', l7solo:'L7 Solo', l7x3:'3× L7 Endgame'
|
||||
};
|
||||
document.getElementById('roiHerePhase').textContent = phase && phaseNames[phase] ? phaseNames[phase] : '—';
|
||||
document.getElementById('roiHerePhase').textContent =
|
||||
customBadge || (phase && phaseNames[phase] ? phaseNames[phase] : "—");
|
||||
|
||||
document.querySelectorAll('.roi-ladder-step').forEach(el => {
|
||||
el.classList.remove('active', 'passed');
|
||||
@@ -2785,11 +2822,11 @@ function updateROI() {
|
||||
if (earned >= 337.50) milestones.push({ icon: '✅', label: '75% ROI Recouped', sub: 'Three quarters there' });
|
||||
if (earned >= deposits) milestones.push({ icon: '💰', label: '100% ROI — Breakeven!', sub: 'Everything from here is pure profit' });
|
||||
|
||||
if (phase === 'l5l3' || phaseIndex > ladderOrder.indexOf('l5l3'))
|
||||
if (phase === 'l5l3' || phaseIndex > ladderOrder.indexOf('l5l3') || customHi >= 5)
|
||||
milestones.push({ icon: '🏅', label: 'L5 Active', sub: 'You reached the serious scaling tier' });
|
||||
if (phase === 'l6l3' || phaseIndex > ladderOrder.indexOf('l6l3'))
|
||||
if (phase === 'l6l3' || phaseIndex > ladderOrder.indexOf('l6l3') || customHi >= 6)
|
||||
milestones.push({ icon: '🏅', label: 'L6 Active', sub: 'Double-digit clicks/day territory' });
|
||||
if (phase === 'l7solo' || phaseIndex > ladderOrder.indexOf('l7solo'))
|
||||
if (phase === 'l7solo' || phaseIndex > ladderOrder.indexOf('l7solo') || customHi >= 7)
|
||||
milestones.push({ icon: '🌟', label: 'L7 — The Big One', sub: '$840/cycle profit, $270/day clicks' });
|
||||
|
||||
if (phase === 'l7x3')
|
||||
@@ -2833,7 +2870,7 @@ function updateROI() {
|
||||
`).join('');
|
||||
|
||||
// Save to localStorage
|
||||
saveROIData(deposits, earned, phase, cyclesDone);
|
||||
saveROIData(deposits, earned, phase, cyclesDone, customEl.value);
|
||||
}
|
||||
|
||||
// ─── localStorage persistence ──────────────────────────
|
||||
@@ -2842,9 +2879,9 @@ function getROIStorageKey() {
|
||||
return 'cbp_roi_' + ref;
|
||||
}
|
||||
|
||||
function saveROIData(deposits, earned, phase, cyclesDone) {
|
||||
function saveROIData(deposits, earned, phase, cyclesDone, customLanes) {
|
||||
const key = getROIStorageKey();
|
||||
const data = { deposits, earned, phase, cyclesDone };
|
||||
const data = { deposits, earned, phase, cyclesDone, customLanes: customLanes || '' };
|
||||
try { localStorage.setItem(key, JSON.stringify(data)); } catch(e) {}
|
||||
}
|
||||
|
||||
@@ -2858,7 +2895,7 @@ function loadROIData() {
|
||||
}
|
||||
|
||||
// Wire up ROI inputs
|
||||
['roiDeposits','roiTotalEarned','roiPhase','roiCyclesDone'].forEach(id => {
|
||||
['roiDeposits','roiTotalEarned','roiPhase','roiCyclesDone','roiCustomLanes'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
el.addEventListener('input', updateROI);
|
||||
el.addEventListener('change', updateROI);
|
||||
@@ -2871,6 +2908,7 @@ if (saved) {
|
||||
document.getElementById('roiTotalEarned').value = saved.earned || '';
|
||||
if (saved.phase) document.getElementById('roiPhase').value = saved.phase;
|
||||
document.getElementById('roiCyclesDone').value = saved.cyclesDone !== undefined ? saved.cyclesDone : '';
|
||||
document.getElementById('roiCustomLanes').value = saved.customLanes || '';
|
||||
}
|
||||
|
||||
// Initial render on page load
|
||||
|
||||
Reference in New Issue
Block a user