데스티니 해방에 단순 계산 모드 주간 보스 설정 추가
WeeklyDefault/BossRow를 bosses/imageBase/hasScheduler prop 받도록 일반화. 데스티니 단순 계산 모드에서 8개 보스의 난이도·파티 인원·완료 상태를 설정할 수 있도록 구현. 주차별 계산은 플레이스홀더. store에 destiny weekly 슬롯 추가하고 v1 migrate로 기존 사용자의 localStorage backfill 처리. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d506c022ca
commit
99500d91af
5 changed files with 101 additions and 18 deletions
|
|
@ -1,9 +1,17 @@
|
|||
import dayjs from 'dayjs'
|
||||
import { DESTINY_CHAPTERS, DESTINY_QUEST_IMAGE_BASE, formatDate } from '../data'
|
||||
import {
|
||||
DESTINY_CHAPTERS,
|
||||
DESTINY_QUEST_IMAGE_BASE,
|
||||
DESTINY_BOSSES,
|
||||
DESTINY_BOSS_IMAGE_BASE,
|
||||
formatDate,
|
||||
} from '../data'
|
||||
import { useLiberationStore } from '../store'
|
||||
import { calcWeekPoints } from '../utils'
|
||||
import ProgressBar from './components/ProgressBar'
|
||||
import QuestSelector from './components/QuestSelector'
|
||||
import PointsInput from './components/PointsInput'
|
||||
import WeeklyDefault from './components/WeeklyDefault'
|
||||
import DatePicker from '../../../components/common/DatePicker'
|
||||
|
||||
export default function Destiny() {
|
||||
|
|
@ -13,6 +21,8 @@ export default function Destiny() {
|
|||
const updateSlot = useLiberationStore((s) => s.updateDestinySlot)
|
||||
const setState = (updater) => updateSlot(updater)
|
||||
|
||||
const weeklyEarn = calcWeekPoints(state.weekly, DESTINY_BOSSES)
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 계산 모드 탭 */}
|
||||
|
|
@ -115,6 +125,31 @@ export default function Destiny() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{calcMode === 'simple' ? (
|
||||
<WeeklyDefault
|
||||
bosses={DESTINY_BOSSES}
|
||||
imageBase={DESTINY_BOSS_IMAGE_BASE}
|
||||
weekly={state.weekly}
|
||||
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
||||
totalWeekly={weeklyEarn}
|
||||
remaining={0}
|
||||
mode="simple"
|
||||
hasScheduler={false}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
className="max-w-3xl mx-auto rounded-2xl border p-16 text-center"
|
||||
style={{
|
||||
background: 'var(--panel-bg)',
|
||||
borderColor: 'var(--panel-border)',
|
||||
boxShadow: 'var(--panel-shadow)',
|
||||
}}
|
||||
>
|
||||
<div className="text-xl font-bold" style={{ color: 'var(--text-emphasis)' }}>주차별 계산 준비 중</div>
|
||||
<div className="text-sm mt-2" style={{ color: 'var(--text-dim)' }}>단순 계산 탭을 이용해주세요.</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,10 @@ import dayjs from 'dayjs'
|
|||
import {
|
||||
GENESIS_CHAPTERS,
|
||||
GENESIS_TOTAL,
|
||||
WEEKLY_BOSSES,
|
||||
MONTHLY_BOSSES,
|
||||
QUEST_BOSS_IMAGE_BASE,
|
||||
LIBERATION_BOSS_IMAGE_BASE,
|
||||
formatDate,
|
||||
} from '../data'
|
||||
import { useLiberationStore } from '../store'
|
||||
|
|
@ -193,6 +195,9 @@ export default function Genesis() {
|
|||
</div>
|
||||
|
||||
<WeeklyDefault
|
||||
bosses={WEEKLY_BOSSES}
|
||||
monthlyBosses={MONTHLY_BOSSES}
|
||||
imageBase={LIBERATION_BOSS_IMAGE_BASE}
|
||||
weekly={state.weekly}
|
||||
onChange={(w) => setState((prev) => ({ ...prev, weekly: w }))}
|
||||
totalWeekly={headerWeekly}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Select from '../../../../components/common/Select'
|
||||
import Tooltip from '../../../../components/common/Tooltip'
|
||||
import WeeklyScheduler from './WeeklyScheduler'
|
||||
import { WEEKLY_BOSSES, MONTHLY_BOSSES, LIBERATION_BOSS_IMAGE_BASE, calcPoints } from '../../data'
|
||||
import { calcPoints } from '../../data'
|
||||
|
||||
const PARTY_OPTIONS = [1, 2, 3, 4, 5, 6].map((n) => ({ value: n, label: `${n}인` }))
|
||||
const NONE_DIFFICULTY = { key: 'none', label: '격파 불가', points: 0 }
|
||||
|
|
@ -16,7 +16,7 @@ function diffLabel(d, party) {
|
|||
)
|
||||
}
|
||||
|
||||
export function BossRow({ boss, sel, onChange, monthly = false, showDone = true }) {
|
||||
export function BossRow({ boss, sel, onChange, imageBase, monthly = false, showDone = true }) {
|
||||
const disabled = sel.difficulty === 'none'
|
||||
const difficultyOptions = [NONE_DIFFICULTY, ...boss.difficulties]
|
||||
.map((d) => ({ value: d.key, label: diffLabel(d, sel.party) }))
|
||||
|
|
@ -24,7 +24,7 @@ export function BossRow({ boss, sel, onChange, monthly = false, showDone = true
|
|||
return (
|
||||
<div className="flex items-center gap-3 rounded-lg px-3 h-16">
|
||||
<Tooltip text={boss.name}>
|
||||
<img src={`${LIBERATION_BOSS_IMAGE_BASE}/${boss.image}`} alt="" className="w-10 h-10 rounded-md object-cover shrink-0" />
|
||||
<img src={`${imageBase}/${boss.image}`} alt="" className="w-10 h-10 rounded-md object-cover shrink-0" />
|
||||
</Tooltip>
|
||||
<span className="text-base font-semibold flex-1 truncate">
|
||||
{boss.name}
|
||||
|
|
@ -81,7 +81,22 @@ export function BossRow({ boss, sel, onChange, monthly = false, showDone = true
|
|||
)
|
||||
}
|
||||
|
||||
export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMonthly, remaining, mode = 'simple', startDate, weeks, onChangeWeeks }) {
|
||||
export default function WeeklyDefault({
|
||||
bosses,
|
||||
monthlyBosses = [],
|
||||
imageBase,
|
||||
weekly,
|
||||
onChange,
|
||||
totalWeekly,
|
||||
totalMonthly = 0,
|
||||
remaining,
|
||||
mode = 'simple',
|
||||
startDate,
|
||||
weeks,
|
||||
onChangeWeeks,
|
||||
hasScheduler = true,
|
||||
label = '주간 보스 설정',
|
||||
}) {
|
||||
const updateBoss = (key, patch) => {
|
||||
onChange({ ...weekly, bosses: { ...weekly.bosses, [key]: { ...weekly.bosses[key], ...patch } } })
|
||||
}
|
||||
|
|
@ -99,13 +114,17 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
|
|||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>주간 보스 설정</div>
|
||||
<div className="text-lg font-semibold" style={{ color: 'var(--accent-bright)' }}>{label}</div>
|
||||
<div className="text-sm tabular-nums">
|
||||
{mode === 'weekly' ? (
|
||||
<>
|
||||
<span className="font-semibold" style={{ color: 'var(--accent-bright)' }}>{totalWeekly}</span>
|
||||
<span className="mx-1" style={{ color: 'var(--text-dim)' }}>+</span>
|
||||
<span className="font-semibold" style={{ color: 'var(--warning-text-bright)' }}>{totalMonthly}</span>
|
||||
{monthlyBosses.length > 0 && (
|
||||
<>
|
||||
<span className="mx-1" style={{ color: 'var(--text-dim)' }}>+</span>
|
||||
<span className="font-semibold" style={{ color: 'var(--warning-text-bright)' }}>{totalMonthly}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="mx-1" style={{ color: 'var(--text-dim)' }}>/</span>
|
||||
<span className="font-semibold" style={{ color: 'var(--text-emphasis)' }}>{(remaining ?? 0).toLocaleString()}</span>
|
||||
</>
|
||||
|
|
@ -115,9 +134,9 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{mode === 'simple' ? (
|
||||
{mode === 'simple' || !hasScheduler ? (
|
||||
<div>
|
||||
{WEEKLY_BOSSES.map((boss, i) => (
|
||||
{bosses.map((boss, i) => (
|
||||
<div
|
||||
key={boss.key}
|
||||
className={i > 0 ? 'border-t' : ''}
|
||||
|
|
@ -127,10 +146,11 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
|
|||
boss={boss}
|
||||
sel={weekly.bosses[boss.key]}
|
||||
onChange={(patch) => updateBoss(boss.key, patch)}
|
||||
imageBase={imageBase}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{MONTHLY_BOSSES.map((boss) => (
|
||||
{monthlyBosses.map((boss) => (
|
||||
<div
|
||||
key={boss.key}
|
||||
className="border-t"
|
||||
|
|
@ -140,6 +160,7 @@ export default function WeeklyDefault({ weekly, onChange, totalWeekly, totalMont
|
|||
boss={boss}
|
||||
sel={weekly.blackMage}
|
||||
onChange={updateBlackMage}
|
||||
imageBase={imageBase}
|
||||
monthly
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { create } from 'zustand'
|
||||
import { persist } from 'zustand/middleware'
|
||||
import dayjs from 'dayjs'
|
||||
import { WEEKLY_BOSSES, MONTHLY_BOSSES, todayKST } from './data'
|
||||
import { WEEKLY_BOSSES, MONTHLY_BOSSES, DESTINY_BOSSES, todayKST } from './data'
|
||||
|
||||
function makeEmptyWeekly() {
|
||||
const bosses = {}
|
||||
|
|
@ -14,6 +14,14 @@ function makeEmptyWeekly() {
|
|||
}
|
||||
}
|
||||
|
||||
function makeEmptyDestinyWeekly() {
|
||||
const bosses = {}
|
||||
DESTINY_BOSSES.forEach((b) => {
|
||||
bosses[b.key] = { difficulty: 'none', party: 1, done: false }
|
||||
})
|
||||
return { bosses }
|
||||
}
|
||||
|
||||
function makeInitialSlot() {
|
||||
return {
|
||||
startChapter: 0,
|
||||
|
|
@ -29,6 +37,7 @@ function makeInitialDestinySlot() {
|
|||
startChapter: 0,
|
||||
currentPoints: 0,
|
||||
startDate: dayjs(todayKST()).toISOString(),
|
||||
weekly: makeEmptyDestinyWeekly(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -72,7 +81,20 @@ export const useLiberationStore = create(persist(
|
|||
return { [key]: makeInitialDestinySlot() }
|
||||
}),
|
||||
}),
|
||||
{ name: 'maple-liberation' },
|
||||
{
|
||||
name: 'maple-liberation',
|
||||
version: 1,
|
||||
migrate: (persisted) => {
|
||||
if (!persisted) return persisted
|
||||
// v0→v1: 데스티니 슬롯에 weekly 필드가 없으면 빈 값으로 채움
|
||||
const fill = (slot) => slot ? { ...slot, weekly: slot.weekly || makeEmptyDestinyWeekly() } : slot
|
||||
return {
|
||||
...persisted,
|
||||
destinySimple: fill(persisted.destinySimple),
|
||||
destinyWeekly: fill(persisted.destinyWeekly),
|
||||
}
|
||||
},
|
||||
},
|
||||
))
|
||||
|
||||
export { makeEmptyWeekly, makeInitialSlot, makeInitialDestinySlot }
|
||||
export { makeEmptyWeekly, makeEmptyDestinyWeekly, makeInitialSlot, makeInitialDestinySlot }
|
||||
|
|
|
|||
|
|
@ -11,17 +11,17 @@ export function bossEarn(boss, sel) {
|
|||
return calcPoints(d.points, sel.party)
|
||||
}
|
||||
|
||||
export function calcWeekPoints(weekData) {
|
||||
export function calcWeekPoints(weekData, bosses = WEEKLY_BOSSES) {
|
||||
let points = 0
|
||||
WEEKLY_BOSSES.forEach((b) => {
|
||||
bosses.forEach((b) => {
|
||||
points += bossEarn(b, weekData.bosses[b.key])
|
||||
})
|
||||
return points
|
||||
}
|
||||
|
||||
export function calcDoneEarn(weekData) {
|
||||
export function calcDoneEarn(weekData, bosses = WEEKLY_BOSSES) {
|
||||
let points = 0
|
||||
WEEKLY_BOSSES.forEach((b) => {
|
||||
bosses.forEach((b) => {
|
||||
const sel = weekData.bosses[b.key]
|
||||
if (sel?.done) points += bossEarn(b, sel)
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue