feat(economy): add vanity shop and harden economy money-safety

Vanity shop (/vanity): cosmetic badges/titles as a pure whale coin sink -
purchases burn coins (not credited to the house) and equip a badge shown on
/profile. New vanity_owned/vanity_active fields, schema sync, and tests.

Money-safety and robustness fixes from a codebase review:

- _parse_amount now rejects negative amounts. Every bet/give/request flows
  through it, so a negative value can no longer mint coins on a loss/transfer
  path that trusts the caller's sign (all call sites already guarded <= 0;
  this closes the source).
- do_blackjack_payout no longer raises on a DB failure. The stake was already
  deducted in do_blackjack_bet, so it now logs critical with the owed amount
  (for admin reconciliation) and returns db_error; all payout call sites render
  a clear "payout failed" notice instead of crashing the interaction.
- Instant "kohv" consumable now cancels the pending reminder DMs for the
  cooldowns it wipes (via new INSTANT_RESET_COMMANDS), so no stale/duplicate
  reminders fire.
- Renamed the misleadingly-named _refund_user_safe -> _debit_house_safe (it
  debits the house) and dropped its ignored first arg.
- Added __all__ to vanity.py and consumables.py so `import *` no longer leaks
  incidental imports into the economy namespace.
- Documented Kõrvaklapid's +25 coin daily bonus in README and DEV_NOTES.

Tests: blackjack payout DB-failure safety and INSTANT_RESET_COMMANDS lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
This commit is contained in:
Rene Arumetsa
2026-09-04 02:09:25 +03:00
parent a2e1601d0e
commit 037628d24f
21 changed files with 393 additions and 31 deletions

View File

@@ -926,13 +926,17 @@ def register_economy_games_commands(
embed = self._cur_embed(game_over=True, hand_results=hand_results)
embed.title = S.TITLE[title_key]
embed.color = color
if res.get("ok"):
result_line = result_str + S.BJ_UI["balance_line"].format(balance=coin(res["balance"]))
else:
result_line = result_str + "\n" + S.ERR["payout_failed"]
embed.add_field(
name=S.BJ["result_field"],
value=result_str + S.BJ_UI["balance_line"].format(balance=coin(res["balance"])),
value=result_line,
inline=False,
)
await self.message.edit(embed=embed, view=self)
if total_payout > total_invested:
if res.get("ok") and total_payout > total_invested:
asyncio.create_task(award_exp(interaction, economy.gamble_exp(total_invested)))
async def _do_dealer_reveal(self, interaction: discord.Interaction) -> None:
@@ -1150,32 +1154,37 @@ def register_economy_games_commands(
await asyncio.sleep(_BJ_DEAL_DELAY)
if _bj_is_blackjack(dealer_hand):
push_res = await economy.do_blackjack_payout(interaction.user.id, bet, bet)
push_line = (
S.BJ["push_result"] + S.BJ_UI["balance_line"].format(balance=coin(push_res["balance"]))
if push_res.get("ok")
else S.BJ["push_result"] + "\n" + S.ERR["payout_failed"]
)
embed = _bj_embed(
player_hand,
dealer_hand,
S.TITLE["blackjack_push"],
0x99AAB5,
hide_dealer=False,
result_field=(
S.BJ["result_field"],
S.BJ["push_result"] + S.BJ_UI["balance_line"].format(balance=coin(push_res["balance"])),
),
result_field=(S.BJ["result_field"], push_line),
)
else:
payout = bet + int(bet * 1.5)
bj_res = await economy.do_blackjack_payout(interaction.user.id, payout, bet)
bj_line = (
f"+{coin(payout)}" + S.BJ_UI["balance_line"].format(balance=coin(bj_res["balance"]))
if bj_res.get("ok")
else f"+{coin(payout)}" + "\n" + S.ERR["payout_failed"]
)
embed = _bj_embed(
player_hand,
dealer_hand,
S.TITLE["blackjack_bj"],
0xF4C430,
hide_dealer=False,
result_field=(
S.BJ["result_field"],
f"+{coin(payout)}" + S.BJ_UI["balance_line"].format(balance=coin(bj_res["balance"])),
),
result_field=(S.BJ["result_field"], bj_line),
)
asyncio.create_task(award_exp(interaction, economy.gamble_exp(bet)))
if bj_res.get("ok"):
asyncio.create_task(award_exp(interaction, economy.gamble_exp(bet)))
active_games.discard(interaction.user.id)
await msg.edit(embed=embed)
return