|
2026-07-25 04:01
조회: 213
추천: 0
울티마 골드 효율 계산기울티마 골드 빨리 벌고 싶어서 만들었어요
스테이지별 골드는 한자님의 메이플 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
|
메이플스토리 인벤 자유 게시판 게시판
인벤 전광판
메이플스토리 인벤에 오신 것을 환영합니다~★


고니조아