- Habits and the account now accumulate XP and levels from check streaks, with streak/level-up celebration banners and push notifications. - The "yesterday missed" banner on /today now lets a habit be explicitly marked failed (not just checked done), backed by a new HabitLog.status column so completion-rate/streak calculations never count a failed day as done. - The habit management tabs scroll horizontally on narrow screens instead of wrapping to two lines, with a pure-CSS edge shadow indicating more content.
49 lines
1.5 KiB
Python
49 lines
1.5 KiB
Python
from app.schemas.level import LevelInfo
|
|
|
|
# 체크 1회당 얻는 기본 경험치.
|
|
XP_PER_CHECK = 10
|
|
|
|
# 예정일을 놓쳤을 때의 페널티. 연속으로 놓칠수록 (miss_streak * MISS_PENALTY_BASE)로 가속되며,
|
|
# MISS_PENALTY_CAP에서 더 이상 커지지 않는다(끝없이 방치된 습관이 비정상적인 값으로 폭주하지 않도록).
|
|
MISS_PENALTY_BASE = 8
|
|
MISS_PENALTY_CAP = 80
|
|
|
|
# 연속 달성일 마일스톤(log_service.MILESTONE_STREAKS와 동일한 값) 도달 시 추가로 얹어주는 보너스 경험치.
|
|
STREAK_MILESTONE_BONUS_XP: dict[int, int] = {
|
|
7: 20,
|
|
30: 80,
|
|
66: 150,
|
|
100: 250,
|
|
200: 500,
|
|
365: 1000,
|
|
}
|
|
|
|
# 레벨업에 필요한 경험치는 LEVEL_BASE_XP에서 시작해 레벨마다 LEVEL_STEP_XP씩 늘어난다
|
|
# (초반엔 빠르게, 갈수록 완만하게 오르는 곡선).
|
|
LEVEL_BASE_XP = 20
|
|
LEVEL_STEP_XP = 10
|
|
|
|
|
|
def _xp_required_for_level(level: int) -> int:
|
|
"""`level`에서 `level + 1`로 올라가는 데 필요한 경험치."""
|
|
return LEVEL_BASE_XP + LEVEL_STEP_XP * (level - 1)
|
|
|
|
|
|
def level_from_xp(xp: int) -> LevelInfo:
|
|
xp = max(xp, 0)
|
|
level = 1
|
|
remaining = xp
|
|
while True:
|
|
needed = _xp_required_for_level(level)
|
|
if remaining < needed:
|
|
progress_pct = round(remaining / needed * 100, 1)
|
|
return LevelInfo(
|
|
xp=xp,
|
|
level=level,
|
|
xp_into_level=remaining,
|
|
xp_for_next_level=needed,
|
|
progress_pct=progress_pct,
|
|
)
|
|
remaining -= needed
|
|
level += 1
|