Merge branch 'master' into marathon-v2
This commit is contained in:
@@ -1335,17 +1335,31 @@ async def complete_bonus_assignment(
|
|||||||
bonus_assignment.proof_url = proof_url
|
bonus_assignment.proof_url = proof_url
|
||||||
|
|
||||||
# Complete the bonus assignment
|
# 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.status = BonusAssignmentStatus.COMPLETED.value
|
||||||
bonus_assignment.proof_comment = comment
|
bonus_assignment.proof_comment = comment
|
||||||
bonus_assignment.points_earned = bonus_assignment.challenge.points
|
|
||||||
bonus_assignment.completed_at = datetime.utcnow()
|
bonus_assignment.completed_at = datetime.utcnow()
|
||||||
|
|
||||||
# If main assignment is already COMPLETED, add bonus points immediately
|
# 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
|
# This handles the case where a bonus was disputed and user is re-completing it
|
||||||
if assignment.status == AssignmentStatus.COMPLETED.value:
|
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 = assignment.participant
|
||||||
participant.total_points += bonus_assignment.points_earned
|
participant.total_points += points_to_add
|
||||||
assignment.points_earned += bonus_assignment.points_earned
|
assignment.points_earned += points_to_add
|
||||||
|
|
||||||
# NOTE: If main is not completed yet, points will be added when main is completed
|
# 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
|
# This prevents exploiting by dropping the main assignment after getting bonus points
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from sqlalchemy.orm import selectinload
|
|||||||
from app.api.deps import DbSession, CurrentUser
|
from app.api.deps import DbSession, CurrentUser
|
||||||
from app.models import (
|
from app.models import (
|
||||||
Marathon, MarathonStatus, Participant, ParticipantRole,
|
Marathon, MarathonStatus, Participant, ParticipantRole,
|
||||||
Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge,
|
Event, EventType, Activity, ActivityType, Assignment, AssignmentStatus, Challenge, Game,
|
||||||
SwapRequest as SwapRequestModel, SwapRequestStatus, User,
|
SwapRequest as SwapRequestModel, SwapRequestStatus, User,
|
||||||
)
|
)
|
||||||
from fastapi import UploadFile, File, Form
|
from fastapi import UploadFile, File, Form
|
||||||
@@ -150,6 +150,46 @@ async def start_event(
|
|||||||
detail="Common enemy event requires challenge_id"
|
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:
|
try:
|
||||||
event = await event_service.start_event(
|
event = await event_service.start_event(
|
||||||
db=db,
|
db=db,
|
||||||
@@ -157,7 +197,9 @@ async def start_event(
|
|||||||
event_type=data.type,
|
event_type=data.type,
|
||||||
created_by_id=current_user.id,
|
created_by_id=current_user.id,
|
||||||
duration_minutes=data.duration_minutes,
|
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:
|
except ValueError as e:
|
||||||
raise HTTPException(status_code=400, detail=str(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:
|
def assignment_to_response(assignment: Assignment) -> AssignmentResponse:
|
||||||
"""Convert Assignment model to 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
|
challenge = assignment.challenge
|
||||||
game = challenge.game
|
game = challenge.game
|
||||||
return AssignmentResponse(
|
return AssignmentResponse(
|
||||||
@@ -969,7 +1046,8 @@ async def get_event_assignment(
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Assignment)
|
select(Assignment)
|
||||||
.options(
|
.options(
|
||||||
selectinload(Assignment.challenge).selectinload(Challenge.game)
|
selectinload(Assignment.challenge).selectinload(Challenge.game),
|
||||||
|
selectinload(Assignment.game), # For playthrough assignments
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
Assignment.participant_id == participant.id,
|
Assignment.participant_id == participant.id,
|
||||||
@@ -1000,10 +1078,19 @@ async def get_event_assignment(
|
|||||||
is_completed=False,
|
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(
|
return EventAssignmentResponse(
|
||||||
assignment=assignment_to_response(assignment) if assignment else None,
|
assignment=assignment_to_response(assignment) if assignment else None,
|
||||||
event_id=event.id if event 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,
|
is_completed=is_completed,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1027,6 +1114,7 @@ async def complete_event_assignment(
|
|||||||
.options(
|
.options(
|
||||||
selectinload(Assignment.participant),
|
selectinload(Assignment.participant),
|
||||||
selectinload(Assignment.challenge).selectinload(Challenge.game),
|
selectinload(Assignment.challenge).selectinload(Challenge.game),
|
||||||
|
selectinload(Assignment.game), # For playthrough assignments
|
||||||
)
|
)
|
||||||
.where(Assignment.id == assignment_id)
|
.where(Assignment.id == assignment_id)
|
||||||
)
|
)
|
||||||
@@ -1080,17 +1168,25 @@ async def complete_event_assignment(
|
|||||||
|
|
||||||
assignment.proof_comment = comment
|
assignment.proof_comment = comment
|
||||||
|
|
||||||
# Get marathon_id
|
# Get marathon_id and base points (handle playthrough vs regular challenge)
|
||||||
marathon_id = assignment.challenge.game.marathon_id
|
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
|
# Get active event for bonus calculation
|
||||||
active_event = await event_service.get_active_event(db, marathon_id)
|
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
|
# Handle common enemy bonus
|
||||||
common_enemy_bonus = 0
|
common_enemy_bonus = 0
|
||||||
common_enemy_closed = False
|
common_enemy_closed = False
|
||||||
@@ -1114,12 +1210,13 @@ async def complete_event_assignment(
|
|||||||
# Log activity
|
# Log activity
|
||||||
activity_data = {
|
activity_data = {
|
||||||
"assignment_id": assignment.id,
|
"assignment_id": assignment.id,
|
||||||
"game": challenge.game.title,
|
"game": game_title,
|
||||||
"challenge": challenge.title,
|
"challenge": challenge_title,
|
||||||
"difficulty": challenge.difficulty,
|
"difficulty": difficulty,
|
||||||
"points": total_points,
|
"points": total_points,
|
||||||
"event_type": EventType.COMMON_ENEMY.value,
|
"event_type": EventType.COMMON_ENEMY.value,
|
||||||
"is_event_assignment": True,
|
"is_event_assignment": True,
|
||||||
|
"is_playthrough": assignment.is_playthrough,
|
||||||
}
|
}
|
||||||
if common_enemy_bonus:
|
if common_enemy_bonus:
|
||||||
activity_data["common_enemy_bonus"] = common_enemy_bonus
|
activity_data["common_enemy_bonus"] = common_enemy_bonus
|
||||||
|
|||||||
@@ -163,10 +163,13 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
|||||||
game = random.choice(available_games)
|
game = random.choice(available_games)
|
||||||
|
|
||||||
if game.game_type == GameType.PLAYTHROUGH.value:
|
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
|
is_playthrough = True
|
||||||
challenge = None
|
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:
|
else:
|
||||||
# Challenges game - select random challenge
|
# Challenges game - select random challenge
|
||||||
if not game.challenges:
|
if not game.challenges:
|
||||||
@@ -203,7 +206,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
|||||||
game_id=game.id,
|
game_id=game.id,
|
||||||
is_playthrough=True,
|
is_playthrough=True,
|
||||||
status=AssignmentStatus.ACTIVE.value,
|
status=AssignmentStatus.ACTIVE.value,
|
||||||
# No event_type for playthrough
|
event_type=active_event.type if active_event else None,
|
||||||
)
|
)
|
||||||
db.add(assignment)
|
db.add(assignment)
|
||||||
await db.flush() # Get assignment.id for bonus assignments
|
await db.flush() # Get assignment.id for bonus assignments
|
||||||
@@ -226,6 +229,8 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
|||||||
"points": game.playthrough_points,
|
"points": game.playthrough_points,
|
||||||
"bonus_challenges_count": len(bonus_challenges),
|
"bonus_challenges_count": len(bonus_challenges),
|
||||||
}
|
}
|
||||||
|
if active_event:
|
||||||
|
activity_data["event_type"] = active_event.type
|
||||||
else:
|
else:
|
||||||
# Regular challenge assignment
|
# Regular challenge assignment
|
||||||
assignment = Assignment(
|
assignment = Assignment(
|
||||||
@@ -325,6 +330,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
|||||||
],
|
],
|
||||||
can_drop=True,
|
can_drop=True,
|
||||||
drop_penalty=drop_penalty,
|
drop_penalty=drop_penalty,
|
||||||
|
event_type=active_event.type if active_event else None,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# Return challenge result
|
# Return challenge result
|
||||||
@@ -348,6 +354,7 @@ async def spin_wheel(marathon_id: int, current_user: CurrentUser, db: DbSession)
|
|||||||
is_playthrough=False,
|
is_playthrough=False,
|
||||||
can_drop=True,
|
can_drop=True,
|
||||||
drop_penalty=drop_penalty,
|
drop_penalty=drop_penalty,
|
||||||
|
event_type=active_event.type if active_event else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -373,9 +380,17 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db
|
|||||||
# Handle playthrough assignments
|
# Handle playthrough assignments
|
||||||
if assignment.is_playthrough:
|
if assignment.is_playthrough:
|
||||||
game = assignment.game
|
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(
|
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
|
# Build bonus challenges response
|
||||||
@@ -425,6 +440,7 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db
|
|||||||
completed_at=assignment.completed_at,
|
completed_at=assignment.completed_at,
|
||||||
drop_penalty=drop_penalty,
|
drop_penalty=drop_penalty,
|
||||||
bonus_challenges=bonus_responses,
|
bonus_challenges=bonus_responses,
|
||||||
|
event_type=assignment.event_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Regular challenge assignment
|
# Regular challenge assignment
|
||||||
@@ -459,6 +475,7 @@ async def get_current_assignment(marathon_id: int, current_user: CurrentUser, db
|
|||||||
started_at=assignment.started_at,
|
started_at=assignment.started_at,
|
||||||
completed_at=assignment.completed_at,
|
completed_at=assignment.completed_at,
|
||||||
drop_penalty=drop_penalty,
|
drop_penalty=drop_penalty,
|
||||||
|
event_type=assignment.event_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -572,19 +589,37 @@ async def complete_assignment(
|
|||||||
if assignment.is_playthrough:
|
if assignment.is_playthrough:
|
||||||
game = assignment.game
|
game = assignment.game
|
||||||
marathon_id = game.marathon_id
|
marathon_id = game.marathon_id
|
||||||
base_points = game.playthrough_points
|
base_playthrough_points = game.playthrough_points
|
||||||
|
|
||||||
# No events for playthrough
|
# Calculate BASE bonus points from completed bonus assignments (before multiplier)
|
||||||
total_points, streak_bonus, _ = points_service.calculate_completion_points(
|
base_bonus_points = sum(
|
||||||
base_points, participant.current_streak, None
|
ba.challenge.points for ba in assignment.bonus_assignments
|
||||||
)
|
|
||||||
|
|
||||||
# Calculate bonus points from completed bonus assignments
|
|
||||||
bonus_points = sum(
|
|
||||||
ba.points_earned for ba in assignment.bonus_assignments
|
|
||||||
if ba.status == BonusAssignmentStatus.COMPLETED.value
|
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)
|
||||||
|
|
||||||
# Apply boost multiplier from consumable
|
# Apply boost multiplier from consumable
|
||||||
boost_multiplier = consumables_service.get_active_boost_multiplier(participant)
|
boost_multiplier = consumables_service.get_active_boost_multiplier(participant)
|
||||||
@@ -623,8 +658,8 @@ async def complete_assignment(
|
|||||||
"game": game.title,
|
"game": game.title,
|
||||||
"is_playthrough": True,
|
"is_playthrough": True,
|
||||||
"points": total_points,
|
"points": total_points,
|
||||||
"base_points": base_points,
|
"base_points": base_playthrough_points,
|
||||||
"bonus_points": bonus_points,
|
"bonus_points": base_bonus_points,
|
||||||
"streak": participant.current_streak,
|
"streak": participant.current_streak,
|
||||||
}
|
}
|
||||||
if is_redo:
|
if is_redo:
|
||||||
@@ -633,6 +668,9 @@ async def complete_assignment(
|
|||||||
activity_data["boost_multiplier"] = boost_multiplier
|
activity_data["boost_multiplier"] = boost_multiplier
|
||||||
if coins_earned > 0:
|
if coins_earned > 0:
|
||||||
activity_data["coins_earned"] = coins_earned
|
activity_data["coins_earned"] = coins_earned
|
||||||
|
if playthrough_event:
|
||||||
|
activity_data["event_type"] = playthrough_event.type
|
||||||
|
activity_data["event_bonus"] = event_bonus
|
||||||
|
|
||||||
activity = Activity(
|
activity = Activity(
|
||||||
marathon_id=marathon_id,
|
marathon_id=marathon_id,
|
||||||
@@ -836,9 +874,17 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS
|
|||||||
game = assignment.game
|
game = assignment.game
|
||||||
marathon_id = game.marathon_id
|
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(
|
penalty = points_service.calculate_drop_penalty(
|
||||||
participant.drop_count, game.playthrough_points, None
|
participant.drop_count, game.playthrough_points, playthrough_event
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for shield - if active, no penalty
|
# Check for shield - if active, no penalty
|
||||||
@@ -877,6 +923,9 @@ async def drop_assignment(assignment_id: int, current_user: CurrentUser, db: DbS
|
|||||||
}
|
}
|
||||||
if shield_used:
|
if shield_used:
|
||||||
activity_data["shield_used"] = True
|
activity_data["shield_used"] = True
|
||||||
|
if playthrough_event:
|
||||||
|
activity_data["event_type"] = playthrough_event.type
|
||||||
|
activity_data["free_drop"] = True
|
||||||
|
|
||||||
activity = Activity(
|
activity = Activity(
|
||||||
marathon_id=marathon_id,
|
marathon_id=marathon_id,
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class AssignmentResponse(BaseModel):
|
|||||||
completed_at: datetime | None = None
|
completed_at: datetime | None = None
|
||||||
drop_penalty: int = 0 # Calculated penalty if dropped
|
drop_penalty: int = 0 # Calculated penalty if dropped
|
||||||
bonus_challenges: list[BonusAssignmentResponse] = [] # Для playthrough
|
bonus_challenges: list[BonusAssignmentResponse] = [] # Для playthrough
|
||||||
|
event_type: str | None = None # Event type if assignment was created during event
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
from_attributes = True
|
from_attributes = True
|
||||||
@@ -70,6 +71,7 @@ class SpinResult(BaseModel):
|
|||||||
bonus_challenges: list[ChallengeResponse] = [] # Для playthrough - список доступных бонусных челленджей
|
bonus_challenges: list[ChallengeResponse] = [] # Для playthrough - список доступных бонусных челленджей
|
||||||
can_drop: bool
|
can_drop: bool
|
||||||
drop_penalty: int
|
drop_penalty: int
|
||||||
|
event_type: str | None = None # Event type if active during spin
|
||||||
|
|
||||||
|
|
||||||
class CompleteResult(BaseModel):
|
class CompleteResult(BaseModel):
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class EventService:
|
|||||||
created_by_id: int | None = None,
|
created_by_id: int | None = None,
|
||||||
duration_minutes: int | None = None,
|
duration_minutes: int | None = None,
|
||||||
challenge_id: int | None = None,
|
challenge_id: int | None = None,
|
||||||
|
game_id: int | None = None,
|
||||||
|
is_playthrough: bool = False,
|
||||||
) -> Event:
|
) -> Event:
|
||||||
"""Start a new event"""
|
"""Start a new event"""
|
||||||
# Check no active event
|
# Check no active event
|
||||||
@@ -63,8 +65,12 @@ class EventService:
|
|||||||
|
|
||||||
# Build event data
|
# Build event data
|
||||||
data = {}
|
data = {}
|
||||||
if event_type == EventType.COMMON_ENEMY.value and challenge_id:
|
if event_type == EventType.COMMON_ENEMY.value and (challenge_id or game_id):
|
||||||
data["challenge_id"] = challenge_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
|
data["completions"] = [] # Track who completed and when
|
||||||
|
|
||||||
event = Event(
|
event = Event(
|
||||||
@@ -79,9 +85,11 @@ class EventService:
|
|||||||
db.add(event)
|
db.add(event)
|
||||||
await db.flush() # Get event.id before committing
|
await db.flush() # Get event.id before committing
|
||||||
|
|
||||||
# Auto-assign challenge to all participants for Common Enemy
|
# Auto-assign challenge/playthrough to all participants for Common Enemy
|
||||||
if event_type == EventType.COMMON_ENEMY.value and challenge_id:
|
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)
|
await self._assign_common_enemy_to_all(
|
||||||
|
db, marathon_id, event.id, challenge_id, game_id, is_playthrough
|
||||||
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
await db.refresh(event)
|
await db.refresh(event)
|
||||||
@@ -105,7 +113,9 @@ class EventService:
|
|||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
marathon_id: int,
|
marathon_id: int,
|
||||||
event_id: int,
|
event_id: int,
|
||||||
challenge_id: int,
|
challenge_id: int | None,
|
||||||
|
game_id: int | None = None,
|
||||||
|
is_playthrough: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create event assignments for all participants in the marathon"""
|
"""Create event assignments for all participants in the marathon"""
|
||||||
# Get all participants
|
# Get all participants
|
||||||
@@ -118,7 +128,9 @@ class EventService:
|
|||||||
for participant in participants:
|
for participant in participants:
|
||||||
assignment = Assignment(
|
assignment = Assignment(
|
||||||
participant_id=participant.id,
|
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,
|
status=AssignmentStatus.ACTIVE.value,
|
||||||
event_type=EventType.COMMON_ENEMY.value,
|
event_type=EventType.COMMON_ENEMY.value,
|
||||||
is_event_assignment=True,
|
is_event_assignment=True,
|
||||||
@@ -290,6 +302,30 @@ class EventService:
|
|||||||
)
|
)
|
||||||
return result.scalar_one_or_none()
|
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:
|
def get_time_remaining(self, event: Event | None) -> int | None:
|
||||||
"""Get remaining time in seconds for an event"""
|
"""Get remaining time in seconds for an event"""
|
||||||
if not event or not event.end_time:
|
if not event or not event.end_time:
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export interface Assignment {
|
|||||||
completed_at: string | null
|
completed_at: string | null
|
||||||
drop_penalty: number
|
drop_penalty: number
|
||||||
bonus_challenges?: BonusAssignment[] // For playthrough
|
bonus_challenges?: BonusAssignment[] // For playthrough
|
||||||
|
event_type?: EventType // Event active when assignment was created
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpinResult {
|
export interface SpinResult {
|
||||||
@@ -284,6 +285,7 @@ export interface SpinResult {
|
|||||||
bonus_challenges?: Challenge[] // Available bonus challenges for playthrough
|
bonus_challenges?: Challenge[] // Available bonus challenges for playthrough
|
||||||
can_drop: boolean
|
can_drop: boolean
|
||||||
drop_penalty: number
|
drop_penalty: number
|
||||||
|
event_type?: EventType // Event active during spin
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompleteResult {
|
export interface CompleteResult {
|
||||||
|
|||||||
@@ -152,11 +152,12 @@ export function formatActivityMessage(activity: Activity): { title: string; deta
|
|||||||
const points = data.points ? `+${data.points}` : ''
|
const points = data.points ? `+${data.points}` : ''
|
||||||
const streak = data.streak && (data.streak as number) > 1 ? `серия ${data.streak}` : ''
|
const streak = data.streak && (data.streak as number) > 1 ? `серия ${data.streak}` : ''
|
||||||
const bonus = data.common_enemy_bonus ? `+${data.common_enemy_bonus} бонус` : ''
|
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 {
|
return {
|
||||||
title: `завершил ${points}`,
|
title: `завершил ${points}`,
|
||||||
details: challenge || undefined,
|
details: challenge || undefined,
|
||||||
extra: [game, streak, bonus].filter(Boolean).join(' • ') || undefined,
|
extra: [game, streak, bonus, eventBonus].filter(Boolean).join(' • ') || undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user