diff --git a/backend/app/api/v1/events.py b/backend/app/api/v1/events.py index 724f33b..48e9401 100644 --- a/backend/app/api/v1/events.py +++ b/backend/app/api/v1/events.py @@ -7,7 +7,7 @@ from sqlalchemy.orm import selectinload from app.api.deps import DbSession, CurrentUser from app.models import ( Marathon, MarathonStatus, Participant, ParticipantRole, - Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge, + Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge, Game, SwapRequest as SwapRequestModel, SwapRequestStatus, User, ) from fastapi import UploadFile, File, Form @@ -150,6 +150,46 @@ async def start_event( detail="Common enemy event requires challenge_id" ) + # Handle playthrough games (negative challenge_id = -game_id) + challenge_id = data.challenge_id + game_id = None + is_playthrough = False + + if data.type == EventType.COMMON_ENEMY.value and challenge_id and challenge_id < 0: + # This is a playthrough game, not a real challenge + game_id = -challenge_id # Convert negative to positive game_id + challenge_id = None + is_playthrough = True + + # Verify game exists and is a playthrough game + from app.models.game import GameType + result = await db.execute( + select(Game).where( + Game.id == game_id, + Game.marathon_id == marathon_id, + Game.game_type == GameType.PLAYTHROUGH.value, + ) + ) + game = result.scalar_one_or_none() + if not game: + raise HTTPException( + status_code=400, + detail="Playthrough game not found" + ) + elif data.type == EventType.COMMON_ENEMY.value and challenge_id and challenge_id > 0: + # Verify regular challenge exists + result = await db.execute( + select(Challenge) + .options(selectinload(Challenge.game)) + .where(Challenge.id == challenge_id) + ) + challenge = result.scalar_one_or_none() + if not challenge or challenge.game.marathon_id != marathon_id: + raise HTTPException( + status_code=400, + detail="Challenge not found in this marathon" + ) + try: event = await event_service.start_event( db=db, @@ -157,7 +197,9 @@ async def start_event( event_type=data.type, created_by_id=current_user.id, duration_minutes=data.duration_minutes, - challenge_id=data.challenge_id, + challenge_id=challenge_id, + game_id=game_id, + is_playthrough=is_playthrough, ) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -919,6 +961,41 @@ async def get_common_enemy_leaderboard( def assignment_to_response(assignment: Assignment) -> AssignmentResponse: """Convert Assignment model to AssignmentResponse""" + # Handle playthrough assignments (no challenge, only game) + if assignment.is_playthrough and assignment.game: + game = assignment.game + return AssignmentResponse( + id=assignment.id, + challenge=ChallengeResponse( + id=-game.id, # Negative ID for playthrough + title=f"Прохождение: {game.title}", + description=game.playthrough_description or "Пройдите игру", + type="completion", + difficulty="medium", + points=game.playthrough_points or 0, + estimated_time=None, + proof_type=game.playthrough_proof_type or "screenshot", + proof_hint=game.playthrough_proof_hint, + game=GameShort( + id=game.id, + title=game.title, + cover_url=storage_service.get_url(game.cover_path, "covers"), + download_url=game.download_url, + game_type=game.game_type, + ), + is_generated=False, + created_at=game.created_at, + ), + status=assignment.status, + proof_url=storage_service.get_url(assignment.proof_path, "proofs") if assignment.proof_path else assignment.proof_url, + proof_comment=assignment.proof_comment, + points_earned=assignment.points_earned, + streak_at_completion=assignment.streak_at_completion, + started_at=assignment.started_at, + completed_at=assignment.completed_at, + ) + + # Regular challenge assignment challenge = assignment.challenge game = challenge.game return AssignmentResponse( @@ -969,7 +1046,8 @@ async def get_event_assignment( result = await db.execute( select(Assignment) .options( - selectinload(Assignment.challenge).selectinload(Challenge.game) + selectinload(Assignment.challenge).selectinload(Challenge.game), + selectinload(Assignment.game), # For playthrough assignments ) .where( Assignment.participant_id == participant.id, @@ -1000,10 +1078,19 @@ async def get_event_assignment( is_completed=False, ) + # Determine challenge_id for response (negative for playthrough) + challenge_id_response = None + if event and event.data: + if event.data.get("is_playthrough"): + game_id = event.data.get("game_id") + challenge_id_response = -game_id if game_id else None + else: + challenge_id_response = event.data.get("challenge_id") + return EventAssignmentResponse( assignment=assignment_to_response(assignment) if assignment else None, event_id=event.id if event else None, - challenge_id=event.data.get("challenge_id") if event and event.data else None, + challenge_id=challenge_id_response, is_completed=is_completed, ) @@ -1027,6 +1114,7 @@ async def complete_event_assignment( .options( selectinload(Assignment.participant), selectinload(Assignment.challenge).selectinload(Challenge.game), + selectinload(Assignment.game), # For playthrough assignments ) .where(Assignment.id == assignment_id) ) @@ -1080,17 +1168,25 @@ async def complete_event_assignment( assignment.proof_comment = comment - # Get marathon_id - marathon_id = assignment.challenge.game.marathon_id + # Get marathon_id and base points (handle playthrough vs regular challenge) + participant = assignment.participant + if assignment.is_playthrough and assignment.game: + marathon_id = assignment.game.marathon_id + base_points = assignment.game.playthrough_points or 0 + challenge_title = f"Прохождение: {assignment.game.title}" + game_title = assignment.game.title + difficulty = "medium" + else: + challenge = assignment.challenge + marathon_id = challenge.game.marathon_id + base_points = challenge.points + challenge_title = challenge.title + game_title = challenge.game.title + difficulty = challenge.difficulty # Get active event for bonus calculation active_event = await event_service.get_active_event(db, marathon_id) - # Calculate base points (no streak bonus for event assignments) - participant = assignment.participant - challenge = assignment.challenge - base_points = challenge.points - # Handle common enemy bonus common_enemy_bonus = 0 common_enemy_closed = False @@ -1114,12 +1210,13 @@ async def complete_event_assignment( # Log activity activity_data = { "assignment_id": assignment.id, - "game": challenge.game.title, - "challenge": challenge.title, - "difficulty": challenge.difficulty, + "game": game_title, + "challenge": challenge_title, + "difficulty": difficulty, "points": total_points, "event_type": EventType.COMMON_ENEMY.value, "is_event_assignment": True, + "is_playthrough": assignment.is_playthrough, } if common_enemy_bonus: activity_data["common_enemy_bonus"] = common_enemy_bonus diff --git a/backend/app/services/events.py b/backend/app/services/events.py index 51e6acb..6b45f14 100644 --- a/backend/app/services/events.py +++ b/backend/app/services/events.py @@ -47,6 +47,8 @@ class EventService: created_by_id: int | None = None, duration_minutes: int | None = None, challenge_id: int | None = None, + game_id: int | None = None, + is_playthrough: bool = False, ) -> Event: """Start a new event""" # Check no active event @@ -63,8 +65,12 @@ class EventService: # Build event data data = {} - if event_type == EventType.COMMON_ENEMY.value and challenge_id: - data["challenge_id"] = challenge_id + if event_type == EventType.COMMON_ENEMY.value and (challenge_id or game_id): + if is_playthrough and game_id: + data["game_id"] = game_id + data["is_playthrough"] = True + else: + data["challenge_id"] = challenge_id data["completions"] = [] # Track who completed and when event = Event( @@ -79,9 +85,11 @@ class EventService: db.add(event) await db.flush() # Get event.id before committing - # Auto-assign challenge to all participants for Common Enemy - if event_type == EventType.COMMON_ENEMY.value and challenge_id: - await self._assign_common_enemy_to_all(db, marathon_id, event.id, challenge_id) + # Auto-assign challenge/playthrough to all participants for Common Enemy + if event_type == EventType.COMMON_ENEMY.value and (challenge_id or game_id): + await self._assign_common_enemy_to_all( + db, marathon_id, event.id, challenge_id, game_id, is_playthrough + ) await db.commit() await db.refresh(event) @@ -105,7 +113,9 @@ class EventService: db: AsyncSession, marathon_id: int, event_id: int, - challenge_id: int, + challenge_id: int | None, + game_id: int | None = None, + is_playthrough: bool = False, ) -> None: """Create event assignments for all participants in the marathon""" # Get all participants @@ -118,7 +128,9 @@ class EventService: for participant in participants: assignment = Assignment( participant_id=participant.id, - challenge_id=challenge_id, + challenge_id=challenge_id if not is_playthrough else None, + game_id=game_id if is_playthrough else None, + is_playthrough=is_playthrough, status=AssignmentStatus.ACTIVE.value, event_type=EventType.COMMON_ENEMY.value, is_event_assignment=True, @@ -290,6 +302,30 @@ class EventService: ) return result.scalar_one_or_none() + async def get_common_enemy_game( + self, + db: AsyncSession, + event: Event + ): + """Get the playthrough game for common enemy event (if it's a playthrough)""" + from app.models import Game + + if event.type != EventType.COMMON_ENEMY.value: + return None + + data = event.data or {} + if not data.get("is_playthrough"): + return None + + game_id = data.get("game_id") + if not game_id: + return None + + result = await db.execute( + select(Game).where(Game.id == game_id) + ) + return result.scalar_one_or_none() + def get_time_remaining(self, event: Event | None) -> int | None: """Get remaining time in seconds for an event""" if not event or not event.end_time: