alabuga/backend/app/services/rank.py
2025-09-27 12:11:19 +03:00

173 lines
6.3 KiB
Python

"""Правила повышения ранга."""
from __future__ import annotations
from sqlalchemy.orm import Session, selectinload
from app.models.journal import JournalEventType
from app.models.mission import SubmissionStatus
from app.models.rank import Rank, RankCompetencyRequirement, RankMissionRequirement
from app.models.user import User
from app.services.journal import log_event
from app.schemas.progress import (
ProgressCompetencyRequirement,
ProgressMissionRequirement,
ProgressRank,
ProgressSnapshot,
ProgressXPMetrics,
)
def _eligible_rank(user: User, db: Session) -> Rank | None:
"""Определяем максимальный ранг, который доступен пользователю."""
ranks = db.query(Rank).order_by(Rank.required_xp).all()
approved_missions = {
submission.mission_id
for submission in user.submissions
if submission.status == SubmissionStatus.APPROVED
}
competencies = {c.competency_id: c.level for c in user.competencies}
candidate: Rank | None = None
for rank in ranks:
if user.xp < rank.required_xp:
break
missions_ok = all(req.mission_id in approved_missions for req in rank.mission_requirements)
competencies_ok = all(
competencies.get(req.competency_id, 0) >= req.required_level
for req in rank.competency_requirements
)
if missions_ok and competencies_ok:
candidate = rank
return candidate
def apply_rank_upgrade(user: User, db: Session) -> Rank | None:
"""Пытаемся повысить ранг и фиксируем событие."""
new_rank = _eligible_rank(user, db)
if not new_rank or user.current_rank_id == new_rank.id:
return None
previous_rank_id = user.current_rank_id
user.current_rank_id = new_rank.id
db.add(user)
db.commit()
db.refresh(user)
log_event(
db,
user_id=user.id,
event_type=JournalEventType.RANK_UP,
title="Повышение ранга",
description=f"Пилот достиг ранга «{new_rank.title}».",
payload={"previous_rank_id": previous_rank_id, "new_rank_id": new_rank.id},
)
return new_rank
def build_progress_snapshot(user: User, db: Session) -> ProgressSnapshot:
"""Собираем агрегированное представление прогресса пользователя."""
ranks = (
db.query(Rank)
.options(
selectinload(Rank.mission_requirements).selectinload(RankMissionRequirement.mission),
selectinload(Rank.competency_requirements).selectinload(RankCompetencyRequirement.competency),
)
.order_by(Rank.required_xp)
.all()
)
current_rank_obj = next((rank for rank in ranks if rank.id == user.current_rank_id), None)
approved_missions = {
submission.mission_id
for submission in user.submissions
if submission.status == SubmissionStatus.APPROVED
}
competency_levels = {item.competency_id: item.level for item in user.competencies}
highest_met_rank: Rank | None = None
next_rank_obj: Rank | None = None
for rank in ranks:
missions_ok = all(req.mission_id in approved_missions for req in rank.mission_requirements)
competencies_ok = all(
competency_levels.get(req.competency_id, 0) >= req.required_level
for req in rank.competency_requirements
)
xp_ok = user.xp >= rank.required_xp
if xp_ok and missions_ok and competencies_ok:
highest_met_rank = rank
continue
if next_rank_obj is None:
next_rank_obj = rank
break
baseline_rank = current_rank_obj or highest_met_rank
baseline_xp = baseline_rank.required_xp if baseline_rank else 0
if next_rank_obj:
target_xp = next_rank_obj.required_xp
remaining_xp = max(0, target_xp - user.xp)
denominator = max(target_xp - baseline_xp, 1)
progress_percent = min(1.0, max(0.0, (user.xp - baseline_xp) / denominator))
else:
target_xp = max(user.xp, baseline_xp)
remaining_xp = 0
progress_percent = 1.0
mission_requirements: list[ProgressMissionRequirement] = []
if next_rank_obj:
for requirement in next_rank_obj.mission_requirements:
title = requirement.mission.title if requirement.mission else f"Миссия #{requirement.mission_id}"
mission_requirements.append(
ProgressMissionRequirement(
mission_id=requirement.mission_id,
mission_title=title,
is_completed=requirement.mission_id in approved_missions,
)
)
competency_requirements: list[ProgressCompetencyRequirement] = []
if next_rank_obj:
for requirement in next_rank_obj.competency_requirements:
current_level = competency_levels.get(requirement.competency_id, 0)
competency_requirements.append(
ProgressCompetencyRequirement(
competency_id=requirement.competency_id,
competency_name=requirement.competency.name if requirement.competency else "",
required_level=requirement.required_level,
current_level=current_level,
is_met=current_level >= requirement.required_level,
)
)
completed_missions = sum(1 for item in mission_requirements if item.is_completed)
met_competencies = sum(1 for item in competency_requirements if item.is_met)
xp_metrics = ProgressXPMetrics(
baseline=baseline_xp,
current=user.xp,
target=target_xp,
remaining=remaining_xp,
progress_percent=round(progress_percent, 4),
)
return ProgressSnapshot(
current_rank=ProgressRank.model_validate(current_rank_obj) if current_rank_obj else None,
next_rank=ProgressRank.model_validate(next_rank_obj) if next_rank_obj else None,
xp=xp_metrics,
mission_requirements=mission_requirements,
competency_requirements=competency_requirements,
completed_missions=completed_missions,
total_missions=len(mission_requirements),
met_competencies=met_competencies,
total_competencies=len(competency_requirements),
)