Add events

This commit is contained in:
2025-12-15 03:22:29 +07:00
parent 1a882fb2e0
commit 4239ea8516
31 changed files with 7288 additions and 75 deletions

View File

@@ -46,6 +46,21 @@ from app.schemas.activity import (
ActivityResponse,
FeedResponse,
)
from app.schemas.event import (
EventCreate,
EventResponse,
EventEffects,
ActiveEventResponse,
SwapRequest,
SwapCandidate,
CommonEnemyLeaderboard,
EVENT_INFO,
COMMON_ENEMY_BONUSES,
SwapRequestCreate,
SwapRequestResponse,
SwapRequestChallengeInfo,
MySwapRequests,
)
from app.schemas.common import (
MessageResponse,
ErrorResponse,
@@ -95,6 +110,20 @@ __all__ = [
# Activity
"ActivityResponse",
"FeedResponse",
# Event
"EventCreate",
"EventResponse",
"EventEffects",
"ActiveEventResponse",
"SwapRequest",
"SwapCandidate",
"CommonEnemyLeaderboard",
"EVENT_INFO",
"COMMON_ENEMY_BONUSES",
"SwapRequestCreate",
"SwapRequestResponse",
"SwapRequestChallengeInfo",
"MySwapRequests",
# Common
"MessageResponse",
"ErrorResponse",

View File

@@ -0,0 +1,174 @@
from datetime import datetime
from pydantic import BaseModel, Field
from typing import Literal
from app.models.event import EventType
from app.schemas.user import UserPublic
# Event type literals for Pydantic
EventTypeLiteral = Literal[
"golden_hour",
"common_enemy",
"double_risk",
"jackpot",
"swap",
"rematch",
]
class EventCreate(BaseModel):
type: EventTypeLiteral
duration_minutes: int | None = Field(
None,
description="Duration in minutes. If not provided, uses default for event type."
)
challenge_id: int | None = Field(
None,
description="For common_enemy event - the challenge everyone will get"
)
class EventEffects(BaseModel):
points_multiplier: float = 1.0
drop_free: bool = False
special_action: str | None = None # "swap", "rematch"
description: str = ""
class EventResponse(BaseModel):
id: int
type: EventTypeLiteral
start_time: datetime
end_time: datetime | None
is_active: bool
created_by: UserPublic | None
data: dict | None
created_at: datetime
class Config:
from_attributes = True
class ActiveEventResponse(BaseModel):
event: EventResponse | None
effects: EventEffects
time_remaining_seconds: int | None = None
class SwapRequest(BaseModel):
target_participant_id: int
class CommonEnemyLeaderboard(BaseModel):
participant_id: int
user: UserPublic
completed_at: datetime | None
rank: int | None
bonus_points: int
# Event descriptions and default durations
EVENT_INFO = {
EventType.GOLDEN_HOUR: {
"name": "Золотой час",
"description": "Все очки x1.5!",
"default_duration": 45,
"points_multiplier": 1.5,
"drop_free": False,
},
EventType.COMMON_ENEMY: {
"name": "Общий враг",
"description": "Все получают одинаковый челлендж. Первые 3 получают бонус!",
"default_duration": None, # Until all complete
"points_multiplier": 1.0,
"drop_free": False,
},
EventType.DOUBLE_RISK: {
"name": "Двойной риск",
"description": "Дропы бесплатны, но очки x0.5",
"default_duration": 120,
"points_multiplier": 0.5,
"drop_free": True,
},
EventType.JACKPOT: {
"name": "Джекпот",
"description": "Следующий спин — сложный челлендж с x3 очками!",
"default_duration": None, # 1 spin
"points_multiplier": 3.0,
"drop_free": False,
},
EventType.SWAP: {
"name": "Обмен",
"description": "Можно поменяться заданием с другим участником",
"default_duration": 60,
"points_multiplier": 1.0,
"drop_free": False,
"special_action": "swap",
},
EventType.REMATCH: {
"name": "Реванш",
"description": "Можно переделать проваленный челлендж за 50% очков",
"default_duration": 240,
"points_multiplier": 0.5,
"drop_free": False,
"special_action": "rematch",
},
}
# Bonus points for Common Enemy top 3
COMMON_ENEMY_BONUSES = {
1: 50,
2: 30,
3: 15,
}
class SwapCandidate(BaseModel):
"""Participant available for assignment swap"""
participant_id: int
user: UserPublic
challenge_title: str
challenge_description: str
challenge_points: int
challenge_difficulty: str
game_title: str
# Two-sided swap confirmation schemas
SwapRequestStatusLiteral = Literal["pending", "accepted", "declined", "cancelled"]
class SwapRequestCreate(BaseModel):
"""Request to swap assignment with another participant"""
target_participant_id: int
class SwapRequestChallengeInfo(BaseModel):
"""Challenge info for swap request display"""
title: str
description: str
points: int
difficulty: str
game_title: str
class SwapRequestResponse(BaseModel):
"""Response for a swap request"""
id: int
status: SwapRequestStatusLiteral
from_user: UserPublic
to_user: UserPublic
from_challenge: SwapRequestChallengeInfo
to_challenge: SwapRequestChallengeInfo
created_at: datetime
responded_at: datetime | None
class Config:
from_attributes = True
class MySwapRequests(BaseModel):
"""User's incoming and outgoing swap requests"""
incoming: list[SwapRequestResponse]
outgoing: list[SwapRequestResponse]

View File

@@ -22,6 +22,7 @@ class MarathonUpdate(BaseModel):
start_date: datetime | None = None
is_public: bool | None = None
game_proposal_mode: str | None = Field(None, pattern="^(all_participants|organizer_only)$")
auto_events_enabled: bool | None = None
class ParticipantInfo(BaseModel):
@@ -47,6 +48,7 @@ class MarathonResponse(MarathonBase):
invite_code: str
is_public: bool
game_proposal_mode: str
auto_events_enabled: bool
start_date: datetime | None
end_date: datetime | None
participants_count: int