This commit is contained in:
咕谷酱
2025-08-21 22:51:36 +08:00
8 changed files with 1034 additions and 10 deletions

View 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),
}

View 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
View 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"),
}

View 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),
}

View 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),
}

View File

@@ -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),

View File

@@ -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
)

View File

@@ -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, "