|
2026-07-25 03:57
조회: 3,607
추천: 4
울티마 스쿼드 골드 효율 계산기울티마 골드 빨리 벌고 싶어서 만들었어요
스테이지별 골드는 한자님의 메이플 AI를 참고해서 만들었습니다 텍스트라서 메모장에 붙여넣기 하고 확장자를 .html로 변경해서 사용하시면 됩니다 자유롭게 바꿔서 사용하시면 됩니다~ 다들 즐메 <!DOCTYPE html> <html lang="ko"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>스테이지 초당 골드 계산기</title> <style> :root{ --bg:#0b0f1a; --panel:#141b2b; --panel2:#1a2338; --border:#293349; --border-strong:#3a4560; --gold:#f0b132; --gold-dim:#c98f22; --green:#3ecf6e; --green-bg:rgba(62,207,110,0.12); --text:#e8e9ee; --text-dim:#8992a8; --danger:#e2534a; --chaos:#b06bff; --chaos-bg:rgba(176,107,255,0.12); } *{box-sizing:border-box;} body{ margin:0; background:radial-gradient(circle at 50% 0%, #131c30 0%, var(--bg) 60%); color:var(--text); font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Malgun Gothic",sans-serif; padding:16px 10px 60px; } .wrap{max-width:820px;margin:0 auto;} h1{ font-size:19px; font-weight:700; margin:0 0 4px; color:var(--gold); letter-spacing:0.2px; text-shadow:0 0 12px rgba(240,177,50,0.35); } .sub{color:var(--text-dim);font-size:12px;margin:0 0 16px;} .mode-tabs{ display:flex; gap:8px; margin-bottom:14px; } .mode-tab{ flex:1; padding:10px; text-align:center; border-radius:10px; border:1px solid var(--border); background:var(--panel); color:var(--text-dim); font-weight:700; font-size:14px; cursor:pointer; transition:all .15s; } .mode-tab.active[data-mode="normal"]{ color:var(--gold); border-color:var(--gold-dim); background:linear-gradient(180deg, rgba(240,177,50,0.15), var(--panel)); box-shadow:0 0 14px rgba(240,177,50,0.15) inset; } .mode-tab.active[data-mode="chaos"]{ color:var(--chaos); border-color:var(--chaos); background:linear-gradient(180deg, rgba(176,107,255,0.15), var(--panel)); box-shadow:0 0 14px rgba(176,107,255,0.2) inset; } .panel{display:none;} .panel.active{display:block;} .card{ background:var(--panel); border:1px solid var(--border); border-radius:12px; padding:14px; margin-bottom:14px; } .card h2{font-size:13px;font-weight:600;color:var(--text-dim);margin:0 0 10px;letter-spacing:0.3px;} .toprow{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; } .toprow label{font-size:13px;color:var(--text-dim);white-space:nowrap;} .toprow input{ width:80px; background:var(--panel2); border:1px solid var(--border); border-radius:8px; color:var(--gold); font-size:16px; font-weight:700; padding:8px 10px; text-align:center; } .toprow input:focus{outline:none;border-color:var(--gold-dim);} .toprow .pct{color:var(--text-dim);font-size:13px;} .save-note{font-size:11px;color:var(--text-dim);margin-left:auto;transition:color .2s;} .save-note.visible{color:var(--green);} .btnrow{display:flex;gap:8px;margin-left:auto;flex-wrap:wrap;} .btn{ background:var(--panel2); border:1px solid var(--border-strong); color:var(--text); border-radius:8px; padding:7px 12px; font-size:12px; cursor:pointer; white-space:nowrap; } .btn:hover{border-color:var(--gold-dim);color:var(--gold);} .btn.danger:hover{border-color:var(--danger);color:var(--danger);} .table-scroll{overflow-x:auto;-webkit-overflow-scrolling:touch;} table{width:100%;border-collapse:collapse;font-size:12.5px;} thead th{ text-align:center;color:var(--text-dim);font-weight:600;font-size:11px; padding:6px 4px;border-bottom:1px solid var(--border-strong);white-space:nowrap; } tbody td{padding:5px 4px;text-align:center;border-bottom:1px solid var(--border);white-space:nowrap;} tbody tr.best td{background:var(--green-bg);} tbody tr.best td:first-child{border-left:3px solid var(--green);} .stage-input,.time-input,.base-input{ width:100%;min-width:50px;background:var(--panel2);border:1px solid var(--border); border-radius:6px;color:var(--text);font-size:12.5px;padding:6px 4px;text-align:center; } .stage-input{min-width:58px;} .stage-input:focus,.time-input:focus,.base-input:focus{outline:none;border-color:var(--gold-dim);} .gps{color:var(--gold);font-weight:700;font-size:13.5px;} tr.best .gps{color:var(--green);} .del-btn{background:none;border:none;color:var(--text-dim);font-size:15px;cursor:pointer;padding:2px 6px;line-height:1;} .del-btn:hover{color:var(--danger);} .add-btn{ width:100%;margin-top:10px;background:var(--panel2);border:1px dashed var(--border-strong); color:var(--gold);border-radius:8px;padding:10px;font-size:13px;cursor:pointer; } .add-btn:hover{border-color:var(--gold-dim);} .best-summary{ margin-top:0;padding:12px 14px;background:var(--green-bg); border:1px solid rgba(62,207,110,0.35);border-radius:10px;font-size:13px; } .best-summary b{color:var(--green);font-size:15px;} .chart-row{display:flex;align-items:center;gap:8px;margin-bottom:8px;} .chart-rank{width:20px;font-size:11px;color:var(--text-dim);text-align:right;flex-shrink:0;} .chart-label{width:56px;font-size:12px;color:var(--text);flex-shrink:0;} .chart-bar-bg{flex:1;background:var(--panel2);border-radius:6px;height:20px;overflow:hidden;position:relative;} .chart-bar-fill{height:100%;border-radius:6px;background:linear-gradient(90deg, var(--gold-dim), var(--gold));} .chart-bar-fill.top{background:linear-gradient(90deg, #1f8f4a, var(--green));} .chart-val{width:78px;font-size:11.5px;color:var(--text-dim);text-align:right;flex-shrink:0;} .crown{color:#ffd85e;margin-right:2px;} .formula{font-size:11.5px;color:var(--text-dim);line-height:1.7;margin-top:10px;} .formula code{color:var(--gold-dim);} </style> </head> <body> <div class="wrap"> <h1>스테이지 초당 골드 계산기</h1> <p class="sub">일반 몬스터 60마리 + 보스(적용골드×20) 기준 · 일반 / 카오스 모드 지원</p> <div class="mode-tabs"> <div class="mode-tab active" data-mode="normal" id="tabNormal">일반 모드</div> <div class="mode-tab" data-mode="chaos" id="tabChaos">카오스 모드</div> </div> <div id="panelNormal" class="panel active"></div> <div id="panelChaos" class="panel"></div> </div> <script> const PANEL_TEMPLATE = (mode) => ` <div class="card"> <div class="toprow"> <label>골드 획득량</label> <input type="text" inputmode="decimal" id="rate-${mode}" style="width:70px;"> <span class="pct">%</span> <label style="margin-left:6px;">몬스터 마리수</label> <input type="text" inputmode="decimal" id="monsterCount-${mode}" style="width:60px;"> <label style="margin-left:6px;">보스 마리수</label> <input type="text" inputmode="decimal" id="bossCount-${mode}" style="width:60px;"> <span class="save-note" id="saveNote-${mode}">저장됨</span> <div class="btnrow"> <button class="btn" id="saveBtn-${mode}">저장</button> <button class="btn" id="csvBtn-${mode}">CSV 내보내기</button> <button class="btn danger" id="resetBtn-${mode}">초기화</button> </div> </div> </div> <div class="card"> <div class="table-scroll"> <table> <thead> <tr> <th>스테이지</th><th>기본골드</th><th>클리어 시간</th><th>총 골드</th><th>초당 골드</th><th>30분당</th><th>1시간당</th><th></th> </tr> </thead> <tbody id="rows-${mode}"></tbody> </table> </div> <button class="add-btn" id="addBtn-${mode}">+ 스테이지 추가</button> <div class="formula"> 적용골드 = 기본골드 × (1 + 획득량%) · 총골드 = (적용골드 × 몬스터 마리수) + (적용골드 × 20 × 보스 마리수) · 초당골드 = 총골드 ÷ 클리어시간(초) · 시간당골드 = 초당골드 × 3600<br> 몬스터/보스 마리수는 스테이지별이 아닌 모드 전체 공통값이며 0으로도 설정할 수 있어요. 시간 입력은 <code>1:33</code> 또는 <code>93</code> 형식 모두 가능 </div> </div> <div class="best-summary" id="bestSummary-${mode}"></div> <div class="card"> <h2>효율 순위 (초당 골드 기준)</h2> <div id="chart-${mode}"></div> </div> `; document.getElementById('panelNormal').innerHTML = PANEL_TEMPLATE('normal'); document.getElementById('panelChaos').innerHTML = PANEL_TEMPLATE('chaos'); function buildDefaultStages(){ const stages = []; const areaBase = { 1:[10,30,50,70,90,110,130,150,170], 2:[400,430,460,490,520,550,580,610,640] }; for(let area=1; area<=3; area++){ for(let n=1; n<=9; n++){ const base = (areaBase[area] && areaBase[area][n-1] !== undefined) ? areaBase[area][n-1] : 0; let time = ''; if(area===1 && n===7) time='1:33'; if(area===1 && n===8) time='2:18'; if(area===1 && n===9) time='2:56'; stages.push({ stage:`${area}-${n}`, base, time }); } } return stages; } const MODES = { normal: { rateKey:'gold-calc-normal-rate-v2', stagesKey:'gold-calc-normal-stages-v2', countsKey:'gold-calc-normal-counts-v2', defaultRate: 15, defaultMonsterCount: 60, defaultBossCount: 1, defaultStages: buildDefaultStages() }, chaos: { rateKey:'gold-calc-chaos-rate-v2', stagesKey:'gold-calc-chaos-stages-v2', countsKey:'gold-calc-chaos-counts-v2', defaultRate: 15, defaultMonsterCount: 60, defaultBossCount: 1, defaultStages: buildDefaultStages().map(s => ({ stage:s.stage, base:0, time:'' })) } }; const state = { normal: { rate: MODES.normal.defaultRate, monsterCount: MODES.normal.defaultMonsterCount, bossCount: MODES.normal.defaultBossCount, stages: JSON.parse(JSON.stringify(MODES.normal.defaultStages)) }, chaos: { rate: MODES.chaos.defaultRate, monsterCount: MODES.chaos.defaultMonsterCount, bossCount: MODES.chaos.defaultBossCount, stages: JSON.parse(JSON.stringify(MODES.chaos.defaultStages)) } }; const saveTimers = {}; function parseTime(str){ if(str === null || str === undefined) return 0; str = String(str).trim(); if(str === '') return 0; if(str.includes(':')){ const parts = str.split(':').map(p => parseFloat(p) || 0); if(parts.length === 2) return parts[0]*60 + parts[1]; if(parts.length === 3) return parts[0]*3600 + parts[1]*60 + parts[2]; return 0; } const n = parseFloat(str); return isNaN(n) ? 0 : n; } function formatNum(n){ return Math.round(n).toLocaleString('ko-KR'); } function computeRow(row, rate, monsterCount, bossCount){ const base = parseFloat(row.base) || 0; const seconds = parseTime(row.time); const applied = base * (1 + rate/100); const mCount = (typeof monsterCount === 'number' && !isNaN(monsterCount)) ? monsterCount : 60; const bCount = (typeof bossCount === 'number' && !isNaN(bossCount)) ? bossCount : 1; const total = applied*mCount + applied*20*bCount; const gps = seconds > 0 ? total/seconds : 0; const gph = gps*3600; const gp30 = gps*1800; return { applied, total, seconds, gps, gph, gp30 }; } const storageAPI = { async get(key){ if(typeof window.storage !== 'undefined' && window.storage){ try{ return await window.storage.get(key, false); } catch(e){ /* fall through to localStorage */ } } try{ const v = localStorage.getItem(key); return v !== null ? { key, value: v, shared:false } : null; }catch(e){ return null; } }, async set(key, value){ if(typeof window.storage !== 'undefined' && window.storage){ try{ return await window.storage.set(key, value, false); } catch(e){ /* fall through to localStorage */ } } try{ localStorage.setItem(key, value); return { key, value, shared:false }; }catch(e){ return null; } } }; const rowRefs = { normal: [], chaos: [] }; function readCounts(mode){ const m = state[mode]; const monsterInput = document.getElementById(`monsterCount-${mode}`); const bossInput = document.getElementById(`bossCount-${mode}`); const mVal = parseFloat(monsterInput.value); const bVal = parseFloat(bossInput.value); m.monsterCount = isNaN(mVal) ? 0 : mVal; m.bossCount = isNaN(bVal) ? 0 : bVal; return m; } function render(mode){ // Full rebuild: only call when rows are added/removed/reset/loaded. const m = state[mode]; const rateInput = document.getElementById(`rate-${mode}`); m.rate = parseFloat(rateInput.value) || 0; readCounts(mode); const tbody = document.getElementById(`rows-${mode}`); tbody.innerHTML = ''; rowRefs[mode] = []; m.stages.forEach((row, i) => { const tr = document.createElement('tr'); const tdStage = document.createElement('td'); const stageInput = document.createElement('input'); stageInput.className = 'stage-input'; stageInput.value = row.stage; stageInput.addEventListener('input', e => { m.stages[i].stage = e.target.value; scheduleSave(mode); renderSummaryAndChart(mode); }); tdStage.appendChild(stageInput); const tdBase = document.createElement('td'); const baseInput = document.createElement('input'); baseInput.className = 'base-input'; baseInput.type = 'text'; baseInput.inputMode = 'decimal'; baseInput.value = row.base; baseInput.addEventListener('input', e => { m.stages[i].base = e.target.value; updateCalcs(mode); }); tdBase.appendChild(baseInput); const tdTime = document.createElement('td'); const timeInput = document.createElement('input'); timeInput.className = 'time-input'; timeInput.placeholder = '1:33'; timeInput.value = row.time; timeInput.addEventListener('input', e => { m.stages[i].time = e.target.value; updateCalcs(mode); }); tdTime.appendChild(timeInput); const tdTotal = document.createElement('td'); const tdGps = document.createElement('td'); tdGps.className = 'gps'; const tdGp30 = document.createElement('td'); const tdGph = document.createElement('td'); const tdDel = document.createElement('td'); const delBtn = document.createElement('button'); delBtn.className = 'del-btn'; delBtn.textContent = '✕'; delBtn.addEventListener('click', () => { m.stages.splice(i,1); render(mode); scheduleSave(mode); }); tdDel.appendChild(delBtn); tr.appendChild(tdStage); tr.appendChild(tdBase); tr.appendChild(tdTime); tr.appendChild(tdTotal); tr.appendChild(tdGps); tr.appendChild(tdGp30); tr.appendChild(tdGph); tr.appendChild(tdDel); tbody.appendChild(tr); rowRefs[mode].push({ tr, tdTotal, tdGps, tdGp30, tdGph }); }); updateCalcs(mode); } function updateCalcs(mode){ // Recompute and update only the calculated cells + best-row highlight. // Never touches the stage/base/time <input> elements, so focus and cursor // position are preserved while typing. const m = state[mode]; const rateInput = document.getElementById(`rate-${mode}`); m.rate = parseFloat(rateInput.value) || 0; readCounts(mode); const computed = m.stages.map(row => computeRow(row, m.rate, m.monsterCount, m.bossCount)); let bestIdx = -1, bestGps = -1; computed.forEach((calc,i) => { if(calc.seconds > 0 && calc.gps > bestGps){ bestGps = calc.gps; bestIdx = i; } }); rowRefs[mode].forEach((refs, i) => { const calc = computed[i]; refs.tr.className = (i === bestIdx) ? 'best' : ''; refs.tdTotal.textContent = formatNum(calc.total) + 'G'; refs.tdGps.textContent = calc.seconds > 0 ? formatNum(calc.gps) + '/s' : '-'; refs.tdGp30.textContent = calc.seconds > 0 ? formatNum(calc.gp30) + 'G' : '-'; refs.tdGph.textContent = calc.seconds > 0 ? formatNum(calc.gph) + 'G' : '-'; }); renderSummaryAndChart(mode); scheduleSave(mode); } function renderSummaryAndChart(mode){ const m = state[mode]; const computed = m.stages.map(row => ({ row, calc: computeRow(row, m.rate, m.monsterCount, m.bossCount) })) .filter(item => item.calc.seconds > 0) .sort((a,b) => b.calc.gps - a.calc.gps); const summary = document.getElementById(`bestSummary-${mode}`); if(computed.length > 0){ const b = computed[0]; summary.innerHTML = `가장 효율 좋은 스테이지: <b>${b.row.stage}</b> — 초당 ${formatNum(b.calc.gps)}G · 시간당 ${formatNum(b.calc.gph)}G (총 ${formatNum(b.calc.total)}G / ${formatNum(b.calc.seconds)}초)`; } else { summary.innerHTML = '클리어 시간을 입력하면 추천 스테이지가 표시됩니다.'; } const chart = document.getElementById(`chart-${mode}`); chart.innerHTML = ''; if(computed.length === 0){ chart.innerHTML = '<div style="color:var(--text-dim);font-size:12px;">시간이 입력된 스테이지가 없습니다.</div>'; return; } const maxGps = computed[0].calc.gps; computed.forEach((item, i) => { const row = document.createElement('div'); row.className = 'chart-row'; const pct = maxGps > 0 ? (item.calc.gps/maxGps*100) : 0; row.innerHTML = ` <div class="chart-rank">${i===0 ? '<span class="crown">👑</span>' : (i+1)}</div> <div class="chart-label">${item.row.stage}</div> <div class="chart-bar-bg"><div class="chart-bar-fill${i===0?' top':''}" style="width:${pct}%"></div></div> <div class="chart-val">${formatNum(item.calc.gps)}/s</div> `; chart.appendChild(row); }); } function scheduleSave(mode){ clearTimeout(saveTimers[mode]); saveTimers[mode] = setTimeout(() => saveState(mode), 500); } async function saveState(mode, manual){ const saveBtn = document.getElementById(`saveBtn-${mode}`); try{ await storageAPI.set(MODES[mode].stagesKey, JSON.stringify(state[mode].stages)); await storageAPI.set(MODES[mode].rateKey, JSON.stringify(state[mode].rate)); await storageAPI.set(MODES[mode].countsKey, JSON.stringify({ monsterCount: state[mode].monsterCount, bossCount: state[mode].bossCount })); const note = document.getElementById(`saveNote-${mode}`); note.textContent = '저장됨'; note.classList.add('visible'); setTimeout(() => note.classList.remove('visible'), 1200); if(manual){ const original = saveBtn.textContent; saveBtn.textContent = '저장됨 ✓'; saveBtn.disabled = true; setTimeout(() => { saveBtn.textContent = original; saveBtn.disabled = false; }, 1000); } }catch(err){ console.error('save failed', err); if(manual){ const original = saveBtn.textContent; saveBtn.textContent = '저장 실패'; setTimeout(() => { saveBtn.textContent = original; }, 1500); } } } async function loadState(mode){ try{ const savedStages = await storageAPI.get(MODES[mode].stagesKey); if(savedStages && savedStages.value) state[mode].stages = JSON.parse(savedStages.value); }catch(err){} try{ const savedRate = await storageAPI.get(MODES[mode].rateKey); if(savedRate && savedRate.value) state[mode].rate = JSON.parse(savedRate.value); }catch(err){} try{ const savedCounts = await storageAPI.get(MODES[mode].countsKey); if(savedCounts && savedCounts.value){ const parsed = JSON.parse(savedCounts.value); state[mode].monsterCount = parsed.monsterCount; state[mode].bossCount = parsed.bossCount; } }catch(err){} document.getElementById(`rate-${mode}`).value = state[mode].rate; document.getElementById(`monsterCount-${mode}`).value = state[mode].monsterCount; document.getElementById(`bossCount-${mode}`).value = state[mode].bossCount; render(mode); } function exportCSV(mode){ const m = state[mode]; const rows = m.stages.map(row => { const c = computeRow(row, m.rate, m.monsterCount, m.bossCount); return [row.stage, row.base, row.time, Math.round(c.applied), Math.round(c.total), c.seconds, Math.round(c.gps), Math.round(c.gp30), Math.round(c.gph)]; }); const header = ['스테이지','기본골드','클리어시간(초)','적용골드','총골드','시간(초)','초당골드','30분당골드','시간당골드']; const meta = [`몬스터 마리수,${m.monsterCount}`, `보스 마리수,${m.bossCount}`, `골드 획득량(%),${m.rate}`, '']; const csvLines = [...meta, header.join(','), ...rows.map(r => r.join(','))]; const csv = 'uFEFF' + csvLines.join('n'); const blob = new Blob([csv], { type:'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `${mode}_gold_stages.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } function resetMode(mode){ state[mode].rate = MODES[mode].defaultRate; state[mode].monsterCount = MODES[mode].defaultMonsterCount; state[mode].bossCount = MODES[mode].defaultBossCount; state[mode].stages = JSON.parse(JSON.stringify(MODES[mode].defaultStages)); document.getElementById(`rate-${mode}`).value = state[mode].rate; document.getElementById(`monsterCount-${mode}`).value = state[mode].monsterCount; document.getElementById(`bossCount-${mode}`).value = state[mode].bossCount; render(mode); scheduleSave(mode); } ['normal','chaos'].forEach(mode => { document.getElementById(`rate-${mode}`).addEventListener('input', () => updateCalcs(mode)); document.getElementById(`monsterCount-${mode}`).addEventListener('input', () => updateCalcs(mode)); document.getElementById(`bossCount-${mode}`).addEventListener('input', () => updateCalcs(mode)); document.getElementById(`addBtn-${mode}`).addEventListener('click', () => { state[mode].stages.push({ stage:'새 스테이지', base:0, time:'' }); render(mode); }); document.getElementById(`csvBtn-${mode}`).addEventListener('click', () => exportCSV(mode)); document.getElementById(`saveBtn-${mode}`).addEventListener('click', () => { clearTimeout(saveTimers[mode]); saveState(mode, true); }); document.getElementById(`resetBtn-${mode}`).addEventListener('click', () => { if(confirm('이 모드의 모든 값을 초기 상태로 되돌릴까요?')) resetMode(mode); }); loadState(mode); }); document.getElementById('tabNormal').addEventListener('click', () => switchTab('normal')); document.getElementById('tabChaos').addEventListener('click', () => switchTab('chaos')); function switchTab(mode){ document.getElementById('tabNormal').classList.toggle('active', mode==='normal'); document.getElementById('tabChaos').classList.toggle('active', mode==='chaos'); document.getElementById('panelNormal').classList.toggle('active', mode==='normal'); document.getElementById('panelChaos').classList.toggle('active', mode==='chaos'); } </script> </body> </html>
EXP
1,066
(33%)
/ 1,201
|
고니조아