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

포인트 이월(캐스케이드) 및 주간/월간 리셋을 정확히 반영하기 위해
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 return result
}, [state]) }, [state])
const currentChapterCap = GENESIS_CHAPTERS[state.startChapter]?.required ?? 0 // : required
const currentChapterFilled = Math.min(state.currentPoints, currentChapterCap) const priorConsumed = GENESIS_CHAPTERS
const initialAccumulated = GENESIS_CHAPTERS
.slice(0, state.startChapter) .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 alreadyDone = initialAccumulated >= GENESIS_TOTAL
const weeklyEarn = calcWeekPoints(state.weekly) const weeklyEarn = calcWeekPoints(state.weekly)
const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0) const remaining = Math.max(GENESIS_TOTAL - initialAccumulated, 0)
// / // /
const doneEarn = calcDoneEarn(state.weekly) const doneEarn = calcDoneEarn(state.weekly)
const monthlyEarn = calcMonthlyEarn(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 function computeCompletionDate() {
let cursor = start.add(1, 'month').startOf('month') if (alreadyDone) return todayKST()
while (!cursor.isAfter(end)) { if (weeklyEarn === 0 && monthlyEarn === 0) return null
count++ if (remaining <= 0) return dayjs(state.startDate).tz('Asia/Seoul').startOf('day').toDate()
cursor = cursor.add(1, 'month').startOf('month')
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') // 1
const currentMonthBMAvailable = monthlyEarn > 0 && monthlyDoneEarn === 0 ? monthlyEarn : 0 if (monthlyEarn > 0) {
const adjustedRemaining = remaining + doneEarn + monthlyDoneEarn let nextMonth = startKST.add(1, 'month').startOf('month')
let weeksNeeded = null for (let i = 0; i < 120; i++) {
if (!alreadyDone && (weeklyEarn > 0 || monthlyEarn > 0)) { events.push({ date: nextMonth, amount: monthlyEarn })
for (let N = 1; N <= 520; N++) { nextMonth = nextMonth.add(1, 'month')
const weeklyCum = N * weeklyEarn
const endKST = startKST.add(N, 'week')
const monthlyCum = currentMonthBMAvailable + monthResetsBetween(startKST, endKST) * monthlyEarn
if (weeklyCum + monthlyCum >= adjustedRemaining) {
weeksNeeded = N
break
} }
} }
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 completionDate = computeCompletionDate()
const startDay = dayjs(state.startDate).tz('Asia/Seoul') const isDone = completionDate !== null
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 updateWeek = (idx, newWeekData) => { const updateWeek = (idx, newWeekData) => {
setState((prev) => ({ setState((prev) => ({