Merge branch 'main' of https://github.com/GooGuTeam/g0v0-server
This commit is contained in:
61
app/achievements/daily_challenge.py
Normal file
61
app/achievements/daily_challenge.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from app.database.daily_challenge import DailyChallengeStats
|
||||
from app.database.score import Beatmap, Score
|
||||
from app.models.achievement import Achievement
|
||||
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def process_streak(
|
||||
streak: int,
|
||||
next_streak: int,
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
if not score.passed:
|
||||
return False
|
||||
if streak < 1:
|
||||
return False
|
||||
if next_streak != 0 and streak >= next_streak:
|
||||
return False
|
||||
stats = (
|
||||
await session.exec(
|
||||
select(DailyChallengeStats).where(
|
||||
DailyChallengeStats.user_id == score.user_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
if not stats:
|
||||
return False
|
||||
if streak <= stats.daily_streak_best < next_streak:
|
||||
return True
|
||||
elif next_streak == 0 and stats.daily_streak_best >= streak:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
MEDALS = {
|
||||
Achievement(
|
||||
id=102,
|
||||
name="Daily Sprout",
|
||||
desc="Ready for anything.",
|
||||
assets_id="all-skill-dc-1",
|
||||
): partial(process_streak, 1, 7),
|
||||
Achievement(
|
||||
id=103,
|
||||
name="Weekly Sapling",
|
||||
desc="Circadian rhythm calibrated.",
|
||||
assets_id="all-skill-dc-7",
|
||||
): partial(process_streak, 7, 30),
|
||||
Achievement(
|
||||
id=104,
|
||||
name="Monthly Shrub",
|
||||
desc="In for the grind.",
|
||||
assets_id="all-skill-dc-30",
|
||||
): partial(process_streak, 30, 0),
|
||||
}
|
||||
650
app/achievements/hush_hush.py
Normal file
650
app/achievements/hush_hush.py
Normal file
@@ -0,0 +1,650 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from app.database.beatmap import calculate_beatmap_attributes
|
||||
from app.database.score import Beatmap, Score
|
||||
from app.dependencies.database import get_redis
|
||||
from app.dependencies.fetcher import get_fetcher
|
||||
from app.models.achievement import Achievement, Medals
|
||||
from app.models.beatmap import BeatmapRankStatus
|
||||
from app.models.mods import get_speed_rate, mod_to_save
|
||||
from app.models.score import Rank
|
||||
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def jackpot(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass a map with a score above 100,000, where every digit of the score is
|
||||
# identical (222,222 / 7,777,777 / 99,999,999 / etc).
|
||||
return (
|
||||
score.passed
|
||||
and score.total_score > 100000
|
||||
and all(d == str(score.total_score)[0] for d in str(score.total_score))
|
||||
)
|
||||
|
||||
|
||||
async def nonstop(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map with a drain time of 8:41 or longer (before mods).
|
||||
return (score.rank == Rank.X or score.rank == Rank.XH) and beatmap.hit_length > 521
|
||||
|
||||
|
||||
async def time_dilation(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map that is 8 minutes or longer (after mods),
|
||||
# Using either of the specified mods: DT, NC.
|
||||
# NF is not allowed, but all other difficulty reducing mods are.
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "NF" in mods_:
|
||||
return False
|
||||
if "DT" not in mods_ and "NC" not in mods_:
|
||||
return False
|
||||
rate = get_speed_rate(score.mods)
|
||||
return beatmap.hit_length / rate > 480
|
||||
|
||||
|
||||
async def to_the_core(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map that contains 'Nightcore' in the title or artist name,
|
||||
# using either of the mods specified: DT, NC
|
||||
if not score.passed:
|
||||
return False
|
||||
if (
|
||||
"Nightcore" not in beatmap.beatmapset.title
|
||||
) and "Nightcore" not in beatmap.beatmapset.artist:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "DT" not in mods_ or "NC" not in mods_:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def wysi(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map by song artist "xi" with X7.27% accuracy (97.27%, 87.27%, etc.).
|
||||
if not score.passed:
|
||||
return False
|
||||
if str(round(score.accuracy, ndigits=4))[3:] != "727":
|
||||
return False
|
||||
if "xi" not in beatmap.beatmapset.artist:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def prepared(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map, using the mods specified: NF
|
||||
if score.rank != Rank.X or score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "NF" not in mods_:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def reckless_adandon(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map, using the mods specified: HR, SD, that is star 3+ after mods.
|
||||
if score.rank != Rank.X or score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "HR" not in mods_ or "SD" not in mods_:
|
||||
return False
|
||||
fetcher = await get_fetcher()
|
||||
redis = get_redis()
|
||||
mods_ = score.mods.copy()
|
||||
attribute = await calculate_beatmap_attributes(
|
||||
beatmap.id, score.gamemode, mods_, redis, fetcher
|
||||
)
|
||||
if attribute.star_rating < 3:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def lights_out(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
):
|
||||
# Pass any map, using the mods specified: FL, NC
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
return "FL" in mods_ and "NC" in mods_
|
||||
|
||||
|
||||
async def camera_shy(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
):
|
||||
# PFC any map, using the mods specified: HD, NF
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
return "HD" in mods_ and "NF" in mods_
|
||||
|
||||
|
||||
async def the_sun_of_all_fears(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
):
|
||||
# "PFC" any map, but miss on the very first or very last combo.
|
||||
if not score.passed:
|
||||
return False
|
||||
return score.max_combo == (beatmap.max_combo or 0) - 1
|
||||
|
||||
|
||||
async def hour_before_the_down(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any difficulty of ginkiha - EOS.
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
return beatmap.beatmapset.artist == "ginkiha" and beatmap.beatmapset.title == "EOS"
|
||||
|
||||
|
||||
async def slow_and_steady(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map, using the mods specified: HT, PF, that is star 3+ after mods.
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "HT" not in mods_ or "PF" not in mods_:
|
||||
return False
|
||||
fetcher = await get_fetcher()
|
||||
redis = get_redis()
|
||||
mods_ = score.mods.copy()
|
||||
attribute = await calculate_beatmap_attributes(
|
||||
beatmap.id, score.gamemode, mods_, redis, fetcher
|
||||
)
|
||||
return attribute.star_rating >= 3
|
||||
|
||||
|
||||
async def no_time_to_spare(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map, using the mods specified, that is 30 seconds or shorter (after mods).
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "DT" not in mods_ and "NC" not in mods_:
|
||||
return False
|
||||
rate = get_speed_rate(score.mods)
|
||||
return (beatmap.total_length / rate) <= 30
|
||||
|
||||
|
||||
async def sognare(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass LeaF - Evanescent using HT (difficulty reduction allowed)
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "HT" not in mods_:
|
||||
return False
|
||||
return (
|
||||
beatmap.beatmapset.artist == "LeaF" and beatmap.beatmapset.title == "Evanescent"
|
||||
)
|
||||
|
||||
|
||||
async def realtor_extraordinaire(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any difficulty of cYsmix - House With Legs using DT/HR (DT/NC interchangeable)
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if not ("DT" in mods_ or "NC" in mods_) or "HR" not in mods_:
|
||||
return False
|
||||
return (
|
||||
beatmap.beatmapset.artist == "cYsmix"
|
||||
and beatmap.beatmapset.title == "House With Legs"
|
||||
)
|
||||
|
||||
|
||||
async def impeccable(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map using the mods specified: DT, PF, that is star 4+ after mods.
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
# DT and NC interchangeable
|
||||
if not ("DT" in mods_ or "NC" in mods_) or "PF" not in mods_:
|
||||
return False
|
||||
fetcher = await get_fetcher()
|
||||
redis = get_redis()
|
||||
mods_ = score.mods.copy()
|
||||
attribute = await calculate_beatmap_attributes(
|
||||
beatmap.id, score.gamemode, mods_, redis, fetcher
|
||||
)
|
||||
return attribute.star_rating >= 4
|
||||
|
||||
|
||||
async def aeon(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any map that was ranked before or during 2011,
|
||||
# using the mods specified: FL, HD, HT.
|
||||
# The map must be at least star 4+ and at least 3 minutes long after mods.
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "FL" not in mods_ or "HD" not in mods_ or "HT" not in mods_:
|
||||
return False
|
||||
if not beatmap.beatmapset.ranked_date or beatmap.beatmapset.ranked_date > datetime(
|
||||
2012, 1, 1
|
||||
):
|
||||
return False
|
||||
if beatmap.total_length < 180:
|
||||
return False
|
||||
fetcher = await get_fetcher()
|
||||
redis = get_redis()
|
||||
mods_ = score.mods.copy()
|
||||
attribute = await calculate_beatmap_attributes(
|
||||
beatmap.id, score.gamemode, mods_, redis, fetcher
|
||||
)
|
||||
return attribute.star_rating >= 4
|
||||
|
||||
|
||||
async def quick_maths(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Get exactly 34 misses on any difficulty of Function Phantom - Variable.
|
||||
if score.nmiss != 34:
|
||||
return False
|
||||
return (
|
||||
beatmap.beatmapset.artist == "Function Phantom"
|
||||
and beatmap.beatmapset.title == "Variable"
|
||||
)
|
||||
|
||||
|
||||
async def kaleidoscope(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass The Flashbulb - DIDJ PVC [EX III] with 80% accuracy or higher,
|
||||
# using the mods specified: EZ HT
|
||||
if not score.passed:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "EZ" not in mods_ or "HT" not in mods_:
|
||||
return False
|
||||
return beatmap.id == 2022237 and score.accuracy >= 0.8
|
||||
|
||||
|
||||
async def valediction(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass a_hisa - Alexithymia | Lupinus | Tokei no Heya to Seishin Sekai
|
||||
# with 90% accuracy or higher.
|
||||
return (
|
||||
score.passed
|
||||
and beatmap.beatmapset.artist == "a_hisa"
|
||||
and beatmap.beatmapset.title
|
||||
== "Alexithymia | Lupinus | Tokei no Heya to Seishin Sekai"
|
||||
and score.accuracy >= 0.9
|
||||
)
|
||||
|
||||
|
||||
async def right_on_time(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Submit a score on Kola Kid - timer on the first minute of any hour
|
||||
if not score.passed:
|
||||
return False
|
||||
if not (
|
||||
beatmap.beatmapset.artist == "Kola Kid" and beatmap.beatmapset.title == "timer"
|
||||
):
|
||||
return False
|
||||
return score.ended_at.minute == 0
|
||||
|
||||
|
||||
async def not_again(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass ARForest - Regret. with 1x Miss and 99%+ accuracy
|
||||
if not score.passed:
|
||||
return False
|
||||
if score.nmiss != 1:
|
||||
return False
|
||||
if score.accuracy < 0.99:
|
||||
return False
|
||||
return (
|
||||
beatmap.beatmapset.artist == "ARForest" and beatmap.beatmapset.title == "Regret"
|
||||
)
|
||||
|
||||
|
||||
async def deliberation(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# PFC any ranked or loved map, with HT, that is 6+ stars after mods
|
||||
if score.rank != Rank.X and score.rank != Rank.XH:
|
||||
return False
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "HT" not in mods_:
|
||||
return False
|
||||
if (
|
||||
not beatmap.beatmap_status.has_pp()
|
||||
or beatmap.beatmap_status != BeatmapRankStatus.LOVED
|
||||
):
|
||||
return False
|
||||
|
||||
fetcher = await get_fetcher()
|
||||
redis = get_redis()
|
||||
mods_copy = score.mods.copy()
|
||||
attribute = await calculate_beatmap_attributes(
|
||||
beatmap.id, score.gamemode, mods_copy, redis, fetcher
|
||||
)
|
||||
return attribute.star_rating >= 6
|
||||
|
||||
|
||||
async def clarity(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass rrtyui's mapset of Camellia vs Akira Complex - Reality Distortion
|
||||
if not score.passed:
|
||||
return False
|
||||
return beatmap.beatmapset.id == 582089
|
||||
|
||||
|
||||
async def autocreation(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map where the artist and the host of the mapset are the same person
|
||||
if not score.passed:
|
||||
return False
|
||||
return beatmap.beatmapset.creator == beatmap.beatmapset.artist
|
||||
|
||||
|
||||
async def value_your_identity(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Achieve a score where max combo equals the last 3 digits of User ID
|
||||
if not score.passed:
|
||||
return False
|
||||
user_id = score.user_id
|
||||
last_3_digits = user_id % 1000
|
||||
if last_3_digits == 0:
|
||||
last_3_digits = 1000
|
||||
return score.max_combo == last_3_digits
|
||||
|
||||
|
||||
async def by_the_skin_of_the_teeth(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Set a play using Accuracy Challenge that is exactly the accuracy specified
|
||||
if not score.passed:
|
||||
return False
|
||||
|
||||
mods_ = mod_to_save(score.mods)
|
||||
if "AC" not in mods_:
|
||||
return False
|
||||
|
||||
for mod in score.mods:
|
||||
if mod.get("acronym") == "AC":
|
||||
if "settings" in mod and "minimum_accuracy" in mod["settings"]:
|
||||
target_accuracy = mod["settings"]["minimum_accuracy"]
|
||||
if isinstance(target_accuracy, int | float):
|
||||
return abs(score.accuracy - float(target_accuracy)) < 0.0001
|
||||
return False
|
||||
|
||||
|
||||
async def meticulous_mayhem(
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
# Pass any map with 15 or more mods enabled
|
||||
if not score.passed:
|
||||
return False
|
||||
return len(score.mods) >= 15
|
||||
|
||||
|
||||
# TODO: Quick Draw, Obsessed, Jack of All Trades, Ten To One, Persistence Is Key
|
||||
# Tribulation, Replica, All Good, Time Sink, You're Here Forever, Hospitality,
|
||||
# True North, Superfan, Resurgence, Festive Fever, Deciduous Arborist,
|
||||
# Infectious Enthusiasm, Exquisite, Mad Scientist
|
||||
MEDALS: Medals = {
|
||||
Achievement(
|
||||
id=105,
|
||||
name="Jackpot",
|
||||
desc="Lucky sevens is a mild understatement.",
|
||||
assets_id="all-secret-jackpot",
|
||||
): jackpot,
|
||||
Achievement(
|
||||
id=106,
|
||||
name="Nonstop",
|
||||
desc="Breaks? What are those?",
|
||||
assets_id="all-secret-nonstop",
|
||||
): nonstop,
|
||||
Achievement(
|
||||
id=107,
|
||||
name="Time Dilation",
|
||||
desc="Longer is shorter when all is said and done.",
|
||||
assets_id="all-secret-tidi",
|
||||
): time_dilation,
|
||||
Achievement(
|
||||
id=108,
|
||||
name="To The Core",
|
||||
desc="In for a penny, in for a pound. Pounding bass, that is.",
|
||||
assets_id="all-secret-tothecore",
|
||||
): to_the_core,
|
||||
Achievement(
|
||||
id=109,
|
||||
name="When You See It",
|
||||
desc="Three numbers which will haunt you forevermore.",
|
||||
assets_id="all-secret-when-you-see-it",
|
||||
): wysi,
|
||||
Achievement(
|
||||
id=110,
|
||||
name="Prepared",
|
||||
desc="Do it for real next time.",
|
||||
assets_id="all-secret-prepared",
|
||||
): prepared,
|
||||
Achievement(
|
||||
id=111,
|
||||
name="Reckless Abandon",
|
||||
desc="Throw it all to the wind.",
|
||||
assets_id="all-secret-reckless",
|
||||
): reckless_adandon,
|
||||
Achievement(
|
||||
id=112,
|
||||
name="Lights Out",
|
||||
desc="The party's just getting started.",
|
||||
assets_id="all-secret-lightsout",
|
||||
): lights_out,
|
||||
Achievement(
|
||||
id=113,
|
||||
name="Camera Shy",
|
||||
desc="Stop being cute.",
|
||||
assets_id="all-secret-uguushy",
|
||||
): camera_shy,
|
||||
Achievement(
|
||||
id=114,
|
||||
name="The Sun of All Fears",
|
||||
desc="Unfortunate.",
|
||||
assets_id="all-secret-nuked",
|
||||
): the_sun_of_all_fears,
|
||||
Achievement(
|
||||
id=115,
|
||||
name="Hour Before The Down",
|
||||
desc="Eleven skies of everlasting sunrise.",
|
||||
assets_id="all-secret-hourbeforethedawn",
|
||||
): hour_before_the_down,
|
||||
Achievement(
|
||||
id=116,
|
||||
name="Slow And Steady",
|
||||
desc="Win the race, or start again.",
|
||||
assets_id="all-secret-slowandsteady",
|
||||
): slow_and_steady,
|
||||
Achievement(
|
||||
id=117,
|
||||
name="No Time To Spare",
|
||||
desc="Places to be, things to do.",
|
||||
assets_id="all-secret-ntts",
|
||||
): no_time_to_spare,
|
||||
Achievement(
|
||||
id=118,
|
||||
name="Sognare",
|
||||
desc="A dream in stop-motion, soon forever gone.",
|
||||
assets_id="all-secret-sognare",
|
||||
): sognare,
|
||||
Achievement(
|
||||
id=119,
|
||||
name="Realtor Extraordinaire",
|
||||
desc="An acre-wide stride.",
|
||||
assets_id="all-secret-realtor",
|
||||
): realtor_extraordinaire,
|
||||
Achievement(
|
||||
id=120,
|
||||
name="Impeccable",
|
||||
desc="Speed matters not to the exemplary.",
|
||||
assets_id="all-secret-impeccable",
|
||||
): impeccable,
|
||||
Achievement(
|
||||
id=121,
|
||||
name="Aeon",
|
||||
desc="In the mire of thawing time, memory shall be your guide.",
|
||||
assets_id="all-secret-aeon",
|
||||
): aeon,
|
||||
Achievement(
|
||||
id=122,
|
||||
name="Quick Maths",
|
||||
desc="Beats per minute over... this isn't quick at all!",
|
||||
assets_id="all-secret-quickmaffs",
|
||||
): quick_maths,
|
||||
Achievement(
|
||||
id=123,
|
||||
name="Kaleidoscope",
|
||||
desc="So many pretty colours. Most of them red.",
|
||||
assets_id="all-secret-kaleidoscope",
|
||||
): kaleidoscope,
|
||||
Achievement(
|
||||
id=124,
|
||||
name="Valediction",
|
||||
desc="One last time.",
|
||||
assets_id="all-secret-valediction",
|
||||
): valediction,
|
||||
# Achievement(
|
||||
# id=125,
|
||||
# name="Exquisite",
|
||||
# desc="Indubitably.",
|
||||
# assets_id="all-secret-exquisite",
|
||||
# ): exquisite,
|
||||
# Achievement(
|
||||
# id=126,
|
||||
# name="Mad Scientist",
|
||||
# desc="The experiment... it's all gone!",
|
||||
# assets_id="all-secret-madscientist",
|
||||
# ): mad_scientist,
|
||||
Achievement(
|
||||
id=127,
|
||||
name="Right On Time",
|
||||
desc="The first minute is always the hardest.",
|
||||
assets_id="all-secret-rightontime",
|
||||
): right_on_time,
|
||||
Achievement(
|
||||
id=128,
|
||||
name="Not Again",
|
||||
desc="Regret everything.",
|
||||
assets_id="all-secret-notagain",
|
||||
): not_again,
|
||||
Achievement(
|
||||
id=129,
|
||||
name="Deliberation",
|
||||
desc="The challenge remains.",
|
||||
assets_id="all-secret-deliberation",
|
||||
): deliberation,
|
||||
Achievement(
|
||||
id=130,
|
||||
name="Clarity",
|
||||
desc="And yet in our memories, you remain crystal clear.",
|
||||
assets_id="all-secret-clarity",
|
||||
): clarity,
|
||||
Achievement(
|
||||
id=131,
|
||||
name="Autocreation",
|
||||
desc="Absolute rule.",
|
||||
assets_id="all-secret-autocreation",
|
||||
): autocreation,
|
||||
Achievement(
|
||||
id=132,
|
||||
name="Value Your Identity",
|
||||
desc="As perfect as you are.",
|
||||
assets_id="all-secret-identity",
|
||||
): value_your_identity,
|
||||
Achievement(
|
||||
id=133,
|
||||
name="By The Skin Of The Teeth",
|
||||
desc="You're that accurate.",
|
||||
assets_id="all-secret-skinoftheteeth",
|
||||
): by_the_skin_of_the_teeth,
|
||||
Achievement(
|
||||
id=134,
|
||||
name="Meticulous Mayhem",
|
||||
desc="How did we get here?",
|
||||
assets_id="all-secret-meticulousmayhem",
|
||||
): meticulous_mayhem,
|
||||
}
|
||||
124
app/achievements/mods.py
Normal file
124
app/achievements/mods.py
Normal file
@@ -0,0 +1,124 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from app.database.score import Beatmap, Score
|
||||
from app.models.achievement import Achievement, Medals
|
||||
from app.models.mods import API_MODS
|
||||
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def process_mod(
|
||||
mod: str,
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
if not score.passed:
|
||||
return False
|
||||
if not beatmap.beatmap_status.has_leaderboard():
|
||||
return False
|
||||
if len(score.mods) != 1 or score.mods[0]["acronym"] != mod:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
async def process_category_mod(
|
||||
category: str,
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
if not score.passed:
|
||||
return False
|
||||
if not beatmap.beatmap_status.has_leaderboard():
|
||||
return False
|
||||
api_mods = {
|
||||
k
|
||||
for k, v in API_MODS[int(score.gamemode)].items() # pyright: ignore[reportArgumentType]
|
||||
if v["Type"] == category
|
||||
}
|
||||
return all(mod["acronym"] in api_mods for mod in score.mods)
|
||||
|
||||
|
||||
MEDALS: Medals = {
|
||||
Achievement(
|
||||
id=89,
|
||||
name="Finality",
|
||||
desc="High stakes, no regrets.",
|
||||
assets_id="all-intro-suddendeath",
|
||||
): partial(process_mod, "SD"),
|
||||
Achievement(
|
||||
id=90,
|
||||
name="Perfectionist",
|
||||
desc="Accept nothing but the best.",
|
||||
assets_id="all-intro-perfect",
|
||||
): partial(process_mod, "PF"),
|
||||
Achievement(
|
||||
id=91,
|
||||
name="Rock Around The Clock",
|
||||
desc="You can't stop the rock.",
|
||||
assets_id="all-intro-hardrock",
|
||||
): partial(process_mod, "HR"),
|
||||
Achievement(
|
||||
id=92,
|
||||
name="Time And A Half",
|
||||
desc="Having a right ol' time. One and a half of them, almost.",
|
||||
assets_id="all-intro-doubletime",
|
||||
): partial(process_mod, "DT"),
|
||||
Achievement(
|
||||
id=93,
|
||||
name="Sweet Rave Party",
|
||||
desc="Founded in the fine tradition of changing things that were just fine as they were.", # noqa: E501
|
||||
assets_id="all-intro-nightcore",
|
||||
): partial(process_mod, "NC"),
|
||||
Achievement(
|
||||
id=94,
|
||||
name="Blindsight",
|
||||
desc="I can see just perfectly.",
|
||||
assets_id="all-intro-hidden",
|
||||
): partial(process_mod, "HD"),
|
||||
Achievement(
|
||||
id=95,
|
||||
name="Are You Afraid Of The Dark?",
|
||||
desc="Harder than it looks, probably because it's hard to look.",
|
||||
assets_id="all-intro-flashlight",
|
||||
): partial(process_mod, "FL"),
|
||||
Achievement(
|
||||
id=96,
|
||||
name="Dial It Right Back",
|
||||
desc="Sometimes you just want to take it easy.",
|
||||
assets_id="all-intro-easy",
|
||||
): partial(process_mod, "EZ"),
|
||||
Achievement(
|
||||
id=97,
|
||||
name="Risk Averse",
|
||||
desc="Safety nets are fun!",
|
||||
assets_id="all-intro-nofail",
|
||||
): partial(process_mod, "NF"),
|
||||
Achievement(
|
||||
id=98,
|
||||
name="Slowboat",
|
||||
desc="You got there. Eventually.",
|
||||
assets_id="all-intro-halftime",
|
||||
): partial(process_mod, "HT"),
|
||||
Achievement(
|
||||
id=99,
|
||||
name="Burned Out",
|
||||
desc="One cannot always spin to win.",
|
||||
assets_id="all-intro-spunout",
|
||||
): partial(process_mod, "SO"),
|
||||
Achievement(
|
||||
id=100,
|
||||
name="Gear Shift",
|
||||
desc="Tailor your experience to your perfect fit.",
|
||||
assets_id="all-intro-conversion",
|
||||
): partial(process_category_mod, "Conversion"),
|
||||
Achievement(
|
||||
id=101,
|
||||
name="Game Night",
|
||||
desc="Mum said it's my turn with the beatmap!",
|
||||
assets_id="all-intro-fun",
|
||||
): partial(process_category_mod, "Fun"),
|
||||
}
|
||||
70
app/achievements/osu_playcount.py
Normal file
70
app/achievements/osu_playcount.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from app.database import UserStatistics
|
||||
from app.database.beatmap import Beatmap
|
||||
from app.database.score import Score
|
||||
from app.models.achievement import Achievement, Medals
|
||||
from app.models.score import GameMode
|
||||
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def process_playcount(
|
||||
pc: int,
|
||||
next_pc: int,
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
if pc < 1:
|
||||
return False
|
||||
if next_pc != 0 and pc >= next_pc:
|
||||
return False
|
||||
if score.gamemode != GameMode.OSU:
|
||||
return False
|
||||
stats = (
|
||||
await session.exec(
|
||||
select(UserStatistics).where(
|
||||
UserStatistics.mode == GameMode.OSU,
|
||||
UserStatistics.user_id == score.user_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
if not stats:
|
||||
return False
|
||||
if pc <= stats.play_count < next_pc:
|
||||
return True
|
||||
elif next_pc == 0 and stats.play_count >= pc:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
MEDALS: Medals = {
|
||||
Achievement(
|
||||
id=73,
|
||||
name="5,000 Plays",
|
||||
desc="There's a lot more where that came from",
|
||||
assets_id="osu-plays-5000",
|
||||
): partial(process_playcount, 5000, 15000),
|
||||
Achievement(
|
||||
id=74,
|
||||
name="15,000 Plays",
|
||||
desc="Must.. click.. circles..",
|
||||
assets_id="osu-plays-15000",
|
||||
): partial(process_playcount, 15000, 25000),
|
||||
Achievement(
|
||||
id=75,
|
||||
name="25,000 Plays",
|
||||
desc="There's no going back.",
|
||||
assets_id="osu-plays-25000",
|
||||
): partial(process_playcount, 25000, 50000),
|
||||
Achievement(
|
||||
id=76,
|
||||
name="50,000 Plays",
|
||||
desc="You're here forever.",
|
||||
assets_id="osu-plays-50000",
|
||||
): partial(process_playcount, 50000, 0),
|
||||
}
|
||||
118
app/achievements/total_hits.py
Normal file
118
app/achievements/total_hits.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import partial
|
||||
|
||||
from app.database.score import Beatmap, Score
|
||||
from app.database.statistics import UserStatistics
|
||||
from app.models.achievement import Achievement, Medals
|
||||
from app.models.score import GameMode
|
||||
|
||||
from sqlmodel import select
|
||||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||||
|
||||
|
||||
async def process_tth(
|
||||
tth: int,
|
||||
next_tth: int,
|
||||
gamemode: GameMode,
|
||||
session: AsyncSession,
|
||||
score: Score,
|
||||
beatmap: Beatmap,
|
||||
) -> bool:
|
||||
if tth < 1:
|
||||
return False
|
||||
if next_tth != 0 and tth >= next_tth:
|
||||
return False
|
||||
if score.gamemode != gamemode:
|
||||
return False
|
||||
stats = (
|
||||
await session.exec(
|
||||
select(UserStatistics).where(
|
||||
UserStatistics.mode == score.gamemode,
|
||||
UserStatistics.user_id == score.user_id,
|
||||
)
|
||||
)
|
||||
).first()
|
||||
if not stats:
|
||||
return False
|
||||
if tth <= stats.total_hits < next_tth:
|
||||
return True
|
||||
elif next_tth == 0 and stats.play_count >= tth:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
MEDALS: Medals = {
|
||||
Achievement(
|
||||
id=77,
|
||||
name="30,000 Drum Hits",
|
||||
desc="Did that drum have a face?",
|
||||
assets_id="taiko-hits-30000",
|
||||
): partial(process_tth, 30000, 300000, GameMode.TAIKO),
|
||||
Achievement(
|
||||
id=78,
|
||||
name="300,000 Drum Hits",
|
||||
desc="The rhythm never stops.",
|
||||
assets_id="taiko-hits-300000",
|
||||
): partial(process_tth, 300000, 3000000, GameMode.TAIKO),
|
||||
Achievement(
|
||||
id=79,
|
||||
name="3,000,000 Drum Hits",
|
||||
desc="Truly, the Don of dons.",
|
||||
assets_id="taiko-hits-3000000",
|
||||
): partial(process_tth, 3000000, 30000000, GameMode.TAIKO),
|
||||
Achievement(
|
||||
id=80,
|
||||
name="30,000,000 Drum Hits",
|
||||
desc="Your rhythm, eternal.",
|
||||
assets_id="taiko-hits-30000000",
|
||||
): partial(process_tth, 30000000, 0, GameMode.TAIKO),
|
||||
Achievement(
|
||||
id=81,
|
||||
name="Catch 20,000 fruits",
|
||||
desc="That is a lot of dietary fiber.",
|
||||
assets_id="fruits-hits-20000",
|
||||
): partial(process_tth, 20000, 200000, GameMode.FRUITS),
|
||||
Achievement(
|
||||
id=82,
|
||||
name="Catch 200,000 fruits",
|
||||
desc="So, I heard you like fruit...",
|
||||
assets_id="fruits-hits-200000",
|
||||
): partial(process_tth, 200000, 2000000, GameMode.FRUITS),
|
||||
Achievement(
|
||||
id=83,
|
||||
name="Catch 2,000,000 fruits",
|
||||
desc="Downright healthy.",
|
||||
assets_id="fruits-hits-2000000",
|
||||
): partial(process_tth, 2000000, 20000000, GameMode.FRUITS),
|
||||
Achievement(
|
||||
id=84,
|
||||
name="Catch 20,000,000 fruits",
|
||||
desc="Nothing left behind.",
|
||||
assets_id="fruits-hits-20000000",
|
||||
): partial(process_tth, 20000000, 0, GameMode.FRUITS),
|
||||
Achievement(
|
||||
id=85,
|
||||
name="40,000 Keys",
|
||||
desc="Just the start of the rainbow.",
|
||||
assets_id="mania-hits-40000",
|
||||
): partial(process_tth, 40000, 400000, GameMode.MANIA),
|
||||
Achievement(
|
||||
id=86,
|
||||
name="400,000 Keys",
|
||||
desc="Four hundred thousand and still not even close.",
|
||||
assets_id="mania-hits-400000",
|
||||
): partial(process_tth, 400000, 4000000, GameMode.MANIA),
|
||||
Achievement(
|
||||
id=87,
|
||||
name="4,000,000 Keys",
|
||||
desc="Is this the end of the rainbow?",
|
||||
assets_id="mania-hits-4000000",
|
||||
): partial(process_tth, 4000000, 40000000, GameMode.MANIA),
|
||||
Achievement(
|
||||
id=88,
|
||||
name="40,000,000 Keys",
|
||||
desc="When someone asks which keys you play, the answer is now 'yes'.",
|
||||
assets_id="mania-hits-40000000",
|
||||
): partial(process_tth, 40000000, 0, GameMode.MANIA),
|
||||
}
|
||||
@@ -87,9 +87,7 @@ async def process_achievements(session: AsyncSession, redis: Redis, score_id: in
|
||||
type=EventType.ACHIEVEMENT,
|
||||
user_id=score.user_id,
|
||||
event_payload={
|
||||
"achievement": UserAchievementResp(
|
||||
achievement_id=r.id, achieved_at=now
|
||||
).model_dump(),
|
||||
"achievement": {"achievement_id": r.id, "achieved_at": now.isoformat()},
|
||||
"user": {
|
||||
"username": score.user.username,
|
||||
"url": settings.web_url + "users/" + str(score.user.id),
|
||||
|
||||
@@ -813,7 +813,7 @@ async def process_score(
|
||||
user_id = user.id
|
||||
await session.commit()
|
||||
await session.refresh(score)
|
||||
if can_get_pp:
|
||||
if can_get_pp and score.pp != 0:
|
||||
previous_pp_best = await get_user_best_pp_in_beatmap(
|
||||
session, beatmap_id, user_id, score.gamemode
|
||||
)
|
||||
|
||||
@@ -4,11 +4,11 @@ import asyncio
|
||||
import math
|
||||
|
||||
from app.calculator import (
|
||||
calculate_pp,
|
||||
calculate_score_to_level,
|
||||
calculate_weighted_acc,
|
||||
calculate_weighted_pp,
|
||||
clamp,
|
||||
pre_fetch_and_calculate_pp,
|
||||
)
|
||||
from app.config import settings
|
||||
from app.const import BANCHOBOT_ID
|
||||
@@ -114,11 +114,14 @@ async def _recalculate_pp(
|
||||
ranked = db_beatmap.beatmap_status.has_pp() | settings.enable_all_beatmap_pp
|
||||
if not ranked or not mods_can_get_pp(int(score.gamemode), score.mods):
|
||||
score.pp = 0
|
||||
break
|
||||
return
|
||||
try:
|
||||
beatmap_raw = await fetcher.get_or_fetch_beatmap_raw(redis, beatmap_id)
|
||||
pp = await calculate_pp(score, beatmap_raw, session)
|
||||
pp = await pre_fetch_and_calculate_pp(
|
||||
score, beatmap_id, session, redis, fetcher
|
||||
)
|
||||
score.pp = pp
|
||||
if pp == 0:
|
||||
return
|
||||
if score.beatmap_id not in prev or prev[score.beatmap_id].pp < pp:
|
||||
best_score = PPBestScore(
|
||||
user_id=user_id,
|
||||
@@ -129,7 +132,7 @@ async def _recalculate_pp(
|
||||
gamemode=score.gamemode,
|
||||
)
|
||||
prev[score.beatmap_id] = best_score
|
||||
break
|
||||
return
|
||||
except HTTPError:
|
||||
time -= 1
|
||||
await asyncio.sleep(2)
|
||||
@@ -138,7 +141,7 @@ async def _recalculate_pp(
|
||||
logger.exception(
|
||||
f"Error calculating pp for score {score.id} on beatmap {beatmap_id}"
|
||||
)
|
||||
break
|
||||
return
|
||||
if time <= 0:
|
||||
logger.warning(
|
||||
f"Failed to fetch beatmap {beatmap_id} after 10 attempts, "
|
||||
|
||||
Reference in New Issue
Block a user