해방 완료일 시뮬레이션 기반 로직으로 교체

포인트 이월(캐스케이드) 및 주간/월간 리셋을 정확히 반영하기 위해
weeksNeeded 공식 대신 이벤트 시뮬레이션으로 완료일을 계산.

- 시작일 당일: (주간 - 완료된 주간 몫) + (이번 달 월간, 검은 마법사 미완료 시)
- 이후 매주 목요일에 주간, 매월 1일에 월간 적립
- 누적이 잔여 흔적을 처음 넘는 이벤트 날짜가 해방일

메이플로드/츄츄지지 계산기 결과와 동일하게 동작함을 확인.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
caadiq 2026-04-14 11:52:44 +09:00
parent 8eaf27d143
commit 1163f77266

View file

@ -146,59 +146,70 @@ export default function Liberation() {
return result
}, [state])
const currentChapterCap = GENESIS_CHAPTERS[state.startChapter]?.required ?? 0
const currentChapterFilled = Math.min(state.currentPoints, currentChapterCap)
const initialAccumulated = GENESIS_CHAPTERS
// : required
const priorConsumed = GENESIS_CHAPTERS
.slice(0, state.startChapter)
.reduce((s, c) => s + c.required, 0) + currentChapterFilled
.reduce((s, c) => s + c.required, 0)
let cascadeIdx = state.startChapter
let cascadeRemain = state.currentPoints
let cascadeConsumed = 0
while (cascadeIdx < GENESIS_CHAPTERS.length && cascadeRemain >= GENESIS_CHAPTERS[cascadeIdx].required) {
cascadeConsumed += GENESIS_CHAPTERS[cascadeIdx].required
cascadeRemain -= GENESIS_CHAPTERS[cascadeIdx].required
cascadeIdx++
}
const initialAccumulated = priorConsumed + cascadeConsumed + cascadeRemain
const alreadyDone = initialAccumulated >= GENESIS_TOTAL
const weeklyEarn = calcWeekPoints(state.weekly)
const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0)
// /
// /
const doneEarn = calcDoneEarn(state.weekly)
const monthlyEarn = calcMonthlyEarn(state.weekly)
const monthlyDoneEarn = calcMonthlyDoneEarn(state.weekly)
const monthlyDoneThisMonth = !!state.weekly.blackMage?.done
function monthResetsBetween(start, end) {
let count = 0
let cursor = start.add(1, 'month').startOf('month')
while (!cursor.isAfter(end)) {
count++
cursor = cursor.add(1, 'month').startOf('month')
//
function computeCompletionDate() {
if (alreadyDone) return todayKST()
if (weeklyEarn === 0 && monthlyEarn === 0) return null
if (remaining <= 0) return dayjs(state.startDate).tz('Asia/Seoul').startOf('day').toDate()
const startKST = dayjs(state.startDate).tz('Asia/Seoul').startOf('day')
const events = []
// : ( - ) + ( , )
const day0Weekly = Math.max(weeklyEarn - doneEarn, 0)
const day0Monthly = monthlyEarn > 0 && !monthlyDoneThisMonth ? monthlyEarn : 0
events.push({ date: startKST, amount: day0Weekly + day0Monthly })
//
const dow = startKST.day()
const daysToNextThu = dow < 4 ? 4 - dow : 11 - dow
let nextThu = startKST.add(daysToNextThu, 'day')
for (let i = 0; i < 520; i++) {
events.push({ date: nextThu, amount: weeklyEarn })
nextThu = nextThu.add(1, 'week')
}
return count
}
const startKST = dayjs(state.startDate).tz('Asia/Seoul')
const currentMonthBMAvailable = monthlyEarn > 0 && monthlyDoneEarn === 0 ? monthlyEarn : 0
const adjustedRemaining = remaining + doneEarn + monthlyDoneEarn
let weeksNeeded = null
if (!alreadyDone && (weeklyEarn > 0 || monthlyEarn > 0)) {
for (let N = 1; N <= 520; N++) {
const weeklyCum = N * weeklyEarn
const endKST = startKST.add(N, 'week')
const monthlyCum = currentMonthBMAvailable + monthResetsBetween(startKST, endKST) * monthlyEarn
if (weeklyCum + monthlyCum >= adjustedRemaining) {
weeksNeeded = N
break
// 1
if (monthlyEarn > 0) {
let nextMonth = startKST.add(1, 'month').startOf('month')
for (let i = 0; i < 120; i++) {
events.push({ date: nextMonth, amount: monthlyEarn })
nextMonth = nextMonth.add(1, 'month')
}
}
events.sort((a, b) => a.date.diff(b.date))
let cumulative = 0
for (const e of events) {
cumulative += e.amount
if (cumulative >= remaining) return e.date.toDate()
}
return null
}
// ( )
const startDay = dayjs(state.startDate).tz('Asia/Seoul')
const dow = startDay.day() // 0= ... 4=
const daysToThu = dow <= 4 ? 4 - dow : 11 - dow
const upcomingThursday = startDay.add(daysToThu, 'day').startOf('day')
const isDone = alreadyDone || weeksNeeded !== null
// = 1, = 2
// = N = upcomingThursday + (weeksNeeded - 2) * 7
const completionDate = alreadyDone
? todayKST()
: weeksNeeded !== null
? upcomingThursday.add(weeksNeeded - 2, 'week').toDate()
: null
const completionDate = computeCompletionDate()
const isDone = completionDate !== null
const updateWeek = (idx, newWeekData) => {
setState((prev) => ({