From 18fe95effc5a71c1d290e95284df360c8e8a4be8 Mon Sep 17 00:00:00 2001 From: Oronemu Date: Mon, 5 Jan 2026 23:41:22 +0700 Subject: [PATCH] Fix events --- backend/app/api/v1/assignments.py | 20 +++++- backend/app/api/v1/wheel.py | 101 ++++++++++++++++++++++-------- backend/app/schemas/assignment.py | 2 + frontend/src/types/index.ts | 2 + frontend/src/utils/activity.ts | 3 +- 5 files changed, 99 insertions(+), 29 deletions(-) diff --git a/backend/app/api/v1/assignments.py b/backend/app/api/v1/assignments.py index 470b14f..81d1da4 100644 --- a/backend/app/api/v1/assignments.py +++ b/backend/app/api/v1/assignments.py @@ -1335,17 +1335,31 @@ async def complete_bonus_assignment( bonus_assignment.proof_url = proof_url # Complete the bonus assignment + # NOTE: We store BASE points here. Event multiplier is applied when main assignment is completed + # This ensures multiplier is applied to the SUM (base + all bonuses), not separately + bonus_assignment.points_earned = bonus_assignment.challenge.points bonus_assignment.status = BonusAssignmentStatus.COMPLETED.value bonus_assignment.proof_comment = comment - bonus_assignment.points_earned = bonus_assignment.challenge.points bonus_assignment.completed_at = datetime.utcnow() # If main assignment is already COMPLETED, add bonus points immediately # This handles the case where a bonus was disputed and user is re-completing it if assignment.status == AssignmentStatus.COMPLETED.value: + from app.models import EventType + from app.services.points import PointsService + + # Apply event multiplier if assignment had one + points_to_add = bonus_assignment.points_earned + if assignment.event_type: + ps = PointsService() + multiplier = ps.EVENT_MULTIPLIERS.get(assignment.event_type, 1.0) + points_to_add = int(bonus_assignment.points_earned * multiplier) + # Update bonus assignment to show multiplied points + bonus_assignment.points_earned = points_to_add + participant = assignment.participant - participant.total_points += bonus_assignment.points_earned - assignment.points_earned += bonus_assignment.points_earned + participant.total_points += points_to_add + assignment.points_earned += points_to_add # NOTE: If main is not completed yet, points will be added when main is completed # This prevents exploiting by dropping the main assignment after getting bonus points diff --git a/backend/app/api/v1/wheel.py b/backend/app/api/v1/wheel.py index b3a636c..aa86799 100644 --- a/backend/app/api/v1/wheel.py +++ b/backend/app/api/v1/wheel.py @@ -161,10 +161,13 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) game = random.choice(available_games) if game.game_type == GameType.PLAYTHROUGH.value: - # Playthrough game - no challenge selection, ignore events + # Playthrough game - no challenge selection + # Events that apply to playthrough: GOLDEN_HOUR, DOUBLE_RISK, COMMON_ENEMY + # Events that DON'T apply: JACKPOT (hard challenges only) is_playthrough = True challenge = None - active_event = None # Ignore events for playthrough + if active_event and active_event.type == EventType.JACKPOT.value: + active_event = None # Jackpot doesn't apply to playthrough else: # Challenges game - select random challenge if not game.challenges: @@ -201,7 +204,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) game_id=game.id, is_playthrough=True, status=AssignmentStatus.ACTIVE.value, - # No event_type for playthrough + event_type=active_event.type if active_event else None, ) db.add(assignment) await db.flush() # Get assignment.id for bonus assignments @@ -224,6 +227,8 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) "points": game.playthrough_points, "bonus_challenges_count": len(bonus_challenges), } + if active_event: + activity_data["event_type"] = active_event.type else: # Regular challenge assignment assignment = Assignment( @@ -323,6 +328,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) ], can_drop=True, drop_penalty=drop_penalty, + event_type=active_event.type if active_event else None, ) else: # Return challenge result @@ -346,6 +352,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession) is_playthrough=False, can_drop=True, drop_penalty=drop_penalty, + event_type=active_event.type if active_event else None, ) @@ -371,9 +378,17 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db # Handle playthrough assignments if assignment.is_playthrough: game = assignment.game - active_event = None # No events for playthrough + # Use stored event_type for playthrough + # All events except JACKPOT apply (DOUBLE_RISK = free drop, others affect points) + playthrough_event = None + if assignment.event_type and assignment.event_type != EventType.JACKPOT.value: + class MockEvent: + def __init__(self, event_type): + self.type = event_type + playthrough_event = MockEvent(assignment.event_type) + drop_penalty = points_service.calculate_drop_penalty( - participant.drop_count, game.playthrough_points, None + participant.drop_count, game.playthrough_points, playthrough_event ) # Build bonus challenges response @@ -423,6 +438,7 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db completed_at=assignment.completed_at, drop_penalty=drop_penalty, bonus_challenges=bonus_responses, + event_type=assignment.event_type, ) # Regular challenge assignment @@ -457,6 +473,7 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db started_at=assignment.started_at, completed_at=assignment.completed_at, drop_penalty=drop_penalty, + event_type=assignment.event_type, ) @@ -570,19 +587,37 @@ async def complete_assignment( if assignment.is_playthrough: game = assignment.game marathon_id = game.marathon_id - base_points = game.playthrough_points + base_playthrough_points = game.playthrough_points - # No events for playthrough - total_points, streak_bonus, _ = points_service.calculate_completion_points( - base_points, participant.current_streak, None - ) - - # Calculate bonus points from completed bonus assignments - bonus_points = sum( - ba.points_earned for ba in assignment.bonus_assignments + # Calculate BASE bonus points from completed bonus assignments (before multiplier) + base_bonus_points = sum( + ba.challenge.points for ba in assignment.bonus_assignments if ba.status == BonusAssignmentStatus.COMPLETED.value ) - total_points += bonus_points + + # Total base = playthrough + all bonuses + total_base_points = base_playthrough_points + base_bonus_points + + # Get event for playthrough (use stored event_type from assignment) + # All events except JACKPOT apply to playthrough + playthrough_event = None + if assignment.event_type and assignment.event_type != EventType.JACKPOT.value: + class MockEvent: + def __init__(self, event_type): + self.type = event_type + playthrough_event = MockEvent(assignment.event_type) + + # Apply multiplier to the TOTAL (base + bonuses), then add streak bonus + total_points, streak_bonus, event_bonus = points_service.calculate_completion_points( + total_base_points, participant.current_streak, playthrough_event + ) + + # Update bonus assignments to reflect multiplied points for display + if playthrough_event: + multiplier = points_service.EVENT_MULTIPLIERS.get(playthrough_event.type, 1.0) + for ba in assignment.bonus_assignments: + if ba.status == BonusAssignmentStatus.COMPLETED.value: + ba.points_earned = int(ba.challenge.points * multiplier) # Update assignment assignment.status = AssignmentStatus.COMPLETED.value @@ -607,12 +642,15 @@ async def complete_assignment( "game": game.title, "is_playthrough": True, "points": total_points, - "base_points": base_points, - "bonus_points": bonus_points, + "base_points": base_playthrough_points, + "bonus_points": base_bonus_points, "streak": participant.current_streak, } if is_redo: activity_data["is_redo"] = True + if playthrough_event: + activity_data["event_type"] = playthrough_event.type + activity_data["event_bonus"] = event_bonus activity = Activity( marathon_id=marathon_id, @@ -796,9 +834,17 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS game = assignment.game marathon_id = game.marathon_id - # No events for playthrough + # Use stored event_type for drop penalty calculation + # DOUBLE_RISK = free drop (0 penalty) + playthrough_event = None + if assignment.event_type and assignment.event_type != EventType.JACKPOT.value: + class MockEvent: + def __init__(self, event_type): + self.type = event_type + playthrough_event = MockEvent(assignment.event_type) + penalty = points_service.calculate_drop_penalty( - participant.drop_count, game.playthrough_points, None + participant.drop_count, game.playthrough_points, playthrough_event ) # Update assignment @@ -823,16 +869,21 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS participant.drop_count += 1 # Log activity + activity_data = { + "game": game.title, + "is_playthrough": True, + "penalty": penalty, + "lost_bonuses": completed_bonuses_count, + } + if playthrough_event: + activity_data["event_type"] = playthrough_event.type + activity_data["free_drop"] = True + activity = Activity( marathon_id=marathon_id, user_id=current_user.id, type=ActivityType.DROP.value, - data={ - "game": game.title, - "is_playthrough": True, - "penalty": penalty, - "lost_bonuses": completed_bonuses_count, - }, + data=activity_data, ) db.add(activity) diff --git a/backend/app/schemas/assignment.py b/backend/app/schemas/assignment.py index 67d892f..f109d44 100644 --- a/backend/app/schemas/assignment.py +++ b/backend/app/schemas/assignment.py @@ -56,6 +56,7 @@ class AssignmentResponse(BaseModel): completed_at: datetime | None = None drop_penalty: int = 0 # Calculated penalty if dropped bonus_challenges: list[BonusAssignmentResponse] = [] # Для playthrough + event_type: str | None = None # Event type if assignment was created during event class Config: from_attributes = True @@ -70,6 +71,7 @@ class SpinResult(BaseModel): bonus_challenges: list[ChallengeResponse] = [] # Для playthrough - список доступных бонусных челленджей can_drop: bool drop_penalty: int + event_type: str | None = None # Event type if active during spin class CompleteResult(BaseModel): diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 396df72..e18b85c 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -268,6 +268,7 @@ export interface Assignment { completed_at: string | null drop_penalty: number bonus_challenges?: BonusAssignment[] // For playthrough + event_type?: EventType // Event active when assignment was created } export interface SpinResult { @@ -279,6 +280,7 @@ export interface SpinResult { bonus_challenges?: Challenge[] // Available bonus challenges for playthrough can_drop: boolean drop_penalty: number + event_type?: EventType // Event active during spin } export interface CompleteResult { diff --git a/frontend/src/utils/activity.ts b/frontend/src/utils/activity.ts index ede41e0..1941aa6 100644 --- a/frontend/src/utils/activity.ts +++ b/frontend/src/utils/activity.ts @@ -152,11 +152,12 @@ export function formatActivityMessage(activity: Activity): { title: string; deta const points = data.points ? `+${data.points}` : '' const streak = data.streak && (data.streak as number) > 1 ? `серия ${data.streak}` : '' const bonus = data.common_enemy_bonus ? `+${data.common_enemy_bonus} бонус` : '' + const eventBonus = data.event_bonus ? `(+${data.event_bonus} ${EVENT_NAMES[data.event_type as EventType] || 'бонус'})` : '' return { title: `завершил ${points}`, details: challenge || undefined, - extra: [game, streak, bonus].filter(Boolean).join(' • ') || undefined, + extra: [game, streak, bonus, eventBonus].filter(Boolean).join(' • ') || undefined, } }