Skip to content

Commit

Permalink
feat: Add real-time notes support to HI3
Browse files Browse the repository at this point in the history
  • Loading branch information
seriaati committed Oct 23, 2024
1 parent 5e3a232 commit 4e89db3
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 127 deletions.
4 changes: 2 additions & 2 deletions hoyo_buddy/cogs/hoyo.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ async def notes_command(
await i.response.defer()

user = user or i.user
account_ = account or await self.bot.get_account(user.id, (Game.GENSHIN, Game.STARRAIL, Game.ZZZ))
account_ = account or await self.bot.get_account(user.id, (Game.GENSHIN, Game.STARRAIL, Game.ZZZ, Game.HONKAI))
settings = await Settings.get(user_id=i.user.id)

view = NotesView(
Expand Down Expand Up @@ -456,13 +456,13 @@ async def profile_game_autocomplete(self, i: Interaction, current: str) -> list[
async def gi_acc_autocomplete(self, i: Interaction, current: str) -> list[app_commands.Choice[str]]:
return await self.bot.get_game_account_choices(i, current, (Game.GENSHIN,))

@notes_command.autocomplete("account")
@challenge_command.autocomplete("account")
@profile_command.autocomplete("account")
@events_command.autocomplete("account")
async def gi_hsr_zzz_acc_autocomplete(self, i: Interaction, current: str) -> list[app_commands.Choice[str]]:
return await self.bot.get_game_account_choices(i, current, (Game.GENSHIN, Game.STARRAIL, Game.ZZZ))

@notes_command.autocomplete("account")
@characters_command.autocomplete("account")
async def gi_hsr_zzz_honkai_acc_autocomplete(self, i: Interaction, current: str) -> list[app_commands.Choice[str]]:
return await self.bot.get_game_account_choices(i, current, (Game.GENSHIN, Game.STARRAIL, Game.ZZZ, Game.HONKAI))
Expand Down
4 changes: 4 additions & 0 deletions hoyo_buddy/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ class NotesNotifyType(IntEnum):
"""ZZZ Video Store Management"""
PLANAR_FISSURE = 16
"""Planar Fissur Double Drop Rate"""
STAMINA = 17
"""Honkai Impact 3rd Stamina"""
HONKAI_DAILY = 18
"""Honkai Impact 3rd Battle Pass"""


class TalentBoost(IntEnum):
Expand Down
160 changes: 86 additions & 74 deletions hoyo_buddy/hoyo/auto_tasks/notes_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import asyncio
import datetime
from collections import defaultdict
from typing import TYPE_CHECKING, ClassVar
from typing import TYPE_CHECKING, ClassVar, TypeAlias

import discord
from discord import Locale
from genshin.models import Announcement, Notes, StarRailNote, VideoStoreState, ZZZNotes
from genshin.models import Announcement, HonkaiNotes, StarRailNote, VideoStoreState, ZZZNotes
from genshin.models import Notes as GenshinNotes

from hoyo_buddy.constants import UID_TZ_OFFSET

Expand Down Expand Up @@ -36,6 +37,8 @@

from ...bot import HoyoBuddy

Notes: TypeAlias = GenshinNotes | HonkaiNotes | StarRailNote | ZZZNotes


class NotesChecker:
_lock: ClassVar[asyncio.Lock] = asyncio.Lock()
Expand All @@ -46,7 +49,7 @@ def _calc_est_time(cls, game: Game, threshold: int, current: int) -> datetime.da
"""Calculate the estimated time for resin/trailblaze power to reach the threshold."""
if game is Game.GENSHIN:
return get_now() + datetime.timedelta(minutes=(threshold - current) * 8)
if game in {Game.STARRAIL, Game.ZZZ}:
if game in {Game.STARRAIL, Game.ZZZ, Game.HONKAI}:
return get_now() + datetime.timedelta(minutes=(threshold - current) * 6)
raise NotImplementedError

Expand All @@ -62,7 +65,7 @@ def _get_notify_error_embed(cls, err: Exception, locale: Locale) -> ErrorEmbed:
return embed

@classmethod
def _get_notify_embed(cls, notify: NotesNotify, locale: Locale) -> DefaultEmbed:
def _get_notify_embed(cls, notify: NotesNotify, notes: Notes | None, locale: Locale) -> DefaultEmbed:
translator = cls._bot.translator

match notify.type:
Expand Down Expand Up @@ -173,6 +176,30 @@ def _get_notify_embed(cls, notify: NotesNotify, locale: Locale) -> DefaultEmbed:
title=LocaleStr(key="planar_fissure_label"),
description=LocaleStr(key="planar_fissure_desc", hour=notify.hours_before),
)
case NotesNotifyType.HONKAI_DAILY:
assert isinstance(notes, HonkaiNotes)
embed = DefaultEmbed(
locale,
translator,
title=LocaleStr(key="honkai_daily_embed_title"),
description=LocaleStr(key="honkai_daily_embed_description", cur=notes.current_train_score, max=600),
)
case NotesNotifyType.STAMINA:
assert isinstance(notes, HonkaiNotes)
embed = DefaultEmbed(
locale,
translator,
title=LocaleStr(key="notes.stamina_label"),
description=LocaleStr(key="threshold.embed.description", threshold=notify.threshold),
)
embed.add_description(
LocaleStr(
key="notes.stamina",
time=datetime.timedelta(seconds=notes.stamina_recover_time),
cur=notes.current_stamina,
max=notes.max_stamina,
)
)

embed.add_acc_info(notify.account, blur=False)
embed.set_footer(text=LocaleStr(key="notif.embed.footer"))
Expand All @@ -187,9 +214,9 @@ async def _reset_notif_count(cls, notify: NotesNotify, *, est_time: datetime.dat
await notify.save(update_fields=("current_notif_count", "est_time"))

@classmethod
async def _notify_user(cls, notify: NotesNotify, notes: StarRailNote | Notes | ZZZNotes | None) -> None:
async def _notify_user(cls, notify: NotesNotify, notes: Notes | None) -> None:
locale = await cls._get_locale(notify)
embed = cls._get_notify_embed(notify, locale)
embed = cls._get_notify_embed(notify, notes, locale)
draw_input = DrawInput(
dark_mode=notify.account.user.settings.dark_mode,
locale=locale,
Expand All @@ -203,7 +230,7 @@ async def _notify_user(cls, notify: NotesNotify, notes: StarRailNote | Notes | Z
buffer = await draw_zzz_notes_card(draw_input, notes, cls._bot.translator)
elif isinstance(notes, StarRailNote):
buffer = await draw_hsr_notes_card(draw_input, notes, cls._bot.translator)
elif isinstance(notes, Notes):
elif isinstance(notes, GenshinNotes):
buffer = await draw_gi_notes_card(draw_input, notes, cls._bot.translator)
else:
buffer = None
Expand Down Expand Up @@ -231,22 +258,7 @@ async def _notify_user(cls, notify: NotesNotify, notes: StarRailNote | Notes | Z
await notify.save(update_fields=("enabled", "last_notif_time", "current_notif_count"))

@classmethod
async def _process_resin_notify(cls, notify: NotesNotify, notes: Notes) -> None:
"""Process resin notification."""
current = notes.current_resin
threshold = notify.threshold
assert threshold is not None

if current < threshold:
est_time = cls._calc_est_time(Game.GENSHIN, threshold, current)
return await cls._reset_notif_count(notify, est_time=est_time)

if notify.current_notif_count < notify.max_notif_count:
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_realm_currency_notify(cls, notify: NotesNotify, notes: Notes) -> None:
async def _process_realm_currency_notify(cls, notify: NotesNotify, notes: GenshinNotes) -> None:
"""Process realm currency notification."""
current = notes.current_realm_currency
threshold = notify.threshold
Expand All @@ -260,21 +272,6 @@ async def _process_realm_currency_notify(cls, notify: NotesNotify, notes: Notes)
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_tbp_notify(cls, notify: NotesNotify, notes: StarRailNote) -> None:
"""Process trailblaze power notification."""
current = notes.current_stamina
threshold = notify.threshold
assert threshold is not None

if current < threshold:
est_time = cls._calc_est_time(Game.STARRAIL, threshold, current)
return await cls._reset_notif_count(notify, est_time=est_time)

if notify.current_notif_count < notify.max_notif_count:
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_rtbp_notify(cls, notify: NotesNotify, notes: StarRailNote) -> None:
"""Process reserved trailblaze power notification."""
Expand All @@ -290,7 +287,7 @@ async def _process_rtbp_notify(cls, notify: NotesNotify, notes: StarRailNote) ->
return None

@classmethod
async def _process_expedition_notify(cls, notify: NotesNotify, notes: Notes | StarRailNote) -> None:
async def _process_expedition_notify(cls, notify: NotesNotify, notes: GenshinNotes | StarRailNote) -> None:
"""Process expedition notification."""
if any(not exped.finished for exped in notes.expeditions):
min_remain_time = min(exped.remaining_time for exped in notes.expeditions if not exped.finished)
Expand All @@ -302,7 +299,7 @@ async def _process_expedition_notify(cls, notify: NotesNotify, notes: Notes | St
return None

@classmethod
async def _process_pt_notify(cls, notify: NotesNotify, notes: Notes) -> None:
async def _process_pt_notify(cls, notify: NotesNotify, notes: GenshinNotes) -> None:
remaining_time = notes.remaining_transformer_recovery_time
if remaining_time is None:
return None
Expand All @@ -316,15 +313,16 @@ async def _process_pt_notify(cls, notify: NotesNotify, notes: Notes) -> None:
return None

@classmethod
async def _process_daily_notify(cls, notify: NotesNotify, notes: Notes | StarRailNote | ZZZNotes) -> None:
async def _process_daily_notify(cls, notify: NotesNotify, notes: Notes) -> None:
if notify.last_check_time is not None and get_now().day != notify.last_check_time.day:
return await cls._reset_notif_count(notify)

if isinstance(notes, Notes) and notes.completed_commissions + notes.daily_task.completed_tasks >= 4:
return await cls._reset_notif_count(notify, est_time=notify.account.server_reset_datetime)
if isinstance(notes, StarRailNote) and notes.current_train_score >= notes.max_train_score:
return await cls._reset_notif_count(notify, est_time=notify.account.server_reset_datetime)
if isinstance(notes, ZZZNotes) and notes.engagement.current >= notes.engagement.max:
gi = isinstance(notes, GenshinNotes) and notes.completed_commissions + notes.daily_task.completed_tasks >= 4
hsr = isinstance(notes, StarRailNote) and notes.current_train_score >= notes.max_train_score
zzz = isinstance(notes, ZZZNotes) and notes.engagement.current >= notes.engagement.max
honkai = isinstance(notes, HonkaiNotes) and notes.current_train_score >= 600

if gi or hsr or zzz or honkai:
return await cls._reset_notif_count(notify, est_time=notify.account.server_reset_datetime)

if notify.current_notif_count < notify.max_notif_count:
Expand All @@ -336,7 +334,7 @@ async def _process_week_boss_discount_notify(cls, notify: NotesNotify, notes: No
if notify.last_check_time is not None and get_now().day != notify.last_check_time.day:
return await cls._reset_notif_count(notify)

if isinstance(notes, Notes) and notes.remaining_resin_discounts == 0:
if isinstance(notes, GenshinNotes) and notes.remaining_resin_discounts == 0:
return None
if isinstance(notes, StarRailNote) and notes.remaining_weekly_discounts == 0:
return None
Expand All @@ -345,20 +343,6 @@ async def _process_week_boss_discount_notify(cls, notify: NotesNotify, notes: No
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_battery_charge_notify(cls, notify: NotesNotify, notes: ZZZNotes) -> None:
current = notes.battery_charge.current
threshold = notify.threshold
assert threshold is not None

if current < threshold:
est_time = cls._calc_est_time(Game.ZZZ, threshold, current)
return await cls._reset_notif_count(notify, est_time=est_time)

if notify.current_notif_count < notify.max_notif_count:
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_scratch_card_notify(cls, notify: NotesNotify, notes: ZZZNotes) -> None:
if notes.scratch_card_completed:
Expand Down Expand Up @@ -402,39 +386,65 @@ async def _process_planar_fissure_notify(cls, notify: NotesNotify, anns: Sequenc

return None

@classmethod
async def _process_stamina_notify(cls, notify: NotesNotify, notes: Notes) -> None:
"""Process stamina notification."""
if isinstance(notes, HonkaiNotes):
current = notes.current_stamina
game = Game.HONKAI
elif isinstance(notes, GenshinNotes):
current = notes.current_resin
game = Game.GENSHIN
elif isinstance(notes, StarRailNote):
current = notes.current_stamina
game = Game.STARRAIL
else:
current = notes.battery_charge.current
game = Game.ZZZ

threshold = notify.threshold
assert threshold is not None

if current < threshold:
est_time = cls._calc_est_time(game, threshold, current)
return await cls._reset_notif_count(notify, est_time=est_time)

if notify.current_notif_count < notify.max_notif_count:
await cls._notify_user(notify, notes)
return None

@classmethod
async def _process_notify(
cls, notify: NotesNotify, notes: Notes | StarRailNote | ZZZNotes | None, anns: Sequence[Announcement] | None
) -> None:
"""Proces notification."""
match notify.type:
case NotesNotifyType.RESIN:
assert isinstance(notes, Notes)
await cls._process_resin_notify(notify, notes)
case NotesNotifyType.TB_POWER:
assert isinstance(notes, StarRailNote)
await cls._process_tbp_notify(notify, notes)
case NotesNotifyType.RESERVED_TB_POWER:
assert isinstance(notes, StarRailNote)
await cls._process_rtbp_notify(notify, notes)
case NotesNotifyType.GI_EXPED | NotesNotifyType.HSR_EXPED:
assert isinstance(notes, Notes | StarRailNote)
assert isinstance(notes, GenshinNotes | StarRailNote)
await cls._process_expedition_notify(notify, notes)
case NotesNotifyType.PT:
assert isinstance(notes, Notes)
assert isinstance(notes, GenshinNotes)
await cls._process_pt_notify(notify, notes)
case NotesNotifyType.REALM_CURRENCY:
assert isinstance(notes, Notes)
assert isinstance(notes, GenshinNotes)
await cls._process_realm_currency_notify(notify, notes)
case NotesNotifyType.GI_DAILY | NotesNotifyType.HSR_DAILY | NotesNotifyType.ZZZ_DAILY:
case (
NotesNotifyType.GI_DAILY
| NotesNotifyType.HSR_DAILY
| NotesNotifyType.ZZZ_DAILY
| NotesNotifyType.HONKAI_DAILY
):
assert notes is not None
await cls._process_daily_notify(notify, notes)
case NotesNotifyType.RESIN_DISCOUNT | NotesNotifyType.ECHO_OF_WAR:
assert isinstance(notes, Notes | StarRailNote)
await cls._process_week_boss_discount_notify(notify, notes)
case NotesNotifyType.BATTERY:
assert isinstance(notes, ZZZNotes)
await cls._process_battery_charge_notify(notify, notes)
case NotesNotifyType.BATTERY | NotesNotifyType.STAMINA | NotesNotifyType.RESIN | NotesNotifyType.TB_POWER:
assert notes is not None
await cls._process_stamina_notify(notify, notes)
case NotesNotifyType.SCRATCH_CARD:
assert isinstance(notes, ZZZNotes)
await cls._process_scratch_card_notify(notify, notes)
Expand Down Expand Up @@ -494,6 +504,8 @@ async def _get_notes(cls, notify: NotesNotify) -> Notes | StarRailNote | ZZZNote
notes = await notify.account.client.get_starrail_notes()
elif notify.account.game is Game.ZZZ:
notes = await notify.account.client.get_zzz_notes()
elif notify.account.game is Game.HONKAI:
notes = await notify.account.client.get_honkai_notes(notify.account.uid)
else:
raise NotImplementedError
return notes
Expand Down
7 changes: 7 additions & 0 deletions hoyo_buddy/l10n.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import contextlib
import datetime
import pathlib
import random
import re
Expand Down Expand Up @@ -181,6 +182,8 @@ def _translate_extras(self, extras: dict[str, Any], locale: Locale) -> dict[str,
extras_[k] = self.translate(v, locale)
elif isinstance(v, list) and isinstance(v[0], LocaleStr):
extras_[k] = "/".join([self.translate(i, locale) for i in v])
elif isinstance(v, datetime.timedelta):
extras_[k] = self.display_timedelta(v, locale)
else:
extras_[k] = v
return extras_
Expand Down Expand Up @@ -222,6 +225,10 @@ def get_trailblazer_name(

return f"{character.name} ({element_str}) ({gender_str})" if gender_str else f"{character.name} ({element_str})"

def display_timedelta(self, timedelta: datetime.timedelta, locale: Locale) -> str:
str_timedelta = str(timedelta)
return str_timedelta.replace("days", self.translate(LocaleStr(key="days"), locale), 1).replace(", 0:00:00", "")


class AppCommandTranslator(app_commands.Translator):
def __init__(self, translator: Translator) -> None:
Expand Down
1 change: 1 addition & 0 deletions hoyo_buddy/ui/hoyo/notes/buttons/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .reserved_tbp import ReservedTBPReminder
from .resin import ResinReminder
from .scratch_card import ScratchCardReminder
from .stamina import StaminaReminder
from .trailblaze_power import TBPReminder
from .video_store import VideoStoreReminder
from .week_boss import WeekBossReminder
1 change: 1 addition & 0 deletions hoyo_buddy/ui/hoyo/notes/buttons/daily.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def callback(self, i: Interaction) -> None:
Game.GENSHIN: NotesNotifyType.GI_DAILY,
Game.STARRAIL: NotesNotifyType.HSR_DAILY,
Game.ZZZ: NotesNotifyType.ZZZ_DAILY,
Game.HONKAI: NotesNotifyType.HONKAI_DAILY,
}
notify_type = notify_types.get(self.view._account.game)
if notify_type is None:
Expand Down
Loading

0 comments on commit 4e89db3

Please sign in to comment.