feat: include refractor card image in tier-up notification embed (#144) #159
@ -4248,30 +4248,41 @@ async def _run_post_game_refractor_hook(db_game_id: int, channel) -> None:
|
||||
"""Post-game refractor processing — non-fatal.
|
||||
|
||||
Updates season stats then evaluates refractor milestones for all
|
||||
participating players. Fires tier-up notifications and triggers variant
|
||||
card renders. Wrapped in try/except so any failure here is non-fatal —
|
||||
the game is already saved and refractor will self-heal on the next
|
||||
evaluate call.
|
||||
participating players. Triggers variant card renders first to obtain
|
||||
image URLs, then fires tier-up notifications with card art included.
|
||||
Wrapped in try/except so any failure here is non-fatal — the game is
|
||||
already saved and refractor will self-heal on the next evaluate call.
|
||||
"""
|
||||
try:
|
||||
await db_post(f"season-stats/update-game/{db_game_id}")
|
||||
evo_result = await db_post(f"refractor/evaluate-game/{db_game_id}")
|
||||
if evo_result and evo_result.get("tier_ups"):
|
||||
for tier_up in evo_result["tier_ups"]:
|
||||
await notify_tier_completion(channel, tier_up)
|
||||
await _trigger_variant_renders(evo_result["tier_ups"])
|
||||
tier_ups = evo_result["tier_ups"]
|
||||
image_url_map = await _trigger_variant_renders(tier_ups)
|
||||
for tier_up in tier_ups:
|
||||
img = image_url_map.get(tier_up.get("player_id"))
|
||||
await notify_tier_completion(channel, tier_up, image_url=img)
|
||||
except Exception as e:
|
||||
logger.warning(f"Post-game refractor processing failed (non-fatal): {e}")
|
||||
|
||||
|
||||
async def _trigger_variant_renders(tier_ups: list) -> None:
|
||||
"""Fire-and-forget: hit card render URLs to trigger S3 upload for new variants.
|
||||
async def _trigger_variant_renders(tier_ups: list) -> dict:
|
||||
"""Trigger S3 card renders for each tier-up variant and return image URLs.
|
||||
|
||||
Each tier-up with a variant_created value gets a GET request to the card
|
||||
render endpoint, which triggers Playwright render + S3 upload as a side effect.
|
||||
Failures are logged but never raised.
|
||||
render endpoint, which triggers Playwright render + S3 upload. The
|
||||
response image_url (if present) is captured and returned so callers can
|
||||
include the card art in tier-up notifications.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict
|
||||
Mapping of player_id -> image_url. Players whose render failed or
|
||||
returned no image_url are omitted; callers should treat a missing
|
||||
key as None.
|
||||
"""
|
||||
today = datetime.date.today().isoformat()
|
||||
image_urls = {}
|
||||
for tier_up in tier_ups:
|
||||
variant = tier_up.get("variant_created")
|
||||
if variant is None:
|
||||
@ -4280,16 +4291,19 @@ async def _trigger_variant_renders(tier_ups: list) -> None:
|
||||
track = tier_up.get("track_name", "Batter")
|
||||
card_type = "pitching" if track.lower() == "pitcher" else "batting"
|
||||
try:
|
||||
await db_get(
|
||||
result = await db_get(
|
||||
f"players/{player_id}/{card_type}card/{today}/{variant}",
|
||||
none_okay=True,
|
||||
)
|
||||
if result and isinstance(result, dict):
|
||||
image_urls[player_id] = result.get("image_url")
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"Failed to trigger variant render for player %d variant %d (non-fatal)",
|
||||
player_id,
|
||||
variant,
|
||||
)
|
||||
return image_urls
|
||||
|
||||
|
||||
async def complete_game(
|
||||
|
||||
@ -19,13 +19,16 @@ logger = logging.getLogger("discord_app")
|
||||
FOOTER_TEXT = "Paper Dynasty Refractor"
|
||||
|
||||
|
||||
def build_tier_up_embed(tier_up: dict) -> discord.Embed:
|
||||
def build_tier_up_embed(tier_up: dict, image_url: str | None = None) -> discord.Embed:
|
||||
"""Build a Discord embed for a tier-up event.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tier_up:
|
||||
Dict with keys: player_name, old_tier, new_tier, current_value, track_name.
|
||||
image_url:
|
||||
Optional S3 URL for the newly rendered refractor card image. When
|
||||
provided, the card art is shown as the embed image.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@ -57,12 +60,14 @@ def build_tier_up_embed(tier_up: dict) -> discord.Embed:
|
||||
color=color,
|
||||
)
|
||||
|
||||
if image_url:
|
||||
embed.set_image(url=image_url)
|
||||
embed.set_footer(text=FOOTER_TEXT)
|
||||
return embed
|
||||
|
||||
|
||||
async def notify_tier_completion(
|
||||
channel: discord.abc.Messageable, tier_up: dict
|
||||
channel: discord.abc.Messageable, tier_up: dict, image_url: str | None = None
|
||||
) -> None:
|
||||
"""Send a tier-up notification embed to the given channel.
|
||||
|
||||
@ -75,9 +80,12 @@ async def notify_tier_completion(
|
||||
A discord.abc.Messageable (e.g. discord.TextChannel).
|
||||
tier_up:
|
||||
Dict with keys: player_name, old_tier, new_tier, current_value, track_name.
|
||||
image_url:
|
||||
Optional S3 URL for the refractor card image. Passed through to
|
||||
build_tier_up_embed so the card art appears in the notification.
|
||||
"""
|
||||
try:
|
||||
embed = build_tier_up_embed(tier_up)
|
||||
embed = build_tier_up_embed(tier_up, image_url=image_url)
|
||||
await channel.send(embed=embed)
|
||||
except Exception as exc:
|
||||
logger.error(
|
||||
|
||||
@ -4,8 +4,8 @@ Mock-based integration tests for the post-game refractor hook.
|
||||
Tests _run_post_game_refractor_hook() which orchestrates:
|
||||
1. POST season-stats/update-game/{game_id} — update player season stats
|
||||
2. POST refractor/evaluate-game/{game_id} — evaluate refractor milestones
|
||||
3. notify_tier_completion() once per tier-up returned
|
||||
4. _trigger_variant_renders() with the full tier_ups list
|
||||
3. _trigger_variant_renders() with the full tier_ups list (returns image_url map)
|
||||
4. notify_tier_completion() once per tier-up, with image_url from render
|
||||
|
||||
The hook is wrapped in try/except so failures are non-fatal — the game
|
||||
result is already persisted before this block runs. These tests cover the
|
||||
@ -118,6 +118,7 @@ class TestTierUpNotifications:
|
||||
patch(
|
||||
"command_logic.logic_gameplay._trigger_variant_renders",
|
||||
new_callable=AsyncMock,
|
||||
return_value={},
|
||||
),
|
||||
):
|
||||
await _run_post_game_refractor_hook(99, channel)
|
||||
@ -157,11 +158,12 @@ class TestTierUpNotifications:
|
||||
patch(
|
||||
"command_logic.logic_gameplay._trigger_variant_renders",
|
||||
new_callable=AsyncMock,
|
||||
return_value={},
|
||||
),
|
||||
):
|
||||
await _run_post_game_refractor_hook(1, channel)
|
||||
|
||||
mock_notify.assert_called_once_with(channel, tier_up)
|
||||
mock_notify.assert_called_once_with(channel, tier_up, image_url=None)
|
||||
|
||||
async def test_no_notify_when_empty_tier_ups(self):
|
||||
"""No notifications sent when evaluate returns an empty tier_ups list."""
|
||||
@ -249,6 +251,7 @@ class TestVariantRenderTriggers:
|
||||
patch(
|
||||
"command_logic.logic_gameplay._trigger_variant_renders",
|
||||
new_callable=AsyncMock,
|
||||
return_value={},
|
||||
) as mock_render,
|
||||
):
|
||||
await _run_post_game_refractor_hook(42, _make_channel())
|
||||
@ -282,11 +285,12 @@ class TestVariantRenderTriggers:
|
||||
|
||||
mock_render.assert_not_called()
|
||||
|
||||
async def test_notifications_before_render_trigger(self):
|
||||
"""All notify_tier_completion calls happen before _trigger_variant_renders.
|
||||
async def test_render_before_notification(self):
|
||||
"""_trigger_variant_renders is called before notify_tier_completion.
|
||||
|
||||
Notifications should be dispatched first so the player sees the message
|
||||
before any background card renders begin.
|
||||
Renders run first so that image URLs are available to include in the
|
||||
tier-up notification embed. The player sees the card art immediately
|
||||
rather than receiving a text-only notification.
|
||||
"""
|
||||
call_order = []
|
||||
tier_up = {"player_id": 1, "variant_created": 5, "track_name": "Batter"}
|
||||
@ -296,11 +300,12 @@ class TestVariantRenderTriggers:
|
||||
return {"tier_ups": [tier_up]}
|
||||
return {}
|
||||
|
||||
async def fake_notify(ch, tu):
|
||||
async def fake_notify(ch, tu, image_url=None):
|
||||
call_order.append("notify")
|
||||
|
||||
async def fake_render(tier_ups):
|
||||
call_order.append("render")
|
||||
return {}
|
||||
|
||||
with (
|
||||
patch(
|
||||
@ -319,7 +324,7 @@ class TestVariantRenderTriggers:
|
||||
):
|
||||
await _run_post_game_refractor_hook(1, _make_channel())
|
||||
|
||||
assert call_order == ["notify", "render"]
|
||||
assert call_order == ["render", "notify"]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Loading…
Reference in New Issue
Block a user