<!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-color: #2d2d2d;
--tab-bg: #3c3c3c;
--input-bg: #505050;
--text-color: #ffffff;
--label-color: #d3d3d3;
--btn-color: #007acc;
--btn-hover: #005f9e;
--result-ok: #ffffff;
--result-no: #fa8072;
}
body {
background-color: #1e1e1e;
color: var(--text-color);
font-family: 'Malgun Gothic', 'Segoe UI', sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
/* 메인 컨테이너 (창 크기 고정) */
.container {
width: 500px;
height: 600px; /* 고정 높이 */
background-color: var(--bg-color);
border: 1px solid #444;
border-radius: 12px;
box-shadow: 0 10px 20px rgba(0,0,0,0.5);
display: flex;
flex-direction: column;
overflow: hidden; /* 크기 고정 */
}
/* 탭 영역 */
.tabs {
display: flex;
height: 35px;
background-color: #1e1e1e;
flex-shrink: 0;
}
.tab {
flex: 1;
background-color: var(--bg-color);
color: #888;
border: none;
outline: none;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
border-right: 1px solid #444;
border-bottom: 1px solid #444;
padding: 0;
}
.tab.active {
background-color: var(--tab-bg);
color: #fff;
border-bottom: none;
font-weight: bold;
}
.tab:last-child {
border-right: none;
}
/* 입력 영역 (스크롤 가능) */
.input-area {
flex: 1; /* 남은 공간 차지 */
padding: 20px;
background-color: var(--tab-bg);
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 15px;
}
/* 입력 필드 레이아웃 (Grid 사용) */
.input-group {
display: grid;
grid-template-columns: 150px 1fr;
align-items: center;
gap: 10px;
}
.input-group label {
color: var(--label-color);
font-size: 14px;
text-align: left;
}
.input-group input {
background-color: var(--input-bg);
border: 1px solid #666;
border-radius: 12px;
color: white;
padding: 8px;
font-size: 14px;
outline: none;
width: 100%;
box-sizing: border-box; /* 패딩 포함 크기 계산 */
}
.input-group input:focus {
border-color: var(--btn-color);
}
/* 버튼 영역 */
.btn-area {
padding: 25px 0;
flex-shrink: 0;
display: flex;
justify-content: center;
}
.calculate-btn {
width: 40%;
height: 50px;
background-color: var(--btn-color);
color: white;
border: none;
border-radius: 12px;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: background 0.2s;
}
.calculate-btn:hover {
background-color: var(--btn-hover);
}
/* 결과 영역 (완벽한 중앙 정렬) */
.result-area {
flex: 1; /* 하단 남은 공간 채우기 */
display: flex;
justify-content: center; /* 가로 중앙 */
align-items: center; /* 세로 중앙 */
text-align: center;
padding: 20px;
white-space: pre-line; /* 줄바꿈 허용 */
font-size: 15px;
line-height: 1.6;
background-color: var(--bg-color);
overflow-y: auto;
}
.result-text {
color: #ffffff;
}
.result-ok { color: var(--result-ok); }
.result-no { color: var(--result-no); }
</style>
</head>
<body>
<div class="container">
<div class="tabs" id="tabContainer">
</div>
<div class="input-area" id="inputContainer">
</div>
<div class="btn-area">
<button class="calculate-btn" onclick="calculate()">계산하기 (Calculate)</button>
</div>
<div class="result-area">
<div id="resultText" class="result-text">재료를 입력하고 계산 버튼을 눌러주세요.</div>
</div>
</div>
<script>
// ==========================================
// [1] 데이터 및 설정
// ==========================================
const MATERIAL_TYPES = {
WL: { name: "벌목", A: "원목", B: "부드러운 목재", S: null, C: "아비도스 원목", P: "벌목의 흔적" },
AR: { name: "고고학", A: "오래된 고대 유물", B: "은은한 고대 유물", S: null, C: "아비도스 고대 유물", P: "고고학의 흔적" },
FS: { name: "낚시", A: "싱싱한 피라미", B: "튼튼한 붕어", S: null, C: "아비도스 금빛 잉어", P: "낚시의 흔적" },
MN: { name: "채광", A: "투박한 철광석", B: "천연 보석", S: null, C: "아비도스 원석", P: "채광의 흔적" },
HR: { name: "수렵", A: "생고기", B: "고급 생고기", S: null, C: "아비도스 농장 생고기", P: "수렵의 흔적" },
CL: { name: "채집", A: "소박한 꽃잎", B: "귀여운 꽃잎", S: null, C: "아비도스 꽃잎", P: "채집의 흔적" }
};
const inputState = {
WL: { A: "", B: "", S: "", C: "", P: "" },
AR: { A: "", B: "", S: "", C: "", P: "" },
FS: { A: "", B: "", S: "", C: "", P: "" },
MN: { A: "", B: "", S: "", C: "", P: "" },
HR: { A: "", B: "", S: "", C: "", P: "" },
CL: { A: "", B: "", S: "", C: "", P: "" }
};
let currentType = 'WL';
// 교환비 (입력 단위 -> 출력 단위)
const RATES = {
StoA: { input: 5n, output: 50n },
BtoA: { input: 25n, output: 50n },
AtoP: { input: 100n, output: 80n }, // 100개 넣어야 80개 나옴
BtoP: { input: 50n, output: 80n },
PtoC: { input: 100n, output: 10n } // 100개 넣어야 10개 나옴
};
const RECIPE = { A: 112n, B: 59n, C: 43n };
// ==========================================
// [2] 핵심 로직 (이진 탐색 + 묶음 단위 적용)
// ==========================================
function runAlgorithm(type, inputs) {
const canExchangeSpecial = (type === 'WL' || type === 'MN');
// BigInt 변환 (빈 문자열은 0n으로 처리)
const res = {
A: BigInt(inputs.A || 0),
B: BigInt(inputs.B || 0),
S: BigInt(inputs.S || 0),
C: BigInt(inputs.C || 0),
P: BigInt(inputs.P || 0)
};
let low = 0n;
// 충분히 큰 상한값 설정
let maxA = res.A + (res.B * 2n) + (res.C * 20n);
let high = (maxA / RECIPE.A) + 2000000000n;
let bestOutput = 0n;
let bestLog = {};
// 이진 탐색 시작
while (low <= high) {
let mid = low + (high - low) / 2n;
if (mid < 0n) break;
const check = canProduce(mid, { ...res }, canExchangeSpecial);
if (check.possible) {
bestOutput = mid;
bestLog = check.log;
low = mid + 1n; // 더 큰 값을 찾아봄
} else {
high = mid - 1n; // 불가능하므로 범위를 줄임
}
}
return { output: bestOutput, logs: bestLog };
}
// 100개 단위 묶음(Batch) 교환 규칙을 엄격하게 적용한 검사 함수
function canProduce(targetCount, inputRes, canExchangeSpecial) {
let log = { StoA: 0n, BtoA: 0n, AtoP: 0n, BtoP: 0n, PtoC: 0n };
if (targetCount === 0n) return { possible: true, log };
let res = { ...inputRes }; // 자원 복사본 사용
// 1. S -> A 변환 (5개 단위)
if (canExchangeSpecial && res.S > 0n) {
let batches = res.S / RATES.StoA.input; // 몫만 취함 (자투리 버림)
log.StoA = batches;
res.A += batches * RATES.StoA.output;
}
// 2. 목표 제작을 위한 필요량 계산
let reqA = targetCount * RECIPE.A;
let reqB = targetCount * RECIPE.B;
let reqC = targetCount * RECIPE.C;
// [검증 1] B 재료 절대량 부족 시 불가
if (res.B < reqB) return { possible: false, log };
// [검증 2] 부족한 C를 채우기 위한 가루(P) 계산 (100개 단위 묶음 적용)
let missingC = 0n;
if (res.C < reqC) {
missingC = reqC - res.C;
}
let neededP = 0n;
if (missingC > 0n) {
// C 10개를 얻기 위해 P 100개가 필요함
let outputPerBatch = RATES.PtoC.output; // 10
let inputPerBatch = RATES.PtoC.input; // 100
// 필요 묶음 수 올림 계산: (부족분 + 9) / 10
let batchesC = (missingC + outputPerBatch - 1n) / outputPerBatch;
neededP = batchesC * inputPerBatch; // 실제 필요한 가루 양 (100의 배수)
log.PtoC = batchesC;
}
// 보유 가루(P) 차감
if (res.P >= neededP) {
res.P -= neededP;
neededP = 0n;
} else {
neededP -= res.P;
res.P = 0n;
}
// [검증 3] 남은 재료를 모두 A로 환산하여 최종 비교 (묶음 단위 적용)
// 3-1. 잉여 B -> A 환산 (25개 단위)
let totalAvailableA = res.A;
// 3-2. 부족한 가루를 A 또는 남는 B로 제작
let remainingP = neededP;
// 먼저 남는 B를 가루로 교환
let surplusB = res.B - reqB;
if (remainingP > 0n && surplusB >= RATES.BtoP.input) {
let possibleBatches = surplusB / RATES.BtoP.input;
let neededBatches = (remainingP + RATES.BtoP.output - 1n) / RATES.BtoP.output;
let batchesBtoP = possibleBatches < neededBatches
? possibleBatches
: neededBatches;
log.BtoP = batchesBtoP;
let producedP = batchesBtoP * RATES.BtoP.output;
remainingP -= producedP;
if (remainingP < 0n) remainingP = 0n;
}
// 아직 부족하면 A를 가루로 교환
let aForP = 0n;
if (remainingP > 0n) {
let batchesAtoP =
(remainingP + RATES.AtoP.output - 1n)
/ RATES.AtoP.output;
aForP = batchesAtoP * RATES.AtoP.input;
log.AtoP = batchesAtoP;
}
// 제작용 철광석 + 가루 제작용 철광석
let totalReqA = reqA + aForP;
if (res.A >= totalReqA) {
return { possible: true, log };
}
return { possible: false, log };
return { possible: false, log };
}
// ==========================================
// [3] UI 컨트롤 및 이벤트
// ==========================================
function init() {
renderTabs();
renderInputs();
}
function renderTabs() {
const tabContainer = document.getElementById('tabContainer');
tabContainer.innerHTML = '';
Object.keys(MATERIAL_TYPES).forEach(key => {
const btn = document.createElement('button');
btn.className = `tab ${key === currentType ? 'active' : ''}`;
btn.innerText = MATERIAL_TYPES[key].name;
btn.onclick = () => {
currentType = key;
renderTabs();
renderInputs();
resetResultBox();
};
tabContainer.appendChild(btn);
});
}
function renderInputs() {
const container = document.getElementById('inputContainer');
container.innerHTML = '';
const info = MATERIAL_TYPES[currentType];
const fields = ['A', 'B', 'S', 'C', 'P'];
fields.forEach(fieldKey => {
const labelText = info[fieldKey];
if (!labelText) return;
const group = document.createElement('div');
group.className = 'input-group';
const label = document.createElement('label');
label.innerText = labelText;
const input = document.createElement('input');
input.type = 'text';
input.value = inputState[currentType][fieldKey];
input.placeholder = "0";
// 숫자만 입력되도록 처리
input.oninput = (e) => {
e.target.value = e.target.value.replace(/[^0-9]/g, '');
inputState[currentType][fieldKey] = e.target.value;
};
group.appendChild(label);
group.appendChild(input);
container.appendChild(group);
});
}
function resetResultBox() {
const resultEl = document.getElementById('resultText');
resultEl.innerText = "재료를 입력하고 계산 버튼을 눌러주세요.";
resultEl.className = "result-text";
}
function calculate() {
const inputs = inputState[currentType];
// 입력값이 하나도 없으면 계산하지 않음
if (Object.values(inputs).every(val => val === "" || val === "0")) {
resetResultBox();
return;
}
const result = runAlgorithm(currentType, inputs);
const info = MATERIAL_TYPES[currentType];
const resultEl = document.getElementById('resultText');
if (result.output === 0n) {
resultEl.innerText = "제작 가능한 수량이 없습니다.";
resultEl.className = "result-text result-no";
} else {
resultEl.className = "result-text result-ok";
const fmt = (n) => n.toLocaleString();
let text = `[ ${info.name} 결과 ]<br><br>`;
text += `총 제작 가능 횟수: ${fmt(result.output)}회<br><br>`;
text += `< 최적 교환 경로 ><br>`;
const logs = result.logs;
// 교환 횟수(묶음 단위)를 표시
if (logs.AtoP > 0n)
text += `<span style="color:#999999">${info.A}</span> -> <span style="color:#D9A441">${info.P}</span> : ${fmt(logs.AtoP)}회 교환<br>`;
if (logs.BtoP > 0n)
text += `<span style="color:#4dac00">${info.B}</span> -> <span style="color:#D9A441">${info.P}</span> : ${fmt(logs.BtoP)}회 교환<br>`;
if (logs.PtoC > 0n)
text += `<span style="color:#D9A441">${info.P}</span> -> <span style="color:#006dc6">${info.C}</span> : ${fmt(logs.PtoC)}회 교환<br>`;
resultEl.innerHTML = text;
}
}
// 초기화 실행
init();
</script>
</body>
</html>
<!--
Original: 상급 아비도스 계산기
Original author: @tuijblab-the-decoder
Original: https://codepen.io/tuijblab-the-decoder/pen/azZpWrW
-->